import { useState, useMemo } from 'react'; import { FileText, FileCode2, Table2, Image as ImageIcon, Download, Copy, ChevronLeft, File, } from 'lucide-react'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface ArtifactFile { id: string; name: string; type: 'markdown' | 'code' | 'table' | 'image' | 'text'; content: string; language?: string; createdAt: Date; sourceStepId?: string; // Links to ToolCallStep that created this artifact } interface ArtifactPanelProps { artifacts: ArtifactFile[]; selectedId?: string | null; onSelect: (id: string) => void; onClose?: () => void; className?: string; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function getFileIcon(type: ArtifactFile['type']) { switch (type) { case 'markdown': return FileText; case 'code': return FileCode2; case 'table': return Table2; case 'image': return ImageIcon; default: return File; } } function getTypeLabel(type: ArtifactFile['type']): string { switch (type) { case 'markdown': return 'MD'; case 'code': return 'CODE'; case 'table': return 'TABLE'; case 'image': return 'IMG'; default: return 'TXT'; } } function getTypeColor(type: ArtifactFile['type']): string { switch (type) { case 'markdown': return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'; case 'code': return 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'; case 'table': return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'; case 'image': return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300'; default: return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'; } } // --------------------------------------------------------------------------- // ArtifactPanel // --------------------------------------------------------------------------- export function ArtifactPanel({ artifacts, selectedId, onSelect, onClose: _onClose, className = '', }: ArtifactPanelProps) { const [viewMode, setViewMode] = useState<'preview' | 'code'>('preview'); const selected = useMemo( () => artifacts.find((a) => a.id === selectedId), [artifacts, selectedId] ); // List view when no artifact is selected if (!selected) { return (
{artifacts.length === 0 ? (

暂无产物文件

Agent 生成文件后将在此显示

) : (
{artifacts.map((artifact) => { const Icon = getFileIcon(artifact.type); return ( ); })}
)}
); } // Detail view const Icon = getFileIcon(selected.type); return (
{/* File header */}
{selected.name} {getTypeLabel(selected.type)}
{/* View mode toggle */}
{/* Content area */}
{viewMode === 'preview' ? (
{selected.type === 'markdown' ? ( ) : selected.type === 'code' ? (
                {selected.content}
              
) : (
                {selected.content}
              
)}
) : (
            {selected.content}
          
)}
{/* Action bar */}
} label="复制" onClick={() => navigator.clipboard.writeText(selected.content)} /> } label="下载" onClick={() => downloadArtifact(selected)} />
); } // --------------------------------------------------------------------------- // ActionButton // --------------------------------------------------------------------------- function ActionButton({ icon, label, onClick }: { icon: React.ReactNode; label: string; onClick: () => void }) { const [copied, setCopied] = useState(false); const handleClick = () => { onClick(); if (label === '复制') { setCopied(true); setTimeout(() => setCopied(false), 1500); } }; return ( ); } // --------------------------------------------------------------------------- // Simple Markdown preview (no external deps) // --------------------------------------------------------------------------- function MarkdownPreview({ content }: { content: string }) { // Basic markdown rendering: headings, bold, code blocks, lists const lines = content.split('\n'); return (
{lines.map((line, i) => { // Heading if (line.startsWith('### ')) { return

{line.slice(4)}

; } if (line.startsWith('## ')) { return

{line.slice(3)}

; } if (line.startsWith('# ')) { return

{line.slice(2)}

; } // Code block (simplified) if (line.startsWith('```')) return null; // List item if (line.startsWith('- ') || line.startsWith('* ')) { return
  • {renderInline(line.slice(2))}
  • ; } // Empty line if (!line.trim()) return
    ; // Regular paragraph return

    {renderInline(line)}

    ; })}
    ); } function renderInline(text: string): React.ReactNode { // Bold const parts = text.split(/\*\*(.*?)\*\*/g); return parts.map((part, i) => i % 2 === 1 ? {part} : part ); } // --------------------------------------------------------------------------- // Download helper // --------------------------------------------------------------------------- function downloadArtifact(artifact: ArtifactFile) { const blob = new Blob([artifact.content], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = artifact.name; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }