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

  1. Edit Mode: Test editing, saving, and discarding changes
  2. Copy Strategies: Test different copy behaviors for different types
  3. File Downloads: Test download functionality and filename generation
  4. Type Changes: Test changing artifact types during editing
  5. Panel States: Test open/closed panel behavior
  6. 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

  1. Collaborative Editing: Multi-user artifact editing
  2. Version History: Track and restore artifact versions
  3. Template System: Pre-built artifact templates
  4. Export Integration: Direct export to various formats
  5. Syntax Validation: Real-time syntax checking for code artifacts
  6. 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.