From 5f45f13faed1d76e154b6171fdd1d078f176aca0 Mon Sep 17 00:00:00 2001 From: qixinbo Date: Mon, 30 Mar 2026 22:30:10 +0800 Subject: [PATCH] feat: more artifacts support --- frontend/src/components/ArtifactPanel.tsx | 81 +++++++++++++++++++---- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/ArtifactPanel.tsx b/frontend/src/components/ArtifactPanel.tsx index 1c4f0b3..a6f4706 100644 --- a/frontend/src/components/ArtifactPanel.tsx +++ b/frontend/src/components/ArtifactPanel.tsx @@ -1,8 +1,11 @@ -import { useState, useEffect } from "react"; -import { Code2, Eye, X, Download, Copy, ExternalLink, Check, ChevronDown } from "lucide-react"; +import { useState, useEffect, useMemo } from "react"; +import { Code2, Eye, X, Download, Copy, ExternalLink, Check, ChevronDown, FileIcon } 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 ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; import { cn } from "@/lib/utils"; interface ArtifactPreviewTarget { @@ -19,23 +22,49 @@ interface ArtifactPanelProps { export function ArtifactPanel({ artifact, onClose }: ArtifactPanelProps) { const { t } = useTranslation(); - const [activeTab, setActiveTab] = useState<'code' | 'preview'>('preview'); + const [activeTab, setActiveTab] = useState<'code' | 'preview' | 'fallback'>('preview'); const [code, setCode] = useState(''); const [loadingCode, setLoadingCode] = useState(false); const [copied, setCopied] = useState(false); + const { canPreview, canCode, isMarkdown } = useMemo(() => { + const extension = artifact.name.split('.').pop()?.toLowerCase() || ''; + const textExtensions = ['py', 'js', 'ts', 'jsx', 'tsx', 'json', 'csv', 'md', 'txt', 'css', 'html', 'xml', 'yaml', 'yml', 'sql', 'sh', 'bat']; + const isTextExt = textExtensions.includes(extension); + const isHtmlExt = extension === 'html' || extension === 'htm'; + const isMd = extension === 'md' || artifact.mimeType === 'text/markdown'; + + const isImage = artifact.mimeType.startsWith('image/'); + const isPdf = artifact.mimeType === 'application/pdf' || extension === 'pdf'; + const isHtml = artifact.mimeType === 'text/html' || isHtmlExt; + const isText = artifact.mimeType.startsWith('text/') || + ["application/json", "application/javascript", "application/xml", "application/sql", "application/x-sh"].includes(artifact.mimeType) || + isTextExt; + + return { + canPreview: isImage || isPdf || isHtml || isMd, + canCode: isText || isHtml || isMd, + isMarkdown: isMd + }; + }, [artifact.mimeType, artifact.name]); + useEffect(() => { // Reset state when artifact changes setCode(''); - if (artifact.mimeType.startsWith("image/") || artifact.mimeType.startsWith("application/pdf")) { + if (canPreview) { setActiveTab('preview'); + } else if (canCode) { + setActiveTab('code'); } else { - setActiveTab('preview'); + setActiveTab('fallback'); } - }, [artifact]); + }, [artifact, canPreview, canCode]); useEffect(() => { - if (activeTab === 'code' && !code && artifact.downloadUrl) { + // Need to fetch code for both 'code' view and markdown 'preview' view + const needsCodeFetch = (activeTab === 'code' || (activeTab === 'preview' && isMarkdown)) && !code && artifact.downloadUrl; + + if (needsCodeFetch) { setLoadingCode(true); fetch(artifact.downloadUrl) .then(res => res.text()) @@ -49,7 +78,7 @@ export function ArtifactPanel({ artifact, onClose }: ArtifactPanelProps) { setLoadingCode(false); }); } - }, [activeTab, artifact.downloadUrl, code]); + }, [activeTab, artifact.downloadUrl, code, isMarkdown]); const handleCopy = async () => { if (code) { @@ -63,7 +92,7 @@ export function ArtifactPanel({ artifact, onClose }: ArtifactPanelProps) { } }; - const isCodeViewSupported = !artifact.mimeType.startsWith("image/") && !artifact.mimeType.startsWith("application/pdf"); + const showToggle = canPreview && canCode; return (
@@ -74,7 +103,7 @@ export function ArtifactPanel({ artifact, onClose }: ArtifactPanelProps) {
- {isCodeViewSupported && ( + {showToggle && (