diff options
Diffstat (limited to 'makima/frontend/src/routes/contract-file.tsx')
| -rw-r--r-- | makima/frontend/src/routes/contract-file.tsx | 659 |
1 files changed, 0 insertions, 659 deletions
diff --git a/makima/frontend/src/routes/contract-file.tsx b/makima/frontend/src/routes/contract-file.tsx deleted file mode 100644 index 9ed25ed..0000000 --- a/makima/frontend/src/routes/contract-file.tsx +++ /dev/null @@ -1,659 +0,0 @@ -import { useEffect, useState, useCallback, useRef } from "react"; -import { useParams, useNavigate } from "react-router"; -import { useAuth } from "../contexts/AuthContext"; -import { Masthead } from "../components/Masthead"; -import { FileDetail, type FocusedElement } from "../components/files/FileDetail"; -import { CliInput } from "../components/files/CliInput"; -import { ConflictNotification } from "../components/files/ConflictNotification"; -import { UpdateNotification } from "../components/files/UpdateNotification"; -import { useFiles } from "../hooks/useFiles"; -import { useVersionHistory } from "../hooks/useVersionHistory"; -import { - useFileSubscription, - type FileUpdateEvent, -} from "../hooks/useFileSubscription"; -import type { FileDetail as FileDetailType, BodyElement } from "../lib/api"; - -/** - * ContractFilePage - Wrapper for viewing files within a contract context - * - * This component handles the /contracts/:contractId/files/:fileId route, - * providing navigation back to the contract and rendering the file detail view. - */ -export default function ContractFilePage() { - const { id: contractId, fileId } = useParams<{ id: string; fileId: string }>(); - const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); - const navigate = useNavigate(); - - // Redirect to login if not authenticated (when auth is configured) - useEffect(() => { - if (!authLoading && isAuthConfigured && !isAuthenticated) { - navigate("/login"); - } - }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); - - // Show loading while checking auth - if (authLoading) { - return ( - <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex items-center justify-center"> - <p className="text-[#7788aa] font-mono text-sm">Loading...</p> - </main> - </div> - ); - } - - // Don't render if not authenticated (will redirect) - if (isAuthConfigured && !isAuthenticated) { - return null; - } - - // Render the file page with contract context - return <ContractAwareFilesPage contractId={contractId} fileId={fileId} />; -} - -// A version of the files page aware of contract context -function ContractAwareFilesPage({ - contractId, - fileId, -}: { - contractId?: string; - fileId?: string; -}) { - const navigate = useNavigate(); - const { error, conflict, clearConflict, fetchFile, editFile, removeFile } = useFiles(); - const [fileDetail, setFileDetail] = useState<FileDetailType | null>(null); - const [detailLoading, setDetailLoading] = useState(false); - const [remoteUpdate, setRemoteUpdate] = useState<FileUpdateEvent | null>(null); - const [remoteFileData, setRemoteFileData] = useState<FileDetailType | null>(null); - const [focusedElement, setFocusedElement] = useState<FocusedElement | null>(null); - const [suggestedPrompt, setSuggestedPrompt] = useState<string | null>(null); - const pendingUpdateRef = useRef(false); - const lastSentVersionRef = useRef<number | null>(null); - const lastSavedVersionRef = useRef<number | null>(null); - const hasLocalChangesRef = useRef(false); - const isActivelyEditingRef = useRef(false); - const currentVersionRef = useRef<number | null>(null); - - // Handle back navigation - go to contract detail instead of /files - const handleBack = useCallback(() => { - if (contractId) { - navigate(`/contracts/${contractId}`); - } else { - navigate("/contracts"); - } - }, [contractId, navigate]); - - const updateHasLocalChanges = useCallback((value: boolean) => { - hasLocalChangesRef.current = value; - }, []); - - const updateIsActivelyEditing = useCallback((value: boolean) => { - isActivelyEditingRef.current = value; - }, []); - - // Version history - const { - versions, - loading: versionsLoading, - selectedVersion, - loadingVersion, - restoring, - fetchVersion, - restoreToVersion, - clearSelectedVersion, - fetchVersions, - } = useVersionHistory({ - fileId: fileId || null, - currentVersion: fileDetail?.version || 0, - }); - - const handleRestoreVersion = useCallback( - async (targetVersion: number) => { - const result = await restoreToVersion(targetVersion); - if (result) { - currentVersionRef.current = result.version; - setFileDetail(result); - updateHasLocalChanges(false); - fetchVersions(); - } - }, - [restoreToVersion, fetchVersions, updateHasLocalChanges] - ); - - // Load file detail when fileId is provided - useEffect(() => { - if (fileId) { - setDetailLoading(true); - updateHasLocalChanges(false); - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - lastSavedVersionRef.current = null; - currentVersionRef.current = null; - setRemoteUpdate(null); - setRemoteFileData(null); - setFocusedElement(null); - fetchFile(fileId).then((detail) => { - if (detail) { - currentVersionRef.current = detail.version; - } - setFileDetail(detail); - setDetailLoading(false); - }); - } else { - setFileDetail(null); - currentVersionRef.current = null; - updateHasLocalChanges(false); - } - }, [fileId, fetchFile, updateHasLocalChanges]); - - // Handle file update events from WebSocket - const handleFileUpdate = useCallback( - async (event: FileUpdateEvent) => { - if (lastSavedVersionRef.current !== null && event.version === lastSavedVersionRef.current) { - lastSavedVersionRef.current = null; - return; - } - - if (pendingUpdateRef.current) { - if (lastSentVersionRef.current !== null) { - const expectedNewVersion = lastSentVersionRef.current + 1; - if (event.version === expectedNewVersion) { - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - return; - } - } - return; - } - - if (currentVersionRef.current !== null && event.version === currentVersionRef.current) { - return; - } - - if (!hasLocalChangesRef.current && !isActivelyEditingRef.current) { - const detail = await fetchFile(event.fileId); - if (detail) { - currentVersionRef.current = detail.version; - } - setFileDetail(detail); - } else { - const remoteData = await fetchFile(event.fileId); - setRemoteFileData(remoteData); - setRemoteUpdate(event); - } - }, - [fetchFile] - ); - - useFileSubscription({ - fileId: fileId || null, - onUpdate: handleFileUpdate, - }); - - const handleDelete = useCallback( - async (id: string) => { - if (confirm("Are you sure you want to delete this file?")) { - const success = await removeFile(id); - if (success && fileId === id) { - handleBack(); - } - } - }, - [removeFile, fileId, handleBack] - ); - - const handleSave = useCallback( - async (id: string, name: string, description: string) => { - if (!fileDetail) return; - pendingUpdateRef.current = true; - lastSentVersionRef.current = fileDetail.version; - try { - const result = await editFile(id, { name, description, version: fileDetail.version }); - if (result) { - lastSavedVersionRef.current = result.version; - currentVersionRef.current = result.version; - setFileDetail(result); - updateHasLocalChanges(false); - } - } finally { - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - } - }, - [editFile, fileDetail, updateHasLocalChanges] - ); - - const handleBodyUpdate = useCallback( - (body: BodyElement[], summary: string | null) => { - if (fileDetail) { - setFileDetail({ - ...fileDetail, - body, - summary, - }); - } - }, - [fileDetail] - ); - - const handleBodyElementUpdate = useCallback( - async (index: number, element: BodyElement) => { - if (fileDetail && fileId) { - const newBody = [...fileDetail.body]; - newBody[index] = element; - - setFileDetail({ - ...fileDetail, - body: newBody, - }); - updateHasLocalChanges(true); - - pendingUpdateRef.current = true; - lastSentVersionRef.current = fileDetail.version; - try { - const result = await editFile(fileId, { body: newBody, version: fileDetail.version }); - if (result) { - lastSavedVersionRef.current = result.version; - currentVersionRef.current = result.version; - setFileDetail(result); - updateHasLocalChanges(false); - } - } finally { - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - } - } - }, - [fileDetail, fileId, editFile, updateHasLocalChanges] - ); - - const handleBodyReorder = useCallback( - async (fromIndex: number, toIndex: number) => { - if (fileDetail && fileId) { - const newBody = [...fileDetail.body]; - const [movedElement] = newBody.splice(fromIndex, 1); - newBody.splice(toIndex, 0, movedElement); - - setFileDetail({ - ...fileDetail, - body: newBody, - }); - updateHasLocalChanges(true); - - pendingUpdateRef.current = true; - lastSentVersionRef.current = fileDetail.version; - try { - const result = await editFile(fileId, { body: newBody, version: fileDetail.version }); - if (result) { - lastSavedVersionRef.current = result.version; - currentVersionRef.current = result.version; - setFileDetail(result); - updateHasLocalChanges(false); - } - } finally { - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - } - } - }, - [fileDetail, fileId, editFile, updateHasLocalChanges] - ); - - const handleBodyElementDelete = useCallback( - async (index: number) => { - if (fileDetail && fileId) { - const newBody = fileDetail.body.filter((_, i) => i !== index); - - setFileDetail({ - ...fileDetail, - body: newBody, - }); - updateHasLocalChanges(true); - - if (focusedElement?.index === index) { - setFocusedElement(null); - } else if (focusedElement && focusedElement.index > index) { - setFocusedElement({ - ...focusedElement, - index: focusedElement.index - 1, - }); - } - - pendingUpdateRef.current = true; - lastSentVersionRef.current = fileDetail.version; - try { - const result = await editFile(fileId, { body: newBody, version: fileDetail.version }); - if (result) { - lastSavedVersionRef.current = result.version; - currentVersionRef.current = result.version; - setFileDetail(result); - updateHasLocalChanges(false); - } - } finally { - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - } - } - }, - [fileDetail, fileId, editFile, updateHasLocalChanges, focusedElement] - ); - - const handleBodyElementDuplicate = useCallback( - async (index: number) => { - if (fileDetail && fileId) { - const elementToDuplicate = fileDetail.body[index]; - if (!elementToDuplicate) return; - - const newBody = [...fileDetail.body]; - newBody.splice(index + 1, 0, { ...elementToDuplicate }); - - setFileDetail({ - ...fileDetail, - body: newBody, - }); - updateHasLocalChanges(true); - - if (focusedElement && focusedElement.index > index) { - setFocusedElement({ - ...focusedElement, - index: focusedElement.index + 1, - }); - } - - pendingUpdateRef.current = true; - lastSentVersionRef.current = fileDetail.version; - try { - const result = await editFile(fileId, { body: newBody, version: fileDetail.version }); - if (result) { - lastSavedVersionRef.current = result.version; - currentVersionRef.current = result.version; - setFileDetail(result); - updateHasLocalChanges(false); - } - } finally { - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - } - } - }, - [fileDetail, fileId, editFile, updateHasLocalChanges, focusedElement] - ); - - const handleFocusElement = useCallback((element: FocusedElement | null) => { - setFocusedElement(element); - }, []); - - const handleClearFocus = useCallback(() => { - setFocusedElement(null); - }, []); - - const handleConvertElement = useCallback( - async (index: number, toType: string) => { - if (!fileDetail || !fileId) return; - - const element = fileDetail.body[index]; - if (!element) return; - - let textContent = ""; - switch (element.type) { - case "heading": - case "paragraph": - textContent = element.text; - break; - case "code": - textContent = element.content; - break; - case "list": - textContent = element.items.join("\n"); - break; - default: - return; - } - - let newElement: BodyElement; - if (toType === "paragraph") { - newElement = { type: "paragraph", text: textContent }; - } else if (toType === "list_unordered") { - const items = textContent.split("\n").filter(line => line.trim()); - newElement = { type: "list", ordered: false, items }; - } else if (toType === "list_ordered") { - const items = textContent.split("\n").filter(line => line.trim()); - newElement = { type: "list", ordered: true, items }; - } else if (toType === "code") { - newElement = { type: "code", content: textContent }; - } else if (toType.startsWith("heading_")) { - const level = parseInt(toType.replace("heading_", ""), 10) as 1 | 2 | 3 | 4 | 5 | 6; - newElement = { type: "heading", level, text: textContent }; - } else { - return; - } - - const newBody = [...fileDetail.body]; - newBody[index] = newElement; - - setFileDetail({ ...fileDetail, body: newBody }); - updateHasLocalChanges(true); - - if (focusedElement?.index === index) { - setFocusedElement({ - index, - type: newElement.type, - preview: textContent.slice(0, 50) + (textContent.length > 50 ? "..." : ""), - }); - } - - pendingUpdateRef.current = true; - lastSentVersionRef.current = fileDetail.version; - try { - const result = await editFile(fileId, { body: newBody, version: fileDetail.version }); - if (result) { - lastSavedVersionRef.current = result.version; - currentVersionRef.current = result.version; - setFileDetail(result); - updateHasLocalChanges(false); - } - } finally { - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - } - }, - [fileDetail, fileId, editFile, updateHasLocalChanges, focusedElement] - ); - - const handleGenerateFromElement = useCallback( - (index: number, action: string) => { - if (!fileDetail) return; - - const element = fileDetail.body[index]; - if (!element) return; - - let preview = ""; - switch (element.type) { - case "heading": - case "paragraph": - preview = element.text.slice(0, 50); - break; - case "code": - preview = element.content.slice(0, 50); - break; - case "list": - preview = element.items[0]?.slice(0, 40) || ""; - break; - default: - preview = "Element"; - } - - setFocusedElement({ - index, - type: element.type, - preview: preview + (preview.length >= 50 ? "..." : ""), - }); - - let prompt = ""; - switch (action) { - case "elaborate": - prompt = "Elaborate and expand on this content"; - break; - case "summarize": - prompt = "Summarize this content"; - break; - case "extract_actions": - prompt = "Extract action items from this content"; - break; - } - setSuggestedPrompt(prompt); - }, - [fileDetail] - ); - - // Conflict resolution handlers - const handleConflictReload = useCallback(async () => { - if (fileId) { - clearConflict(); - const detail = await fetchFile(fileId); - if (detail) { - currentVersionRef.current = detail.version; - } - setFileDetail(detail); - updateHasLocalChanges(false); - } - }, [fileId, clearConflict, fetchFile, updateHasLocalChanges]); - - const handleConflictForceOverwrite = useCallback(async () => { - if (fileId && fileDetail) { - clearConflict(); - const latest = await fetchFile(fileId); - if (latest) { - pendingUpdateRef.current = true; - lastSentVersionRef.current = latest.version; - try { - const result = await editFile(fileId, { body: fileDetail.body, version: latest.version }); - if (result) { - lastSavedVersionRef.current = result.version; - currentVersionRef.current = result.version; - setFileDetail(result); - updateHasLocalChanges(false); - } - } finally { - pendingUpdateRef.current = false; - lastSentVersionRef.current = null; - } - } - } - }, [fileId, fileDetail, clearConflict, fetchFile, editFile, updateHasLocalChanges]); - - const handleRemoteUpdateRefresh = useCallback(async () => { - if (fileId) { - const detail = await fetchFile(fileId); - if (detail) { - currentVersionRef.current = detail.version; - } - setFileDetail(detail); - setRemoteUpdate(null); - setRemoteFileData(null); - updateHasLocalChanges(false); - } - }, [fileId, fetchFile, updateHasLocalChanges]); - - const handleRemoteUpdateDismiss = useCallback(() => { - setRemoteUpdate(null); - setRemoteFileData(null); - }, []); - - return ( - <div className="relative z-10 h-screen flex flex-col overflow-hidden"> - <Masthead showTicker={false} showNav /> - - <main className="flex-1 p-4 md:p-6 min-h-0 overflow-hidden flex flex-col"> - {error && ( - <div className="mb-4 p-3 border border-red-400/50 bg-red-400/10 text-red-400 font-mono text-sm shrink-0"> - {error} - </div> - )} - - {fileId && fileDetail ? ( - <div className="flex-1 flex flex-col min-h-0 overflow-hidden"> - <div className="flex-1 min-h-0 overflow-hidden"> - <FileDetail - file={fileDetail} - loading={detailLoading} - onBack={handleBack} - onSave={handleSave} - onDelete={handleDelete} - onBodyElementUpdate={handleBodyElementUpdate} - onBodyReorder={handleBodyReorder} - onBodyElementDelete={handleBodyElementDelete} - onBodyElementDuplicate={handleBodyElementDuplicate} - onConvertElement={handleConvertElement} - onGenerateFromElement={handleGenerateFromElement} - onEditingChange={updateIsActivelyEditing} - hasPendingRemoteUpdate={!!remoteUpdate} - onOverwrite={handleRemoteUpdateDismiss} - focusedElement={focusedElement} - onFocusElement={handleFocusElement} - versions={versions} - versionsLoading={versionsLoading} - selectedVersion={selectedVersion} - loadingVersion={loadingVersion} - restoring={restoring} - onSelectVersion={fetchVersion} - onRestoreVersion={handleRestoreVersion} - onClearVersionSelection={clearSelectedVersion} - /> - </div> - <div className="shrink-0"> - <CliInput - fileId={fileId} - onUpdate={handleBodyUpdate} - focusedElement={focusedElement} - onClearFocus={handleClearFocus} - suggestedPrompt={suggestedPrompt} - onClearSuggestedPrompt={() => setSuggestedPrompt(null)} - /> - </div> - </div> - ) : fileId && detailLoading ? ( - <div className="panel h-full flex items-center justify-center"> - <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> - </div> - ) : ( - <div className="panel h-full flex items-center justify-center"> - <div className="text-center"> - <p className="font-mono text-sm text-[#555] mb-4"> - File not found - </p> - <button - onClick={handleBack} - className="px-4 py-2 font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors" - > - ← Back to Contract - </button> - </div> - </div> - )} - </main> - - {/* Conflict notification */} - {conflict?.hasConflict && ( - <ConflictNotification - onReload={handleConflictReload} - onForceOverwrite={handleConflictForceOverwrite} - onDismiss={clearConflict} - /> - )} - - {/* Remote update notification */} - {remoteUpdate && ( - <UpdateNotification - updatedBy={remoteUpdate.updatedBy} - localBody={fileDetail?.body || []} - remoteBody={remoteFileData?.body || []} - onRefresh={handleRemoteUpdateRefresh} - onDismiss={handleRemoteUpdateDismiss} - /> - )} - </div> - ); -} |
