personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add first-run onboarding flow for new journals

When a journal has no facets, redirect root to settings where an inline
"Welcome" section offers one-click creation of a Personal facet (🏠, blue).
Home app also shows a friendly empty state pointing to settings.

- Root route checks for facets, redirects to settings if empty
- Settings shows setup section when no facets exist
- create_facet API now accepts optional emoji and color params
- Home displays welcome message with link to settings when no facets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+153 -5
+52
apps/home/workspace.html
··· 1 1 <div class="workspace-content"> 2 2 <div class="home-loading" id="home-loading">Loading dashboard...</div> 3 3 4 + <!-- Empty journal state (no facets) --> 5 + <div class="home-empty-journal" id="home-empty-journal" style="display: none;"> 6 + <div class="empty-journal-icon">📓</div> 7 + <h2>Welcome to your journal</h2> 8 + <p>Get started by creating your first facet to organize your content.</p> 9 + <a href="/app/settings" class="empty-journal-btn">Go to Settings</a> 10 + </div> 11 + 4 12 <div class="home-dashboard" id="home-dashboard" style="display: none;"> 5 13 <!-- Top row: Todos and Events --> 6 14 <div class="home-grid"> ··· 40 48 text-align: center; 41 49 padding: 4em; 42 50 color: #666; 51 + } 52 + 53 + /* Empty journal state */ 54 + .home-empty-journal { 55 + text-align: center; 56 + padding: 4em 2em; 57 + max-width: 400px; 58 + margin: 2em auto; 59 + } 60 + .home-empty-journal .empty-journal-icon { 61 + font-size: 4em; 62 + margin-bottom: 0.25em; 63 + } 64 + .home-empty-journal h2 { 65 + margin: 0 0 0.5em 0; 66 + font-size: 1.5em; 67 + font-weight: 600; 68 + color: #333; 69 + } 70 + .home-empty-journal p { 71 + margin: 0 0 1.5em 0; 72 + color: #666; 73 + line-height: 1.5; 74 + } 75 + .home-empty-journal .empty-journal-btn { 76 + display: inline-block; 77 + padding: 0.75em 1.5em; 78 + background: #007bff; 79 + color: white; 80 + text-decoration: none; 81 + border-radius: 8px; 82 + font-weight: 600; 83 + transition: background 0.2s; 84 + } 85 + .home-empty-journal .empty-journal-btn:hover { 86 + background: #0056b3; 43 87 } 44 88 45 89 .home-dashboard { ··· 383 427 } 384 428 385 429 async function loadSummary() { 430 + // Check if journal has no facets 431 + const hasFacets = window.facetsData && window.facetsData.length > 0; 432 + if (!hasFacets) { 433 + document.getElementById('home-loading').style.display = 'none'; 434 + document.getElementById('home-empty-journal').style.display = 'block'; 435 + return; 436 + } 437 + 386 438 try { 387 439 const response = await fetch(`/app/home/api/summary/${currentDay}`); 388 440 if (!response.ok) throw new Error('Failed to load');
+9 -3
apps/settings/routes.py
··· 71 71 72 72 Accepts JSON with: 73 73 title: Display title (required) 74 + emoji: Icon emoji (optional, default: "📦") 75 + color: Hex color (optional, default: "#667eea") 74 76 75 77 The facet name (slug) is auto-generated from the title. 76 78 """ ··· 82 84 title = data.get("title", "").strip() 83 85 if not title: 84 86 return jsonify({"error": "Title is required"}), 400 87 + 88 + # Optional fields with defaults 89 + emoji = data.get("emoji", "📦") 90 + color = data.get("color", "#667eea") 85 91 86 92 # Generate slug from title: lowercase, replace spaces/special chars with hyphens 87 93 slug = re.sub(r"[^a-z0-9]+", "-", title.lower()) ··· 107 113 config = { 108 114 "title": title, 109 115 "description": "", 110 - "color": "#667eea", 111 - "emoji": "📦", 116 + "color": color, 117 + "emoji": emoji, 112 118 } 113 119 114 120 config_file = facet_path / "facet.json" ··· 121 127 app="settings", 122 128 facet=slug, 123 129 action="facet_create", 124 - params={"title": title}, 130 + params={"title": title, "emoji": emoji, "color": color}, 125 131 ) 126 132 127 133 return jsonify({"success": True, "facet": slug, "config": config}), 201
+81 -1
apps/settings/workspace.html
··· 134 134 .log-more-btn { background: none; border: none; color: #667eea; cursor: pointer; font-size: 0.85em; padding: 0.5em 1em; border-radius: 4px; transition: background 0.2s; } 135 135 .log-more-btn:hover { background: rgba(102, 126, 234, 0.1); } 136 136 .log-more-btn:disabled { color: #ccc; cursor: default; } 137 + 138 + /* Setup section for first-run onboarding */ 139 + .setup-section { max-width: 600px; margin-bottom: 2em; padding: 1.5em; background: linear-gradient(135deg, #007bff08 0%, #007bff15 100%); border: 2px solid #007bff30; border-radius: 12px; } 140 + .setup-section h2 { color: #333; font-size: 1.4em; margin: 0 0 0.5em 0; font-weight: 600; } 141 + .setup-section p { color: #555; margin: 0 0 1.25em 0; line-height: 1.5; } 142 + .setup-btn { display: inline-flex; align-items: center; gap: 0.5em; padding: 0.75em 1.5em; background: #007bff; color: white; border: none; border-radius: 8px; font-size: 1em; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s; } 143 + .setup-btn:hover { background: #0056b3; } 144 + .setup-btn:active { transform: scale(0.98); } 145 + .setup-btn .btn-icon { font-size: 1.2em; } 146 + .setup-btn:disabled { opacity: 0.7; cursor: not-allowed; } 137 147 </style> 138 148 139 149 <div class="workspace-content"> 140 150 <div id="config"> 151 + <!-- Setup Section (shown when no facets exist) --> 152 + <div class="setup-section" id="setupSection" style="display: none;"> 153 + <h2>Welcome to solstone</h2> 154 + <p>Create your first facet to start organizing your journal. Facets help you separate different areas of your life.</p> 155 + <button class="setup-btn" id="createPersonalBtn"> 156 + <span class="btn-icon">🏠</span> 157 + <span>Create Personal Facet</span> 158 + </button> 159 + </div> 160 + 141 161 <!-- Identity Section (shown in all-facet mode) --> 142 162 <div class="config-section" id="identityConfig"> 143 163 <h2>Journal Configuration</h2> ··· 1034 1054 } 1035 1055 }); 1036 1056 1057 + // ========== SETUP SECTION (First-run onboarding) ========== 1058 + const setupSection = document.getElementById('setupSection'); 1059 + const createPersonalBtn = document.getElementById('createPersonalBtn'); 1060 + 1061 + function checkShowSetup() { 1062 + // Show setup section if no facets exist 1063 + const hasFacets = window.facetsData && window.facetsData.length > 0; 1064 + if (!hasFacets && !window.selectedFacet) { 1065 + setupSection.style.display = 'block'; 1066 + } else { 1067 + setupSection.style.display = 'none'; 1068 + } 1069 + } 1070 + 1071 + async function createPersonalFacet() { 1072 + createPersonalBtn.disabled = true; 1073 + createPersonalBtn.querySelector('span:last-child').textContent = 'Creating...'; 1074 + 1075 + try { 1076 + const response = await fetch('/app/settings/api/facet', { 1077 + method: 'POST', 1078 + headers: {'Content-Type': 'application/json'}, 1079 + body: JSON.stringify({ 1080 + title: 'Personal', 1081 + emoji: '🏠', 1082 + color: '#007bff' 1083 + }) 1084 + }); 1085 + 1086 + const data = await response.json(); 1087 + 1088 + if (data.success) { 1089 + // Auto-select the new facet and reload 1090 + document.cookie = `selectedFacet=${data.facet}; path=/; SameSite=Lax`; 1091 + window.location.href = '/app/home'; 1092 + } else { 1093 + throw new Error(data.error || 'Failed to create facet'); 1094 + } 1095 + } catch (err) { 1096 + console.error('Error creating personal facet:', err); 1097 + createPersonalBtn.disabled = false; 1098 + createPersonalBtn.querySelector('span:last-child').textContent = 'Create Personal Facet'; 1099 + 1100 + if (window.AppServices && window.AppServices.notifications) { 1101 + window.AppServices.notifications.show({ 1102 + app: 'settings', 1103 + icon: '❌', 1104 + title: 'Creation Failed', 1105 + message: err.message || 'Error creating facet', 1106 + dismissible: true, 1107 + autoDismiss: 5000 1108 + }); 1109 + } 1110 + } 1111 + } 1112 + 1113 + createPersonalBtn.addEventListener('click', createPersonalFacet); 1114 + 1037 1115 // Initialize visibility based on current facet selection 1038 1116 const identityConfig = document.getElementById('identityConfig'); 1039 1117 const facetConfig = document.getElementById('facetConfig'); ··· 1042 1120 // Facet selected: show ONLY facet settings 1043 1121 identityConfig.style.display = 'none'; 1044 1122 facetConfig.style.display = 'block'; 1123 + setupSection.style.display = 'none'; 1045 1124 loadFacetConfig(window.selectedFacet); 1046 1125 } else { 1047 - // All-facet mode: show ONLY journal settings 1126 + // All-facet mode: show journal settings and maybe setup 1048 1127 identityConfig.style.display = 'block'; 1049 1128 facetConfig.style.display = 'none'; 1129 + checkShowSetup(); 1050 1130 } 1051 1131 1052 1132 // Load config on page load
+11 -1
convey/root.py
··· 18 18 url_for, 19 19 ) 20 20 21 + from think.facets import get_facets 21 22 from think.utils import get_config 22 23 23 24 ··· 109 110 110 111 @bp.route("/") 111 112 def index() -> Any: 112 - """Root redirect to home app.""" 113 + """Root redirect - to settings if no facets, otherwise home.""" 114 + try: 115 + facets = get_facets() 116 + except Exception: 117 + facets = {} 118 + 119 + if not facets: 120 + # New journal - direct to settings for initial setup 121 + return redirect(url_for("app:settings.index")) 122 + 113 123 return redirect(url_for("app:home.index"))