added optional local web dashboard for memory, with seeing/editing memory, daily facts (syncs with discord) and each user
This commit is contained in:
68
src/bot.js
68
src/bot.js
@@ -1,10 +1,12 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config({ path: "../.env" });
|
||||
import { Client, GatewayIntentBits, Partials, ChannelType } from 'discord.js';
|
||||
import { Client, GatewayIntentBits, Partials, ChannelType, ActivityType } from 'discord.js';
|
||||
import { config } from './config.js';
|
||||
import { chatCompletion } from './openai.js';
|
||||
import { appendShortTerm, prepareContext, recordInteraction } from './memory.js';
|
||||
import { searchWeb, appendSearchLog, detectFilteredPhrase } from './search.js';
|
||||
import { getDailyMood, setMoodByName, getDailyThought, generateDailyThought } from './mood.js';
|
||||
import { startDashboard } from './dashboard.js';
|
||||
const client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
@@ -19,8 +21,6 @@ let coderPingTimer;
|
||||
const continuationState = new Map();
|
||||
let isSleeping = false;
|
||||
|
||||
// mood override for testing
|
||||
let overrideMood = null;
|
||||
|
||||
function enterSleepMode() {
|
||||
if (isSleeping) return;
|
||||
@@ -120,12 +120,35 @@ function stopContinuationForUser(userId) {
|
||||
continuationState.set(userId, state);
|
||||
}
|
||||
|
||||
client.once('clientReady', () => {
|
||||
if (config.dashboardEnabled) {
|
||||
startDashboard();
|
||||
}
|
||||
|
||||
async function onReady() {
|
||||
console.log(`[bot] Logged in as ${client.user.tag}`);
|
||||
scheduleCoderPing();
|
||||
const m = getDailyMood();
|
||||
console.log(`[bot] current mood on startup: ${m.name} — ${m.description}`);
|
||||
});
|
||||
|
||||
try {
|
||||
await generateDailyThought();
|
||||
const thought = await getDailyThought();
|
||||
if (thought && client.user) {
|
||||
console.log(`[bot] setting presence with thought: "${thought}"`);
|
||||
await client.user.setPresence({
|
||||
status: 'online',
|
||||
activities: [{ name: thought, type: ActivityType.Playing }]
|
||||
});
|
||||
console.log('[bot] presence set successfully');
|
||||
} else {
|
||||
console.warn('[bot] no thought or client user available');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[bot] failed to set presence:', err);
|
||||
}
|
||||
}
|
||||
|
||||
client.once('ready', onReady);
|
||||
|
||||
function shouldRespond(message) {
|
||||
if (message.author.bot) return false;
|
||||
@@ -160,26 +183,6 @@ const toneHints = [
|
||||
{ label: 'excited', regex: /(excited|hyped|omg|yay|stoked)/i },
|
||||
];
|
||||
|
||||
const dailyMoods = [
|
||||
['Calm','Soft tone; minimal emojis; low sarcasm; concise and soothing.'],
|
||||
['Goblin','Chaotic, high‑energy replies; random emojis; extra sarcasm and hype.'],
|
||||
['Philosopher','Deep, reflective answers; longer and thoughtful, a bit poetic.'],
|
||||
['Hype','Enthusiastic and upbeat; lots of exclamation marks, emojis and hype.'],
|
||||
['Sassy','Playful sarcasm without being mean; snappy replies and quips.'],
|
||||
].map(([n,d])=>({name:n,description:d}));
|
||||
|
||||
function getDailyMood() {
|
||||
if (overrideMood) return overrideMood;
|
||||
const day = Math.floor(Date.now() / 86400000);
|
||||
return dailyMoods[day % dailyMoods.length];
|
||||
}
|
||||
|
||||
function setMoodByName(name) {
|
||||
if (!name) return null;
|
||||
const found = dailyMoods.find((m) => m.name.toLowerCase() === name.toLowerCase());
|
||||
if (found) overrideMood = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
|
||||
function detectTone(text) {
|
||||
@@ -557,8 +560,15 @@ client.on('messageCreate', async (message) => {
|
||||
});
|
||||
|
||||
if (!config.discordToken) {
|
||||
console.error('Missing DISCORD_TOKEN. Check your .env file.');
|
||||
process.exit(1);
|
||||
if (config.dashboardEnabled) {
|
||||
console.warn('[bot] DISCORD_TOKEN not set; running in dashboard-only mode.');
|
||||
} else {
|
||||
console.error('Missing DISCORD_TOKEN. Check your .env file.');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
client.login(config.discordToken).catch((err) => {
|
||||
console.error('[bot] login failed:', err);
|
||||
if (!config.dashboardEnabled) process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
client.login(config.discordToken);
|
||||
|
||||
@@ -36,6 +36,10 @@ export const config = {
|
||||
memoryPruneThreshold: 0.2,
|
||||
maxMemories: 8000,
|
||||
relevantMemoryCount: 5,
|
||||
// Optional local dashboard that runs alongside the bot. Enable with
|
||||
// `ENABLE_DASHBOARD=true` and customize port with `DASHBOARD_PORT`.
|
||||
dashboardEnabled: process.env.ENABLE_DASHBOARD === 'true',
|
||||
dashboardPort: process.env.DASHBOARD_PORT ? parseInt(process.env.DASHBOARD_PORT, 10) : 3000,
|
||||
// Proactive continuation settings: when a user stops replying, Nova can continue
|
||||
// the conversation every `continuationIntervalMs` milliseconds until the user
|
||||
// signals to stop or the `continuationMaxProactive` limit is reached.
|
||||
|
||||
123
src/dashboard.js
Normal file
123
src/dashboard.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { config } from './config.js';
|
||||
import {
|
||||
listUsers,
|
||||
getAllShortTerm,
|
||||
getLongTermMemories,
|
||||
deleteLongTerm,
|
||||
findSimilar,
|
||||
} from './memory.js';
|
||||
import { getDailyMood, getDailyThought, setDailyThought } from './mood.js';
|
||||
|
||||
export function startDashboard() {
|
||||
if (!config.dashboardEnabled) return;
|
||||
|
||||
const app = express();
|
||||
app.use((req, res, next) => {
|
||||
console.log(`[dashboard] ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
app.use(express.json());
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const publicDir = path.join(__dirname, './public');
|
||||
console.log('[dashboard] static directory:', publicDir);
|
||||
const indexPath = path.join(publicDir, 'index.html');
|
||||
console.log('[dashboard] index file path:', indexPath);
|
||||
import('fs')
|
||||
.then((fs) => fs.promises.stat(indexPath))
|
||||
.then(() => console.log('[dashboard] index.html is present'))
|
||||
.catch((e) => console.warn('[dashboard] index.html missing or inaccessible', e.message));
|
||||
app.use(express.static(publicDir));
|
||||
|
||||
app.get('/api/users', async (req, res) => {
|
||||
console.log('[dashboard] GET /api/users');
|
||||
try {
|
||||
const users = await listUsers();
|
||||
console.log('[dashboard] returning', users.length, 'users');
|
||||
res.json(users);
|
||||
} catch (err) {
|
||||
console.error('[dashboard] failed to list users', err);
|
||||
res.status(500).json({ error: 'internal' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/users/:id/short', async (req, res) => {
|
||||
console.log('[dashboard] GET /api/users/' + req.params.id + '/short');
|
||||
try {
|
||||
const rows = await getAllShortTerm(req.params.id);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error('[dashboard] fetch short-term failed', err);
|
||||
res.status(500).json({ error: 'internal' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/users/:id/long', async (req, res) => {
|
||||
console.log('[dashboard] GET /api/users/' + req.params.id + '/long');
|
||||
try {
|
||||
const rows = await getLongTermMemories(req.params.id);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error('[dashboard] fetch long-term failed', err);
|
||||
res.status(500).json({ error: 'internal' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/users/:id/long/:memId', async (req, res) => {
|
||||
console.log('[dashboard] DELETE /api/users/' + req.params.id + '/long/' + req.params.memId);
|
||||
try {
|
||||
await deleteLongTerm(req.params.id, req.params.memId);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error('[dashboard] delete memory failed', err);
|
||||
res.status(500).json({ error: 'internal' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/users/:id/search', async (req, res) => {
|
||||
console.log('[dashboard] POST /api/users/' + req.params.id + '/search', req.body);
|
||||
try {
|
||||
const { query } = req.body;
|
||||
const results = await findSimilar(req.params.id, query);
|
||||
res.json(results);
|
||||
} catch (err) {
|
||||
console.error('[dashboard] similarity search failed', err);
|
||||
res.status(500).json({ error: 'internal' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/mood', async (req, res) => {
|
||||
console.log('[dashboard] GET /api/mood');
|
||||
try {
|
||||
const thought = await getDailyThought();
|
||||
res.json({ mood: getDailyMood(), thought });
|
||||
} catch (err) {
|
||||
console.error('[dashboard] failed to get mood', err);
|
||||
res.status(500).json({ error: 'internal' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/mood/thought', async (req, res) => {
|
||||
console.log('[dashboard] POST /api/mood/thought', req.body);
|
||||
try {
|
||||
const { thought } = req.body;
|
||||
if (!thought || typeof thought !== 'string') {
|
||||
return res.status(400).json({ error: 'thought must be a string' });
|
||||
}
|
||||
await setDailyThought(thought);
|
||||
const updatedThought = await getDailyThought();
|
||||
res.json({ ok: true, thought: updatedThought });
|
||||
} catch (err) {
|
||||
console.error('[dashboard] failed to set thought', err);
|
||||
res.status(500).json({ error: 'internal' });
|
||||
}
|
||||
});
|
||||
|
||||
const port = config.dashboardPort || 3000;
|
||||
app.listen(port, () => {
|
||||
console.log(`[dashboard] listening on http://localhost:${port}`);
|
||||
});
|
||||
}
|
||||
@@ -113,6 +113,11 @@ const createSchema = (db) => {
|
||||
timestamp INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS daily_thoughts (
|
||||
date TEXT PRIMARY KEY,
|
||||
thought TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
};
|
||||
|
||||
@@ -301,8 +306,7 @@ const retrieveRelevantMemories = async (db, userId, query) => {
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, config.relevantMemoryCount)
|
||||
.map(({ score, ...rest }) => rest);
|
||||
.slice(0, config.relevantMemoryCount);
|
||||
};
|
||||
|
||||
export async function appendShortTerm(userId, role, content) {
|
||||
@@ -357,4 +361,55 @@ export async function pruneLowImportanceMemories(userId) {
|
||||
ensureUser(db, userId);
|
||||
run(db, 'DELETE FROM long_term WHERE user_id = ? AND importance < ?', [userId, config.memoryPruneThreshold]);
|
||||
await persistDb(db);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Dashboard helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function listUsers() {
|
||||
const db = await loadDatabase();
|
||||
return all(db, 'SELECT id, summary, last_updated FROM users');
|
||||
}
|
||||
|
||||
export async function getAllShortTerm(userId) {
|
||||
const db = await loadDatabase();
|
||||
return fullShortTerm(db, userId);
|
||||
}
|
||||
|
||||
export async function getLongTermMemories(userId) {
|
||||
const db = await loadDatabase();
|
||||
return all(
|
||||
db,
|
||||
'SELECT id, content, importance, timestamp FROM long_term WHERE user_id = ? ORDER BY timestamp DESC',
|
||||
[userId],
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteLongTerm(userId, entryId) {
|
||||
const db = await loadDatabase();
|
||||
run(db, 'DELETE FROM long_term WHERE user_id = ? AND id = ?', [userId, entryId]);
|
||||
await persistDb(db);
|
||||
}
|
||||
|
||||
export async function findSimilar(userId, query) {
|
||||
const db = await loadDatabase();
|
||||
return retrieveRelevantMemories(db, userId, query);
|
||||
}
|
||||
|
||||
// Daily thought storage
|
||||
export async function getDailyThoughtFromDb(date) {
|
||||
const db = await loadDatabase();
|
||||
const row = get(db, 'SELECT thought FROM daily_thoughts WHERE date = ?', [date]);
|
||||
return row?.thought || null;
|
||||
}
|
||||
|
||||
export async function saveDailyThought(date, thought) {
|
||||
const db = await loadDatabase();
|
||||
run(db, 'INSERT OR REPLACE INTO daily_thoughts (date, thought, created_at) VALUES (?, ?, ?)', [
|
||||
date,
|
||||
thought,
|
||||
Date.now(),
|
||||
]);
|
||||
await persistDb(db);
|
||||
}
|
||||
|
||||
95
src/mood.js
Normal file
95
src/mood.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { chatCompletion } from './openai.js';
|
||||
import { getDailyThoughtFromDb, saveDailyThought } from './memory.js';
|
||||
|
||||
const dailyMoods = [
|
||||
['Calm','Soft tone; minimal emojis; low sarcasm; concise and soothing.'],
|
||||
['Goblin','Chaotic, high‑energy replies; random emojis; extra sarcasm and hype.'],
|
||||
['Philosopher','Deep, reflective answers; longer and thoughtful, a bit poetic.'],
|
||||
['Hype','Enthusiastic and upbeat; lots of exclamation marks, emojis and hype.'],
|
||||
['Sassy','Playful sarcasm without being mean; snappy replies and quips.'],
|
||||
].map(([n,d])=>({name:n,description:d}));
|
||||
|
||||
let overrideMood = null;
|
||||
let currentDailyMood = null;
|
||||
|
||||
/**
|
||||
* Get today's date as YYYY-MM-DD for comparison
|
||||
*/
|
||||
function getTodayDate() {
|
||||
const d = new Date();
|
||||
return d.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
function pickMood(){
|
||||
return dailyMoods[Math.floor(Math.random() * dailyMoods.length)];
|
||||
}
|
||||
|
||||
function getDailyMood() {
|
||||
if (overrideMood) return overrideMood;
|
||||
if (!currentDailyMood) currentDailyMood = pickMood();
|
||||
return currentDailyMood;
|
||||
}
|
||||
|
||||
function setMoodByName(name) {
|
||||
if (!name) return null;
|
||||
const found = dailyMoods.find((m) => m.name.toLowerCase() === name.toLowerCase());
|
||||
if (found) overrideMood = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
async function getDailyThought() {
|
||||
const today = getTodayDate();
|
||||
return await getDailyThoughtFromDb(today);
|
||||
}
|
||||
|
||||
async function setDailyThought(thought) {
|
||||
const today = getTodayDate();
|
||||
await saveDailyThought(today, thought);
|
||||
}
|
||||
|
||||
async function generateDailyThought() {
|
||||
const today = getTodayDate();
|
||||
|
||||
// Check if we already have a thought for today in the DB
|
||||
const existingThought = await getDailyThoughtFromDb(today);
|
||||
if (existingThought) {
|
||||
console.log('[mood] using existing thought for today:', existingThought);
|
||||
return existingThought;
|
||||
}
|
||||
|
||||
let newThought = null;
|
||||
try {
|
||||
const prompt =
|
||||
'Write a short (one sentence, <20 words, exactly 120 characters max) quirky Discord "nova status" that a friendly bot might use today.';
|
||||
const messages = [
|
||||
{ role: 'system', content: 'You are Nova, a playful Discord AI companion.' },
|
||||
{ role: 'user', content: prompt },
|
||||
];
|
||||
const resp = await chatCompletion(messages, { temperature: 0.8, maxTokens: 40 });
|
||||
newThought = (resp && resp.trim()) || '';
|
||||
|
||||
// Truncate to 120 characters if it exceeds
|
||||
if (newThought.length > 120) {
|
||||
newThought = newThought.substring(0, 117) + '...';
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[mood] failed to generate daily thought:', err);
|
||||
}
|
||||
|
||||
if (!newThought) {
|
||||
const fallbacks = [
|
||||
'Vibing in the server like a code ghost.',
|
||||
'I swear I understand humans… probably.',
|
||||
'Got an error? I am the error.',
|
||||
'Just refreshed my cache and I feel alive.',
|
||||
];
|
||||
newThought = fallbacks[Math.floor(Math.random() * fallbacks.length)];
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await saveDailyThought(today, newThought);
|
||||
console.log('[mood] generated and saved new thought for today:', newThought);
|
||||
return newThought;
|
||||
}
|
||||
|
||||
export { getDailyMood, setMoodByName, getDailyThought, setDailyThought, generateDailyThought, dailyMoods };
|
||||
@@ -58,7 +58,6 @@ async function postJson(path, body) {
|
||||
}
|
||||
return res.json();
|
||||
} catch (err) {
|
||||
// normalize AbortError into a retryable code
|
||||
if (err.name === 'AbortError' || err.message?.includes('timed out')) {
|
||||
const e = new Error(`Connect Timeout Error after ${timeout}ms`);
|
||||
e.code = 'UND_ERR_CONNECT_TIMEOUT';
|
||||
@@ -85,7 +84,6 @@ export async function chatCompletion(messages, options = {}) {
|
||||
};
|
||||
|
||||
const data = await withRetry(() => postJson('/chat/completions', payload));
|
||||
// OpenRouter uses OpenAI-compatible response shape
|
||||
const text = data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || '';
|
||||
return (text && String(text).trim()) || '';
|
||||
}
|
||||
|
||||
172
src/public/index.html
Normal file
172
src/public/index.html
Normal file
@@ -0,0 +1,172 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nova Memory Dashboard</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box;}
|
||||
body{
|
||||
font-family:system-ui, sans-serif;
|
||||
background:#121212;
|
||||
color:#e0e0e0;
|
||||
min-height:100vh;
|
||||
padding:1rem;
|
||||
}
|
||||
.container{max-width:900px;margin:0 auto;}
|
||||
h1{font-size:2rem;font-weight:700;color:#fff;margin-bottom:1rem;text-align:center;}
|
||||
.mood-card,.section-card{background:#1e1e1e;border-radius:8px;padding:1.5rem;margin:1rem 0;box-shadow:0 4px 12px rgba(0,0,0,0.6);}
|
||||
h2{font-size:1.5rem;color:#fff;border-bottom:2px solid #333;padding-bottom:0.5rem;margin-bottom:1rem;}
|
||||
h3{font-size:1.2rem;color:#ddd;margin-top:1rem;margin-bottom:0.75rem;}
|
||||
.user-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:0.75rem;}
|
||||
.user-card{background:#292929;padding:1rem;border-radius:6px;cursor:pointer;transition:transform .2s,box-shadow .2s;color:#fff;}
|
||||
.user-card:hover{transform:translateY(-3px);box-shadow:0 6px 20px rgba(0,0,0,0.7);}
|
||||
.user-id{font-weight:600;font-size:1.1rem;margin-bottom:0.4rem;}
|
||||
.user-summary{font-size:0.9rem;opacity:0.85;line-height:1.3;}
|
||||
table{width:100%;border-collapse:collapse;margin-bottom:1rem;font-size:0.9rem;}
|
||||
th{background:#333;color:#fff;padding:.75rem;text-align:left;}
|
||||
td{padding:.75rem;border-bottom:1px solid #2a2a2a;}
|
||||
tr:nth-child(even){background:#1e1e1e;}
|
||||
.empty{color:#777;font-style:italic;text-align:center;padding:1rem;}
|
||||
button{background:#444;color:#fff;border:none;padding:.5rem 1rem;border-radius:4px;cursor:pointer;transition:background .2s;}
|
||||
button:hover{background:#555;}
|
||||
.delete-btn{background:#880000;}
|
||||
.delete-btn:hover{background:#aa0000;}
|
||||
input[type=text]{width:100%;padding:.6rem;border:1px solid #333;border-radius:4px;background:#212121;color:#e0e0e0;}
|
||||
.search-box{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0;}
|
||||
.search-results{background:#1e1e1e;border-left:4px solid #555;padding:1rem;border-radius:4px;margin-top:1rem;}
|
||||
.search-results ul{list-style:none;}
|
||||
.search-results li{padding:.6rem;margin-bottom:.4rem;background:#292929;border-radius:4px;}
|
||||
.score-badge{display:inline-block;background:#555;color:#fff;padding:.2rem .4rem;border-radius:3px;font-size:.8rem;margin-right:.4rem;}
|
||||
.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);display:none;align-items:center;justify-content:center;}
|
||||
.modal{background:#1e1e1e;padding:2rem;border-radius:8px;max-height:90vh;overflow:auto;width:90%;max-width:800px;position:relative;}
|
||||
.modal-close{position:absolute;top:0.5rem;right:0.75rem;font-size:1.5rem;color:#bbb;cursor:pointer;}
|
||||
.loading{color:#999;text-align:center;font-weight:600;padding:1rem;}
|
||||
.error{background:#550000;color:#fff;padding:1rem;border-radius:4px;}
|
||||
@media(max-width:600px){.user-list{grid-template-columns:1fr;}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧠 Nova Memory Dashboard</h1>
|
||||
<div id="mood"></div>
|
||||
<div class="section-card">
|
||||
<h2>Users</h2>
|
||||
<div id="user-list" class="user-list">loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<span class="modal-close" onclick="closeModal()">×</span>
|
||||
<div id="detail"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
async function fetchJson(path, opts){const res=await fetch(path,opts);if(!res.ok)throw new Error(res.statusText);return res.json();}
|
||||
function formatDate(ts){return new Date(ts).toLocaleString();}
|
||||
function closeModal(){document.getElementById('modal').style.display='none';}
|
||||
|
||||
async function loadMood(){
|
||||
try{
|
||||
const data=await fetchJson('/api/mood');
|
||||
const moodCard=document.createElement('div');
|
||||
moodCard.className='mood-card';
|
||||
moodCard.innerHTML='<strong>'+data.mood.name+'</strong><p>'+data.mood.description+'</p><p>'+data.thought+'</p>';
|
||||
const old=document.getElementById('mood');
|
||||
old.replaceWith(moodCard);
|
||||
moodCard.id='mood';
|
||||
}catch(e){console.error(e);}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
console.log('client: fetching users');
|
||||
try {
|
||||
const users = await fetchJson('/api/users');
|
||||
console.log('client: got users', users);
|
||||
const container = document.getElementById('user-list');
|
||||
if (!users.length) {
|
||||
container.innerHTML = '<div class="empty">No users yet</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = users.map(u =>
|
||||
'<div class="user-card" onclick="showUser(\'' + u.id + '\')">' +
|
||||
'<div class="user-id">👤 ' + u.id + '</div>' +
|
||||
'<div class="user-summary">' + (u.summary || 'No summary yet') + '</div>' +
|
||||
'</div>'
|
||||
).join('');
|
||||
} catch (e) {
|
||||
console.error('client: loadUsers error', e);
|
||||
document.getElementById('user-list').innerHTML = '<div class="error">Error loading users: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function showUser(id){
|
||||
console.log('client: showUser',id);
|
||||
const detail=document.getElementById('detail');
|
||||
detail.innerHTML='<div class="loading">Loading…</div>';
|
||||
document.getElementById('modal').style.display='flex';
|
||||
try{
|
||||
const [shortR,longR]=await Promise.all([
|
||||
fetchJson('/api/users/'+id+'/short'),
|
||||
fetchJson('/api/users/'+id+'/long')
|
||||
]);
|
||||
let html='<h2>Memory for '+id+'</h2>';
|
||||
html+='<h3>Short-term</h3>';
|
||||
if(shortR.length){
|
||||
html+='<table><tr><th>Role</th><th>Content</th><th>When</th></tr>';
|
||||
shortR.forEach(r=>{
|
||||
html+='<tr><td><strong>'+r.role+'</strong></td><td>'+r.content.substring(0,100)+(r.content.length>100?'…':'')+'</td><td>'+formatDate(r.timestamp)+'</td></tr>';
|
||||
});
|
||||
html+='</table>';
|
||||
}else html+='<p class="empty">none</p>';
|
||||
html+='<h3>Long-term</h3>';
|
||||
if(longR.length){
|
||||
html+='<table><tr><th>Imp</th><th>Content</th><th>When</th><th></th></tr>';
|
||||
longR.forEach(r=>{
|
||||
const imp=r.importance? r.importance.toFixed(2):'N/A';
|
||||
const impCol=r.importance? (r.importance>0.8?'#ff6b6b':r.importance>0.5?'#ffd43b':'#51cf66'):'#999';
|
||||
html+='<tr><td><span class="score-badge" style="background:'+impCol+'">'+imp+'</span></td>'+
|
||||
'<td>'+r.content.substring(0,100).replace(/</g,'<')+(r.content.length>100?'…':'')+'</td>'+
|
||||
'<td>'+formatDate(r.timestamp)+'</td>'+
|
||||
'<td><button class="delete-btn" onclick="deleteEntry(\''+id+'\',\''+r.id+'\')">Del</button></td></tr>';
|
||||
});
|
||||
html+='</table>';
|
||||
}else html+='<p class="empty">none</p>';
|
||||
html+='<h3>Search</h3><div class="search-box"><input id="search-q" type="text" placeholder="query…"/><button onclick="runSearch(\''+id+'\')">Search</button></div><div id="search-results"></div>';
|
||||
detail.innerHTML=html;
|
||||
}catch(e){
|
||||
detail.innerHTML='<div class="error">error: '+e.message+'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEntry(user, id) {
|
||||
console.log('client: deleteEntry', user, id);
|
||||
if (!confirm('Are you sure you want to delete this memory?')) return;
|
||||
try {
|
||||
await fetch('/api/users/' + user + '/long/' + id, { method: 'DELETE' });
|
||||
showUser(user);
|
||||
} catch (e) {
|
||||
alert('Error deleting memory: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function runSearch(user){
|
||||
const q=document.getElementById('search-q').value;
|
||||
if(!q){return;}
|
||||
const container=document.getElementById('search-results');
|
||||
container.innerHTML='<div class="loading">Searching…</div>';
|
||||
try{
|
||||
const res=await fetchJson('/api/users/'+user+'/search',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:q})});
|
||||
if(!res.length){container.innerHTML='<div class="search-results"><p class="empty">none</p></div>';return;}
|
||||
let html='<div class="search-results"><ul>';
|
||||
res.forEach(r=>{const score=r.score?r.score.toFixed(3):'?';html+='<li><span class="score-badge">'+score+'</span>'+r.content.substring(0,100).replace(/</g,'<')+(r.content.length>100?'…':'')+'</li>';});
|
||||
html+='</ul></div>';
|
||||
container.innerHTML=html;
|
||||
}catch(e){container.innerHTML='<div class="error">search error</div>';}
|
||||
}
|
||||
|
||||
loadMood();
|
||||
loadUsers();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user