From 9fd14f87f714f0e7ebe2f43a688fcdade9d2774a Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Fri, 24 Apr 2026 21:57:58 +0200 Subject: [PATCH] Polish memory UI and add auto tags --- server.js | 49 +++- src/App.css | 775 +++++++++++++++++++++------------------------------- src/App.jsx | 328 +++++++++++----------- 3 files changed, 528 insertions(+), 624 deletions(-) diff --git a/server.js b/server.js index 2c2dd2e..7f2b35b 100644 --- a/server.js +++ b/server.js @@ -83,6 +83,48 @@ function getEntryCount(content) { return content.split('\n').filter((line) => line.trim().startsWith('- [')).length } +const TAG_STOPWORDS = new Set([ + 'about', 'after', 'again', 'also', 'been', 'being', 'between', 'could', 'daily', 'does', 'done', 'from', 'have', 'into', 'just', 'like', 'main', 'make', 'maybe', 'memory', 'more', 'most', 'much', 'need', 'note', 'notes', 'only', 'other', 'over', 'really', 'same', 'some', 'still', 'than', 'that', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'today', 'very', 'want', 'were', 'what', 'when', 'with', 'work', 'would', 'your', 'yours', 'jimmi', 'rook', 'said', 'todo', 'todos' +]) + +function generateTags(content, filename) { + const tags = [] + const pushTag = (value) => { + const cleaned = String(value || '').toLowerCase().replace(/[^a-z0-9+-]+/g, '').trim() + if (!cleaned || cleaned.length < 3 || TAG_STOPWORDS.has(cleaned) || tags.includes(cleaned)) return + tags.push(cleaned) + } + + if (filename === 'MEMORY.md') pushTag('core') + + for (const line of content.split('\n')) { + const trimmed = line.trim() + if (trimmed.startsWith('#')) { + trimmed.replace(/^#+\s*/, '').split(/[^a-zA-Z0-9+-]+/).forEach(pushTag) + } + const hashtagMatches = trimmed.match(/#[a-zA-Z0-9+-]+/g) || [] + hashtagMatches.forEach((tag) => pushTag(tag.slice(1))) + } + + const frequency = new Map() + const normalized = content + .toLowerCase() + .replace(/\[[^\]]*\]/g, ' ') + .replace(/[^a-z0-9+\-\s]/g, ' ') + + for (const token of normalized.split(/\s+/)) { + if (!token || token.length < 4 || TAG_STOPWORDS.has(token)) continue + frequency.set(token, (frequency.get(token) || 0) + 1) + } + + Array.from(frequency.entries()) + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .slice(0, 8) + .forEach(([token]) => pushTag(token)) + + return tags.slice(0, 4) +} + function formatDateLabel(filename) { const match = filename.match(/^(\d{4})-(\d{2})-(\d{2})\.md$/) if (!match) return filename @@ -206,6 +248,7 @@ function buildFileMeta(doc) { wordCount: wordCount(content), entryCount: getEntryCount(content), dateLabel: formatDateLabel(doc.filename), + tags: generateTags(content, doc.filename), } } @@ -239,6 +282,7 @@ app.get('/api/meta', async (req, res) => { size: Buffer.byteLength(mainDoc.content || '', 'utf8'), wordCount: wordCount(mainDoc.content || ''), entryCount: getEntryCount(mainDoc.content || ''), + tags: generateTags(mainDoc.content || '', 'MEMORY.md'), } : null const dailyFiles = dailyDocs.map(buildFileMeta) @@ -279,7 +323,7 @@ app.get('/api/memories/:filename', async (req, res) => { await ensureSync() const doc = await readMemoryDocument(filename) if (!doc) return res.status(404).json({ error: 'File not found' }) - return res.json({ content: doc.content || '' }) + return res.json({ content: doc.content || '', tags: generateTags(doc.content || '', filename) }) } catch (error) { return res.status(500).json({ error: error.message }) } @@ -290,7 +334,7 @@ app.get('/api/main-memory', async (req, res) => { await ensureSync() const doc = await readMemoryDocument('MEMORY.md') if (!doc) return res.status(404).json({ error: 'MEMORY.md not found' }) - res.json({ content: doc.content || '' }) + res.json({ content: doc.content || '', tags: generateTags(doc.content || '', 'MEMORY.md') }) } catch (error) { res.status(500).json({ error: error.message }) } @@ -369,6 +413,7 @@ app.get('/api/search', async (req, res) => { dateLabel: formatDateLabel(doc.filename), entryCount: getEntryCount(content), wordCount: wordCount(content), + tags: generateTags(content, doc.filename), matchCount: (lower.match(new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length, snippet: `${windowStart > 0 ? '…' : ''}${snippet}${windowEnd < content.length ? '…' : ''}`, matchingLines, diff --git a/src/App.css b/src/App.css index 88716b7..bc701d9 100644 --- a/src/App.css +++ b/src/App.css @@ -3,20 +3,29 @@ :root { color-scheme: dark; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - background: - radial-gradient(circle at top, rgba(112, 89, 255, 0.18), transparent 35%), - linear-gradient(180deg, #071019 0%, #090d16 45%, #05070d 100%); - color: #edf2ff; + background: #101214; + color: #f3f4f6; + --bg: #101214; + --panel: #171a1d; + --panel-2: #1c2024; + --border: #2a2f35; + --border-strong: #3a4149; + --text: #f3f4f6; + --muted: #9ca3af; + --muted-2: #7b828c; + --accent: #d8dde3; +} + +html, body, #root { + min-height: 100%; } body { - min-height: 100vh; - background: transparent; - color: #edf2ff; + background: var(--bg); + color: var(--text); } -button, -input { +button, input, textarea { font: inherit; } @@ -24,79 +33,51 @@ button { border: 0; } -#root { - min-height: 100vh; -} - .app-shell { - position: relative; min-height: 100vh; - overflow: hidden; -} - -.nebula { - position: fixed; - inset: auto; - border-radius: 999px; - filter: blur(80px); - opacity: 0.5; - pointer-events: none; -} - -.nebula-a { - top: -6rem; - left: -3rem; - width: 24rem; - height: 24rem; - background: rgba(78, 168, 255, 0.2); -} - -.nebula-b { - right: -5rem; - top: 10rem; - width: 28rem; - height: 28rem; - background: rgba(172, 98, 255, 0.18); + background: var(--bg); } .app { - position: relative; max-width: 1180px; margin: 0 auto; - padding: 2.5rem 1.25rem 4rem; + padding: 2rem 1.25rem 4rem; +} + +.hero, +.hero-grid, +.main-grid { + display: grid; + gap: 1rem; } .hero { - display: flex; - justify-content: space-between; - gap: 1.5rem; - align-items: flex-end; - margin-bottom: 1.5rem; + grid-template-columns: minmax(0, 1fr) auto; + align-items: end; + margin-bottom: 1rem; } .eyebrow, -.card-kicker, -.results-header, -.section-label, +.section-kicker, .viewer-kicker { text-transform: uppercase; - letter-spacing: 0.18em; - font-size: 0.7rem; - color: #8ca3c7; + letter-spacing: 0.16em; + font-size: 0.72rem; + color: var(--muted-2); } .hero h1 { - font-size: clamp(2.2rem, 4vw, 4rem); - line-height: 0.95; - max-width: 10ch; - margin-top: 0.45rem; + margin-top: 0.35rem; + font-size: clamp(2.2rem, 5vw, 4.4rem); + font-weight: 600; + letter-spacing: -0.06em; } .subtitle { - max-width: 40rem; - margin-top: 0.85rem; - color: #aeb9d7; - line-height: 1.6; + margin-top: 0.7rem; + max-width: 38rem; + line-height: 1.5; + color: var(--muted); } .hero-actions { @@ -106,171 +87,200 @@ button { gap: 0.75rem; } -.ghost-btn, -.primary-btn, -.back-btn, -.file-card, -.result-item { - cursor: pointer; -} - -.ghost-btn, -.primary-btn, -.back-btn { - border-radius: 999px; - padding: 0.8rem 1.1rem; - transition: transform 140ms ease, box-shadow 140ms ease, border-color 140ms ease; -} - -.ghost-btn, -.back-btn { - background: rgba(15, 22, 37, 0.7); - color: #edf2ff; - border: 1px solid rgba(156, 179, 221, 0.2); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.05); -} - -.primary-btn { - background: linear-gradient(135deg, #62b0ff, #8b6dff); - color: #09101d; - font-weight: 700; - min-width: 7rem; -} - -.ghost-btn:hover, -.primary-btn:hover, -.back-btn:hover, -.file-card:hover, -.result-item:hover { - transform: translateY(-2px); -} - -.primary-btn:disabled { - opacity: 0.6; - cursor: default; - transform: none; -} - -.slash-hint { - color: #8ca3c7; - font-size: 0.82rem; -} - -kbd { - background: rgba(255,255,255,0.08); - border: 1px solid rgba(255,255,255,0.12); - border-bottom-width: 2px; - border-radius: 0.5rem; - padding: 0.15rem 0.42rem; - font-size: 0.8rem; -} - -.dashboard-grid { - display: grid; - grid-template-columns: 1.35fr 0.9fr 1fr; - gap: 1rem; - margin-bottom: 1rem; -} - -.feature-card, -.composer-card, +.panel, .file-card, .result-item, .memory-content, +.memory-editor, .viewer-header { - background: rgba(12, 19, 33, 0.72); - border: 1px solid rgba(146, 166, 206, 0.14); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.25), - inset 0 1px 0 rgba(255,255,255,0.04); - backdrop-filter: blur(18px); + background: var(--panel); + border: 1px solid var(--border); } -.feature-card, -.composer-card { - border-radius: 24px; - padding: 1.2rem 1.25rem; +.panel, +.file-card, +.result-item, +.memory-content, +.memory-editor, +.viewer-header, +.search-input, +.ghost-btn, +.primary-btn, +.tag-pill { + border-radius: 10px; } -.spark-text { - margin-top: 0.85rem; - font-size: 1.1rem; - line-height: 1.6; - color: #f3f6ff; +.panel, +.file-card, +.result-item, +.viewer-header, +.memory-content, +.memory-editor { + box-shadow: none; } -.spark-source { - margin-top: 0.75rem; - color: #8ca3c7; - font-size: 0.82rem; -} - -.stat-card { - display: grid; - gap: 0.75rem; -} - -.mini-stat { - color: #c9d6f0; - font-size: 0.92rem; -} - -.mini-stat span { - display: inline-block; - min-width: 3.8rem; - font-size: 1.35rem; - font-weight: 800; - color: #78c1ff; -} - -.recap-card { - display: grid; - gap: 0.8rem; -} - -.recap-line { - color: #d8e1f6; - line-height: 1.5; -} - -.composer-card { +.hero-grid { + grid-template-columns: 1.35fr 0.9fr 1fr; margin-bottom: 1rem; } -.composer-title { - margin-top: 0.35rem; - color: #d8e1f6; +.panel { + padding: 1rem; +} + +.lead-copy { + margin-top: 0.75rem; + font-size: 1.05rem; + line-height: 1.6; +} + +.lead-meta, +.file-age, +.result-meta { + color: var(--muted-2); + font-size: 0.82rem; +} + +.lead-meta { + margin-top: 0.65rem; +} + +.stats-panel { + display: grid; + gap: 0.65rem; +} + +.stat-row, +.rail-row, +.result-head, +.file-card-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.8rem; +} + +.stat-row { + padding-bottom: 0.65rem; + border-bottom: 1px solid var(--border); +} + +.stat-row:last-child { + padding-bottom: 0; + border-bottom: 0; +} + +.stat-row span, +.recap-line, +.rail-row span, +.file-stats, +.file-snippet, +.result-snippet, +.slash-hint { + color: var(--muted); +} + +.stat-row strong, +.rail-metrics strong { + font-size: 1.15rem; + font-weight: 600; +} + +.recap-panel { + display: grid; + gap: 0.65rem; +} + +.main-grid { + grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); + margin-bottom: 1rem; +} + +.section-heading, +.compact-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.section-heading h2, +.compact-heading h2 { + margin-top: 0.2rem; + font-size: 1.1rem; + font-weight: 600; + letter-spacing: -0.02em; +} + +.rail-list { + margin-top: 1rem; + display: grid; + gap: 0.6rem; +} + +.rail-row { + width: 100%; + text-align: left; + background: var(--panel-2); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.85rem 0.9rem; + cursor: pointer; +} + +.rail-date, +.file-label, +.result-name { + font-weight: 600; + letter-spacing: -0.01em; +} + +.rail-metrics { + display: grid; + text-align: right; } .composer-form { margin-top: 1rem; display: flex; - gap: 0.8rem; + gap: 0.75rem; +} + +.search-block { + margin-top: 1rem; } .search-section { position: relative; - margin-bottom: 1.25rem; + margin: 1rem 0; } -.search-input { +.search-input, +.memory-editor { width: 100%; - background: rgba(13, 20, 34, 0.78); - border: 1px solid rgba(141, 164, 207, 0.18); - border-radius: 18px; - padding: 0.9rem 1rem; - color: #edf2ff; + background: #121518; + color: var(--text); + border: 1px solid var(--border-strong); outline: none; - transition: border-color 0.15s ease, box-shadow 0.15s ease; + padding: 0.9rem 1rem; + transition: border-color 140ms ease, background 140ms ease; } -.search-input:focus { - border-color: rgba(113, 190, 255, 0.9); - box-shadow: 0 0 0 4px rgba(98, 176, 255, 0.12); +.memory-editor { + min-height: 68vh; + resize: vertical; + line-height: 1.6; } -.search-input::placeholder { - color: #7282a4; +.search-input:focus, +.memory-editor:focus { + border-color: #626973; + background: #101316; +} + +.search-input::placeholder, +.memory-editor::placeholder { + color: var(--muted-2); } .search-spinner { @@ -278,327 +288,172 @@ kbd { right: 1rem; top: 50%; transform: translateY(-50%); + color: var(--muted-2); } -.files-grid { +.files-grid, +.search-results { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 0.9rem; -} - -.file-card { - text-align: left; - border-radius: 20px; - padding: 1rem; - border: 1px solid rgba(146, 166, 206, 0.14); - transition: transform 140ms ease, border-color 140ms ease, box-shadow 140ms ease; -} - -.file-card.light:hover, -.result-item:hover { - border-color: rgba(116, 191, 255, 0.42); -} - -.file-card.active:hover { - border-color: rgba(169, 132, 255, 0.52); -} - -.file-card.intense:hover { - border-color: rgba(255, 122, 191, 0.52); -} - -.file-card-top { - display: flex; - align-items: flex-start; - justify-content: space-between; gap: 0.75rem; } -.file-date { - color: #7ec8ff; - font-size: 0.82rem; - font-weight: 700; -} - -.file-label { - margin-top: 0.2rem; - color: #dbe5fb; - font-weight: 600; -} - -.file-orb { - width: 0.9rem; - height: 0.9rem; - border-radius: 999px; - background: radial-gradient(circle at 35% 35%, #fff, rgba(126,200,255,0.92) 30%, rgba(126,200,255,0.08) 70%); - box-shadow: 0 0 20px rgba(126, 200, 255, 0.55); - flex: 0 0 auto; -} - -.file-card.active .file-orb { - background: radial-gradient(circle at 35% 35%, #fff, rgba(172,128,255,0.92) 30%, rgba(172,128,255,0.08) 70%); - box-shadow: 0 0 22px rgba(172, 128, 255, 0.48); -} - -.file-card.intense .file-orb { - background: radial-gradient(circle at 35% 35%, #fff, rgba(255,124,190,0.92) 30%, rgba(255,124,190,0.08) 70%); - box-shadow: 0 0 22px rgba(255, 124, 190, 0.48); -} - -.file-stats { - display: flex; - gap: 0.45rem; - flex-wrap: wrap; - margin-top: 0.75rem; - color: #8ca3c7; - font-size: 0.8rem; -} - -.entry-tag { - border-radius: 999px; - padding: 0.18rem 0.55rem; - background: rgba(113, 190, 255, 0.12); - color: #7ec8ff; -} - -.file-age { - margin-top: 0.6rem; - color: #7282a4; - font-size: 0.76rem; -} - -.search-results { - display: grid; - gap: 0.65rem; -} - -.results-header, -.section-label { - margin-bottom: 0.65rem; +.file-card, +.result-item, +.ghost-btn, +.primary-btn { + cursor: pointer; } +.file-card, .result-item { + padding: 1rem; text-align: left; - border-radius: 18px; - padding: 1rem 1.05rem; - border: 1px solid rgba(146, 166, 206, 0.14); + transition: border-color 140ms ease, transform 140ms ease; } -.result-name { - color: #7ec8ff; - font-weight: 700; +.file-card:hover, +.result-item:hover, +.ghost-btn:hover, +.primary-btn:hover, +.back-btn:hover, +.rail-row:hover { + transform: translateY(-1px); + border-color: #505862; } -.result-snippet, -.result-lines { - margin-top: 0.45rem; - color: #d6e0f6; +.file-date { + color: var(--muted-2); + font-size: 0.82rem; +} + +.file-snippet, +.result-snippet { + margin-top: 0.55rem; line-height: 1.55; } -.result-lines { - color: #97abd3; - font-size: 0.88rem; +.file-stats { + margin-top: 0.75rem; + display: flex; + gap: 0.6rem; + font-size: 0.82rem; } -.result-meta { - margin-top: 0.55rem; - color: #7282a4; - font-size: 0.76rem; +.tag-strip { + margin-top: 0.8rem; + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.tag-pill { + display: inline-flex; + align-items: center; + min-height: 1.8rem; + padding: 0.22rem 0.55rem; + background: #111417; + border: 1px solid var(--border-strong); + color: #d7dce2; + font-size: 0.75rem; + text-transform: lowercase; +} + +.ghost-btn, +.primary-btn, +.back-btn { + padding: 0.8rem 1rem; +} + +.ghost-btn, +.back-btn { + background: transparent; + color: var(--text); + border: 1px solid var(--border-strong); +} + +.primary-btn { + background: #e8ebef; + color: #111315; + border: 1px solid #e8ebef; + font-weight: 600; +} + +.primary-btn:disabled { + opacity: 0.55; + cursor: default; + transform: none; +} + +kbd { + background: #15191d; + border: 1px solid var(--border-strong); + padding: 0.1rem 0.4rem; + border-radius: 6px; + font-size: 0.78rem; } .viewer-shell { - max-width: 1000px; + max-width: 1080px; margin: 0 auto; padding: 2rem 1.25rem 3rem; } .viewer-header { - display: flex; + display: grid; + grid-template-columns: auto 1fr auto; align-items: center; gap: 1rem; - border-radius: 20px; padding: 1rem; - margin-bottom: 1rem; -} - -.viewer-save { - margin-left: auto; + margin-bottom: 0.8rem; } .viewer-title { margin-top: 0.2rem; - color: #edf2ff; - font-size: 1.05rem; - font-weight: 700; -} - -.loading, -.no-results { - padding: 2rem; - text-align: center; - color: #8ca3c7; + font-size: 1.35rem; + font-weight: 600; + letter-spacing: -0.03em; } .memory-content { + margin-top: 0.8rem; + padding: 1.2rem; white-space: pre-wrap; - border-radius: 24px; - padding: 1.35rem; - line-height: 1.7; - color: #ecf2ff; - overflow-x: auto; + line-height: 1.65; + overflow: auto; } -.memory-editor { - width: 100%; - min-height: 70vh; - resize: vertical; - border-radius: 24px; - padding: 1.35rem; - line-height: 1.7; - color: #ecf2ff; - background: rgba(12, 19, 33, 0.72); - border: 1px solid rgba(146, 166, 206, 0.14); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.25), - inset 0 1px 0 rgba(255,255,255,0.04); - backdrop-filter: blur(18px); - outline: none; +.muted-panel { + color: var(--muted); } -.memory-editor:focus { - border-color: rgba(113, 190, 255, 0.9); - box-shadow: - 0 20px 40px rgba(0, 0, 0, 0.25), - inset 0 1px 0 rgba(255,255,255,0.04), - 0 0 0 4px rgba(98, 176, 255, 0.12); -} - -.skyline-card { - margin-bottom: 1rem; -} - -.skyline-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -.skyline-title { - margin-top: 0.35rem; - color: #d8e1f6; -} - -.skyline-plot { - position: relative; - height: 170px; - margin-top: 1.2rem; - margin-bottom: 1rem; -} - -.skyline-line { - position: absolute; - left: 0; - right: 0; - top: 50%; - border-top: 1px dashed rgba(140, 163, 199, 0.25); -} - -.star-node { - position: absolute; - transform: translate(-50%, -50%); - border-radius: 999px; - border: 1px solid rgba(255,255,255,0.18); - background: radial-gradient(circle at 35% 35%, #ffffff, rgba(126,200,255,0.95) 35%, rgba(126,200,255,0.08) 75%); - box-shadow: 0 0 18px rgba(126, 200, 255, 0.35); - transition: transform 140ms ease, box-shadow 140ms ease; -} - -.star-node:hover { - transform: translate(-50%, -50%) scale(1.08); - box-shadow: 0 0 26px rgba(172, 128, 255, 0.48); -} - -.star-tooltip { - position: absolute; - left: 50%; - bottom: calc(100% + 0.75rem); - transform: translateX(-50%); - white-space: nowrap; - font-size: 0.72rem; - color: #edf2ff; - background: rgba(7, 12, 22, 0.88); - border: 1px solid rgba(146, 166, 206, 0.18); - border-radius: 999px; - padding: 0.3rem 0.55rem; - opacity: 0; - pointer-events: none; -} - -.star-node:hover .star-tooltip { - opacity: 1; -} - -.skyline-labels { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); - gap: 0.55rem; -} - -.skyline-label { - text-align: left; - background: rgba(255,255,255,0.03); - border: 1px solid rgba(146, 166, 206, 0.12); - color: #aeb9d7; - border-radius: 14px; - padding: 0.65rem 0.75rem; -} - -.skyline-label span, -.skyline-label strong { - display: block; -} - -.skyline-label span { - font-size: 0.76rem; -} - -.skyline-label strong { - margin-top: 0.18rem; - color: #edf2ff; -} - -@media (max-width: 900px) { - .dashboard-grid { +@media (max-width: 980px) { + .hero, + .hero-grid, + .main-grid { grid-template-columns: 1fr; } - .hero { - flex-direction: column; - align-items: flex-start; - } - .hero-actions { align-items: flex-start; } - - .composer-form { - flex-direction: column; - } } -@media (max-width: 640px) { +@media (max-width: 720px) { .app, .viewer-shell { - padding-inline: 1rem; + padding-left: 0.9rem; + padding-right: 0.9rem; } - .hero h1 { - max-width: none; + .composer-form, + .viewer-header { + grid-template-columns: 1fr; + display: grid; } - .files-grid { + .files-grid, + .search-results { grid-template-columns: 1fr; } } diff --git a/src/App.jsx b/src/App.jsx index b3a8646..17ae7b7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,30 +31,34 @@ function pickSpark(content) { .filter((line) => line && !line.startsWith('#')) const candidate = lines.find((line) => line.startsWith('- ')) || lines[0] || '' - return clipText(candidate.replace(/^-\s*/, '')) + return clipText(candidate.replace(/^[-*]\s*/, '')) } -function intensityClass(wordCount) { - if (wordCount >= 1800) return 'intense' - if (wordCount >= 900) return 'active' - return 'light' -} - -function FileCard({ f, onClick }) { +function TagStrip({ tags = [] }) { + if (!tags.length) return null return ( - ) } @@ -64,6 +68,7 @@ function Viewer({ selected, onBack }) { const [draft, setDraft] = useState('') const [loading, setLoading] = useState(true) const [saveState, setSaveState] = useState('idle') + const [tags, setTags] = useState(selected.tags || []) const isMainMemory = selected.type === 'main' useEffect(() => { @@ -75,10 +80,11 @@ function Viewer({ selected, onBack }) { fetch(url) .then((r) => r.json()) - .then((d) => { - const next = d.content || '' + .then((data) => { + const next = data.content || '' setContent(next) setDraft(next) + setTags(data.tags || selected.tags || []) setLoading(false) }) .catch(() => setLoading(false)) @@ -95,6 +101,8 @@ function Viewer({ selected, onBack }) { }) if (!r.ok) throw new Error('save failed') setContent(draft) + const refreshed = await fetch(`${API}/api/main-memory`).then((res) => res.json()) + setTags(refreshed.tags || []) setSaveState('saved') setTimeout(() => setSaveState('idle'), 1800) } catch { @@ -105,19 +113,20 @@ function Viewer({ selected, onBack }) { return (
- -
-
{isMainMemory ? 'core memory editor' : 'memory viewer'}
+ +
+
{isMainMemory ? 'core memory' : 'daily note'}
{selected.title}
{isMainMemory && !loading && ( )}
+ {loading ? ( -
Loading memory fog…
+
Loading…
) : isMainMemory ? (