'use client' import { useState, useMemo } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@nextsparkjs/core/components/ui/button' import type { Span } from '../../types/observability.types' import { TraceStatusBadge } from './TraceStatusBadge' // Internal chain patterns to filter (LangChain infrastructure noise) const INTERNAL_CHAIN_PATTERNS = [ /^Chain:\s*(Runnable|Channel|Compiled)/i, /^RunnableSequence$/i, /^RunnableLambda$/i, /^ChannelWrite$/i, /^ChannelRead$/i, ] interface SpansListProps { spans: Span[] className?: string } export function SpansList({ spans, className = '' }: SpansListProps) { const t = useTranslations('observability') const [expandedSpans, setExpandedSpans] = useState>(new Set()) const [showInternalChains, setShowInternalChains] = useState(false) // Filter internal chain spans const { displaySpans, hiddenCount } = useMemo(() => { if (showInternalChains) { return { displaySpans: spans, hiddenCount: 0 } } const filtered = spans.filter((span) => { // Always show non-chain spans if (span.type !== 'chain') return true // Always show chains with errors if (span.error) return true // Filter internal chains by pattern return !INTERNAL_CHAIN_PATTERNS.some((pattern) => pattern.test(span.name)) }) return { displaySpans: filtered, hiddenCount: spans.length - filtered.length, } }, [spans, showInternalChains]) const toggleSpan = (spanId: string) => { setExpandedSpans((prev) => { const next = new Set(prev) if (next.has(spanId)) { next.delete(spanId) } else { next.add(spanId) } return next }) } const formatDuration = (ms?: number) => { if (!ms) return '-' if (ms < 1000) return `${ms}ms` return `${(ms / 1000).toFixed(2)}s` } const formatTokens = (input?: number, output?: number) => { if (!input && !output) return null return `${input || 0}/${output || 0} tokens` } const getSpanTypeLabel = (type: string) => { return type.toUpperCase() } if (spans.length === 0) { return (
{t('detail.noSpans')}
) } return (
{/* Filter toggle */} {hiddenCount > 0 && (
{t('detail.hiddenSpans', { count: hiddenCount })}
)} {showInternalChains && hiddenCount > 0 && (
)}
{displaySpans.map((span) => { const isExpanded = expandedSpans.has(span.spanId) const hasDetails = (span.toolInput !== undefined && span.toolInput !== null) || (span.toolOutput !== undefined && span.toolOutput !== null) || !!span.error return (
{getSpanTypeLabel(span.type)} {span.name}
{span.provider && ( {span.provider} / {span.model} )} {span.toolName && {span.toolName}} {formatDuration(span.durationMs)} {formatTokens(span.inputTokens, span.outputTokens) && ( {formatTokens(span.inputTokens, span.outputTokens)} )}
{hasDetails && ( )} {isExpanded && (
{span.toolInput !== undefined && span.toolInput !== null && (
{t('detail.toolInput')}
                          {JSON.stringify(span.toolInput, null, 2)}
                        
)} {span.toolOutput !== undefined && span.toolOutput !== null && (
{t('detail.toolOutput')}
                          {JSON.stringify(span.toolOutput, null, 2)}
                        
)} {span.error && (
{t('detail.error')}
                          {span.error}
                        
)}
)}
) })}
) }