summary system backend implementation

This commit is contained in:
Storme-bit
2026-04-19 06:50:24 -07:00
parent 15c1bec609
commit 2769f436fa
10 changed files with 210 additions and 106 deletions

View File

@@ -25,13 +25,13 @@ export default function MessageBubble({ message }) {
<div style={{
maxWidth: '70%',
padding: '10px 14px',
borderRadius: isUser ? '18px 18px 4px 18px' : '18px 18px 18px 4px',
padding: '14px 14px',
borderRadius: isUser ? '18px 4px 4px 18px' : '4px 18px 18px 4px',
background: isUser ? 'var(--bubble-user)' : 'var(--bubble-ai)',
color: 'var(--text-primary)',
fontSize: '14px',
lineHeight: '1.6',
border: isUser ? 'none' : '1px solid var(--border)',
fontSize: '18px',
lineHeight: '1.8',
border: isUser ? 'none' : '2px solid var(--border)',
wordBreak: 'break-word',
}}>
<ReactMarkdown
@@ -60,7 +60,7 @@ export default function MessageBubble({ message }) {
}} />
)}
{message.error && (
<div className="text-xs" style={{ marginTop: '6px', color: '#ff6b6b' }}>
<div className="text-xs" style={{ marginTop: '6px', color: 'var(--warning)' }}>
Failed to complete response
</div>
)}

View File

@@ -126,7 +126,7 @@ export default function Sidebar({
{/* Header */}
<div className="panel-header" style={{ justifyContent: 'space-between', padding: '0 12px 0 16px' }}>
<span className="text-base" style={{ fontWeight: 500, color: 'var(--text-secondary)' }}>NexusAI</span>
<span className="text-base" style={{ fontWeight: 1000, color: 'var(--text-secondary)' }}>NexusAI</span>
<button className="btn-icon" onClick={onToggle}></button>
</div>
@@ -166,8 +166,8 @@ export default function Sidebar({
padding: '10px',
borderRadius: 'var(--radius-md)',
border: '1px dashed var(--border)',
color: 'var(--text-muted)',
fontSize: '12px',
color: 'var(--text-sb-hdr)',
fontSize: '13px',
textAlign: 'center',
}}>
No projects yet
@@ -226,11 +226,17 @@ export default function Sidebar({
display: 'flex', alignItems: 'center', gap: '6px',
padding: '6px 16px 2px',
}}>
<div style={{
width: '6px', height: '6px', borderRadius: '50%', flexShrink: 0,
background: project?.colour ?? 'var(--accent)',
}} />
<span className="text-xs text-muted truncate">
<span className=" text-muted truncate"
style={{
fontSize: '12px',
textTransform: 'uppercase',
fontWeight: '500',
textAlign: 'center',
borderRadius: 'var(--radius-md)',
border: `1px solid ${project.colour ?? 'var(--border)'}`,
padding: '2px 2px',
width: '100%'
}}>
{project?.name ?? 'Project'}
</span>
</div>
@@ -246,7 +252,7 @@ export default function Sidebar({
<>
{Object.keys(grouped).length > 0 && (
<div style={{ padding: '6px 16px 2px' }}>
<span className="text-xs text-muted">Other</span>
<span className=" text-muted " style={{fontSize: '12px', textTransform: 'uppercase', fontWeight: '500', textAlign: 'center',}}>Other</span>
</div>
)}
{unassigned.map(session => (
@@ -334,14 +340,14 @@ function SectionHeader({ label, isOpen, onToggle }) {
className="btn-reset label-upper"
style={{
width: '100%', padding: '8px 16px',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
color: 'var(--text-muted)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--text-sb-hdr)',
}}
onMouseEnter={e => e.currentTarget.style.color = 'var(--text-secondary)'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--text-muted)'}
>
<span>{label}</span>
<span style={{ fontSize: '10px' }}>{isOpen ? '▾' : '▸'}</span>
<span style={{ fontSize: '13px' }}>{isOpen ? '▾' : '▸'}</span>
</button>
);
}
@@ -357,6 +363,9 @@ function SessionRow({ session, isActive, isHovered, onHover, onSelect, onRename,
background: isActive || isHovered ? 'var(--bg-elevated)' : 'transparent',
borderLeft: isActive ? '2px solid var(--accent)' : '2px solid transparent',
transition: 'background 0.1s',
overflow: 'hidden',
width: '100%',
boxSizing: 'border-box',
}}
>
<button
@@ -365,7 +374,9 @@ function SessionRow({ session, isActive, isHovered, onHover, onSelect, onRename,
style={{
flex: 1, padding: '8px 16px',
paddingRight: isHovered && !session.isNew ? '4px' : '16px',
textAlign: 'left', minWidth: 0,
textAlign: 'left',
minWidth: 0,
overflow: 'hidden',
}}
>
<span className="text-base truncate" style={{
@@ -380,14 +391,22 @@ function SessionRow({ session, isActive, isHovered, onHover, onSelect, onRename,
)}
</button>
{isHovered && !session.isNew && (
<div className="flex items-center flex-shrink" style={{ gap: '2px', paddingRight: '8px' }}>
<button className="btn-icon" title="Rename" onClick={onRename}
style={{ padding: '2px 4px', fontSize: '12px' }}></button>
<button className="btn-icon" title="Delete" onClick={onDelete}
style={{ padding: '2px 4px', fontSize: '12px', color: '#ff6b6b' }}></button>
</div>
)}
<div
style={{
display: 'flex', alignItems: 'center',
gap: '2px',
paddingRight: isHovered && !session.isNew ? '8px' : '0px',
flexShrink: 0,
width: isHovered && !session.isNew ? '44px' : '0px',
overflow: 'hidden',
transition: 'width 0.1s ease',
}}
>
<button className="btn-icon" title="Rename" onClick={onRename}
style={{ padding: '2px 4px', fontSize: '12px' }}></button>
<button className="btn-icon" title="Delete" onClick={onDelete}
style={{ padding: '2px 4px', fontSize: '12px', color: '#ff6b6b' }}></button>
</div>
</div>
);
}

View File

@@ -1,17 +1,19 @@
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-base: #0f1117;
--bg-surface: #0e0d0d;
--bg-elevated: #222536;
--border: #2e3150;
--accent: #3d3a79;
--bg-base: #9c9a9a;
--bg-surface: #000000;
--bg-elevated: #111111;
--border: #989899;
--accent: #333335;
--accent-hover: #574fd6;
--text-primary: #e8e8f0;
--text-secondary: #8b8fa8;
--text-muted: #555870;
--bubble-user: #4742a8;
--bubble-ai: #20264d;
--text-muted: #ababaf;
--text-sb-hdr: #ffffff;
--bubble-user: #020202;
--bubble-ai: #303033;
--warning: #ec5353;
--sidebar-width: 180px;
--panel-width: 200px;
--header-height: 40px;
@@ -64,7 +66,9 @@ html, body, #root {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-start;
min-width: 0;
overflow: hidden;
}
.btn-icon {
@@ -105,5 +109,5 @@ html, body, #root {
.text-muted { color: var(--text-muted); }
.text-secondary { color: var(--text-secondary); }
.text-accent { color: var(--accent); }
.label-upper { font-size: 11px; font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; }
.label-upper { font-size: 13px; font-weight: 750; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

View File

@@ -38,6 +38,18 @@ function getDB() {
db.exec(`ALTER TABLE projects ADD COLUMN system_prompt TEXT`);
} catch {}
try {
db.exec(`ALTER TABLE summaries ADD COLUMN project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE`);
} catch {}
try {
db.exec(`ALTER TABLE summaries ADD COLUMN token_count INTEGER`);
} catch {}
try {
db.exec(`CREATE INDEX IF NOT EXISTS idx_summaries_project ON summaries(project_id)`);
} catch {}
// Sync FTS index with any existing episodes data
db.exec(`INSERT OR REPLACE INTO episodes_fts(rowid, user_message, ai_response)
SELECT id, user_message, ai_response FROM episodes`);

View File

@@ -41,12 +41,17 @@ const schema = `
CREATE TABLE IF NOT EXISTS summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER REFERENCES sessions(id) ON DELETE CASCADE,
project_id INTEGER REFERENCES projects(id) ON DELETE CASCADE,
content TEXT NOT NULL,
token_count INTEGER,
episode_range TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
metadata TEXT
);
CREATE INDEX IF NOT EXISTS idx_summaries_session ON summaries(session_id);
CREATE INDEX IF NOT EXISTS idx_summaries_project ON summaries(project_id);
CREATE INDEX IF NOT EXISTS idx_episodes_session
ON episodes(session_id);
CREATE INDEX IF NOT EXISTS idx_episodes_created

View File

@@ -0,0 +1,53 @@
const { getDB } = require('./index');
const { parseRow } = require('@nexusai/shared');
function createSummary({ sessionId = null, projectId = null, content, tokenCount = null, episodeRange = null, metadata = null }) {
const db = getDB();
const result = db.prepare(`
INSERT INTO summaries (session_id, project_id, content, token_count, episode_range, metadata)
VALUES (?, ?, ?, ?, ?, ?)
`).run(sessionId, projectId, content, tokenCount, episodeRange, metadata ? JSON.stringify(metadata) : null);
return getSummary(result.lastInsertRowid);
}
function getSummary(id) {
const db = getDB();
const row = db.prepare(`SELECT * FROM summaries WHERE id = ?`).get(id);
return row ? parseRow(row) : null;
}
function getSummariesBySession(sessionId) {
const db = getDB();
return db.prepare(`SELECT * FROM summaries WHERE session_id = ? ORDER BY created_at ASC`)
.all(sessionId).map(parseRow);
}
function getSummariesByProject(projectId) {
const db = getDB();
return db.prepare(`SELECT * FROM summaries WHERE project_id = ? ORDER BY created_at ASC`)
.all(projectId).map(parseRow);
}
function updateSummary(id, { content, tokenCount, episodeRange, metadata }) {
const db = getDB();
const fields = [];
const values = [];
if (content !== undefined) { fields.push('content = ?'); values.push(content); }
if (tokenCount !== undefined) { fields.push('token_count = ?'); values.push(tokenCount); }
if (episodeRange !== undefined){ fields.push('episode_range = ?'); values.push(episodeRange); }
if (metadata !== undefined) { fields.push('metadata = ?'); values.push(JSON.stringify(metadata)); }
if (!fields.length) return getSummary(id);
values.push(id);
db.prepare(`UPDATE summaries SET ${fields.join(', ')} WHERE id = ?`).run(...values);
return getSummary(id);
}
function deleteSummary(id) {
getDB().prepare(`DELETE FROM summaries WHERE id = ?`).run(id);
}
module.exports = { createSummary, getSummary, getSummariesBySession, getSummariesByProject, updateSummary, deleteSummary };