project view updates
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { fetchSessions, updateSession } from '../api/orchestration';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { fetchSessions, updateProject, deleteProject } from '../api/orchestration';
|
||||
import ProjectModal from './ProjectModal';
|
||||
|
||||
export default function ProjectView({ project, onNavigate, onSelectSession, onNewProjectChat }) {
|
||||
export default function ProjectView({ project, onNavigate, onBack, onSelectSession, onNewProjectChat, onProjectsChange }) {
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [input, setInput] = useState('');
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const [modal, setModal] = useState(null); // { mode: 'edit' | 'confirm-delete' }
|
||||
|
||||
useEffect(() => { load(); }, [project.id]);
|
||||
|
||||
@@ -19,6 +22,40 @@ export default function ProjectView({ project, onNavigate, onSelectSession, onNe
|
||||
}
|
||||
}
|
||||
|
||||
function handleSend() {
|
||||
const text = input.trim();
|
||||
if (!text) return;
|
||||
setInput('');
|
||||
onNewProjectChat(text);
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave({ name, description, colour, icon, isolated }) {
|
||||
try {
|
||||
await updateProject(project.id, { name, description, colour, icon, isolated });
|
||||
onProjectsChange?.();
|
||||
setModal(null);
|
||||
} catch (err) {
|
||||
console.error('[ProjectView] Update failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await deleteProject(project.id);
|
||||
onProjectsChange?.();
|
||||
onBack();
|
||||
} catch (err) {
|
||||
console.error('[ProjectView] Delete failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimestamp(ts) {
|
||||
if (!ts) return '—';
|
||||
const date = new Date(ts * 1000);
|
||||
@@ -38,52 +75,63 @@ export default function ProjectView({ project, onNavigate, onSelectSession, onNe
|
||||
<div className="flex-col flex-1 overflow-hidden" style={{ background: 'var(--bg-base)' }}>
|
||||
|
||||
{/* Colour accent bar */}
|
||||
<div style={{
|
||||
height: '3px', flexShrink: 0,
|
||||
background: project.colour ?? 'var(--accent)',
|
||||
}} />
|
||||
<div style={{ height: '3px', flexShrink: 0, background: project.colour ?? 'var(--accent)' }} />
|
||||
|
||||
{/* Header */}
|
||||
<div className="panel-header" style={{ padding: '0 24px', justifyContent: 'space-between' }}>
|
||||
<button
|
||||
className="btn-reset text-xs text-muted"
|
||||
onClick={onBack} title="Back"
|
||||
onClick={onBack}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
|
||||
onMouseEnter={e => e.currentTarget.style.color = 'var(--text-secondary)'}
|
||||
onMouseLeave={e => e.currentTarget.style.color = 'var(--text-muted)'}
|
||||
>
|
||||
← All Projects
|
||||
</button>
|
||||
<button
|
||||
className="btn-primary"
|
||||
onClick={onNewProjectChat}
|
||||
style={{ padding: '5px 12px', fontSize: '12px' }}
|
||||
>
|
||||
+ New Chat
|
||||
</button>
|
||||
|
||||
{/* ⋮ menu */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button
|
||||
className="btn-icon"
|
||||
onClick={() => setMenuOpen(o => !o)}
|
||||
title="Project options"
|
||||
style={{ fontSize: '18px', letterSpacing: '1px' }}
|
||||
>⋮</button>
|
||||
|
||||
{menuOpen && (
|
||||
<>
|
||||
{/* Click-away backdrop */}
|
||||
<div
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 40 }}
|
||||
onClick={() => setMenuOpen(false)}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute', top: '100%', right: 0,
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
padding: '4px', zIndex: 50, minWidth: '150px',
|
||||
}}>
|
||||
<MenuButton onClick={() => { setMenuOpen(false); setModal({ mode: 'edit' }); }}>
|
||||
✎ Edit details
|
||||
</MenuButton>
|
||||
<MenuButton danger onClick={() => { setMenuOpen(false); setModal({ mode: 'confirm-delete' }); }}>
|
||||
✕ Delete project
|
||||
</MenuButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 scroll-y" style={{ padding: '32px 24px' }}>
|
||||
|
||||
{/* Project title + meta */}
|
||||
{/* Project title + description */}
|
||||
<div style={{ marginBottom: '32px' }}>
|
||||
<div className="flex items-center" style={{ gap: '10px', marginBottom: '8px' }}>
|
||||
<h1 style={{ fontSize: '22px', fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||
{project.name}
|
||||
</h1>
|
||||
{project.isolated === 1 && (
|
||||
<span style={{
|
||||
fontSize: '11px', fontWeight: 500,
|
||||
padding: '2px 8px', borderRadius: '99px',
|
||||
background: 'var(--bg-elevated)',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--text-muted)',
|
||||
}}>
|
||||
Project Only
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h1 style={{ fontSize: '22px', fontWeight: 600, color: 'var(--text-primary)', marginBottom: '8px' }}>
|
||||
{project.name}
|
||||
</h1>
|
||||
{project.description && (
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)', maxWidth: '560px', lineHeight: 1.6 }}>
|
||||
{project.description}
|
||||
@@ -91,56 +139,163 @@ export default function ProjectView({ project, onNavigate, onSelectSession, onNe
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sessions list */}
|
||||
{/* Conversations */}
|
||||
<div>
|
||||
<p className="label-upper" style={{ marginBottom: '12px' }}>Conversations</p>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-sm text-muted">Loading...</div>
|
||||
|
||||
) : sessions.length === 0 ? (
|
||||
<div style={{
|
||||
padding: '32px', textAlign: 'center',
|
||||
border: '1px dashed var(--border)', borderRadius: 'var(--radius-lg)',
|
||||
color: 'var(--text-muted)',
|
||||
}}>
|
||||
<p className="text-sm">No conversations in this project yet</p>
|
||||
<p className="text-xs" style={{ marginTop: '6px' }}>
|
||||
Click "+ New Chat" to start one
|
||||
</p>
|
||||
/* 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
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
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>
|
||||
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{sessions.map((session, i) => (
|
||||
<button
|
||||
key={session.external_id}
|
||||
className="btn-reset"
|
||||
onClick={() => { onSelectSession(session); onNavigate('chat'); }}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
borderBottom: i < sessions.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
borderRadius: i === 0
|
||||
? 'var(--radius-md) var(--radius-md) 0 0'
|
||||
: i === sessions.length - 1
|
||||
? '0 0 var(--radius-md) var(--radius-md)'
|
||||
: '0',
|
||||
background: 'var(--bg-surface)',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-elevated)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-surface)'}
|
||||
>
|
||||
<span className="text-base" style={{ color: 'var(--text-primary)' }}>
|
||||
{session.name || session.external_id}
|
||||
</span>
|
||||
<span className="text-xs text-muted" style={{ flexShrink: 0, marginLeft: '16px' }}>
|
||||
{formatTimestamp(session.updated_at)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
/* Sessions list + new conversation input */
|
||||
<>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: '24px' }}>
|
||||
{sessions.map((session, i) => (
|
||||
<button
|
||||
key={session.external_id}
|
||||
className="btn-reset"
|
||||
onClick={() => { onSelectSession(session); onNavigate('chat'); }}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
borderBottom: i < sessions.length - 1 ? '1px solid var(--border)' : 'none',
|
||||
borderRadius: i === 0
|
||||
? 'var(--radius-md) var(--radius-md) 0 0'
|
||||
: i === sessions.length - 1
|
||||
? '0 0 var(--radius-md) var(--radius-md)'
|
||||
: '0',
|
||||
background: 'var(--bg-surface)',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-elevated)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-surface)'}
|
||||
>
|
||||
<span className="text-base" style={{ color: 'var(--text-primary)' }}>
|
||||
{session.name || session.external_id}
|
||||
</span>
|
||||
<span className="text-xs text-muted" style={{ flexShrink: 0, marginLeft: '16px' }}>
|
||||
{formatTimestamp(session.updated_at)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* New conversation input */}
|
||||
<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
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={`New conversation in ${project.name}…`}
|
||||
rows={1}
|
||||
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>
|
||||
|
||||
{/* Modal */}
|
||||
{modal && (
|
||||
<ProjectModal
|
||||
project={project}
|
||||
mode={modal.mode}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
onClose={() => setModal(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuButton({ children, onClick, danger }) {
|
||||
return (
|
||||
<button
|
||||
className="btn-reset text-sm"
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: '100%', padding: '8px 12px',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
justifyContent: 'flex-start',
|
||||
color: danger ? '#ff6b6b' : 'var(--text-primary)',
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-surface)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
|
||||
>{children}</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user