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 ? (
) : (
{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);
}