import { useEffect, useMemo, useState } from "react"; import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react"; import { AlertCircle, ChevronRight, FileText, 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 }; function supportsHoverCloseControl(): boolean { if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false; return window.matchMedia("(hover: hover) and (pointer: fine)").matches; } 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); const [supportsHoverClose, setSupportsHoverClose] = useState(supportsHoverCloseControl); useEffect(() => { const frame = window.requestAnimationFrame(() => setEntered(true)); return () => window.cancelAnimationFrame(frame); }, []); useEffect(() => { if (typeof window.matchMedia !== "function") return undefined; const query = window.matchMedia("(hover: hover) and (pointer: fine)"); const update = () => setSupportsHoverClose(query.matches); update(); if (typeof query.addEventListener === "function") { query.addEventListener("change", update); return () => query.removeEventListener("change", update); } query.addListener(update); return () => query.removeListener(update); }, []); 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 breadcrumbs = useMemo( () => normalizedPreviewPath.split("/").filter(Boolean), [normalizedPreviewPath], ); const compactBreadcrumbs = useMemo( () => (breadcrumbs.length > 2 ? breadcrumbs.slice(-2) : breadcrumbs), [breadcrumbs], ); const hasCompactPrefix = breadcrumbs.length > compactBreadcrumbs.length; return ( ); }