Artifacts and Generative UI Components in Modern AI Chatbots: A Technical Deep Dive


Table of Contents

  1. Introduction
  2. What are Artifacts?
  3. Common Artifact Types
  4. Architecture Patterns
  5. Streaming Implementation
  6. React Rendering & Timing
  7. TypeScript Type Safety
  8. Component-Specific Implementations
  9. Performance Optimization
  10. Best Practices
  11. Future Trends

Introduction

Modern AI chatbots have evolved beyond simple text conversations into rich, interactive platforms capable of displaying complex data visualizations, documents, and dynamic content. This transformation is powered by Generative UI – a paradigm where AI doesn’t just generate text but creates entire user interface components on-the-fly based on context and user needs.

Artifacts represent the core building blocks of this generative UI approach. They are self-contained, interactive components that the AI can create, modify, and present to users during conversations. From document viewers to data tables, from code editors to charts, artifacts enable chatbots to deliver information in the most appropriate format.

This article explores the technical implementation of artifacts in production chatbot systems, drawing from real-world patterns, particularly in document-heavy domains like real estate analysis.


What are Artifacts?

Definition

An artifact is a persistent, interactive UI component that:

  • Lives outside the main chat flow but remains contextually linked
  • Can be created, updated, and versioned through AI interactions
  • Maintains its own state and lifecycle
  • Provides domain-specific functionality (viewing, editing, downloading)

Core Characteristics

export type UIArtifact = {
  title: string; // Human-readable name
  documentId: string; // Unique identifier
  kind: ArtifactKind; // Type discriminator
  content: string; // Primary data payload
  isVisible: boolean; // Visibility state
  status: 'streaming' | 'idle'; // Lifecycle state
  boundingBox: {
    // Animation coordinates
    top: number;
    left: number;
    width: number;
    height: number;
  };
};

Benefits

  1. Contextual Presentation: Display information in its optimal format
  2. Separation of Concerns: Keep chat clean while showing complex data
  3. Interactivity: Enable user manipulation beyond conversation
  4. Persistence: Maintain state across conversation turns
  5. Versioning: Track changes and allow rollback
  6. Reusability: Share components across different chat sessions

Common Artifact Types

1. Document Viewers

Purpose: Display uploaded documents (PDFs, Word files, etc.) with navigation, zoom, and annotation capabilities.

Use Cases:

  • Contract review
  • Report analysis
  • Research document exploration

Key Features:

  • Page navigation
  • Zoom controls
  • Text search
  • Annotation support
  • Download functionality

Implementation Considerations:

type PdfArtifactMetadata = {
  originalFilename?: string;
  pageNumber?: number;
  totalPages?: number;
  blobUrl?: string;
};

// PDFs typically load from blob storage, not streamed
initialize: ({ setMetadata }) => {
  setMetadata({
    pageNumber: 1,
    totalPages: 1
  });
};

2. Vector Search Results

Purpose: Display semantically relevant document chunks retrieved from vector databases.

Use Cases:

  • Document Q&A
  • Knowledge base search
  • Citation discovery
  • Evidence gathering

Key Features:

  • Similarity scores
  • Expandable content previews
  • Quick document access
  • Filter indicators
  • Collapsible sections

Technical Pattern:

export type VectorSearchResult = {
  rank: number;
  chunkId: string;
  content: string;
  similarity: number;
  metadata: {
    documentType?: DocumentType;
    pageNumber?: number;
    sectionTitle?: string;
    workspaceId?: string;
  };
  documentId: string;
  chunkIndex: number;
};

UI Pattern:

  • Closed by default to avoid overwhelming users
  • Progressive disclosure with “Show more” interactions
  • Direct links to source documents
  • Visual similarity indicators (badges)

3. Smart Citations

Purpose: Provide interactive references to source documents with context.

Use Cases:

  • Source verification
  • Deep diving into references
  • Building trust in AI responses

Key Features:

type SmartCitationProps = {
  citationNumber: number;
  documentId?: string;
  pageNumber?: number;
  documentType?: string;
  documentTitle?: string;
  excerpt?: string;
};

UX Considerations:

  • Inline display without breaking text flow
  • Hover tooltips with excerpt previews
  • Click-through to full documents
  • Truncated titles for readability
  • Visual consistency (badges, colors)

Optimization Techniques:

// Regex patterns defined at top level for performance
const FILE_EXTENSION_REGEX = /\.(pdf|docx?|xlsx?|txt)$/i;
const COMMON_PREFIX_REGEX = /^(Sample_|Document_|File_)/i;

// Memoization to prevent unnecessary re-renders
export const SmartCitation = memo(CitationComponent);

4. Data Sheets / Tables

Purpose: Display structured numerical data with sorting, filtering, and export capabilities.

Use Cases:

  • Financial analysis
  • Rent rolls
  • Budget comparisons
  • T12 reports

Key Features:

  • CSV parsing and serialization
  • Editable cells
  • Sort and filter
  • Copy to clipboard
  • Export as CSV/Excel

Implementation:

export const sheetArtifact = new Artifact<'sheet', Metadata>({
  kind: 'sheet',
  description: 'Useful for working with spreadsheets',
  onStreamPart: ({ setArtifact, streamPart }) => {
    if (streamPart.type === 'data-sheetDelta') {
      setArtifact(draftArtifact => ({
        ...draftArtifact,
        content: streamPart.data,
        isVisible: true,
        status: 'streaming'
      }));
    }
  },
  actions: [
    {
      icon: <CopyIcon />,
      description: 'Copy as .csv',
      onClick: ({ content }) => {
        const parsed = parse<string[]>(content, { skipEmptyLines: true });
        const cleanedCsv = unparse(parsed);
        navigator.clipboard.writeText(cleanedCsv);
      }
    }
  ]
});

5. Code Editors

Purpose: Display and edit code with syntax highlighting and execution capabilities.

Use Cases:

  • Code generation
  • Script debugging
  • SQL query building
  • Configuration editing

Key Features:

  • Syntax highlighting
  • Multiple language support
  • Line numbers
  • Copy/download
  • Theme switching

6. Data Visualizations (Charts & Graphs)

Purpose: Transform numerical data into visual representations.

Use Cases:

  • Trend analysis
  • Portfolio performance
  • Market comparisons
  • Budget breakdowns

Common Types:

  • Line charts (trends over time)
  • Bar charts (comparisons)
  • Pie charts (distributions)
  • Scatter plots (correlations)
  • Heat maps (multi-dimensional data)

Implementation Recommendations:

// Library selection: Recharts, Chart.js, or D3.js
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip
} from 'recharts';

// Lazy loading for performance
const ChartArtifact = lazy(() => import('./chart-artifact'));

// Handle large datasets
const useChartData = (rawData: unknown[]) => {
  return useMemo(() => {
    // Downsample if too many points
    if (rawData.length > 1000) {
      return downsampleData(rawData, 1000);
    }
    return rawData;
  }, [rawData]);
};

7. Image Editors

Purpose: Display and manipulate images with annotations and transformations.

Use Cases:

  • Property photos
  • Floor plans
  • Charts from documents
  • Diagram annotations

8. Text Documents

Purpose: Display and edit rich text with formatting.

Use Cases:

  • Report generation
  • Email drafting
  • Note-taking
  • Summary creation

Architecture Patterns

Artifact Factory Pattern

The factory pattern enables type-safe, extensible artifact creation:

export class Artifact<T extends string, M = any> {
  readonly kind: T;
  readonly description: string;
  readonly content: ComponentType<ArtifactContent<M>>;
  readonly actions: ArtifactAction<M>[];
  readonly toolbar: ArtifactToolbarItem[];
  readonly initialize?: (parameters: InitializeParameters) => void;
  readonly onStreamPart: (args: {
    setMetadata: Dispatch<SetStateAction<M>>;
    setArtifact: Dispatch<SetStateAction<UIArtifact>>;
    streamPart: DataUIPart<CustomUIDataTypes>;
  }) => void;

  constructor(config: ArtifactConfig<T, M>) {
    this.kind = config.kind;
    this.description = config.description;
    this.content = config.content;
    this.actions = config.actions || [];
    this.toolbar = config.toolbar || [];
    this.initialize = config.initialize;
    this.onStreamPart = config.onStreamPart;
  }
}

Registry Pattern

Centralized artifact registration ensures type safety and discoverability:

export const artifactDefinitions = [
  textArtifact,
  codeArtifact,
  imageArtifact,
  sheetArtifact,
  uploadedArtifact,
  pdfArtifact,
  excelArtifact
];

export type ArtifactKind = (typeof artifactDefinitions)[number]['kind'];

Metadata Pattern

Each artifact type defines its own metadata structure:

// PDF metadata
type PdfArtifactMetadata = {
  originalFilename?: string;
  pageNumber?: number;
  totalPages?: number;
  blobUrl?: string;
};

// Sheet metadata can be different
type SheetMetadata = {
  rowCount?: number;
  columnCount?: number;
  hasHeaders?: boolean;
};

Separation of Client/Server Logic

artifacts/
  pdf/
    client.tsx  // UI components, interactions
    server.ts   // Server-side processing, validation

Streaming Implementation

Why Streaming Matters

Streaming provides:

  1. Perceived Performance: Show content as it arrives
  2. Responsiveness: User sees progress immediately
  3. Scalability: Handle large responses without timeouts
  4. UX: Reduce perceived latency

Stream Architecture

// 1. Server: Create stream
const stream = createUIMessageStream({
  execute: ({ writer: dataStream }) => {
    const result = streamText({
      model: provider.languageModel(model),
      system: systemPrompt(),
      messages: convertToModelMessages(uiMessages),
      experimental_transform: smoothStream({ chunking: 'word' }),
      tools: {
        vectorSearch: createVectorSearchTool({ workspaceId }),
        createDocument: createDocument({ session, dataStream })
      }
    });

    return result.toTextStreamResponse();
  }
});

// 2. Client: Consume stream
const { messages, status } = useChat<ChatMessage>({
  id,
  experimental_throttle: 100, // Debounce updates
  onData: dataPart => {
    setDataStream(ds => (ds ? [...ds, dataPart] : []));
  }
});

Custom Data Stream Parts

// Define custom stream part types
export type CustomUIDataTypes = {
  'data-sheetDelta': string;
  'data-codeDelta': string;
  'data-textDelta': string;
  'data-pdfMetadata': PdfMetadata;
  'data-usage': UsageData;
  'data-id': string;
  'data-title': string;
  'data-kind': ArtifactKind;
  'data-clear': void;
  'data-finish': void;
};

// Server: Send custom data
dataStream.writeData({
  type: 'data-sheetDelta',
  data: csvContent
});

// Client: Process stream parts
if (streamPart.type === 'data-sheetDelta') {
  setArtifact(draft => ({
    ...draft,
    content: streamPart.data,
    status: 'streaming'
  }));
}

DataStreamHandler Pattern

export function DataStreamHandler() {
  const { dataStream } = useDataStream();
  const { artifact, setArtifact, setMetadata } = useArtifact();
  const lastProcessedIndex = useRef(-1);

  useEffect(() => {
    if (!dataStream?.length) return;

    // Process only new deltas
    const newDeltas = dataStream.slice(lastProcessedIndex.current + 1);
    lastProcessedIndex.current = dataStream.length - 1;

    for (const delta of newDeltas) {
      const artifactDef = artifactDefinitions.find(
        def => def.kind === artifact.kind
      );

      // Delegate to artifact-specific handler
      artifactDef?.onStreamPart({
        streamPart: delta,
        setArtifact,
        setMetadata
      });

      // Handle common stream events
      switch (delta.type) {
        case 'data-id':
          setArtifact(draft => ({
            ...draft,
            documentId: delta.data,
            status: 'streaming'
          }));
          break;
        case 'data-finish':
          setArtifact(draft => ({
            ...draft,
            status: 'idle'
          }));
          break;
      }
    }
  }, [dataStream, setArtifact, setMetadata, artifact]);

  return null;
}

Stream Resumption

Handle network interruptions gracefully:

export function useAutoResume({
  autoResume,
  initialMessages,
  resumeStream,
  setMessages
}: UseAutoResumeParams) {
  useEffect(() => {
    if (!autoResume) return;

    const lastMessage = initialMessages.at(-1);
    if (
      lastMessage?.role === 'assistant' &&
      lastMessage.parts.some(p => p.type === 'text' && !p.text)
    ) {
      // Resume incomplete stream
      resumeStream();
    }
  }, [autoResume, initialMessages, resumeStream]);
}

React Rendering & Timing

Challenge: Streaming + React State

Streaming content creates rapid state updates that can:

  1. Cause excessive re-renders
  2. Block the UI thread
  3. Create layout thrashing
  4. Impact performance

Solution Strategies

1. Throttling

const { messages, status } = useChat({
  experimental_throttle: 100 // Update max every 100ms
});

2. Debouncing

const debouncedHandleContentChange = useDebounceCallback(
  handleContentChange,
  2000 // Wait 2s after typing stops
);

3. Memoization

// Prevent re-renders when props haven't changed
export const Artifact = memo(PureArtifact, (prevProps, nextProps) => {
  if (prevProps.status !== nextProps.status) return false;
  if (!equal(prevProps.votes, nextProps.votes)) return false;
  if (prevProps.input !== nextProps.input) return false;
  return true;
});

// Memoize expensive computations
const processedData = useMemo(() => {
  return parseAndFormatLargeDataset(rawData);
}, [rawData]);

4. useCallback for Stable References

const handleContentChange = useCallback(
  (updatedContent: string) => {
    if (!artifact) return;

    mutate<Document[]>(
      `/api/document?id=${artifact.documentId}`,
      async currentDocuments => {
        // Update logic
      },
      { revalidate: false } // Avoid unnecessary refetch
    );
  },
  [artifact, mutate]
);

5. Ref for Non-Reactive Values

// Track processed indices without causing re-renders
const lastProcessedIndex = useRef(-1);

// Store current values for callbacks
const currentModelIdRef = useRef(currentModelId);
useEffect(() => {
  currentModelIdRef.current = currentModelId;
}, [currentModelId]);

Animation Timing

Artifacts use Framer Motion for smooth transitions:

<motion.div
  initial={{
    opacity: 1,
    x: artifact.boundingBox.left,
    y: artifact.boundingBox.top,
    height: artifact.boundingBox.height,
    width: artifact.boundingBox.width,
    borderRadius: 50,
  }}
  animate={{
    opacity: 1,
    x: 400,
    y: 0,
    height: windowHeight,
    width: windowWidth - 400,
    borderRadius: 0,
    transition: {
      type: "spring",
      stiffness: 300,
      damping: 30,
    },
  }}
  exit={{
    opacity: 0,
    scale: 0.5,
    transition: {
      delay: 0.1,
      type: "spring",
      stiffness: 600,
      damping: 30,
    },
  }}
>

Key Patterns:

  • Capture click position for natural expansion origin
  • Use spring animations for organic feel
  • Sequence animations with delays
  • Exit animations faster than enter

Handling Large Lists

For search results or large datasets:

// Virtualization for long lists
import { useVirtualizer } from '@tanstack/react-virtual';

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 100,
  overscan: 5
});

// Render only visible items
{
  virtualizer.getVirtualItems().map(virtualItem => (
    <div
      key={virtualItem.key}
      style={{
        height: virtualItem.size,
        transform: `translateY(${virtualItem.start}px)`
      }}
    >
      {items[virtualItem.index]}
    </div>
  ));
}

TypeScript Type Safety

Discriminated Unions

// Tool result types with discriminated union
export type ToolResult =
  | { type: 'vector-search'; data: VectorSearchResponse }
  | { type: 'document-created'; data: DocumentMetadata }
  | { type: 'analysis-complete'; data: AnalysisResult };

// Type narrowing
function handleToolResult(result: ToolResult) {
  switch (result.type) {
    case 'vector-search':
      // TypeScript knows result.data is VectorSearchResponse
      return <VectorSearchResults result={result.data} />;
    case 'document-created':
      return <DocumentCreatedNotification doc={result.data} />;
  }
}

Zod for Runtime Validation

import { z } from 'zod';

// Schema definition with descriptions for AI
const SearchOptionsSchema = z.object({
  query: z.string().describe('Search query to find relevant document chunks'),
  documentTypes: z
    .array(z.enum(['rent_roll', 't12', 'budget', 'lease']))
    .optional()
    .describe('Filter by document types'),
  limit: z
    .number()
    .min(1)
    .max(50)
    .default(10)
    .describe('Maximum results to return')
});

// Infer TypeScript type from schema
type SearchOptions = z.infer<typeof SearchOptionsSchema>;

// Runtime validation
export function createVectorSearchTool() {
  return tool({
    inputSchema: SearchOptionsSchema,
    execute: async options => {
      // options is type-safe and validated
    }
  });
}

Best Practices:

  1. Descriptions: Add .describe() for AI tool understanding
  2. Constraints: Use .min(), .max(), .email() etc.
  3. Defaults: Provide sensible defaults
  4. Optionality: Mark optional fields explicitly
  5. Enums: Use for fixed sets of values

Generic Artifact Types

// Generic artifact class supports custom metadata
export class Artifact<T extends string, M = any> {
  readonly kind: T;
  readonly content: ComponentType<ArtifactContent<M>>;

  constructor(config: ArtifactConfig<T, M>) {
    // Type-safe construction
  }
}

// Usage with specific types
const pdfArtifact = new Artifact<'pdf', PdfArtifactMetadata>({
  kind: 'pdf',
  content: PdfViewer
  // TypeScript ensures all handlers receive PdfArtifactMetadata
});

Strict Mode Configuration

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noUncheckedIndexedAccess": true
  }
}

Component-Specific Implementations

Vector Search Results Component

Key Implementation Details:

export function VectorSearchResults({
  result,
  isReadonly,
  onViewDocument
}: VectorSearchResultsProps) {
  const { setArtifact } = useArtifact();
  const [expandedResults, setExpandedResults] = useState<Set<string>>(
    new Set()
  );
  const [isOpen, setIsOpen] = useState(false); // Closed by default

  const handleViewDocument = (
    documentId: string,
    pageNumber?: number,
    event?: React.MouseEvent<HTMLButtonElement>
  ) => {
    // Prevent viewing in readonly/shared chats
    if (isReadonly) {
      toast.error('Viewing documents in shared chats is not supported.');
      return;
    }

    // Capture click position for animation origin
    const rect = event?.currentTarget?.getBoundingClientRect() || {
      top: 0,
      left: 0,
      width: 0,
      height: 0
    };

    // Open artifact with captured position
    setArtifact({
      documentId,
      kind: 'pdf',
      content: '',
      title: `Document (Page ${pageNumber || 1})`,
      isVisible: true,
      status: 'idle',
      boundingBox: {
        top: rect.top,
        left: rect.left,
        width: rect.width,
        height: rect.height
      }
    });
  };

  return (
    <SearchResults>
      <CollapsibleSearchResult onOpenChange={setIsOpen} open={isOpen}>
        <CollapsibleSearchResultTrigger isExpanded={isOpen}>
          <SearchResultsHeader
            query={result.query}
            totalFound={result.totalFound}
          />
        </CollapsibleSearchResultTrigger>

        <CollapsibleSearchResultContent>
          {result.results.map(searchResult => (
            <SearchResultCard
              key={searchResult.chunkId}
              rank={searchResult.rank}
            >
              <SearchResultHeader
                documentType={searchResult.metadata.documentType}
                onViewDocument={event =>
                  handleViewDocument(
                    searchResult.documentId,
                    searchResult.metadata.pageNumber,
                    event
                  )
                }
                similarity={searchResult.similarity}
              />

              <SearchResultContent>
                <SearchResultText>
                  {searchResult.content.length > 300
                    ? expandedResults.has(searchResult.chunkId)
                      ? searchResult.content
                      : `${searchResult.content.slice(0, 300)}...`
                    : searchResult.content}
                </SearchResultText>

                {searchResult.content.length > 300 && (
                  <button
                    onClick={() => {
                      setExpandedResults(prev => {
                        const newSet = new Set(prev);
                        if (newSet.has(searchResult.chunkId)) {
                          newSet.delete(searchResult.chunkId);
                        } else {
                          newSet.add(searchResult.chunkId);
                        }
                        return newSet;
                      });
                    }}
                    type='button'
                  >
                    {expandedResults.has(searchResult.chunkId)
                      ? 'Show less'
                      : 'Show more'}
                  </button>
                )}

                <SearchResultMetadata
                  chunkIndex={searchResult.chunkIndex}
                  pageNumber={searchResult.metadata.pageNumber}
                  sectionTitle={searchResult.metadata.sectionTitle}
                />
              </SearchResultContent>
            </SearchResultCard>
          ))}
        </CollapsibleSearchResultContent>
      </CollapsibleSearchResult>
    </SearchResults>
  );
}

Design Decisions:

  1. Closed by Default: Avoid overwhelming users with large result sets
  2. Progressive Disclosure: Show truncated content with expand option
  3. Similarity Badges: Visual feedback on match quality
  4. Direct Navigation: One-click access to source documents
  5. Graceful Degradation: Handle readonly mode cleanly

Citation System

Integration Pattern:

// 1. Tool returns sources
return {
  results: formattedResults,
  sources: formattedResults.map((result, index) => ({
    sourceId: index + 1,
    documentId: result.documentId,
    pageNumber: result.metadata.pageNumber,
    documentType: result.metadata.documentType,
    documentTitle: result.metadata.documentTitle,
    excerpt: result.content.slice(0, 150)
  })),
  message: 'Found 5 sources. Use [1], [2], etc. to cite them.'
};

// 2. AI references sources in response
('According to the lease agreement [1], the rent is $2,500/month...');

// 3. Client parses and renders citations
function parseCitations(text: string, sources: Source[]) {
  return text.replace(/\[(\d+)\]/g, (match, num) => {
    const source = sources[Number.parseInt(num) - 1];
    return `<SmartCitation 
      citationNumber={${num}}
      documentId="${source.documentId}"
      pageNumber={${source.pageNumber}}
      documentTitle="${source.documentTitle}"
    />`;
  });
}

Document Version Management

// Version tracking
const [currentVersionIndex, setCurrentVersionIndex] = useState(-1);
const [mode, setMode] = useState<'edit' | 'diff'>('edit');

const handleVersionChange = (type: 'next' | 'prev' | 'toggle' | 'latest') => {
  if (!documents) return;

  switch (type) {
    case 'latest':
      setCurrentVersionIndex(documents.length - 1);
      setMode('edit');
      break;
    case 'toggle':
      setMode(current => (current === 'edit' ? 'diff' : 'edit'));
      break;
    case 'prev':
      if (currentVersionIndex > 0) {
        setCurrentVersionIndex(i => i - 1);
      }
      break;
    case 'next':
      if (currentVersionIndex < documents.length - 1) {
        setCurrentVersionIndex(i => i + 1);
      }
      break;
  }
};

// Diff view component
{
  mode === 'diff' && (
    <DiffView
      oldContent={getDocumentContentById(currentVersionIndex - 1)}
      newContent={getDocumentContentById(currentVersionIndex)}
    />
  );
}

Performance Optimization

1. Code Splitting

// Lazy load heavy artifacts
const CodeEditor = lazy(() => import('./code-editor'));
const ChartViewer = lazy(() => import('./chart-viewer'));

// Use Suspense boundary
<Suspense fallback={<ArtifactSkeleton />}>
  <CodeEditor content={content} />
</Suspense>;

2. Regex Optimization

// BAD: Compile regex on every render
function processText(text: string) {
  return text.replace(/\.(pdf|docx?)$/i, '');
}

// GOOD: Define regex at module level
const FILE_EXTENSION_REGEX = /\.(pdf|docx?|xlsx?|txt)$/i;
function processText(text: string) {
  return text.replace(FILE_EXTENSION_REGEX, '');
}

3. Virtualization

For lists with 50+ items, use virtualization:

import { FixedSizeList } from 'react-window';

<FixedSizeList height={600} itemCount={items.length} itemSize={80} width='100%'>
  {({ index, style }) => (
    <div style={style}>
      <SearchResult result={items[index]} />
    </div>
  )}
</FixedSizeList>;

4. Image Optimization

// Use Next.js Image component
import Image from 'next/image';

<Image
  src={imageUrl}
  alt={description}
  width={800}
  height={600}
  loading='lazy'
  placeholder='blur'
  blurDataURL={thumbnailUrl}
/>;

5. Data Fetching Strategies

// Use SWR for caching and revalidation
const { data, isLoading, mutate } = useSWR<Document[]>(
  artifact.documentId !== 'init' && artifact.status !== 'streaming'
    ? `/api/document?id=${artifact.documentId}`
    : null,
  fetcher,
  {
    revalidateOnFocus: false,
    revalidateOnReconnect: true,
    dedupingInterval: 2000
  }
);

6. Optimistic Updates

const saveContent = useCallback(
  async (content: string) => {
    // Optimistic update
    mutate(
      `/api/document?id=${docId}`,
      current => {
        if (!current) return current;
        return [...current, { ...current.at(-1), content }];
      },
      { revalidate: false } // Don't refetch yet
    );

    // Actual save
    try {
      await fetch(`/api/document?id=${docId}`, {
        method: 'POST',
        body: JSON.stringify({ content })
      });
    } catch (error) {
      // Revert on error
      mutate(`/api/document?id=${docId}`);
    }
  },
  [docId, mutate]
);

7. Debounced Saves

// Don't save on every keystroke
const debouncedSave = useDebounceCallback(
  (content: string) => {
    saveToServer(content);
  },
  2000 // 2 seconds after typing stops
);

<Editor
  onChange={content => {
    setLocalContent(content); // Update UI immediately
    debouncedSave(content); // Save after delay
  }}
/>;

Best Practices

1. Error Handling

// Comprehensive error handling in tools
export function createVectorSearchTool() {
  return tool({
    execute: async options => {
      try {
        const results = await vectorStorage.search(options);
        return {
          results,
          totalFound: results.length,
          message: `Found ${results.length} results`
        };
      } catch (error) {
        console.error('Vector search error:', error);
        return {
          results: [],
          totalFound: 0,
          error: error instanceof Error ? error.message : 'Unknown error',
          message: 'Failed to search documents. Please try again.'
        };
      }
    }
  });
}

// UI error boundaries
class ArtifactErrorBoundary extends React.Component {
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Artifact error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <ArtifactErrorFallback onRetry={this.reset} />;
    }
    return this.props.children;
  }
}

2. Accessibility

// Always provide ARIA labels
<button
  aria-label={`Citation: ${documentTitle}, Page ${pageNumber}`}
  onClick={handleViewDocument}
  type='button'
>
  [{citationNumber}]
</button>;

// Keyboard navigation
const handleKeyDown = (e: React.KeyboardEvent) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    handleViewDocument();
  }
};

// Focus management
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
  if (isExpanded) {
    buttonRef.current?.focus();
  }
}, [isExpanded]);

3. Security

// Server-side context injection (prevent LLM from setting workspace)
export function createVectorSearchTool(secureContext: { workspaceId: string }) {
  return tool({
    execute: async options => {
      // Use server-provided workspaceId, not from LLM
      const { workspaceId } = secureContext;

      const results = await vectorStorage.searchSimilarChunks(queryEmbedding, {
        workspaceId, // From server, not client or LLM
        ...options
      });
    }
  });
}

// Sanitize user input
import DOMPurify from 'isomorphic-dompurify';

function renderUserContent(html: string) {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href']
  });
}

4. Testing

// Unit tests for artifact logic
describe('SmartCitation', () => {
  it('formats document title correctly', () => {
    const { getByText } = render(
      <SmartCitation
        citationNumber={1}
        documentTitle='Sample_Lease_Agreement.pdf'
        pageNumber={3}
      />
    );

    // Should remove prefix and extension
    expect(getByText(/Lease Agreement/)).toBeInTheDocument();
  });
});

// Integration tests for streaming
describe('DataStreamHandler', () => {
  it('processes sheet deltas correctly', async () => {
    const mockStream = [
      { type: 'data-id', data: 'doc-123' },
      { type: 'data-kind', data: 'sheet' },
      { type: 'data-sheetDelta', data: 'Name,Age\nJohn,30' },
      { type: 'data-finish' }
    ];

    // Test stream processing
  });
});

// E2E tests with Playwright
test('artifact opens from citation click', async ({ page }) => {
  await page.goto('/chat/test-chat');
  await page.click('[data-testid="citation-1"]');
  await expect(page.locator('[data-testid="artifact"]')).toBeVisible();
});

5. Documentation

/**
 * Vector search tool for finding relevant document chunks.
 *
 * @remarks
 * - Automatically scoped to current workspace
 * - Supports re-ranking for improved relevance
 * - Returns sources array for citation mapping
 *
 * @example
 * ```typescript
 * const tool = createVectorSearchTool({ workspaceId: 'ws-123' });
 * const results = await tool.execute({
 *   query: "What is the monthly rent?",
 *   limit: 10,
 *   rerank: true,
 * });
 * ```
 */
export function createVectorSearchTool(secureContext: { workspaceId: string }) {
  // Implementation
}

6. Monitoring

// Track artifact usage
const logArtifactView = (artifactKind: ArtifactKind) => {
  analytics.track('artifact_viewed', {
    kind: artifactKind,
    chatId: currentChatId,
    timestamp: Date.now()
  });
};

// Monitor performance
const measureArtifactLoad = (artifactKind: ArtifactKind) => {
  const startTime = performance.now();

  return () => {
    const duration = performance.now() - startTime;
    analytics.track('artifact_load_time', {
      kind: artifactKind,
      duration
    });
  };
};

1. Real-Time Collaboration

Future artifacts will support multiple users editing simultaneously:

// WebSocket-based collaboration
const useCollaborativeArtifact = (artifactId: string) => {
  const [users, setUsers] = useState<CollaborationUser[]>([]);

  useEffect(() => {
    const ws = new WebSocket(`wss://api.example.com/collab/${artifactId}`);

    ws.onmessage = event => {
      const update = JSON.parse(event.data);
      handleRemoteUpdate(update);
    };

    return () => ws.close();
  }, [artifactId]);
};

2. AI-Powered Artifact Suggestions

Context-aware artifact recommendations:

// Analyze content and suggest optimal artifact type
const suggestArtifactType = async (content: string): Promise<ArtifactKind> => {
  if (content.includes(',') && content.split('\n').length > 3) {
    return 'sheet';
  }
  if (content.startsWith('```')) {
    return 'code';
  }
  // Use LLM for complex cases
  const suggestion = await llm.classify(content);
  return suggestion.artifactType;
};

3. Cross-Artifact References

Link artifacts together for complex workflows:

type ArtifactReference = {
  sourceArtifactId: string;
  targetArtifactId: string;
  relationship: 'derived_from' | 'references' | 'visualizes';
};

// "Create a chart from Sheet A"
// -> Chart artifact references Sheet artifact

4. Artifact Templates

Pre-built templates for common use cases:

const ARTIFACT_TEMPLATES = {
  'financial-analysis': {
    kind: 'sheet',
    initialContent: generateTemplate('financial-analysis'),
    suggestedActions: ['add-chart', 'export-pdf']
  },
  'code-review': {
    kind: 'code',
    initialContent: '',
    toolbar: ['run-linter', 'suggest-improvements']
  }
};

5. Embedded Mini-Apps

Full applications within artifacts:

// Calculator artifact
const calculatorArtifact = new Artifact<'calculator', CalculatorState>({
  kind: 'calculator',
  content: ({ metadata, setMetadata }) => (
    <Calculator
      history={metadata.history}
      onCalculate={result => {
        setMetadata(m => ({
          ...m,
          history: [...m.history, result]
        }));
      }}
    />
  )
});

6. Voice and Multimodal Interactions

// "Show me a chart of the last 5 years"
// -> Speech input triggers chart artifact with voice hints

const useVoiceCommand = (artifact: UIArtifact) => {
  const handleVoiceCommand = async (transcript: string) => {
    if (transcript.includes('zoom in')) {
      artifact.zoom(1.5);
    }
    if (transcript.includes('next page')) {
      artifact.nextPage();
    }
  };
};

7. Progressive Web Artifact Apps

Installable artifacts that work offline:

// Service worker for offline support
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/artifact-sw.js');
}

// Cache artifacts locally
const cacheArtifact = async (artifact: UIArtifact) => {
  const cache = await caches.open('artifacts-v1');
  await cache.put(
    `/artifact/${artifact.documentId}`,
    new Response(JSON.stringify(artifact))
  );
};

Conclusion

Artifacts and generative UI represent a paradigm shift in how we interact with AI systems. By moving beyond text-only conversations to rich, interactive components, modern chatbots can:

  • Present information in optimal formats
  • Enable complex interactions beyond simple Q&A
  • Maintain context and state across conversation turns
  • Support real-world workflows like document analysis, data exploration, and code generation

The implementation patterns covered in this article—streaming architecture, React optimization, TypeScript safety, and component-specific best practices—provide a foundation for building production-ready generative UI systems.

As AI capabilities continue to advance, we can expect artifacts to become even more sophisticated, supporting real-time collaboration, cross-artifact workflows, and embedded mini-applications. The key to success will be balancing innovation with performance, accessibility, and user experience.

Key Takeaways

  1. Design for streaming: Users expect immediate feedback
  2. Optimize React rendering: Use throttling, debouncing, and memoization
  3. Enforce type safety: Leverage TypeScript and Zod for robust code
  4. Plan for errors: Graceful degradation is critical
  5. Measure performance: Monitor and optimize continuously
  6. Prioritize accessibility: Make artifacts usable for everyone
  7. Think modular: Build reusable, composable components
  8. Document extensively: Help future developers understand your patterns

The future of conversational AI is not just in smarter language models, but in more capable, interactive, and context-aware user interfaces. Artifacts are the building blocks of this future.


Additional Resources


This article is based on production implementations in real-world AI chatbot systems, particularly in document-intensive domains like real estate analysis. All code examples are simplified for clarity but reflect actual patterns used in production.