import { SessionIO } from "@/src/components/session"; import { GroupedScoreBadges } from "@/src/components/grouped-score-badge"; import { JsonSkeleton } from "@/src/components/ui/CodeJsonViewer"; import { Card } from "@/src/components/ui/card"; import { type RouterOutputs } from "@/src/utils/api"; import { getNumberFromMap } from "@/src/utils/map-utils"; import Link from "next/link"; import React, { useEffect, useState, useCallback, useRef, useLayoutEffect, } from "react"; import { AnnotateDrawer } from "@/src/features/scores/components/AnnotateDrawer"; import { CommentDrawerButton } from "@/src/features/comments/CommentDrawerButton"; import { ItemBadge } from "@/src/components/ItemBadge"; import { NewDatasetItemFromTraceId } from "@/src/components/session/NewDatasetItemFromTrace"; import { AnnotationQueueObjectType } from "@langfuse/shared"; import { CreateNewAnnotationQueueItem } from "@/src/features/annotation-queues/components/CreateNewAnnotationQueueItem"; // Skeleton placeholder for trace cards const TraceSkeleton = () => { return (
); }; // Trace card with all the heavy content (memoized to prevent unnecessary re-renders) const TraceRow = React.memo( ({ trace, projectId, openPeek, traceCommentCounts, showCorrections, }: { trace: RouterOutputs["sessions"]["byIdWithScores"]["traces"][number]; projectId: string; openPeek: (id: string, row: any) => void; traceCommentCounts: Map | undefined; showCorrections: boolean; }) => { return (
{ if (!e.metaKey && !e.ctrlKey && !e.shiftKey) { e.preventDefault(); openPeek(trace.id, trace); } }} >
{trace.name} ({trace.id}) ↗ {trace.timestamp.toLocaleString()}

Scores

); }, ); TraceRow.displayName = "TraceRow"; /** * Progressive hydration wrapper for trace rows in virtualized lists. * Renders a cheap skeleton initially, then swaps to the full TraceRow when scrolled into view. * This prevents layout thrashing by decoupling virtualization (positioning) from heavy content rendering. * The virtualizer measures the skeleton first, then remeasures once after loading completes. */ const listeners = new Map void>(); let sharedObserver: IntersectionObserver; function observe(element: Element, callback: () => void) { // Lazy-init the observer only once if (!sharedObserver) { sharedObserver = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const cb = listeners.get(entry.target); cb?.(); // Run the callback } }); }, { rootMargin: "400px" }, ); } listeners.set(element, callback); sharedObserver.observe(element); // Return simple cleanup function return () => { listeners.delete(element); sharedObserver.unobserve(element); }; } export const LazyTraceRow = React.forwardRef< HTMLDivElement, { trace: RouterOutputs["sessions"]["byIdWithScores"]["traces"][number]; projectId: string; openPeek: (id: string, row: any) => void; index: number; traceCommentCounts: Map | undefined; showCorrections: boolean; onLoad?: (index: number) => void; } >((props, measureRef) => { const { index, onLoad: onLoad, showCorrections, ...cardProps } = props; const [shouldLoad, setShouldLoad] = useState(false); const internalRef = useRef(null); useEffect(() => { if (!internalRef.current || shouldLoad) return; return observe(internalRef.current, () => setShouldLoad(true)); }, [shouldLoad]); // Notify virtualizer when content changes (fixes height) useLayoutEffect(() => { if (shouldLoad && onLoad) { onLoad(index); } }, [shouldLoad, onLoad, index]); // Merge refs (Virtualizer + Local) const combinedRef = useCallback( (node: HTMLDivElement | null) => { internalRef.current = node; if (typeof measureRef === "function") measureRef(node); else if (measureRef) measureRef.current = node; }, [measureRef], ); return (
{shouldLoad ? ( ) : ( )}
); }); LazyTraceRow.displayName = "LazyTraceRow";