/** * GenericRenderer Component * * Fallback renderer for unknown tool types or when no display data is available. * Displays raw JSON/text content with syntax highlighting. */ import { useState } from 'react'; import { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; import hljs from 'highlight.js/lib/core'; import json from 'highlight.js/lib/languages/json'; // Register JSON language hljs.registerLanguage('json', json); interface GenericRendererProps { /** Raw content to display */ content: unknown; /** Maximum lines before truncation (default: 20) */ maxLines?: number; /** Whether to start expanded (default: false) */ defaultExpanded?: boolean; } /** * Escape HTML entities to prevent XSS when using dangerouslySetInnerHTML */ function escapeHtml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Renders generic tool result content with syntax highlighting. */ export function GenericRenderer({ content, maxLines = 20, defaultExpanded = false, }: GenericRendererProps) { const [expanded, setExpanded] = useState(defaultExpanded); const [showAll, setShowAll] = useState(false); const [copied, setCopied] = useState(false); // Format content as string const formattedContent = typeof content === 'string' ? content : JSON.stringify(content, null, 2); const lines = formattedContent.split('\n'); const shouldTruncate = lines.length > maxLines && !showAll; const displayContent = shouldTruncate ? lines.slice(0, maxLines).join('\n') : formattedContent; // Syntax highlight if it looks like JSON, otherwise escape HTML for safety let highlightedContent: string; try { if (typeof content === 'object' || formattedContent.startsWith('{')) { const result = hljs.highlight(displayContent, { language: 'json' }); highlightedContent = result.value; } else { // Not JSON - escape HTML entities for plain text highlightedContent = escapeHtml(displayContent); } } catch { // Highlight failed - escape HTML entities for safety highlightedContent = escapeHtml(displayContent); } const handleCopy = async () => { await navigator.clipboard.writeText(formattedContent); setCopied(true); setTimeout(() => setCopied(false), 2000); }; if (!expanded) { return ( ); } return (
                {shouldTruncate && (
                    
                )}
            
); }