Skip to main content

Viewer Implementation Guide

Overview

This guide provides detailed implementation instructions for developers working with the Bike4Mind viewer system. It covers component development, integration patterns, security considerations, and best practices.

Quick Start

Basic Viewer Implementation

import React from 'react';
import { type ArtifactViewerProps } from '@b4m-core/common/types/entities/ArtifactTypes';

interface CustomArtifact {
id: string;
type: 'custom';
title: string;
content: string;
metadata?: {
format: string;
version: string;
};
}

const CustomArtifactViewer: React.FC<ArtifactViewerProps<CustomArtifact>> = ({
artifact,
onError,
onEdit,
className
}) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

// Validation
useEffect(() => {
if (!artifact.content) {
const errorMsg = 'Invalid artifact: missing content';
setError(errorMsg);
onError?.(errorMsg);
}
}, [artifact.content, onError]);

// Error handling
if (error) {
return (
<Alert color="danger">
<Typography level="title-sm">Error</Typography>
<Typography level="body-sm">{error}</Typography>
</Alert>
);
}

return (
<Box className={className} sx={{ width: '100%', height: '100%' }}>
{/* Your viewer implementation */}
<Typography>{artifact.content}</Typography>
</Box>
);
};

export default CustomArtifactViewer;

Component Integration

1. Register in KnowledgeViewer

File: packages/client/app/components/Knowledge/KnowledgeViewer.tsx

// 1. Add to knowledge item types
interface ICustomKnowledgeItem extends IBaseKnowledgeItem {
type: 'custom';
content: CustomArtifact;
}

// 2. Update type union
type KnowledgeItem =
| IFileKnowledgeItem
| IQuestMasterKnowledgeItem
| ICodeKnowledgeItem
| IMermaidKnowledgeItem
| IReactKnowledgeItem
| IHtmlKnowledgeItem
| ISvgKnowledgeItem
| ICustomKnowledgeItem; // Add your type

// 3. Add dynamic import
const CustomArtifactViewer = dynamic(() => import('./CustomArtifactViewer'), {
ssr: false,
loading: () => <CircularProgress />
});

// 4. Register in KnowledgeContent component
const KnowledgeContent: React.FC<{...}> = ({ item, isLoading, currentSession }) => {
switch (item.type) {
// ... existing cases
case 'custom':
const customData = item.content;
return <CustomArtifactViewer artifact={customData} />;
default:
return <Typography>Unsupported content type</Typography>;
}
};

2. Add to Artifact Processing

File: packages/client/app/hooks/useSessionLayout.ts

// Update ArtifactData interface
export interface ArtifactData {
type: 'questmaster' | 'code' | 'mermaid' | 'react' | 'html' | 'svg' | 'custom';
content: QuestMasterData | CodeArtifactData | string | MermaidArtifact |
ReactArtifact | HtmlArtifact | SvgArtifact | CustomArtifact;
mimeType: string;
id: string;
}

3. Update Core Types

File: b4m-core/packages/core/common/types/entities/ArtifactTypes.ts

// Add to artifact type enum
export const ArtifactTypeSchema = z.enum([
'mermaid', 'rechart', 'python', 'react', 'html', 'svg', 'code', 'quest', 'file', 'custom'
]);

// Create specific artifact schema
export const CustomArtifactSchema = ArtifactSchema.extend({
type: z.literal('custom'),
metadata: ArtifactMetadataSchema.extend({
format: z.string().optional(),
version: z.string().optional(),
}),
});

export type CustomArtifact = z.infer<typeof CustomArtifactSchema>;

// Add to type union
export type SpecificArtifact = ReactArtifact | HtmlArtifact | SvgArtifact |
MermaidArtifact | QuestMasterArtifact | CustomArtifact;

Security Implementation

Sandboxed Viewer Pattern

For viewers that execute untrusted content:

import React, { useRef, useEffect, useState } from 'react';

const SecureArtifactViewer: React.FC<ArtifactViewerProps<UntrustedArtifact>> = ({
artifact
}) => {
const iframeRef = useRef<HTMLIFrameElement>(null);
const [iframeSrc, setIframeSrc] = useState<string | null>(null);

const generateSandboxHTML = (content: string) => {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';">
<title>Secure Viewer</title>
</head>
<body>
<div id="content">${escapeHtml(content)}</div>
<script>
// Safe execution environment
window.addEventListener('error', (event) => {
parent.postMessage({
type: 'error',
message: event.error?.message || event.message
}, '*');
});
</script>
</body>
</html>`;
};

useEffect(() => {
const sandboxHTML = generateSandboxHTML(artifact.content);
const blob = new Blob([sandboxHTML], { type: 'text/html' });
const url = URL.createObjectURL(blob);
setIframeSrc(url);

return () => URL.revokeObjectURL(url);
}, [artifact.content]);

// Message handling
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.source !== iframeRef.current?.contentWindow) return;

if (event.data.type === 'error') {
console.error('Sandbox error:', event.data.message);
}
};

window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, []);

return (
<iframe
ref={iframeRef}
src={iframeSrc || ''}
title={artifact.title}
sandbox="allow-scripts"
style={{ width: '100%', height: '100%', border: 'none' }}
/>
);
};

Content Validation

import DOMPurify from 'dompurify';
import { z } from 'zod';

// Validation schema
const ContentValidationSchema = z.object({
content: z.string().min(1).max(1000000), // 1MB limit
format: z.enum(['html', 'markdown', 'plain']),
allowedTags: z.array(z.string()).optional(),
});

export const validateArtifactContent = (
type: ArtifactType,
content: string
): { isValid: boolean; errors: string[] } => {
const errors: string[] = [];

try {
// Basic validation
ContentValidationSchema.parse({ content, format: 'html' });

// Content-specific validation
switch (type) {
case 'html':
// Sanitize HTML content
const sanitized = DOMPurify.sanitize(content);
if (sanitized !== content) {
errors.push('Content contains potentially unsafe HTML');
}
break;

case 'react':
// Validate React component structure
if (!content.includes('export default') && !content.includes('function')) {
errors.push('React component must export a function or component');
}
break;
}

return { isValid: errors.length === 0, errors };
} catch (error) {
return {
isValid: false,
errors: [error instanceof Error ? error.message : 'Validation failed']
};
}
};

Performance Optimization

Memoization Patterns

import React, { memo, useMemo, useCallback } from 'react';

const OptimizedViewer = memo<ArtifactViewerProps<CustomArtifact>>(({
artifact,
onError
}) => {
// Memoize expensive computations
const processedContent = useMemo(() => {
return processLargeContent(artifact.content);
}, [artifact.content]);

// Memoize callbacks to prevent child re-renders
const handleError = useCallback((error: string) => {
console.error('Viewer error:', error);
onError?.(error);
}, [onError]);

// Memoize complex objects
const viewerConfig = useMemo(() => ({
theme: 'dark',
language: artifact.metadata?.language || 'text',
lineNumbers: true,
}), [artifact.metadata?.language]);

return (
<ViewerComponent
content={processedContent}
config={viewerConfig}
onError={handleError}
/>
);
});

// Display name for debugging
OptimizedViewer.displayName = 'OptimizedViewer';

Virtual Scrolling

For large content viewers:

import { FixedSizeList as List } from 'react-window';

const VirtualizedViewer: React.FC<ArtifactViewerProps<LargeDataArtifact>> = ({
artifact
}) => {
const items = useMemo(() =>
artifact.content.split('\n'),
[artifact.content]
);

const Row = useCallback(({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
<Typography sx={{ fontFamily: 'monospace', fontSize: '14px' }}>
{items[index]}
</Typography>
</div>
), [items]);

return (
<List
height={600}
itemCount={items.length}
itemSize={20}
width="100%"
>
{Row}
</List>
);
};

Error Handling Patterns

Error Boundary Implementation

import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface State {
hasError: boolean;
error?: Error;
}

class ViewerErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Viewer error boundary caught an error:', error, errorInfo);
this.props.onError?.(error, errorInfo);
}

render() {
if (this.state.hasError) {
return this.props.fallback || (
<Alert color="danger" sx={{ m: 2 }}>
<Typography level="title-sm">Viewer Error</Typography>
<Typography level="body-sm">
{this.state.error?.message || 'An unknown error occurred'}
</Typography>
</Alert>
);
}

return this.props.children;
}
}

// Usage in viewer
const SafeViewer: React.FC<ArtifactViewerProps<CustomArtifact>> = (props) => (
<ViewerErrorBoundary>
<CustomArtifactViewer {...props} />
</ViewerErrorBoundary>
);

Async Error Handling

const AsyncViewer: React.FC<ArtifactViewerProps<AsyncArtifact>> = ({ 
artifact,
onError
}) => {
const [content, setContent] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const loadContent = async () => {
try {
setIsLoading(true);
setError(null);

const response = await fetchArtifactContent(artifact.id);
setContent(response.data);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load content';
setError(errorMessage);
onError?.(errorMessage);
} finally {
setIsLoading(false);
}
};

loadContent();
}, [artifact.id, onError]);

if (isLoading) {
return <CircularProgress />;
}

if (error) {
return (
<Alert color="danger">
<Typography level="title-sm">Loading Error</Typography>
<Typography level="body-sm">{error}</Typography>
</Alert>
);
}

return <ContentRenderer content={content} />;
};

Testing Strategies

Unit Testing

import { render, screen, waitFor } from '@testing-library/react';
import { vi } from 'vitest';
import CustomArtifactViewer from './CustomArtifactViewer';

describe('CustomArtifactViewer', () => {
const mockArtifact: CustomArtifact = {
id: 'test-1',
type: 'custom',
title: 'Test Artifact',
content: 'Test content',
createdAt: new Date(),
updatedAt: new Date(),
};

it('renders content correctly', () => {
render(<CustomArtifactViewer artifact={mockArtifact} />);
expect(screen.getByText('Test content')).toBeInTheDocument();
});

it('handles errors gracefully', async () => {
const onError = vi.fn();
const invalidArtifact = { ...mockArtifact, content: '' };

render(<CustomArtifactViewer artifact={invalidArtifact} onError={onError} />);

await waitFor(() => {
expect(onError).toHaveBeenCalledWith('Invalid artifact: missing content');
});
});

it('calls onEdit when content is modified', () => {
const onEdit = vi.fn();
render(<CustomArtifactViewer artifact={mockArtifact} onEdit={onEdit} />);

// Simulate edit action
// ... test implementation
});
});

Integration Testing

import { render, screen } from '@testing-library/react';
import { SessionsProvider } from '@client/app/contexts/SessionsContext';
import KnowledgeViewer from './KnowledgeViewer';

describe('KnowledgeViewer Integration', () => {
const renderWithProviders = (ui: React.ReactElement) => {
return render(
<SessionsProvider>
{ui}
</SessionsProvider>
);
};

it('integrates custom viewer correctly', async () => {
renderWithProviders(<KnowledgeViewer />);

// Test integration flow
await waitFor(() => {
expect(screen.getByRole('tabpanel')).toBeInTheDocument();
});
});
});

Advanced Features

Real-time Updates

const LiveViewer: React.FC<ArtifactViewerProps<LiveArtifact>> = ({ 
artifact
}) => {
const [content, setContent] = useState(artifact.content);
const { subscribeToAction } = useWebsocket();

useEffect(() => {
const unsubscribe = subscribeToAction('artifact_updated', (message) => {
if (message.artifactId === artifact.id) {
setContent(message.newContent);
}
});

return unsubscribe;
}, [artifact.id, subscribeToAction]);

return <ContentRenderer content={content} />;
};

Collaborative Features

const CollaborativeViewer: React.FC<ArtifactViewerProps<CollaborativeArtifact>> = ({ 
artifact,
onEdit
}) => {
const [cursors, setCursors] = useState<UserCursor[]>([]);
const { currentUser } = useUser();
const { subscribeToAction } = useWebsocket();

// Handle collaborative cursors
useEffect(() => {
const unsubscribe = subscribeToAction('cursor_moved', (message) => {
if (message.artifactId === artifact.id && message.userId !== currentUser?.id) {
setCursors(prev => updateCursor(prev, message));
}
});

return unsubscribe;
}, [artifact.id, currentUser?.id, subscribeToAction]);

const handleCursorMove = useCallback((position: CursorPosition) => {
// Emit cursor position to other users
// ... implementation
}, []);

return (
<Box sx={{ position: 'relative' }}>
<ContentEditor
content={artifact.content}
onEdit={onEdit}
onCursorMove={handleCursorMove}
/>
{cursors.map(cursor => (
<CursorIndicator key={cursor.userId} cursor={cursor} />
))}
</Box>
);
};

Deployment Considerations

Dynamic Imports Configuration

File: next.config.js

const nextConfig = {
webpack: (config) => {
// Optimize dynamic imports for viewers
config.optimization.splitChunks.cacheGroups.viewers = {
name: 'viewers',
test: /\/components\/Knowledge\/.*Viewer\.tsx$/,
chunks: 'async',
priority: 10,
};

return config;
},
};

Bundle Analysis

# Analyze bundle size impact
npx webpack-bundle-analyzer .next/static/chunks/*.js

Performance Monitoring

const PerformanceMonitoredViewer: React.FC<ArtifactViewerProps<any>> = (props) => {
useEffect(() => {
const startTime = performance.now();

return () => {
const endTime = performance.now();
const renderTime = endTime - startTime;

// Log performance metrics
if (renderTime > 1000) { // Log slow renders
console.warn(`Slow viewer render: ${renderTime}ms for ${props.artifact.type}`);
}
};
}, [props.artifact.type]);

return <ActualViewer {...props} />;
};

Troubleshooting

Common Issues

1. Sandbox CSP Violations

// Solution: Update CSP policy
const cspPolicy = `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
`;

2. Memory Leaks in Viewers

// Solution: Proper cleanup
useEffect(() => {
const cleanup = setupViewer();

return () => {
cleanup?.();
// Clear any remaining references
};
}, []);

3. Performance Issues with Large Content

// Solution: Implement virtualization
const useLargeContentStrategy = (content: string) => {
const [strategy, setStrategy] = useState<'normal' | 'virtual' | 'paginated'>('normal');

useEffect(() => {
if (content.length > 100000) {
setStrategy('virtual');
} else if (content.length > 10000) {
setStrategy('paginated');
}
}, [content.length]);

return strategy;
};

Debug Tools

// Development helper for viewer debugging
const ViewerDebugger: React.FC<{ children: React.ReactNode }> = ({ children }) => {
if (process.env.NODE_ENV !== 'development') {
return <>{children}</>;
}

return (
<Box sx={{ position: 'relative' }}>
{children}
<Box sx={{ position: 'absolute', top: 0, right: 0, p: 1, bgcolor: 'warning.main' }}>
<Typography level="body-xs">DEBUG MODE</Typography>
</Box>
</Box>
);
};

This implementation guide provides the foundation for developing robust, secure, and performant viewers within the Bike4Mind ecosystem. Follow these patterns for consistent and maintainable viewer implementations.