ArtifactPanel Component
The ArtifactPanel component provides a comprehensive side panel interface for viewing, editing, and managing code and content artifacts generated during chat conversations. It features dual-mode display (preview/edit), intelligent copy strategies, file download capabilities, and real-time interactive previews for supported artifact types.
Overview
ArtifactPanel delivers advanced artifact management capabilities including:
- Dual-mode interface with preview and edit tabs
- Real-time interactive previews for HTML, React, and JavaScript artifacts
- Intelligent copy strategies based on artifact type
- Comprehensive file download with proper extensions
- Type-aware content rendering and syntax highlighting
- Responsive layout with fixed and inline positioning options
- Context-driven artifact selection and management
Architecture
Core Dependencies
import { ChevronLeft, ChevronRight, X, Edit3, Eye, Copy, Check, Download, Save, RotateCcw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import Markdown from 'react-markdown';
import { useArtifacts } from '@/contexts/ArtifactContext';
import { ArtifactPreview } from './ArtifactPreview';
import { artifactMarkdownComponents } from '@/components/markdown/artifactComponents';
import { copyAsMarkdown, copyContentToClipboard, copyAsBasicHTML } from '@/utils/copyToClipboard';
import QuartoExportButton, { type ExportFormat } from '@/components/QuartoExportButton';
Component Structure
ArtifactPanel
├── Toggle Button (when panel closed)
├── Panel Header
│ ├── Title & Artifact Count Badge
│ └── Action Buttons (Copy, Download, Close)
├── Artifact Selector
│ └── Dropdown with artifact list
└── Content Area
├── Preview/Edit Tabs
├── Preview Tab
│ ├── Rendered Content (Markdown with syntax highlighting)
│ └── Interactive Preview (HTML/React/JS)
└── Edit Tab
├── Type Selector
└── Content Editor (Textarea with save/discard)
Props Interface
interface ArtifactPanelProps {
onArtifactUpdate?: (artifactId: string, newContent: string) => void;
className?: string;
hideToggleButton?: boolean; // Hide the floating toggle button
inline?: boolean; // Inline layout instead of fixed positioning
}
Core Functionality
1. Artifact Context Integration
// From ArtifactPanel.tsx:54-62
const {
artifacts,
activeArtifactId,
setActiveArtifactId,
isPanelOpen,
setPanelOpen,
updateArtifact
} = useArtifacts();
// From ArtifactPanel.tsx:74
const currentArtifact = activeArtifactId ? artifacts.get(activeArtifactId) : null;
Context Features:
- Centralized State: All artifact data managed through context
- Active Selection: Tracks currently selected artifact
- Panel State: Manages open/closed state
- Update Coordination: Synchronized updates across components
2. Edit Mode Management
// From ArtifactPanel.tsx:77-108
const handleEditToggle = useCallback(() => {
if (currentArtifact) {
if (isEditing) {
// Save changes (both content and type)
const contentChanged = editedContent !== currentArtifact.content;
const typeChanged = editedType !== currentArtifact.type;
if (contentChanged || typeChanged) {
const updatedArtifact = {
...currentArtifact,
content: editedContent,
type: editedType
};
updateArtifact(currentArtifact.id, editedContent, editedType);
onArtifactUpdate?.(currentArtifact.id, editedContent);
toast({
title: "Artifact Updated",
description: `"${currentArtifact.title}" has been saved${typeChanged ? ` (type changed to ${editedType})` : ''}.`,
});
}
setIsEditing(false);
} else {
setEditedContent(currentArtifact.content);
setEditedType(currentArtifact.type);
setIsEditing(true);
}
}
}, [currentArtifact, isEditing, editedContent, editedType, updateArtifact, onArtifactUpdate, toast]);
Edit Features:
- Change Detection: Only saves when content or type actually changed
- Type Modification: Supports changing artifact type during editing
- User Feedback: Toast notifications for successful saves
- State Management: Proper initialization and cleanup of edit state
3. Intelligent Copy Strategies
The component implements sophisticated copy logic based on artifact type:
// From ArtifactPanel.tsx:119-203
const handleCopyRendered = useCallback(async () => {
if (!currentArtifact) return;
const contentToCopy = isEditing ? editedContent : currentArtifact.content;
const artifactType = isEditing ? editedType : currentArtifact.type;
try {
// Define which types should use simple text copy vs rich copy
const simpleTextTypes = [
'javascript', 'js', 'typescript', 'ts', 'python', 'py',
'css', 'sql', 'bash', 'sh', 'json', 'yaml', 'yml', 'xml'
];
const richContentTypes = [
'markdown', 'md', 'html', 'react', 'jsx', 'tsx'
];
const lowerType = artifactType.toLowerCase();
// Simple text copy for code artifacts - users usually want the raw code
if (simpleTextTypes.includes(lowerType)) {
await navigator.clipboard.writeText(contentToCopy);
// Success handling...
return;
}
// Rich copy for content artifacts - users want the rendered output with formatting
if (richContentTypes.includes(lowerType)) {
const artifactContainer = document.querySelector(`[data-artifact-content="${currentArtifact.id}"]`) as HTMLElement;
if (artifactContainer) {
const success = await copyContentToClipboard({
messageContainer: artifactContainer,
cleanedContent: contentToCopy,
markdownComponents: artifactMarkdownComponents
});
// Handle success/fallback...
}
}
// Final fallback to plain text for any unknown types
await navigator.clipboard.writeText(contentToCopy);
} catch (err) {
// Error handling...
}
}, [currentArtifact, isEditing, editedContent, editedType]);
Copy Strategy Features:
- Type-Aware Logic: Different copy strategies for code vs content artifacts
- Rich Formatting: Preserves formatting for markdown, HTML, React components
- Plain Text Fallback: Always provides a working copy option
- User Feedback: Detailed toast messages explaining what was copied
4. File Download with Proper Extensions
// From ArtifactPanel.tsx:205-255
const handleDownload = useCallback(() => {
if (currentArtifact) {
const contentToDownload = isEditing ? editedContent : currentArtifact.content;
const blob = new Blob([contentToDownload], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${currentArtifact.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.${getFileExtension(currentArtifact.type)}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({
title: "Download Started",
description: `"${currentArtifact.title}" download initiated.`,
});
}
}, [currentArtifact, isEditing, editedContent]);
// From ArtifactPanel.tsx:226-255
const getFileExtension = (type: string): string => {
switch (type.toLowerCase()) {
case 'markdown':
case 'md':
return 'md';
case 'html':
return 'html';
case 'javascript':
case 'js':
return 'js';
case 'typescript':
case 'ts':
return 'ts';
case 'python':
case 'py':
return 'py';
case 'json':
return 'json';
case 'yaml':
case 'yml':
return 'yml';
case 'css':
return 'css';
case 'svg':
return 'svg';
default:
return 'txt';
}
};
Download Features:
- Proper Extensions: Automatic file extension based on artifact type
- Safe Filenames: Sanitizes artifact titles for valid filenames
- Current State: Downloads edited content if in edit mode
- Resource Cleanup: Proper blob URL cleanup after download
5. Type-Aware Content Rendering
// From ArtifactPanel.tsx:432-476
{(() => {
const content = isEditing ? editedContent : currentArtifact.content;
const artifactType = isEditing ? editedType : currentArtifact.type;
// For artifacts with previews, don't show code - they can see it in Edit tab
const isReactComponent = ['react', 'jsx', 'tsx'].includes(artifactType.toLowerCase());
const isJavaScriptWithPreview = artifactType.toLowerCase() === 'javascript' || artifactType.toLowerCase() === 'js';
const isHtmlWithPreview = artifactType.toLowerCase() === 'html';
if (isReactComponent || isJavaScriptWithPreview || isHtmlWithPreview) {
return ''; // No code display - just show interactive preview below
}
// Check if this is a code artifact type
const codeTypes = ['javascript', 'typescript', 'python', 'html', 'css', 'json', 'yaml', 'sql', 'bash', 'js', 'ts', 'py', 'sh', 'shell', 'xml', 'java', 'c', 'cpp', 'go', 'rust', 'php', 'ruby', 'swift'];
if (codeTypes.includes(artifactType.toLowerCase())) {
// Wrap in markdown code block for syntax highlighting
return `\`\`\`${artifactType.toLowerCase()}\n${content}\n\`\`\``;
}
// For markdown type, render as-is
if (artifactType.toLowerCase() === 'markdown' || artifactType.toLowerCase() === 'md') {
return content;
}
// For plain text, detect if it contains markdown table syntax
if (artifactType.toLowerCase() === 'text' || artifactType.toLowerCase() === 'txt') {
const hasMarkdownTable = content.includes('|') && content.includes('---');
const hasMarkdownHeaders = /^#{1,6}\s/.test(content) || content.includes('**');
if (hasMarkdownTable || hasMarkdownHeaders) {
return content; // Render as markdown to preserve table formatting
} else {
return `\`\`\`\n${content}\n\`\`\``; // Wrap plain text in code block
}
}
// For all other types, render as markdown
return content;
})()}
Rendering Features:
- Smart Display Logic: Hides code for types with interactive previews
- Syntax Highlighting: Automatic code block wrapping for code types
- Markdown Detection: Intelligent markdown rendering for text artifacts
- Fallback Handling: Sensible defaults for unknown types
User Interface Design
Panel Layout Options
// From ArtifactPanel.tsx:280-289
<div
className={twMerge(
inline
? 'h-full bg-background flex flex-col'
: 'fixed top-0 bottom-0 right-0 z-30 w-[40%] min-w-[400px] max-w-[600px] bg-background border-l shadow-lg transition-transform duration-300 ease-in-out flex flex-col',
inline ? '' : (isPanelOpen ? 'translate-x-0' : 'translate-x-full'),
className
)}
onWheel={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
>
Layout Features:
- Fixed Positioning: Overlay panel on right side with smooth transitions
- Inline Option: Can be embedded within page layout
- Responsive Width: 40% width with min/max constraints
- Event Isolation: Prevents scroll/touch events from propagating
Toggle Button
// From ArtifactPanel.tsx:266-277
{!hideToggleButton && !isPanelOpen && (
<Button
variant="outline"
size="sm"
className="fixed top-1/2 right-4 z-50 transform -translate-y-1/2 shadow-lg"
onClick={() => setPanelOpen(true)}
>
<ChevronLeft className="h-4 w-4" />
<span className="ml-1">Artifacts ({artifacts.size})</span>
</Button>
)}
Toggle Features:
- Always Accessible: Fixed positioning when panel is closed
- Artifact Count: Shows number of available artifacts
- Optional Display: Can be hidden for embedded use cases
Artifact Selector
// From ArtifactPanel.tsx:335-368
<Select value={activeArtifactId || undefined} onValueChange={setActiveArtifactId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select an artifact">
{currentArtifact && (
<div className="flex items-center justify-between w-full">
<div className="flex flex-col items-start text-left">
<span className="font-medium truncate">{currentArtifact.title}</span>
<span className="text-xs text-muted-foreground">ID: {currentArtifact.id}</span>
</div>
<Badge variant="outline" className="ml-2 text-xs">
{currentArtifact.type}
</Badge>
</div>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{artifactList.map((artifact: ArtifactData) => (
<SelectItem key={artifact.id} value={artifact.id}>
{/* Similar structure with title, ID, and type badge */}
</SelectItem>
))}
</SelectContent>
</Select>
Selector Features:
- Rich Display: Shows title, ID, and type for each artifact
- Visual Hierarchy: Clear typography and badge system
- Comprehensive List: All available artifacts accessible
Dual-Mode Tabs
// From ArtifactPanel.tsx:375-408
<Tabs defaultValue="preview" className="flex-1 flex flex-col overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b flex-shrink-0">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="preview" className="flex items-center space-x-1">
<Eye className="h-3 w-3" />
<span>Preview</span>
</TabsTrigger>
<TabsTrigger value="edit" className="flex items-center space-x-1">
<Edit3 className="h-3 w-3" />
<span>Edit</span>
</TabsTrigger>
</TabsList>
{isEditing && (
<div className="flex items-center space-x-1 ml-2">
<Button variant="ghost" size="sm" onClick={handleDiscardChanges} title="Discard changes">
<RotateCcw className="h-3 w-3" />
</Button>
<Button variant="ghost" size="sm" onClick={handleEditToggle} title="Save changes">
<Save className="h-3 w-3" />
</Button>
</div>
)}
</div>
</Tabs>
Tab Features:
- Visual Mode Indicators: Icons clearly indicate preview vs edit modes
- Contextual Actions: Save/discard buttons appear only when editing
- Keyboard Accessibility: Full keyboard navigation support
Edit Interface
// From ArtifactPanel.tsx:490-550
<TabsContent value="edit" className="flex-1 overflow-hidden">
<div className="h-full p-3 flex flex-col">
{/* Type selector */}
<div className="mb-3">
<label className="block text-xs font-medium text-muted-foreground mb-1">
Artifact Type
</label>
<Select
value={isEditing ? editedType : currentArtifact.type}
onValueChange={(value) => {
if (!isEditing) {
setEditedContent(currentArtifact.content);
setEditedType(value);
setIsEditing(true);
} else {
setEditedType(value);
}
}}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="markdown">Markdown (.md)</SelectItem>
<SelectItem value="html">HTML (.html)</SelectItem>
<SelectItem value="javascript">JavaScript (.js)</SelectItem>
<SelectItem value="typescript">TypeScript (.ts)</SelectItem>
<SelectItem value="react">React (.jsx)</SelectItem>
<SelectItem value="jsx">JSX (.jsx)</SelectItem>
<SelectItem value="tsx">TSX (.tsx)</SelectItem>
<SelectItem value="python">Python (.py)</SelectItem>
{/* More options... */}
</SelectContent>
</Select>
</div>
<ScrollArea className="flex-1">
<Textarea
value={isEditing ? editedContent : currentArtifact.content}
onChange={(e) => {
// Handle content changes with automatic edit mode activation
}}
className="min-h-[calc(100vh-400px)] resize-none font-mono text-sm border-0 focus-visible:ring-0 p-0"
placeholder="Edit artifact content..."
/>
</ScrollArea>
</div>
</TabsContent>
Edit Features:
- Type Selection: Comprehensive list of supported artifact types
- Automatic Edit Mode: Starts editing when user changes type or content
- Monospace Editor: Proper font for code editing
- Responsive Height: Adapts to available space
Interactive Preview Integration
// From ArtifactPanel.tsx:480-486
{/* Interactive preview for HTML/React artifacts */}
<ArtifactPreview
content={isEditing ? editedContent : currentArtifact.content}
type={isEditing ? editedType : currentArtifact.type}
title={currentArtifact.title}
/>
Preview Features:
- Real-time Updates: Shows edited content immediately
- Type-Specific Rendering: Different preview modes for different artifact types
- Safe Execution: Secure execution environment for code artifacts
Performance Optimizations
Event Handling Optimization
// Memoized callbacks prevent unnecessary re-renders
const handleEditToggle = useCallback(() => { /* ... */ }, [dependencies]);
const handleDiscardChanges = useCallback(() => { /* ... */ }, [dependencies]);
const handleCopyRendered = useCallback(() => { /* ... */ }, [dependencies]);
const handleDownload = useCallback(() => { /* ... */ }, [dependencies]);
State Management
// From ArtifactPanel.tsx:66-71
useEffect(() => {
setIsEditing(false);
setEditedContent('');
setEditedType('');
}, [activeArtifactId]);
Performance Features:
- Callback Memoization: Prevents unnecessary child re-renders
- State Cleanup: Resets edit state when artifact changes
- Efficient Updates: Only re-renders when necessary
Usage Examples
Basic Panel
import ArtifactPanel from '@/components/ArtifactPanel';
function MyApp() {
return (
<div className="flex h-screen">
<div className="flex-1">
{/* Main content */}
</div>
<ArtifactPanel
onArtifactUpdate={(id, content) => {
console.log(`Artifact ${id} updated`);
}}
/>
</div>
);
}
Inline Panel
<ArtifactPanel
inline={true}
hideToggleButton={true}
className="w-96 border-l"
onArtifactUpdate={handleArtifactUpdate}
/>
Custom Styling
<ArtifactPanel
className="custom-artifact-panel"
onArtifactUpdate={handleUpdate}
/>
Testing Considerations
Key Test Areas
- Edit Mode: Test editing, saving, and discarding changes
- Copy Strategies: Test different copy behaviors for different types
- File Downloads: Test download functionality and filename generation
- Type Changes: Test changing artifact types during editing
- Panel States: Test open/closed panel behavior
- Interactive Preview: Test preview functionality for supported types
Mock Requirements
// Required mocks for testing
jest.mock('@/contexts/ArtifactContext');
jest.mock('@/components/ArtifactPreview');
jest.mock('@/utils/copyToClipboard');
jest.mock('@/components/hooks/use-toast');
jest.mock('react-markdown');
// Mock clipboard API
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(),
write: jest.fn(),
},
});
// Mock URL.createObjectURL
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
Example Test Structure
describe('ArtifactPanel', () => {
it('should handle artifact editing and saving', () => {
// Test edit workflow
});
it('should copy different artifact types appropriately', () => {
// Test copy strategies
});
it('should download files with correct extensions', () => {
// Test download functionality
});
it('should render content based on artifact type', () => {
// Test type-aware rendering
});
});
Error Handling
Copy Error Handling
// From ArtifactPanel.tsx:195-202
} catch (err) {
console.error('Failed to copy artifact content:', err);
toast({
title: "Copy Failed",
description: "Could not copy to clipboard.",
variant: "destructive",
});
}
Graceful Degradation
- No Artifacts: Component returns null when no artifacts available
- Copy Failures: Progressive fallback through copy strategies
- Download Issues: Error messages for download failures
- Preview Errors: Safe preview rendering with error boundaries
Future Enhancements
Potential Improvements
- Collaborative Editing: Multi-user artifact editing
- Version History: Track and restore artifact versions
- Template System: Pre-built artifact templates
- Export Integration: Direct export to various formats
- Syntax Validation: Real-time syntax checking for code artifacts
- Custom Themes: Syntax highlighting themes
The ArtifactPanel component provides a sophisticated, user-friendly interface for artifact management that adapts to different content types while maintaining excellent performance and accessibility standards.