project view updates

This commit is contained in:
Storme-bit
2026-04-18 23:06:59 -07:00
parent c87760cc01
commit ee8f5bb5f0

View File

@@ -7,7 +7,7 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [modal, setModal] = useState(null); // { mode: 'edit' | 'confirm-delete' } const [modal, setModal] = useState(null);
useEffect(() => { load(); }, [project.id]); useEffect(() => { load(); }, [project.id]);
@@ -89,7 +89,6 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
All Projects All Projects
</button> </button>
{/* ⋮ menu */}
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<button <button
className="btn-icon" className="btn-icon"
@@ -100,11 +99,7 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
{menuOpen && ( {menuOpen && (
<> <>
{/* Click-away backdrop */} <div style={{ position: 'fixed', inset: 0, zIndex: 40 }} onClick={() => setMenuOpen(false)} />
<div
style={{ position: 'fixed', inset: 0, zIndex: 40 }}
onClick={() => setMenuOpen(false)}
/>
<div style={{ <div style={{
position: 'absolute', top: '100%', right: 0, position: 'absolute', top: '100%', right: 0,
background: 'var(--bg-elevated)', background: 'var(--bg-elevated)',
@@ -124,7 +119,7 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
</div> </div>
</div> </div>
{/* Content */} {/* Scrollable content */}
<div className="flex-1 scroll-y" style={{ padding: '32px 24px' }}> <div className="flex-1 scroll-y" style={{ padding: '32px 24px' }}>
{/* Project title + description */} {/* Project title + description */}
@@ -139,62 +134,28 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
)} )}
</div> </div>
{/* Conversations */} {/* ── Conversations ── */}
<div> <div style={{ marginBottom: '40px' }}>
<p className="label-upper" style={{ marginBottom: '12px' }}>Conversations</p> <p className="label-upper" style={{ marginBottom: '12px' }}>Conversations</p>
{loading ? ( {loading ? (
<div className="text-sm text-muted">Loading...</div> <div className="text-sm text-muted">Loading...</div>
) : sessions.length === 0 ? ( ) : sessions.length === 0 ? (
/* Empty state */
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '16px', padding: '32px 0' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '16px', padding: '32px 0' }}>
<p className="text-sm text-muted">No conversations yet start one below</p> <p className="text-sm text-muted">No conversations yet start one below</p>
<div style={{ width: '100%', maxWidth: '520px' }}> <ChatInput
<div style={{
background: 'var(--bg-elevated)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-lg)',
padding: '12px 14px',
}}>
<textarea
value={input} value={input}
onChange={e => setInput(e.target.value)} onChange={setInput}
onKeyDown={handleKeyDown} onSend={handleSend}
placeholder={`Start a conversation in ${project.name}`} placeholder={`Start a conversation in ${project.name}`}
rows={1}
autoFocus autoFocus
style={{
width: '100%', background: 'transparent',
border: 'none', outline: 'none',
color: 'var(--text-primary)', fontSize: '14px',
lineHeight: '1.6', resize: 'none', fontFamily: 'inherit',
maxHeight: '120px', overflowY: 'auto',
}}
onInput={e => {
e.target.style.height = 'auto';
e.target.style.height = `${e.target.scrollHeight}px`;
}}
/> />
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '8px' }}>
<button
onClick={handleSend}
disabled={!input.trim()}
className="btn-primary"
style={{ width: '32px', height: '32px', fontSize: '16px', border: '1px solid var(--border)' }}
></button>
</div>
</div>
<p className="text-xs text-muted" style={{ textAlign: 'center', marginTop: '8px' }}>
Enter to send · Shift+Enter for new line
</p>
</div>
</div> </div>
) : ( ) : (
/* Sessions list + new conversation input */
<> <>
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: '24px' }}> <div style={{ display: 'flex', flexDirection: 'column', marginBottom: '16px' }}>
{sessions.map((session, i) => ( {sessions.map((session, i) => (
<button <button
key={session.external_id} key={session.external_id}
@@ -225,7 +186,78 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
))} ))}
</div> </div>
{/* New conversation input */} <ChatInput
value={input}
onChange={setInput}
onSend={handleSend}
placeholder={`New conversation in ${project.name}`}
/>
</>
)}
</div>
{/* ── 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>
<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 ── */}
<NotesSection projectId={project.id} initialNotes={project.notes ?? ''} />
</div>
{/* Modal */}
{modal && (
<ProjectModal
project={project}
mode={modal.mode}
onSave={handleSave}
onDelete={handleDelete}
onClose={() => setModal(null)}
/>
)}
</div>
);
}
// ── Sub-components ─────────────────────────────────────────
function ChatInput({ value, onChange, onSend, placeholder, autoFocus }) {
function handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSend();
}
}
return (
<div style={{ width: '100%', maxWidth: '520px' }}> <div style={{ width: '100%', maxWidth: '520px' }}>
<div style={{ <div style={{
background: 'var(--bg-elevated)', background: 'var(--bg-elevated)',
@@ -234,11 +266,12 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
padding: '12px 14px', padding: '12px 14px',
}}> }}>
<textarea <textarea
value={input} value={value}
onChange={e => setInput(e.target.value)} onChange={e => onChange(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={`New conversation in ${project.name}`} placeholder={placeholder}
rows={1} rows={1}
autoFocus={autoFocus}
style={{ style={{
width: '100%', background: 'transparent', width: '100%', background: 'transparent',
border: 'none', outline: 'none', border: 'none', outline: 'none',
@@ -253,8 +286,8 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
/> />
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '8px' }}> <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '8px' }}>
<button <button
onClick={handleSend} onClick={onSend}
disabled={!input.trim()} disabled={!value.trim()}
className="btn-primary" className="btn-primary"
style={{ width: '32px', height: '32px', fontSize: '16px', border: '1px solid var(--border)' }} style={{ width: '32px', height: '32px', fontSize: '16px', border: '1px solid var(--border)' }}
>↑</button> >↑</button>
@@ -264,20 +297,62 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
Enter to send · Shift+Enter for new line Enter to send · Shift+Enter for new line
</p> </p>
</div> </div>
</> );
}
function NotesSection({ projectId, initialNotes }) {
const [notes, setNotes] = useState(initialNotes);
const [saving, setSaving] = useState(false);
const isDirty = notes !== initialNotes;
async function handleSave() {
setSaving(true);
try {
await updateProject(projectId, { notes });
} catch (err) {
console.error('[NotesSection] Save failed:', err.message);
} finally {
setSaving(false);
}
}
return (
<div style={{ marginBottom: '40px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
<p className="label-upper">Project Notes</p>
{isDirty && (
<button
className="btn-primary"
style={{ padding: '5px 12px', fontSize: '12px' }}
disabled={saving}
onClick={handleSave}
>
{saving ? 'Saving' : 'Save'}
</button>
)} )}
</div> </div>
</div> <textarea
value={notes}
{/* Modal */} onChange={e => setNotes(e.target.value)}
{modal && ( placeholder="Add notes about this project — references, goals, context, anything useful…"
<ProjectModal rows={6}
project={project} style={{
mode={modal.mode} width: '100%',
onSave={handleSave} background: 'var(--bg-surface)',
onDelete={handleDelete} border: '1px solid var(--border)',
onClose={() => setModal(null)} borderRadius: 'var(--radius-lg)',
padding: '14px 16px',
color: 'var(--text-primary)',
fontSize: '13px', lineHeight: '1.6',
resize: 'vertical', fontFamily: 'inherit',
outline: 'none', boxSizing: 'border-box',
}}
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
onBlur={e => e.target.style.borderColor = 'var(--border)'}
/> />
{!isDirty && notes && (
<p className="text-xs text-muted" style={{ marginTop: '6px' }}>Saved</p>
)} )}
</div> </div>
); );