project summaries addition
This commit is contained in:
@@ -65,7 +65,7 @@ export default function App() {
|
||||
streaming,
|
||||
lastTokenCount,
|
||||
lastModel,
|
||||
useChat,
|
||||
summarising,
|
||||
} = useChat({ activeSession, appendMessage, updateLastMessage, refreshSessions });
|
||||
|
||||
function navigate(nextView) {
|
||||
|
||||
@@ -212,3 +212,14 @@ export async function fetchSessionSummaries(sessionId) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function generateProjectSummary(projectId) {
|
||||
const res = await fetch(`${BASE_URL}/summaries/project/${projectId}/generate`, { method: 'POST' });
|
||||
if (!res.ok) throw new Error(`Failed to generate project summary: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchProjectOverviewSummary(projectId) {
|
||||
const res = await fetch(`${BASE_URL}/summaries/project/${projectId}/overview`);
|
||||
if (!res.ok) throw new Error(`Failed to fetch project overview: ${res.status}`);
|
||||
return res.json(); // null if none exists yet
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { fetchSessions, updateProject, deleteProject } from '../api/orchestration';
|
||||
import { fetchSessions, updateProject, deleteProject, generateProjectSummary, fetchProjectOverviewSummary } from '../api/orchestration';
|
||||
import ProjectModal from './ProjectModal';
|
||||
|
||||
export default function ProjectView({ project, onNavigate, onBack, onSelectSession, onNewProjectChat, onProjectsChange }) {
|
||||
@@ -8,9 +8,27 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
const [input, setInput] = useState('');
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [modal, setModal] = useState(null);
|
||||
const [overview, setOverview] = useState(null);
|
||||
const [overviewLoading, setOverviewLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [generateError, setGenerateError] = useState(null);
|
||||
|
||||
useEffect(() => { load(); }, [project.id]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadOverview() {
|
||||
setOverviewLoading(true);
|
||||
try {
|
||||
setOverview(await fetchProjectOverviewSummary(project.id));
|
||||
} catch (err) {
|
||||
console.error('[ProjectView] Failed to load overview:', err.message);
|
||||
} finally {
|
||||
setOverviewLoading(false);
|
||||
}
|
||||
}
|
||||
loadOverview();
|
||||
}, [project.id]);
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -70,6 +88,23 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
if (diffDays === 1) return 'Yesterday';
|
||||
return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
async function handleGenerateSummary() {
|
||||
setGenerating(true);
|
||||
setGenerateError(null);
|
||||
try {
|
||||
setOverview(await generateProjectSummary(project.id));
|
||||
} catch (err) {
|
||||
// 422 means no session summaries exist yet — surface a friendly message
|
||||
setGenerateError(
|
||||
err.message.includes('422')
|
||||
? 'No conversations found in this project yet.'
|
||||
: 'Failed to generate summary. Please try again.'
|
||||
);
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-col flex-1 overflow-hidden" style={{ background: 'var(--bg-base)' }}>
|
||||
@@ -198,34 +233,61 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
|
||||
{/* ── Project Memory ── */}
|
||||
<div style={{ marginBottom: '40px' }}>
|
||||
<p className="label-upper" style={{ marginBottom: '12px' }}>Project Memory</p>
|
||||
<div style={{
|
||||
background: 'var(--bg-surface)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-lg)',
|
||||
padding: '20px',
|
||||
display: 'flex', flexDirection: 'column', gap: '10px',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<span style={{ fontSize: '20px', opacity: 0.4 }}>◈</span>
|
||||
<span className="text-sm" style={{ fontWeight: 500, color: 'var(--text-primary)' }}>
|
||||
Project Summary
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '11px', padding: '2px 8px',
|
||||
borderRadius: '999px',
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--text-muted)',
|
||||
}}>Coming soon</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
|
||||
<p className="label-upper">Project Memory</p>
|
||||
<button
|
||||
className="btn-primary"
|
||||
style={{ padding: '5px 12px', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}
|
||||
onClick={handleGenerateSummary}
|
||||
disabled={generating}
|
||||
>
|
||||
{generating
|
||||
? <><span className="spinner" />Generating…</>
|
||||
: overview ? 'Regenerate' : 'Generate Summary'
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: 'var(--bg-surface)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-lg)',
|
||||
padding: '20px',
|
||||
}}>
|
||||
{overviewLoading ? (
|
||||
<p className="text-sm text-muted">Loading…</p>
|
||||
|
||||
) : generateError ? (
|
||||
<p className="text-sm" style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
||||
{generateError}
|
||||
</p>
|
||||
|
||||
) : overview ? (
|
||||
<>
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)', lineHeight: 1.7, whiteSpace: 'pre-wrap' }}>
|
||||
{overview.content}
|
||||
</p>
|
||||
<p className="text-xs text-muted" style={{ marginTop: '12px' }}>
|
||||
Last generated {formatTimestamp(overview.created_at)}
|
||||
</p>
|
||||
</>
|
||||
|
||||
) : (
|
||||
// No overview exists yet — explain what this section is for
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<span style={{ fontSize: '20px', opacity: 0.4 }}>◈</span>
|
||||
<span className="text-sm" style={{ fontWeight: 500, color: 'var(--text-primary)' }}>
|
||||
No project summary yet
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted" style={{ lineHeight: 1.6, maxWidth: '520px' }}>
|
||||
Generate a summary to create a concise overview of this project's goals,
|
||||
progress, and key decisions — built from your session summaries.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted" style={{ lineHeight: 1.6, maxWidth: '520px' }}>
|
||||
Once this project has enough conversations, NexusAI will automatically
|
||||
generate a rolling summary of key themes, decisions, and context — giving
|
||||
the model a condensed view of the project's memory without consuming the
|
||||
full context window.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Notes ── */}
|
||||
|
||||
@@ -114,4 +114,14 @@ html, body, #root {
|
||||
.text-secondary { color: var(--text-secondary); }
|
||||
.text-accent { color: var(--accent); }
|
||||
.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; }
|
||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--text-muted);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
Reference in New Issue
Block a user