diff --git a/frontend/src/components/ArtifactPanel.tsx b/frontend/src/components/ArtifactPanel.tsx new file mode 100644 index 0000000..1c4f0b3 --- /dev/null +++ b/frontend/src/components/ArtifactPanel.tsx @@ -0,0 +1,202 @@ +import { useState, useEffect } from "react"; +import { Code2, Eye, X, Download, Copy, ExternalLink, Check, ChevronDown } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import { cn } from "@/lib/utils"; + +interface ArtifactPreviewTarget { + name: string; + mimeType: string; + previewUrl: string; + downloadUrl: string; +} + +interface ArtifactPanelProps { + artifact: ArtifactPreviewTarget; + onClose: () => void; +} + +export function ArtifactPanel({ artifact, onClose }: ArtifactPanelProps) { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState<'code' | 'preview'>('preview'); + const [code, setCode] = useState(''); + const [loadingCode, setLoadingCode] = useState(false); + const [copied, setCopied] = useState(false); + + useEffect(() => { + // Reset state when artifact changes + setCode(''); + if (artifact.mimeType.startsWith("image/") || artifact.mimeType.startsWith("application/pdf")) { + setActiveTab('preview'); + } else { + setActiveTab('preview'); + } + }, [artifact]); + + useEffect(() => { + if (activeTab === 'code' && !code && artifact.downloadUrl) { + setLoadingCode(true); + fetch(artifact.downloadUrl) + .then(res => res.text()) + .then(text => { + setCode(text); + setLoadingCode(false); + }) + .catch(err => { + console.error("Failed to fetch code", err); + setCode("Failed to load code."); + setLoadingCode(false); + }); + } + }, [activeTab, artifact.downloadUrl, code]); + + const handleCopy = async () => { + if (code) { + try { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy text: ', err); + } + } + }; + + const isCodeViewSupported = !artifact.mimeType.startsWith("image/") && !artifact.mimeType.startsWith("application/pdf"); + + return ( +
+ {/* Header */} +
+
+ {artifact.name} + +
+ + {isCodeViewSupported && ( +
+ + +
+ )} + +
+ {activeTab === 'code' && ( + + )} + + + + + + +
+ +
+
+ + {/* Content */} +
+ {activeTab === 'preview' ? ( +
+ {artifact.mimeType.startsWith("image/") ? ( + {artifact.name} + ) : ( +