// Primus IQ — Project Detail Screen const PROJECT_FILES_PAGE_SIZE = 50; // rows rendered per "page" — windowed like Library so a big project doesn't mount every file at once const ScreenProjectDetail = ({ setRoute, onStartChat, chatSessions = [], onOpenChat, openTagging, openSharePoint, openTextInput, openUrlInput, openInvite, openEditProject, onDeleteProject, uploadedFiles, removeFile, downloadFile, editFile, viewFile, activeProject, projectMembers, maintenance = false }) => { const [dragHover, setDragHover] = React.useState(false); const [pendingCount, setPendingCount] = React.useState(0); const [tab, setTab] = React.useState('chats'); // 'chats' | 'sources' const [composerVal, setComposerVal] = React.useState(''); const [composerFocused, setComposerFocused] = React.useState(false); const [composerFile, setComposerFile] = React.useState(null); // doc attached to the launching chat const composerFileRef = React.useRef(null); const [filter, setFilter] = React.useState(''); const [visibleFiles, setVisibleFiles] = React.useState(PROJECT_FILES_PAGE_SIZE); const [menuOpen, setMenuOpen] = React.useState(false); // project actions dropdown const menuRef = React.useRef(null); React.useEffect(() => { if (!menuOpen) return; const handler = (e) => { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [menuOpen]); const projectName = activeProject?.name || 'Project'; const projectPractice = activeProject?.practice || ''; const fq = filter.trim().toLowerCase(); // Real chat sessions scoped to this project (single source of truth = app sidebar list). const projectChats = chatSessions.filter(s => s.project_id === activeProject?.id); const filteredChats = fq ? projectChats.filter(c => (c.title || '').toLowerCase().includes(fq)) : projectChats; const filteredFiles = fq ? uploadedFiles.filter(f => (f.name || '').toLowerCase().includes(fq)) : uploadedFiles; // Windowed rendering — reset to the first page whenever the file search changes. React.useEffect(() => { setVisibleFiles(PROJECT_FILES_PAGE_SIZE); }, [fq]); const shownFiles = filteredFiles.slice(0, visibleFiles); const hasMoreFiles = filteredFiles.length > visibleFiles; React.useEffect(() => { if (!activeProject?.id) return; PrimusAPI.listJoinRequests(activeProject.id) .then(data => setPendingCount((data || []).length)) .catch(() => setPendingCount(0)); }, [activeProject?.id]); const submitComposer = () => { if (maintenance) return; // chat is greyed out in maintenance mode const q = composerVal.trim(); if (!q) return; const file = composerFile; setComposerVal(''); setComposerFile(null); // Open a real, project-scoped chat in the main chat screen (sends project_id) and // carry any attached doc so it's saved to that new session as chat context. onStartChat && onStartChat(activeProject?.id, q, file); }; const canSend = !maintenance && !!composerVal.trim(); return (
{/* Crumb */}
/ {projectName}
{/* Header */}

{projectName}

{projectPractice && {projectPractice}} · {uploadedFiles.length} files
{projectMembers && projectMembers.length > 0 && (
{projectMembers.slice(0, 3).map((m, i) => (
{(m.full_name || '?')[0].toUpperCase()}
))} {projectMembers.length > 3 && (
+{projectMembers.length - 3}
)}
)}
{menuOpen && (
)}
{/* TOP ROW: New chat composer (left) + Add sources (right) — sized to content */}
{/* LEFT: New chat in */}
New chat in {projectName}
Ask anything — this project's files are referenced automatically.