import { useState, useMemo, useCallback } from "react"; import { FileText, ChevronDown, ChevronUp, ChevronRight, RefreshCw, Search, X } from "lucide-react"; import type { TaskDocumentWithTask, TaskDetail } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; import { fetchTaskDetail, fetchWorkspaceFileContent, type MarkdownFileEntry } from "../api"; import { useDocuments } from "../hooks/useDocuments"; export interface DocumentsViewProps { projectId?: string; addToast: (message: string, type?: ToastType) => void; onOpenDetail: (task: TaskDetail) => void; } interface DocumentCardProps { document: TaskDocumentWithTask; onOpenTask: (taskId: string) => void; } interface ProjectFileCardProps { file: MarkdownFileEntry; expanded: boolean; loading: boolean; error: string | null; content: string; onOpen: (filePath: string) => Promise; } function formatTimestamp(iso?: string): string { if (!iso) return ""; return new Date(iso).toLocaleString(); } function formatFileSize(bytes: number): string { if (bytes < 1024) { return `${bytes} B`; } if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(bytes >= 10 * 1024 ? 0 : 1)} KB`; } return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function getContentPreview(content: string, maxLength: number = 200): string { if (content.length <= maxLength) return content; return content.substring(0, maxLength) + "…"; } function DocumentCard({ document, onOpenTask }: DocumentCardProps) { const [expanded, setExpanded] = useState(false); const preview = getContentPreview(document.content); const showExpand = document.content.length > 200; return (
{document.key} v{document.revision}
{document.author} · {formatTimestamp(document.updatedAt)}
{expanded ? (
{document.content}
) : (

{preview}

)} {showExpand && !expanded && (

)}
); } function ProjectFileCard({ file, expanded, loading, error, content, onOpen }: ProjectFileCardProps) { const preview = getContentPreview(file.contentPreview, 200); const isExpanded = expanded; return (
{isExpanded && (
{loading ? (

Loading file content…

) : error ? (

{error}

) : (
{content}
)}
)}
); } interface TaskGroupProps { taskId: string; taskTitle?: string; documents: TaskDocumentWithTask[]; onOpenTask: (taskId: string) => void; } function TaskGroup({ taskId, taskTitle, documents, onOpenTask }: TaskGroupProps) { const [expanded, setExpanded] = useState(false); return (
{documents.length} doc{documents.length !== 1 ? "s" : ""} {expanded && (
{documents.map((doc) => ( ))}
)}
); } export function DocumentsView({ projectId, addToast, onOpenDetail }: DocumentsViewProps) { const [searchQuery, setSearchQuery] = useState(""); const [projectFilesExpanded, setProjectFilesExpanded] = useState(true); const [openProjectFilePath, setOpenProjectFilePath] = useState(null); const [openProjectFileContent, setOpenProjectFileContent] = useState(""); const [openProjectFileLoading, setOpenProjectFileLoading] = useState(false); const [openProjectFileError, setOpenProjectFileError] = useState(null); const { documents, projectFiles, loading, error, refresh } = useDocuments({ projectId, searchQuery: searchQuery || undefined, }); // Group documents by task const groupedDocuments = useMemo(() => { const groups = new Map(); for (const doc of documents) { const existing = groups.get(doc.taskId) || []; groups.set(doc.taskId, [...existing, doc]); } // Sort groups by the most recently updated document return Array.from(groups.entries()) .map(([taskId, docs]) => ({ taskId, taskTitle: docs[0].taskTitle, documents: docs.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)), latestUpdated: docs[0].updatedAt, })) .sort((a, b) => b.latestUpdated.localeCompare(a.latestUpdated)); }, [documents]); const handleSearchChange = useCallback((e: React.ChangeEvent) => { setSearchQuery(e.target.value); }, []); const clearSearch = useCallback(() => { setSearchQuery(""); }, []); const toggleProjectFilesExpanded = useCallback(() => { setProjectFilesExpanded((current) => !current); }, []); // Wrapper to open task detail by fetching full task first const handleOpenTask = useCallback(async (taskId: string) => { try { const task = await fetchTaskDetail(taskId, projectId); onOpenDetail(task); } catch { addToast(`Failed to open task ${taskId}`, "error"); } }, [projectId, onOpenDetail, addToast]); const handleOpenProjectFile = useCallback(async (filePath: string) => { if (openProjectFilePath === filePath) { setOpenProjectFilePath(null); setOpenProjectFileContent(""); setOpenProjectFileError(null); setOpenProjectFileLoading(false); return; } setOpenProjectFilePath(filePath); setOpenProjectFileLoading(true); setOpenProjectFileError(null); setOpenProjectFileContent(""); try { const file = await fetchWorkspaceFileContent("project", filePath, projectId); setOpenProjectFileContent(file.content); } catch (err) { const message = err instanceof Error ? err.message : `Failed to open ${filePath}`; setOpenProjectFileError(message); addToast(message, "error"); } finally { setOpenProjectFileLoading(false); } }, [openProjectFilePath, projectId, addToast]); if (error) { return (

Failed to load documents: {error}

); } return (

Documents

{loading ? "…" : `${documents.length + projectFiles.length} total`}
{searchQuery && ( )}
{loading ? (

Loading documents…

) : (
{projectFilesExpanded && ( projectFiles.length === 0 ? (

No Markdown files found in the project directory.

) : (
{projectFiles.map((file) => ( ))}
) )}
{groupedDocuments.length === 0 ? (
{searchQuery ? (

No task documents match "{searchQuery}".

) : ( <>

No task documents yet.

Documents are created in task detail tabs.

)}
) : (
{groupedDocuments.map(({ taskId, taskTitle, documents: taskDocs }) => ( ))}
)}
)}
); }