diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceSteps.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceSteps.tsx new file mode 100644 index 00000000000..acf463e1289 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceSteps.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +import { cn } from "@/lib/cva.config"; + +import { SPAN_PILL_CLASS, SpanTypePill } from "./TracePills"; +import { fmtCost, fmtMs, previewText, spanLabel, type TraceStep } from "./traceUtils"; + +interface TraceStepsProps { + steps: TraceStep[]; + selectedId: string | null; + onSelect: (spanId: string) => void; +} + +const SUBAGENT_INDENT_PX = 28; +const BASE_PADDING_PX = 12; + +function StepTitle({ step }: { step: TraceStep }) { + const { span } = step; + if (span.type === "agent") { + return ( + <> + + subagent: {span.name} + {fmtMs(span.duration_ms)} + + ); + } + const cost = span.type === "llm" ? ` ยท ${fmtCost(span.litellm?.spend)}` : ""; + return ( + <> + + {spanLabel(span)} + + {fmtMs(span.duration_ms)} + {cost} + + + ); +} + +/** Numbered narrative: every LLM decision and tool result in time order, subagent steps indented. */ +export function TraceSteps({ steps, selectedId, onSelect }: TraceStepsProps) { + const listRef = useRef(null); + + useEffect(() => { + if (!selectedId) return; + const node = listRef.current?.querySelector(`[data-span-id="${selectedId}"]`); + if (node && "scrollIntoView" in node) (node as HTMLElement).scrollIntoView({ block: "nearest" }); + }, [selectedId]); + + if (steps.length === 0) { + return
This trace has no LLM or tool spans.
; + } + + return ( +
    + {steps.map((step) => { + const pill = step.span.type === "agent" ? "agent" : step.span.type; + return ( +
  1. onSelect(step.span.span_id)} + style={{ paddingLeft: BASE_PADDING_PX + step.depth * SUBAGENT_INDENT_PX }} + className={cn( + "grid cursor-pointer grid-cols-[22px_1fr] gap-2.5 py-2 pr-3 hover:bg-muted/60", + step.span.span_id === selectedId && "bg-primary/5", + )} + > + + {step.number} + +
    +
    + + {step.subagent && in {step.subagent}} +
    + {step.span.input_preview && ( +
    + {previewText(step.span.input_preview)} +
    + )} + {step.toolNames.length > 0 && ( +
    + {step.toolNames.map((name, i) => ( + + {name}() + + ))} +
    + )} +
    +
  2. + ); + })} +
+ ); +}