import { useEffect, useMemo, useState } from "react"; import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react"; import { AlertCircle, ChevronRight, Loader2, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { CodeBlock } from "@/components/CodeBlock"; import { splitFilePath } from "@/components/FileReferenceChip"; import { ApiError, fetchFilePreview } from "@/lib/api"; import type { FilePreviewPayload } from "@/lib/types"; import { cn } from "@/lib/utils"; interface FilePreviewPanelProps { sessionKey: string; path: string; token: string; desktopWidth?: number; isClosing?: boolean; onResizeStart?: (event: ReactPointerEvent) => void; onClose: () => void; } type PreviewState = | { status: "loading" } | { status: "error"; message: string } | { status: "ready"; payload: FilePreviewPayload }; export function FilePreviewPanel({ sessionKey, path, token, desktopWidth = 544, isClosing = false, onResizeStart, onClose, }: FilePreviewPanelProps) { const { t } = useTranslation(); const [state, setState] = useState({ status: "loading" }); const [entered, setEntered] = useState(false); useEffect(() => { const frame = window.requestAnimationFrame(() => setEntered(true)); return () => window.cancelAnimationFrame(frame); }, []); useEffect(() => { let cancelled = false; setState({ status: "loading" }); fetchFilePreview(token, sessionKey, path) .then((payload) => { if (!cancelled) setState({ status: "ready", payload }); }) .catch((error: unknown) => { if (cancelled) return; const message = error instanceof ApiError ? (error.status === 404 && /API route not found/i.test(error.message) ? t("filePreview.routeMissing", { defaultValue: "File preview needs the latest gateway. Restart nanobot gateway and try again.", }) : error.message) : t("filePreview.failed", { defaultValue: "Could not preview this file." }); setState({ status: "error", message }); }); return () => { cancelled = true; }; }, [path, sessionKey, t, token]); const displayPath = state.status === "ready" ? state.payload.display_path : path; const previewPath = state.status === "ready" ? state.payload.path : displayPath; const normalizedPreviewPath = previewPath.replace(/\\/g, "/"); const hasRootPrefix = normalizedPreviewPath.startsWith("/"); const { name } = splitFilePath(displayPath); const fileName = name || displayPath; const pathParts = useMemo( () => normalizedPreviewPath.split("/").filter(Boolean), [normalizedPreviewPath], ); const directoryParts = useMemo( () => (pathParts.length > 1 ? pathParts.slice(0, -1) : []), [pathParts], ); const breadcrumbParts = useMemo( () => (directoryParts.length > 0 ? [...directoryParts, fileName] : [fileName]), [directoryParts, fileName], ); const compactBreadcrumbParts = useMemo( () => (breadcrumbParts.length > 3 ? breadcrumbParts.slice(-3) : breadcrumbParts), [breadcrumbParts], ); const hasCompactPrefix = breadcrumbParts.length > compactBreadcrumbParts.length; const breadcrumbTitle = `${hasRootPrefix ? "/" : ""}${[ ...directoryParts, fileName, ].join("/")}`; return ( ); }