import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { FileText, ChevronDown, ChevronUp, Plus, Trash2, History } from "lucide-react"; import "./DocumentsView.css"; import "./TaskDocumentsTab.css"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import type { ArtifactWithTask, Task, TaskDocument, TaskDocumentRevision } from "@fusion/core"; import { getErrorMessage } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; import { fetchTaskDocuments, fetchTaskDocumentRevisions, putTaskDocument, deleteTaskDocument, artifactMediaUrl, } from "../api"; import { useArtifacts } from "../hooks/useArtifacts"; import { LoadingSpinner } from "./LoadingSpinner"; import { ArtifactMedia, getArtifactTypeLabel } from "./ArtifactMedia"; // Document key validation: alphanumeric, hyphens, underscores, 1-64 chars const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/; const MAX_CONTENT_PREVIEW = 200; interface TaskDocumentsTabProps { taskId: string; addToast: (message: string, type?: ToastType) => void; onTaskUpdated?: (task: Task) => void; projectId?: string; canEdit?: boolean; } function formatTimestamp(iso?: string): string { if (!iso) return ""; return new Date(iso).toLocaleString(); } function getContentPreview(content: string, maxLength: number = MAX_CONTENT_PREVIEW): string { if (content.length <= maxLength) return content; return content.substring(0, maxLength) + "…"; } 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`; } interface TaskArtifactCardProps { artifact: ArtifactWithTask; projectId?: string; } function TaskArtifactCard({ artifact, projectId }: TaskArtifactCardProps) { const { t } = useTranslation("app"); const mediaUrl = artifactMediaUrl(artifact.id, projectId); const typeLabel = getArtifactTypeLabel(t, artifact.type); const preview = artifact.content ? getContentPreview(artifact.content, 320) : artifact.description; const title = artifact.title || t("documents.untitledArtifact", "Untitled artifact"); return (
{typeLabel} {artifact.authorId}
{title}
{artifact.description &&

{artifact.description}

}
{formatTimestamp(artifact.createdAt)} {artifact.sizeBytes !== undefined && {formatFileSize(artifact.sizeBytes)}}
); } export function TaskDocumentsTab({ taskId, addToast, onTaskUpdated: _onTaskUpdated, projectId, canEdit = false, }: TaskDocumentsTabProps) { const { t } = useTranslation("app"); const [documents, setDocuments] = useState([]); const [loading, setLoading] = useState(true); const [expandedDocKey, setExpandedDocKey] = useState(null); const [expandedContent, setExpandedContent] = useState(""); const [editingDocKey, setEditingDocKey] = useState(null); const [editContent, setEditContent] = useState(""); const [showHistory, setShowHistory] = useState(null); const [revisions, setRevisions] = useState([]); const [loadingRevisions, setLoadingRevisions] = useState(false); const [showCreateForm, setShowCreateForm] = useState(false); const [newDocKey, setNewDocKey] = useState(""); const [newDocContent, setNewDocContent] = useState(""); const [saving, setSaving] = useState(false); const [deletingKey, setDeletingKey] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const [renderMarkdown, setRenderMarkdown] = useState(false); const { artifacts, loading: artifactsLoading, error: artifactsError } = useArtifacts({ projectId, taskId }); const loadDocuments = useCallback(async () => { try { const docs = await fetchTaskDocuments(taskId, projectId); setDocuments(docs); } catch (error) { addToast(getErrorMessage(error) || t("taskDocuments.failedToLoad", "Failed to load documents"), "error"); } finally { setLoading(false); } }, [taskId, projectId, addToast]); useEffect(() => { void loadDocuments(); }, [loadDocuments]); useEffect(() => { if (artifactsError) { addToast(artifactsError || t("taskDocuments.failedToLoadArtifacts", "Failed to load artifacts"), "error"); } }, [addToast, artifactsError, t]); async function handleExpandDocument(doc: TaskDocument) { if (expandedDocKey === doc.key) { setExpandedDocKey(null); setExpandedContent(""); setEditingDocKey(null); setEditContent(""); setShowHistory(null); setRevisions([]); setRenderMarkdown(false); } else { setExpandedDocKey(doc.key); setExpandedContent(doc.content); setEditingDocKey(null); setEditContent(""); setShowHistory(null); setRevisions([]); setRenderMarkdown(false); } } async function handleToggleHistory(docKey: string) { if (showHistory === docKey) { setShowHistory(null); setRevisions([]); } else { setShowHistory(docKey); setLoadingRevisions(true); try { const revs = await fetchTaskDocumentRevisions(taskId, docKey, projectId); setRevisions(revs); } catch (error) { addToast(getErrorMessage(error) || t("taskDocuments.failedToLoadRevisions", "Failed to load revisions"), "error"); } finally { setLoadingRevisions(false); } } } function handleStartEdit() { if (expandedDocKey) { setEditingDocKey(expandedDocKey); setEditContent(expandedContent); } } function handleCancelEdit() { setEditingDocKey(null); setEditContent(""); } async function handleSaveEdit() { if (!editingDocKey || !editContent.trim()) return; setSaving(true); try { await putTaskDocument(taskId, editingDocKey, editContent, {}, projectId); setEditingDocKey(null); setEditContent(""); await loadDocuments(); // Refresh expanded content const updated = documents.find((d) => d.key === editingDocKey); if (updated) { setExpandedContent(updated.content); } addToast(t("taskDocuments.saved", "Document saved"), "success"); } catch (error) { addToast(getErrorMessage(error) || t("taskDocuments.failedToSave", "Failed to save document"), "error"); } finally { setSaving(false); } } async function handleCreateDocument() { const key = newDocKey.trim(); const content = newDocContent.trim(); // Validate key if (!key) { addToast(t("taskDocuments.keyRequired", "Document key is required"), "error"); return; } if (!DOCUMENT_KEY_REGEX.test(key)) { addToast(t("taskDocuments.invalidKeyFormat", "Invalid key format. Use 1-64 alphanumeric characters, hyphens, or underscores."), "error"); return; } if (!content) { addToast(t("taskDocuments.contentRequired", "Content is required"), "error"); return; } setSaving(true); try { await putTaskDocument(taskId, key, content, {}, projectId); setShowCreateForm(false); setNewDocKey(""); setNewDocContent(""); await loadDocuments(); addToast(t("taskDocuments.created", "Document created"), "success"); } catch (error) { addToast(getErrorMessage(error) || t("taskDocuments.failedToCreate", "Failed to create document"), "error"); } finally { setSaving(false); } } async function handleDeleteDocument(key: string) { setDeletingKey(key); try { await deleteTaskDocument(taskId, key, projectId); setConfirmDelete(null); setDeletingKey(null); if (expandedDocKey === key) { setExpandedDocKey(null); setExpandedContent(""); } if (showHistory === key) { setShowHistory(null); setRevisions([]); } await loadDocuments(); addToast(t("taskDocuments.deleted", "Document deleted"), "success"); } catch (error) { addToast(getErrorMessage(error) || t("taskDocuments.failedToDelete", "Failed to delete document"), "error"); } finally { setDeletingKey(null); } } function handleViewRevision(revision: TaskDocumentRevision) { setExpandedContent(revision.content); setEditingDocKey(null); setEditContent(""); } if (loading || artifactsLoading) { return (

{t("taskDocuments.heading", "Artifacts")}

); } const isEmpty = documents.length === 0 && artifacts.length === 0 && !showCreateForm; return (

{t("taskDocuments.heading", "Artifacts")}

{isEmpty && (
{t("taskDocuments.noDocuments", "No documents or artifacts yet.")}
)} {/* * FNXC:ArtifactRegistry 2026-06-21-21:44: * The per-task Artifacts tab must surface both traditional task documents and agent-created media artifacts so users can inspect all task-scoped outputs without leaving the task modal. */} {artifacts.length > 0 && (
{t("taskDocuments.artifactsSubheading", "Media artifacts")}
{t("taskDocuments.artifactCount", "{{count}} artifact{{plural}}", { count: artifacts.length, plural: artifacts.length === 1 ? "" : "s" })}
{artifacts.map((artifact) => ( ))}
)}
{t("taskDocuments.documentsSubheading", "Task documents")}
{documents.length > 0 && ( {t("taskDocuments.documentCount", "{{count}} document{{plural}}", { count: documents.length, plural: documents.length === 1 ? "" : "s" })} )}
{/* Create Form */} {showCreateForm && (
{t("taskDocuments.newDocumentTitle", "New Document")}
setNewDocKey(e.target.value)} placeholder={t("taskDocuments.keyPlaceholder", "e.g., plan, notes, research")} disabled={saving} /> {t("taskDocuments.keyHint", "Alphanumeric, hyphens, underscores (1-64 chars)")}