diff --git a/server.js b/server.js index 7f2b35b..7991401 100644 --- a/server.js +++ b/server.js @@ -84,26 +84,59 @@ function getEntryCount(content) { } 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' + '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', 'session', 'update', 'updated', 'using', 'used', 'kind', 'actually', 'awesome', 'maybe', 'seems' ]) +const SMART_TAG_RULES = [ + { tag: 'appwrite', patterns: [/appwrite/i, /tablesdb/i, /database/i, /collection/i] }, + { tag: 'agents', patterns: [/openclaw/i, /agent/i, /subagent/i, /codex/i, /claude code/i] }, + { tag: 'design', patterns: [/design/i, /ui\b/i, /visual/i, /theme/i, /scandinavian/i, /gradient/i] }, + { tag: 'memory', patterns: [/memory\.md/i, /memory app/i, /memories/i, /archive/i] }, + { tag: 'infra', patterns: [/cloudflare/i, /systemd/i, /service/i, /deploy/i, /domain/i, /proxy/i] }, + { tag: 'auth', patterns: [/auth/i, /login/i, /session cookie/i, /oauth/i, /permission/i] }, + { tag: 'product', patterns: [/idea/i, /validation/i, /launch/i, /mvp/i, /portfolio/i] }, + { tag: 'kidsstories', patterns: [/kidsstories/i, /stories/i, /child/i] }, + { tag: 'dashboard', patterns: [/dashboard/i, /command center/i, /pixel office/i] }, + { tag: 'family', patterns: [/kids/i, /family/i, /home/i] }, +] + +const THEME_PROFILES = { + nord: { name: 'Nord', accent: '#8fbcbb', accentSoft: 'rgba(143,188,187,0.18)', accentStrong: '#a3d5d3', glow: 'rgba(143,188,187,0.28)' }, + plum: { name: 'Plum', accent: '#b48ead', accentSoft: 'rgba(180,142,173,0.18)', accentStrong: '#d1a9ca', glow: 'rgba(180,142,173,0.28)' }, + amber: { name: 'Amber', accent: '#d0a86e', accentSoft: 'rgba(208,168,110,0.18)', accentStrong: '#e5bf8c', glow: 'rgba(208,168,110,0.28)' }, + ice: { name: 'Ice', accent: '#81a1c1', accentSoft: 'rgba(129,161,193,0.18)', accentStrong: '#a9c2db', glow: 'rgba(129,161,193,0.28)' }, +} + +function isProbablyNoiseTag(value) { + return /^\d{4}-\d{2}-\d{2}$/.test(value) || /^\d+$/.test(value) +} + function generateTags(content, filename) { const tags = [] + const scores = new Map() + const addScore = (tag, amount) => scores.set(tag, (scores.get(tag) || 0) + amount) 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 + if (!cleaned || cleaned.length < 3 || TAG_STOPWORDS.has(cleaned) || tags.includes(cleaned) || isProbablyNoiseTag(cleaned)) return tags.push(cleaned) } - if (filename === 'MEMORY.md') pushTag('core') + if (filename === 'MEMORY.md') addScore('core', 5) + + for (const rule of SMART_TAG_RULES) { + for (const pattern of rule.patterns) { + const matches = content.match(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)) || [] + if (matches.length) addScore(rule.tag, matches.length * 3) + } + } for (const line of content.split('\n')) { const trimmed = line.trim() if (trimmed.startsWith('#')) { - trimmed.replace(/^#+\s*/, '').split(/[^a-zA-Z0-9+-]+/).forEach(pushTag) + trimmed.replace(/^#+\s*/, '').split(/[^a-zA-Z0-9+-]+/).forEach((token) => addScore(token.toLowerCase(), 2)) } const hashtagMatches = trimmed.match(/#[a-zA-Z0-9+-]+/g) || [] - hashtagMatches.forEach((tag) => pushTag(tag.slice(1))) + hashtagMatches.forEach((tag) => addScore(tag.slice(1).toLowerCase(), 4)) } const frequency = new Map() @@ -113,16 +146,28 @@ function generateTags(content, filename) { .replace(/[^a-z0-9+\-\s]/g, ' ') for (const token of normalized.split(/\s+/)) { - if (!token || token.length < 4 || TAG_STOPWORDS.has(token)) continue + if (!token || token.length < 4 || TAG_STOPWORDS.has(token) || isProbablyNoiseTag(token)) continue frequency.set(token, (frequency.get(token) || 0) + 1) } - Array.from(frequency.entries()) + for (const [token, count] of frequency.entries()) { + if (count >= 2) addScore(token, count) + } + + Array.from(scores.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) + return tags.slice(0, 5) +} + +function buildThemeProfile(content = '', tags = []) { + const haystack = `${content} ${(tags || []).join(' ')}`.toLowerCase() + if (/design|theme|visual|scandinavian|style/.test(haystack)) return THEME_PROFILES.plum + if (/infra|deploy|cloudflare|service|proxy|appwrite/.test(haystack)) return THEME_PROFILES.ice + if (/kidsstories|family|home|story/.test(haystack)) return THEME_PROFILES.amber + return THEME_PROFILES.nord } function formatDateLabel(filename) { @@ -293,7 +338,9 @@ app.get('/api/meta', async (req, res) => { return acc }, { dailyFileCount: 0, totalDailyEntries: 0, totalDailyWords: 0 }) - res.json({ mainMemory, dailyFiles, summary }) + const theme = buildThemeProfile(mainDoc?.content || '', mainMemory?.tags || []) + + res.json({ mainMemory, dailyFiles, summary, theme }) } catch (error) { res.status(500).json({ error: error.message }) } diff --git a/src/App.css b/src/App.css index bc701d9..8309620 100644 --- a/src/App.css +++ b/src/App.css @@ -6,39 +6,48 @@ background: #101214; color: #f3f4f6; --bg: #101214; + --bg-2: #15181c; --panel: #171a1d; --panel-2: #1c2024; --border: #2a2f35; --border-strong: #3a4149; --text: #f3f4f6; - --muted: #9ca3af; - --muted-2: #7b828c; - --accent: #d8dde3; + --muted: #a5adb7; + --muted-2: #7f8791; + --accent: #8fbcbb; + --accent-soft: rgba(143,188,187,0.18); + --accent-strong: #a3d5d3; + --accent-glow: rgba(143,188,187,0.28); + --pointer-x: 18%; + --pointer-y: 12%; } -html, body, #root { - min-height: 100%; -} - -body { - background: var(--bg); - color: var(--text); -} - -button, input, textarea { - font: inherit; -} - -button { - border: 0; -} +html, body, #root { min-height: 100%; } +body { background: var(--bg); color: var(--text); } +button, input, textarea { font: inherit; } +button { border: 0; } .app-shell { + position: relative; min-height: 100vh; - background: var(--bg); + background: + radial-gradient(circle at var(--pointer-x) var(--pointer-y), var(--accent-soft), transparent 26%), + linear-gradient(180deg, #101214 0%, #111419 100%); + overflow: hidden; +} + +.dynamic-wash { + position: absolute; + inset: 0; + pointer-events: none; + background: + radial-gradient(circle at calc(var(--pointer-x) + 10%) calc(var(--pointer-y) + 8%), var(--accent-glow), transparent 16%), + linear-gradient(135deg, transparent 0%, rgba(255,255,255,0.01) 100%); + opacity: 0.9; } .app { + position: relative; max-width: 1180px; margin: 0 auto; padding: 2rem 1.25rem 4rem; @@ -68,15 +77,15 @@ button { .hero h1 { margin-top: 0.35rem; - font-size: clamp(2.2rem, 5vw, 4.4rem); - font-weight: 600; - letter-spacing: -0.06em; + font-size: clamp(2.4rem, 5vw, 4.8rem); + font-weight: 620; + letter-spacing: -0.07em; } .subtitle { - margin-top: 0.7rem; - max-width: 38rem; - line-height: 1.5; + margin-top: 0.75rem; + max-width: 42rem; + line-height: 1.55; color: var(--muted); } @@ -93,21 +102,9 @@ button { .memory-content, .memory-editor, .viewer-header { - background: var(--panel); + background: linear-gradient(180deg, rgba(23,26,29,0.96), rgba(20,23,27,0.96)); border: 1px solid var(--border); -} - -.panel, -.file-card, -.result-item, -.memory-content, -.memory-editor, -.viewer-header, -.search-input, -.ghost-btn, -.primary-btn, -.tag-pill { - border-radius: 10px; + border-radius: 12px; } .panel, @@ -116,44 +113,54 @@ button { .viewer-header, .memory-content, .memory-editor { - box-shadow: none; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); +} + +.interactive-card, +.ghost-btn, +.primary-btn, +.tag-pill, +.rail-row { + transition: transform 160ms ease, border-color 160ms ease, background 160ms ease, box-shadow 160ms ease; +} + +.interactive-card:hover, +.ghost-btn:hover, +.primary-btn:hover, +.rail-row:hover { + transform: translateY(-2px); + border-color: color-mix(in srgb, var(--accent) 44%, var(--border)); + box-shadow: 0 16px 34px rgba(0,0,0,0.2); } .hero-grid { - grid-template-columns: 1.35fr 0.9fr 1fr; + grid-template-columns: 1.25fr 0.92fr 0.95fr; margin-bottom: 1rem; } -.panel { - padding: 1rem; -} +.panel { padding: 1rem; } +.accent-panel { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); } .lead-copy { margin-top: 0.75rem; - font-size: 1.05rem; - line-height: 1.6; + font-size: 1.08rem; + line-height: 1.65; } .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; -} +.result-meta, +.theme-copy { color: var(--muted-2); font-size: 0.82rem; } +.lead-meta { margin-top: 0.65rem; } +.theme-title { margin-top: 0.35rem; font-size: 1.2rem; font-weight: 600; letter-spacing: -0.03em; } +.theme-copy { margin-top: 0.55rem; line-height: 1.55; } +.stats-panel { display: grid; gap: 0.65rem; } .stat-row, -.rail-row, .result-head, -.file-card-top { +.file-card-top, +.section-heading, +.compact-heading { display: flex; align-items: center; justify-content: space-between; @@ -162,127 +169,85 @@ button { .stat-row { padding-bottom: 0.65rem; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid rgba(255,255,255,0.05); } - -.stat-row:last-child { - padding-bottom: 0; - border-bottom: 0; -} - +.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); -} - +.slash-hint, +.recap-line, +.rail-metrics span { 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; -} +.rail-metrics strong { font-size: 1.15rem; font-weight: 620; } +.cluster-panel, +.search-block, +.main-grid { margin-top: 1rem; } +.main-grid { grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr); } .section-heading h2, .compact-heading h2 { margin-top: 0.2rem; font-size: 1.1rem; - font-weight: 600; + font-weight: 620; letter-spacing: -0.02em; } -.rail-list { - margin-top: 1rem; - display: grid; - gap: 0.6rem; -} - +.rail-list { margin-top: 1rem; display: grid; gap: 0.7rem; } .rail-row { width: 100%; text-align: left; - background: var(--panel-2); + background: rgba(28,32,36,0.92); border: 1px solid var(--border); - border-radius: 10px; + border-radius: 11px; padding: 0.85rem 0.9rem; cursor: pointer; } - +.rail-copy { min-width: 0; } .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.75rem; -} - -.search-block { - margin-top: 1rem; -} - -.search-section { - position: relative; - margin: 1rem 0; +.result-name { font-weight: 620; letter-spacing: -0.01em; } +.rail-metrics { display: grid; text-align: right; min-width: 8rem; } +.rail-bar { + width: 100%; + height: 4px; + margin-top: 0.45rem; + background: rgba(255,255,255,0.06); + border-radius: 999px; + overflow: hidden; +} +.rail-bar span { + display: block; + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, var(--accent), var(--accent-strong)); } +.composer-form { margin-top: 1rem; display: flex; gap: 0.75rem; } +.search-section { position: relative; margin: 1rem 0; } .search-input, .memory-editor { width: 100%; - background: #121518; + background: rgba(18,21,24,0.98); color: var(--text); border: 1px solid var(--border-strong); + border-radius: 12px; outline: none; - padding: 0.9rem 1rem; - transition: border-color 140ms ease, background 140ms ease; + padding: 0.92rem 1rem; } - .memory-editor { min-height: 68vh; resize: vertical; - line-height: 1.6; + line-height: 1.65; } - .search-input:focus, .memory-editor:focus { - border-color: #626973; - background: #101316; + border-color: color-mix(in srgb, var(--accent) 70%, var(--border-strong)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-soft) 90%, transparent); } - .search-input::placeholder, -.memory-editor::placeholder { - color: var(--muted-2); -} - +.memory-editor::placeholder { color: var(--muted-2); } .search-spinner { position: absolute; right: 1rem; @@ -295,50 +260,19 @@ button { .search-results { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 0.75rem; -} - -.file-card, -.result-item, -.ghost-btn, -.primary-btn { - cursor: pointer; + gap: 0.8rem; } .file-card, .result-item { padding: 1rem; text-align: left; - transition: border-color 140ms ease, transform 140ms ease; + cursor: pointer; } - -.file-card:hover, -.result-item:hover, -.ghost-btn:hover, -.primary-btn:hover, -.back-btn:hover, -.rail-row:hover { - transform: translateY(-1px); - border-color: #505862; -} - -.file-date { - color: var(--muted-2); - font-size: 0.82rem; -} - +.file-date { color: var(--accent-strong); font-size: 0.82rem; } .file-snippet, -.result-snippet { - margin-top: 0.55rem; - line-height: 1.55; -} - -.file-stats { - margin-top: 0.75rem; - display: flex; - gap: 0.6rem; - font-size: 0.82rem; -} +.result-snippet { margin-top: 0.55rem; line-height: 1.6; } +.file-stats { margin-top: 0.75rem; display: flex; gap: 0.6rem; font-size: 0.82rem; } .tag-strip { margin-top: 0.8rem; @@ -350,43 +284,46 @@ button { .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; + min-height: 1.85rem; + padding: 0.24rem 0.58rem; + background: rgba(255,255,255,0.02); + border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border)); + border-radius: 999px; + color: var(--accent-strong); font-size: 0.75rem; text-transform: lowercase; + cursor: pointer; +} +.tag-pill.active, +.tag-pill:hover { + background: var(--accent-soft); + border-color: color-mix(in srgb, var(--accent) 70%, var(--border)); } .ghost-btn, .primary-btn, -.back-btn { - padding: 0.8rem 1rem; +.back-btn, +.small-btn { + padding: 0.82rem 1rem; + border-radius: 12px; } - .ghost-btn, .back-btn { - background: transparent; + background: rgba(255,255,255,0.02); 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; + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + color: #0d1116; + border: 1px solid transparent; + font-weight: 620; } +.primary-btn:disabled { opacity: 0.55; cursor: default; transform: none; } +.small-btn { padding: 0.56rem 0.82rem; } kbd { - background: #15191d; + background: rgba(255,255,255,0.03); border: 1px solid var(--border-strong); padding: 0.1rem 0.4rem; border-radius: 6px; @@ -398,7 +335,6 @@ kbd { margin: 0 auto; padding: 2rem 1.25rem 3rem; } - .viewer-header { display: grid; grid-template-columns: auto 1fr auto; @@ -407,53 +343,34 @@ kbd { padding: 1rem; margin-bottom: 0.8rem; } - .viewer-title { margin-top: 0.2rem; font-size: 1.35rem; - font-weight: 600; + font-weight: 620; letter-spacing: -0.03em; } - .memory-content { margin-top: 0.8rem; padding: 1.2rem; white-space: pre-wrap; - line-height: 1.65; + line-height: 1.68; overflow: auto; } - -.muted-panel { - color: var(--muted); -} +.muted-panel { color: var(--muted); } @media (max-width: 980px) { .hero, .hero-grid, - .main-grid { - grid-template-columns: 1fr; - } - - .hero-actions { - align-items: flex-start; - } + .main-grid { grid-template-columns: 1fr; } + .hero-actions { align-items: flex-start; } } @media (max-width: 720px) { .app, - .viewer-shell { - padding-left: 0.9rem; - padding-right: 0.9rem; - } - + .viewer-shell { padding-left: 0.9rem; padding-right: 0.9rem; } .composer-form, - .viewer-header { - grid-template-columns: 1fr; - display: grid; - } - + .viewer-header { display: grid; grid-template-columns: 1fr; } .files-grid, - .search-results { - grid-template-columns: 1fr; - } + .search-results { grid-template-columns: 1fr; } + .rail-row { grid-template-columns: 1fr; } } diff --git a/src/App.jsx b/src/App.jsx index 17ae7b7..aac9502 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -34,27 +34,34 @@ function pickSpark(content) { return clipText(candidate.replace(/^[-*]\s*/, '')) } -function TagStrip({ tags = [] }) { +function TagStrip({ tags = [], onPick, activeTag }) { if (!tags.length) return null return (