'use client'; import React, { useEffect, useRef } from 'react'; import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'; import { DataInspector } from './ui/data-inspector'; import { Skeleton } from './ui/skeleton'; // ────────────────────────────────────────────────────────────────────────── // Helpers // ────────────────────────────────────────────────────────────────────────── function deserializeChunkText(text: string): string { try { const parsed = JSON.parse(text); if (typeof parsed === 'string') { return parsed; } return JSON.stringify(parsed, null, 2); } catch { return text; } } function parseChunkData(text: string): unknown { try { return JSON.parse(text); } catch { return text; } } // ────────────────────────────────────────────────────────────────────────── // Types // ────────────────────────────────────────────────────────────────────────── export interface StreamChunk { id: number; text: string; } type Chunk = StreamChunk; interface StreamViewerProps { streamId: string; chunks: Chunk[]; isLive: boolean; error?: string | null; /** True while the initial stream connection is being established */ isLoading?: boolean; /** Called when the user scrolls near the bottom, for triggering pagination */ onScrollEnd?: () => void; } // ────────────────────────────────────────────────────────────────────────── // Chunk row — memoized to prevent remounts during polling // ────────────────────────────────────────────────────────────────────────── const ChunkRow = React.memo(function ChunkRow({ chunk, index, }: { chunk: Chunk; index: number; }) { const parsed = parseChunkData(chunk.text); return (
[{index}] {typeof parsed === 'string' ? ( {deserializeChunkText(parsed)} ) : ( )}
); }); // ────────────────────────────────────────────────────────────────────────── // Skeleton loading // ────────────────────────────────────────────────────────────────────────── function StreamSkeleton() { return (
{[1, 2, 3, 4].map((i) => ( ))}
); } // ────────────────────────────────────────────────────────────────────────── // Main component // ────────────────────────────────────────────────────────────────────────── /** * StreamViewer component that displays real-time stream data. * Each chunk is rendered with DataInspector for proper display * of complex types (Map, Set, Date, custom classes, etc.). */ export function StreamViewer({ streamId: _streamId, chunks, isLive, error, isLoading, onScrollEnd, }: StreamViewerProps) { const virtuosoRef = useRef(null); const prevChunkCountRef = useRef(0); // Auto-scroll to bottom when new chunks arrive (live streaming) useEffect(() => { if (chunks.length > prevChunkCountRef.current && chunks.length > 0) { virtuosoRef.current?.scrollToIndex({ index: chunks.length - 1, align: 'end', }); } prevChunkCountRef.current = chunks.length; }, [chunks.length]); // Show skeleton when loading and no chunks have arrived yet if (isLoading && chunks.length === 0) { return (
); } return (
{/* Live indicator */} {isLive && (
Live
)} {/* Header */} {chunks.length > 0 && (
Stream Chunks ({chunks.length})
)} {/* Content */}
{error ? (
Error reading stream:
{error}
) : chunks.length === 0 ? (
{isLive ? 'Waiting for stream data...' : 'Stream is empty'}
) : ( onScrollEnd?.()} itemContent={(index) => (
)} style={{ flex: 1, minHeight: 0 }} /> )}
); }