gitdataai/src/components/chat/CodePreviewPanel.tsx

152 lines
5.9 KiB
TypeScript

import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { Check, Copy, PanelRightClose, Square } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { CodePreviewPayload } from "@/components/chat/CodePreviewContext";
interface CodePreviewPanelProps {
code: CodePreviewPayload | null;
onClose: () => void;
}
export const CodePreviewPanel = memo(function CodePreviewPanel({ code, onClose }: CodePreviewPanelProps) {
const [copied, setCopied] = useState(false);
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
const lines = useMemo(() => code?.code.replace(/\n$/, "").split("\n") ?? [], [code?.code]);
const canPreview = code?.language === "html";
const isSubAgent = code?.kind === "subagent";
const handleCopy = useCallback(() => {
if (!code) return;
navigator.clipboard.writeText(code.code).then(() => {
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
});
}, [code]);
// Sync viewMode when code changes
useEffect(() => {
if (code?.previewMode) {
setViewMode(code.previewMode);
}
}, [code?.previewMode]);
return (
<aside
className="h-full shrink-0 overflow-hidden border-l transition-[width,opacity,transform] duration-300 ease-out"
style={{
width: code ? "min(48vw, 760px)" : 0,
opacity: code ? 1 : 0,
transform: code ? "translateX(0)" : "translateX(18px)",
borderColor: "var(--border-subtle)",
background: "var(--surface-ground)",
}}
aria-hidden={!code}
>
<div className="flex h-full w-[min(48vw,760px)] flex-col">
<div
className={isSubAgent ? "flex min-h-14 shrink-0 items-center justify-between gap-3 border-b px-4 py-3" : "flex h-12 shrink-0 items-center justify-between gap-3 border-b px-3"}
style={{ borderColor: "var(--border-subtle)" }}
>
<div className="min-w-0">
<div className={isSubAgent ? "truncate text-sm font-medium" : "truncate text-sm font-medium"} style={{ color: "var(--text-primary)" }}>
{code?.title || code?.language || "text"}
</div>
<div className={isSubAgent ? "mt-0.5 line-clamp-1 text-xs" : "text-xs"} style={{ color: "var(--text-muted)" }}>
{code?.subtitle || `${code?.lineCount ?? 0} lines`}
</div>
</div>
<div className="flex items-center gap-1">
{isSubAgent && code?.status && (
<span
className="mr-2 rounded-md border px-2 py-1 text-xs"
style={{
borderColor: "var(--border-subtle)",
color: "var(--text-muted)",
backgroundColor: "var(--surface-elevated)",
}}
>
{code.status === "ok" ? "completed" : code.status}
</span>
)}
{isSubAgent && code?.status === "pending" && (
<Button variant="ghost" size="sm" onClick={code.onStop}>
<Square data-icon="inline-start" />
Stop
</Button>
)}
{canPreview && (
<div className="flex items-center rounded-md border overflow-hidden mr-2" style={{ borderColor: "var(--border-default)" }}>
<button
onClick={() => setViewMode("code")}
className="px-3 py-1 text-xs font-medium transition-colors"
style={{
backgroundColor: viewMode === "code" ? "var(--accent)" : "transparent",
color: viewMode === "code" ? "var(--accent-fg)" : "var(--text-secondary)",
}}
>
Code
</button>
<button
onClick={() => setViewMode("preview")}
className="px-3 py-1 text-xs font-medium transition-colors"
style={{
backgroundColor: viewMode === "preview" ? "var(--accent)" : "transparent",
color: viewMode === "preview" ? "var(--accent-fg)" : "var(--text-secondary)",
}}
>
Preview
</button>
</div>
)}
<Button variant="ghost" size="sm" onClick={handleCopy} disabled={!code}>
{copied ? <Check data-icon="inline-start" /> : <Copy data-icon="inline-start" />}
{copied ? "Copied" : "Copy"}
</Button>
<Button variant="ghost" size="icon-sm" onClick={onClose} aria-label="Close code preview">
<PanelRightClose />
</Button>
</div>
</div>
<div className="min-h-0 flex-1 overflow-auto">
{viewMode === "preview" && canPreview ? (
<iframe
title="HTML Preview"
srcDoc={code?.code ?? ""}
className="w-full h-full border-0"
sandbox="allow-scripts"
/>
) : (
<div
className="grid min-w-max grid-cols-[auto_1fr] text-[13px] leading-[1.65]"
>
<div
className="select-none border-r px-3 py-4 text-right font-mono tabular-nums"
style={{
borderColor: "var(--border-subtle)",
color: "var(--text-muted)",
backgroundColor: "var(--surface-elevated)",
}}
aria-hidden="true"
>
{lines.map((_, index) => (
<div key={index}>{index + 1}</div>
))}
</div>
<pre
className="m-0 min-h-full px-4 py-4 font-mono outline-none"
style={{
color: "var(--text-primary)",
backgroundColor: "var(--surface-ground)",
}}
>
<code>{code?.code ?? ""}</code>
</pre>
</div>
)}
</div>
</div>
</aside>
);
});