Add mood sensing and auto collections to memory cloud
This commit is contained in:
@@ -100,6 +100,44 @@ const SMART_TAG_RULES = [
|
||||
{ tag: 'family', patterns: [/kids/i, /family/i, /home/i] },
|
||||
]
|
||||
|
||||
const TAG_ALIASES = {
|
||||
login: 'auth',
|
||||
oauth: 'auth',
|
||||
permissions: 'auth',
|
||||
permission: 'auth',
|
||||
visual: 'design',
|
||||
theme: 'design',
|
||||
ui: 'design',
|
||||
archive: 'memory',
|
||||
memories: 'memory',
|
||||
deploy: 'infra',
|
||||
deployment: 'infra',
|
||||
cloudflare: 'infra',
|
||||
service: 'infra',
|
||||
openclaw: 'agents',
|
||||
agentboard: 'agents',
|
||||
codex: 'agents',
|
||||
child: 'kidsstories',
|
||||
stories: 'kidsstories',
|
||||
home: 'family',
|
||||
}
|
||||
|
||||
const MOOD_PROFILES = [
|
||||
{ mood: 'focused', score: 0, patterns: [/fix/i, /ship/i, /build/i, /implement/i, /polish/i, /deploy/i, /migrate/i] },
|
||||
{ mood: 'playful', score: 0, patterns: [/fun/i, /mischief/i, /awesome/i, /play/i, /spark/i] },
|
||||
{ mood: 'reflective', score: 0, patterns: [/remember/i, /reflect/i, /journal/i, /learn/i, /insight/i] },
|
||||
{ mood: 'tense', score: 0, patterns: [/blocked/i, /worry/i, /broken/i, /error/i, /issue/i, /frustrat/i] },
|
||||
{ mood: 'warm', score: 0, patterns: [/family/i, /kids/i, /home/i, /story/i, /love/i] },
|
||||
]
|
||||
|
||||
const COLLECTION_RULES = [
|
||||
{ id: 'infra-week', label: 'Infra week', tags: ['infra', 'appwrite', 'auth'] },
|
||||
{ id: 'design-streak', label: 'Design streak', tags: ['design', 'memory'] },
|
||||
{ id: 'family-notes', label: 'Family notes', tags: ['family', 'kidsstories'] },
|
||||
{ id: 'agent-ops', label: 'Agent ops', tags: ['agents', 'appwrite', 'dashboard'] },
|
||||
{ id: 'product-loop', label: 'Product loop', tags: ['product', 'design', 'appwrite'] },
|
||||
]
|
||||
|
||||
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)' },
|
||||
@@ -111,12 +149,21 @@ function isProbablyNoiseTag(value) {
|
||||
return /^\d{4}-\d{2}-\d{2}$/.test(value) || /^\d+$/.test(value)
|
||||
}
|
||||
|
||||
function normalizeTag(value) {
|
||||
const cleaned = String(value || '').toLowerCase().replace(/[^a-z0-9+-]+/g, '').trim()
|
||||
return TAG_ALIASES[cleaned] || cleaned
|
||||
}
|
||||
|
||||
function generateTags(content, filename) {
|
||||
const tags = []
|
||||
const scores = new Map()
|
||||
const addScore = (tag, amount) => scores.set(tag, (scores.get(tag) || 0) + amount)
|
||||
const addScore = (tag, amount) => {
|
||||
const normalized = normalizeTag(tag)
|
||||
if (!normalized) return
|
||||
scores.set(normalized, (scores.get(normalized) || 0) + amount)
|
||||
}
|
||||
const pushTag = (value) => {
|
||||
const cleaned = String(value || '').toLowerCase().replace(/[^a-z0-9+-]+/g, '').trim()
|
||||
const cleaned = normalizeTag(value)
|
||||
if (!cleaned || cleaned.length < 3 || TAG_STOPWORDS.has(cleaned) || tags.includes(cleaned) || isProbablyNoiseTag(cleaned)) return
|
||||
tags.push(cleaned)
|
||||
}
|
||||
@@ -170,6 +217,35 @@ function buildThemeProfile(content = '', tags = []) {
|
||||
return THEME_PROFILES.nord
|
||||
}
|
||||
|
||||
function detectMood(content = '', tags = []) {
|
||||
const haystack = `${content} ${(tags || []).join(' ')}`.toLowerCase()
|
||||
let best = { mood: 'steady', score: 0 }
|
||||
for (const profile of MOOD_PROFILES) {
|
||||
let score = 0
|
||||
for (const pattern of profile.patterns) {
|
||||
const matches = haystack.match(new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)) || []
|
||||
score += matches.length
|
||||
}
|
||||
if (score > best.score) best = { mood: profile.mood, score }
|
||||
}
|
||||
return best.mood
|
||||
}
|
||||
|
||||
function buildCollections(files = []) {
|
||||
return COLLECTION_RULES.map((rule) => {
|
||||
const matchedFiles = files.filter((file) => rule.tags.some((tag) => (file.tags || []).includes(tag)))
|
||||
if (!matchedFiles.length) return null
|
||||
return {
|
||||
id: rule.id,
|
||||
label: rule.label,
|
||||
count: matchedFiles.length,
|
||||
tags: rule.tags,
|
||||
latest: matchedFiles[0]?.dateLabel,
|
||||
filenames: matchedFiles.slice(0, 6).map((file) => file.filename),
|
||||
}
|
||||
}).filter(Boolean)
|
||||
}
|
||||
|
||||
function formatDateLabel(filename) {
|
||||
const match = filename.match(/^(\d{4})-(\d{2})-(\d{2})\.md$/)
|
||||
if (!match) return filename
|
||||
@@ -285,6 +361,7 @@ async function readMemoryDocument(filename) {
|
||||
|
||||
function buildFileMeta(doc) {
|
||||
const content = doc.content || ''
|
||||
const tags = generateTags(content, doc.filename)
|
||||
const mtimeMs = doc.$updatedAt ? new Date(doc.$updatedAt).getTime() : Date.now()
|
||||
return {
|
||||
filename: doc.filename,
|
||||
@@ -293,7 +370,8 @@ function buildFileMeta(doc) {
|
||||
wordCount: wordCount(content),
|
||||
entryCount: getEntryCount(content),
|
||||
dateLabel: formatDateLabel(doc.filename),
|
||||
tags: generateTags(content, doc.filename),
|
||||
tags,
|
||||
mood: detectMood(content, tags),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,8 +417,10 @@ app.get('/api/meta', async (req, res) => {
|
||||
}, { dailyFileCount: 0, totalDailyEntries: 0, totalDailyWords: 0 })
|
||||
|
||||
const theme = buildThemeProfile(mainDoc?.content || '', mainMemory?.tags || [])
|
||||
const collections = buildCollections(dailyFiles)
|
||||
const mood = detectMood(mainDoc?.content || '', mainMemory?.tags || [])
|
||||
|
||||
res.json({ mainMemory, dailyFiles, summary, theme })
|
||||
res.json({ mainMemory: mainMemory ? { ...mainMemory, mood } : null, dailyFiles, summary, theme, collections })
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message })
|
||||
}
|
||||
@@ -370,7 +450,8 @@ 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 || '', tags: generateTags(doc.content || '', filename) })
|
||||
const tags = generateTags(doc.content || '', filename)
|
||||
return res.json({ content: doc.content || '', tags, mood: detectMood(doc.content || '', tags) })
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error.message })
|
||||
}
|
||||
@@ -381,7 +462,8 @@ 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 || '', tags: generateTags(doc.content || '', 'MEMORY.md') })
|
||||
const tags = generateTags(doc.content || '', 'MEMORY.md')
|
||||
res.json({ content: doc.content || '', tags, mood: detectMood(doc.content || '', tags) })
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message })
|
||||
}
|
||||
@@ -455,12 +537,14 @@ app.get('/api/search', async (req, res) => {
|
||||
const windowEnd = Math.min(content.length, firstMatchIdx + 120)
|
||||
const snippet = content.slice(windowStart, windowEnd).replace(/\n/g, ' ')
|
||||
|
||||
const tags = generateTags(content, doc.filename)
|
||||
results.push({
|
||||
filename: doc.filename,
|
||||
dateLabel: formatDateLabel(doc.filename),
|
||||
entryCount: getEntryCount(content),
|
||||
wordCount: wordCount(content),
|
||||
tags: generateTags(content, doc.filename),
|
||||
tags,
|
||||
mood: detectMood(content, tags),
|
||||
matchCount: (lower.match(new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length,
|
||||
snippet: `${windowStart > 0 ? '…' : ''}${snippet}${windowEnd < content.length ? '…' : ''}`,
|
||||
matchingLines,
|
||||
|
||||
Reference in New Issue
Block a user