"use client"; import { useState, useEffect, useMemo } from "react"; import { useTheme } from "next-themes"; import { HugeiconsIcon } from "@hugeicons/react"; import { Cancel01Icon, Copy01Icon, Tick01Icon, Loading02Icon } from "@hugeicons/core-free-icons"; import { Button } from "@/components/ui/button"; import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { atomOneLight } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { Streamdown } from "streamdown"; import { cjk } from "@streamdown/cjk"; import { code } from "@streamdown/code"; import { math } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import type { FilePreview as FilePreviewType } from "@/types"; const streamdownPlugins = { cjk, code, math, mermaid }; type ViewMode = "source" | "rendered"; interface DocPreviewProps { filePath: string; viewMode: ViewMode; onViewModeChange: (mode: ViewMode) => void; onClose: () => void; width: number; } /** Extensions that support a rendered preview */ const RENDERABLE_EXTENSIONS = new Set([".md", ".mdx", ".html", ".htm"]); function getExtension(filePath: string): string { const dot = filePath.lastIndexOf("."); return dot >= 0 ? filePath.slice(dot).toLowerCase() : ""; } function isRenderable(filePath: string): boolean { return RENDERABLE_EXTENSIONS.has(getExtension(filePath)); } function isHtml(filePath: string): boolean { const ext = getExtension(filePath); return ext === ".html" || ext === ".htm"; } export function DocPreview({ filePath, viewMode, onViewModeChange, onClose, width, }: DocPreviewProps) { const { resolvedTheme } = useTheme(); const isDark = resolvedTheme === "dark"; const [preview, setPreview] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); useEffect(() => { let cancelled = false; async function loadPreview() { setLoading(true); setError(null); try { const res = await fetch( `/api/files/preview?path=${encodeURIComponent(filePath)}&maxLines=500` ); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Failed to load file"); } const data = await res.json(); if (!cancelled) { setPreview(data.preview); } } catch (err) { if (!cancelled) { setError(err instanceof Error ? err.message : "Failed to load file"); } } finally { if (!cancelled) { setLoading(false); } } } loadPreview(); return () => { cancelled = true; }; }, [filePath]); const handleCopyContent = async () => { const text = preview?.content || filePath; await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const fileName = filePath.split("/").pop() || filePath; // Build breadcrumb — show last 3 segments const breadcrumb = useMemo(() => { const segments = filePath.split("/").filter(Boolean); const display = segments.slice(-3); const prefix = display.length < segments.length ? ".../" : ""; return prefix + display.join("/"); }, [filePath]); const canRender = isRenderable(filePath); return (
{/* Header */}

{fileName}

{canRender && ( )}
{/* Breadcrumb + language — subtle, no border */}

{breadcrumb}

{preview && ( {preview.language} )}
{/* Content */}
{loading ? (
) : error ? (

{error}

) : preview ? ( viewMode === "rendered" && canRender ? ( ) : ( ) ) : null}
); } /** Capsule toggle for Source / Preview view mode */ function ViewModeToggle({ value, onChange, }: { value: ViewMode; onChange: (v: ViewMode) => void; }) { return (
); } /** Source code view using react-syntax-highlighter */ function SourceView({ preview, isDark }: { preview: FilePreviewType; isDark: boolean }) { return (
{preview.content}
); } /** Rendered view for markdown / HTML files */ function RenderedView({ content, filePath, }: { content: string; filePath: string; }) { if (isHtml(filePath)) { return (