project view updates
This commit is contained in:
@@ -7,7 +7,7 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [input, setInput] = useState('');
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [modal, setModal] = useState(null); // { mode: 'edit' | 'confirm-delete' }
|
||||
const [modal, setModal] = useState(null);
|
||||
|
||||
useEffect(() => { load(); }, [project.id]);
|
||||
|
||||
@@ -89,7 +89,6 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
← All Projects
|
||||
</button>
|
||||
|
||||
{/* ⋮ menu */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button
|
||||
className="btn-icon"
|
||||
@@ -100,11 +99,7 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
|
||||
{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={{
|
||||
position: 'absolute', top: '100%', right: 0,
|
||||
background: 'var(--bg-elevated)',
|
||||
@@ -124,7 +119,7 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 scroll-y" style={{ padding: '32px 24px' }}>
|
||||
|
||||
{/* Project title + description */}
|
||||
@@ -139,62 +134,28 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Conversations */}
|
||||
<div>
|
||||
{/* ── Conversations ── */}
|
||||
<div style={{ marginBottom: '40px' }}>
|
||||
<p className="label-upper" style={{ marginBottom: '12px' }}>Conversations</p>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-sm text-muted">Loading...</div>
|
||||
|
||||
) : sessions.length === 0 ? (
|
||||
/* Empty state */
|
||||
<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>
|
||||
<div style={{ width: '100%', maxWidth: '520px' }}>
|
||||
<div style={{
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-lg)',
|
||||
padding: '12px 14px',
|
||||
}}>
|
||||
<textarea
|
||||
<ChatInput
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={setInput}
|
||||
onSend={handleSend}
|
||||
placeholder={`Start a conversation in ${project.name}…`}
|
||||
rows={1}
|
||||
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>
|
||||
|
||||
) : (
|
||||
/* 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) => (
|
||||
<button
|
||||
key={session.external_id}
|
||||
@@ -225,7 +186,78 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
))}
|
||||
</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={{
|
||||
background: 'var(--bg-elevated)',
|
||||
@@ -234,11 +266,12 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
padding: '12px 14px',
|
||||
}}>
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={`New conversation in ${project.name}…`}
|
||||
placeholder={placeholder}
|
||||
rows={1}
|
||||
autoFocus={autoFocus}
|
||||
style={{
|
||||
width: '100%', background: 'transparent',
|
||||
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' }}>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim()}
|
||||
onClick={onSend}
|
||||
disabled={!value.trim()}
|
||||
className="btn-primary"
|
||||
style={{ width: '32px', height: '32px', fontSize: '16px', border: '1px solid var(--border)' }}
|
||||
>↑</button>
|
||||
@@ -264,20 +297,62 @@ export default function ProjectView({ project, onNavigate, onBack, onSelectSessi
|
||||
Enter to send · Shift+Enter for new line
|
||||
</p>
|
||||
</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>
|
||||
|
||||
{/* Modal */}
|
||||
{modal && (
|
||||
<ProjectModal
|
||||
project={project}
|
||||
mode={modal.mode}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
onClose={() => setModal(null)}
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={e => setNotes(e.target.value)}
|
||||
placeholder="Add notes about this project — references, goals, context, anything useful…"
|
||||
rows={6}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'var(--bg-surface)',
|
||||
border: '1px solid var(--border)',
|
||||
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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user