import { useState, useCallback, useEffect, useRef } from "react";
import { useParams, useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import { FileList } from "../components/files/FileList";
import { FileDetail } 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 {
useFileSubscription,
type FileUpdateEvent,
} from "../hooks/useFileSubscription";
import type { FileDetail as FileDetailType, BodyElement } from "../lib/api";
export default function FilesPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { files, loading, error, conflict, clearConflict, fetchFile, editFile, removeFile, saveFile } = useFiles();
const [fileDetail, setFileDetail] = useState<FileDetailType | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [remoteUpdate, setRemoteUpdate] = useState<FileUpdateEvent | null>(null);
const [hasLocalChanges, setHasLocalChanges] = useState(false);
const pendingUpdateRef = useRef(false);
// Load file detail when URL has an id
useEffect(() => {
if (id) {
setDetailLoading(true);
setHasLocalChanges(false);
fetchFile(id).then((detail) => {
setFileDetail(detail);
setDetailLoading(false);
});
} else {
setFileDetail(null);
setHasLocalChanges(false);
}
}, [id, fetchFile]);
// Handle file update events from WebSocket
const handleFileUpdate = useCallback(
async (event: FileUpdateEvent) => {
// Ignore our own updates
if (pendingUpdateRef.current) {
pendingUpdateRef.current = false;
return;
}
// If no local changes, auto-refresh
if (!hasLocalChanges) {
const detail = await fetchFile(event.fileId);
setFileDetail(detail);
} else {
// Show notification about remote update
setRemoteUpdate(event);
}
},
[hasLocalChanges, fetchFile]
);
// Subscribe to file updates
useFileSubscription({
fileId: id || null,
onUpdate: handleFileUpdate,
});
const handleSelectFile = useCallback(
(fileId: string) => {
navigate(`/files/${fileId}`);
},
[navigate]
);
const handleBack = useCallback(() => {
navigate("/files");
}, [navigate]);
const handleDelete = useCallback(
async (fileId: string) => {
if (confirm("Are you sure you want to delete this file?")) {
const success = await removeFile(fileId);
if (success && id === fileId) {
navigate("/files");
}
}
},
[removeFile, id, navigate]
);
const handleSave = useCallback(
async (fileId: string, name: string, description: string) => {
if (!fileDetail) return;
pendingUpdateRef.current = true;
const result = await editFile(fileId, { name, description, version: fileDetail.version });
if (result) {
setFileDetail(result);
setHasLocalChanges(false);
}
},
[editFile, fileDetail]
);
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 && id) {
// Create new body array with updated element
const newBody = [...fileDetail.body];
newBody[index] = element;
// Update local state immediately for responsiveness
setFileDetail({
...fileDetail,
body: newBody,
});
setHasLocalChanges(true);
// Save to backend with version for optimistic locking
pendingUpdateRef.current = true;
const result = await editFile(id, { body: newBody, version: fileDetail.version });
if (result) {
setFileDetail(result);
setHasLocalChanges(false);
}
}
},
[fileDetail, id, editFile]
);
const handleBodyReorder = useCallback(
async (fromIndex: number, toIndex: number) => {
if (fileDetail && id) {
// Create new body array with reordered elements
const newBody = [...fileDetail.body];
const [movedElement] = newBody.splice(fromIndex, 1);
newBody.splice(toIndex, 0, movedElement);
// Update local state immediately for responsiveness
setFileDetail({
...fileDetail,
body: newBody,
});
setHasLocalChanges(true);
// Save to backend with version for optimistic locking
pendingUpdateRef.current = true;
const result = await editFile(id, { body: newBody, version: fileDetail.version });
if (result) {
setFileDetail(result);
setHasLocalChanges(false);
}
}
},
[fileDetail, id, editFile]
);
const handleCreate = useCallback(async () => {
if (creating) return;
setCreating(true);
try {
const newFile = await saveFile({
name: `Untitled ${new Date().toLocaleDateString()}`,
transcript: [],
});
if (newFile) {
navigate(`/files/${newFile.id}`);
}
} finally {
setCreating(false);
}
}, [creating, saveFile, navigate]);
// Conflict resolution handlers
const handleConflictReload = useCallback(async () => {
if (id) {
clearConflict();
const detail = await fetchFile(id);
setFileDetail(detail);
setHasLocalChanges(false);
}
}, [id, clearConflict, fetchFile]);
const handleConflictForceOverwrite = useCallback(async () => {
if (id && fileDetail) {
clearConflict();
// Fetch latest version first
const latest = await fetchFile(id);
if (latest) {
// Retry with latest version
pendingUpdateRef.current = true;
const result = await editFile(id, { body: fileDetail.body, version: latest.version });
if (result) {
setFileDetail(result);
setHasLocalChanges(false);
}
}
}
}, [id, fileDetail, clearConflict, fetchFile, editFile]);
// Remote update handlers
const handleRemoteUpdateRefresh = useCallback(async () => {
if (id) {
const detail = await fetchFile(id);
setFileDetail(detail);
setRemoteUpdate(null);
setHasLocalChanges(false);
}
}, [id, fetchFile]);
const handleRemoteUpdateDismiss = useCallback(() => {
setRemoteUpdate(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>
)}
{id && 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}
/>
</div>
<div className="shrink-0">
<CliInput fileId={id} onUpdate={handleBodyUpdate} />
</div>
</div>
) : id && detailLoading ? (
<div className="panel h-full flex items-center justify-center">
<div className="font-mono text-[#9bc3ff] text-sm">Loading...</div>
</div>
) : (
<FileList
files={files}
loading={loading || creating}
onSelect={handleSelectFile}
onDelete={handleDelete}
onCreate={handleCreate}
/>
)}
</main>
{/* Conflict notification */}
{conflict?.hasConflict && (
<ConflictNotification
onReload={handleConflictReload}
onForceOverwrite={handleConflictForceOverwrite}
onDismiss={clearConflict}
/>
)}
{/* Remote update notification */}
{remoteUpdate && (
<UpdateNotification
updatedBy={remoteUpdate.updatedBy}
onRefresh={handleRemoteUpdateRefresh}
onDismiss={handleRemoteUpdateDismiss}
/>
)}
</div>
);
}