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 [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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user