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.