From 35437dd7f5a29e95edb0592145a1655dbeb14642 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Sun, 4 Oct 2026 11:56:07 -0700 Subject: [PATCH] refactor(lens): restructure the trace drawer and polish its layout Split the 547-line TraceDrawer into run/, tree/, span/, content/ and conversation/ modules. Step rows now sit on one line with colored span family tiles, and the per-row timing bar moved into an optional Waterfall layout with a time axis. The steps and details panes are separated by the shadcn Resizable handle, with the split remembered per orientation. Span payloads go through one pure classifier (payloadView) that picks messages, a tool result, a nested field tree or text. JSON-encoded field values unfold into a tree, prose renders as markdown, repr and tracebacks stay monospace, and every section offers a Raw view. LangChain's serialized messages now render as conversation cards. Co-Authored-By: Claude Opus 5.5 --- ui/litellm-dashboard/package-lock.json | 11 + ui/litellm-dashboard/package.json | 1 + ui/litellm-dashboard/src/app/globals.css | 21 +- .../lens/investigations/Evidence.tsx | 2 +- .../FindingDetails.integration.test.tsx | 2 +- .../lens/traces/detail/AttributesDetail.tsx | 44 -- .../lens/traces/detail/DetailContent.tsx | 259 --------- .../lens/traces/detail/DetailPane.tsx | 240 -------- .../lens/traces/detail/KeyValueRows.tsx | 103 ---- .../lens/traces/detail/MessageCard.tsx | 221 ------- .../lens/traces/detail/RequestDetail.tsx | 79 --- .../lens/traces/detail/SpanTree.tsx | 415 ------------- .../lens/traces/detail/TraceDrawer.tsx | 547 ------------------ .../lens/traces/detail/content/Block.tsx | 95 +++ .../lens/traces/detail/content/ContentTab.tsx | 105 ++++ .../lens/traces/detail/content/FieldTree.tsx | 139 +++++ .../lens/traces/detail/content/Markdown.tsx | 60 ++ .../Messages.test.tsx} | 10 +- .../lens/traces/detail/content/Messages.tsx | 148 +++++ .../traces/detail/content/PayloadBody.tsx | 54 ++ .../lens/traces/detail/content/Section.tsx | 81 +++ .../lens/traces/detail/content/SpanError.tsx | 123 ++++ .../traces/detail/content/payload.test.ts | 155 +++++ .../lens/traces/detail/content/payload.ts | 98 ++++ .../TraceConversation.integration.test.tsx | 14 +- .../{ => conversation}/TraceConversation.tsx | 11 +- .../{ => conversation}/conversation.test.ts | 4 +- .../detail/{ => conversation}/conversation.ts | 14 +- .../lens/traces/detail/run/PagingBanner.tsx | 64 ++ .../lens/traces/detail/run/RunBody.tsx | 121 ++++ .../lens/traces/detail/run/RunHeader.tsx | 117 ++++ .../RunView.test.tsx} | 50 +- .../lens/traces/detail/run/RunView.tsx | 174 ++++++ .../lens/traces/detail/run/useRunTree.ts | 147 +++++ .../lens/traces/detail/span/AttributesTab.tsx | 36 ++ .../lens/traces/detail/span/DetailGroup.tsx | 8 + .../DetailPane.integration.test.tsx | 85 ++- .../lens/traces/detail/span/DetailPane.tsx | 37 ++ .../lens/traces/detail/span/GroupPane.tsx | 74 +++ .../lens/traces/detail/span/PaneHeader.tsx | 55 ++ .../lens/traces/detail/span/RequestTab.tsx | 80 +++ .../lens/traces/detail/span/SpanPane.tsx | 85 +++ .../detail/{ => tree}/SpanHoverCard.tsx | 12 +- .../lens/traces/detail/tree/SpanTree.tsx | 259 +++++++++ .../lens/traces/detail/tree/TreeRows.tsx | 260 +++++++++ .../lens/traces/detail/tree/timeline.test.ts | 56 ++ .../lens/traces/detail/tree/timeline.ts | 34 ++ .../AgentTracesSection.integration.test.tsx | 2 +- .../lens/traces/list/AgentTracesSection.tsx | 2 +- .../src/components/lens/traces/ui/PaneBar.tsx | 5 - .../components/lens/traces/ui/SpanIcon.tsx | 47 +- .../src/components/lens/traces/utils.test.ts | 15 + .../src/components/lens/traces/utils.ts | 35 +- .../src/components/ui/Resizable.tsx | 41 ++ 54 files changed, 2970 insertions(+), 1987 deletions(-) delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/AttributesDetail.tsx delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/DetailContent.tsx delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/DetailPane.tsx delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/KeyValueRows.tsx delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/MessageCard.tsx delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/RequestDetail.tsx delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/SpanTree.tsx delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/TraceDrawer.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/Block.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/ContentTab.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/FieldTree.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/Markdown.tsx rename ui/litellm-dashboard/src/components/lens/traces/detail/{MessageCard.test.tsx => content/Messages.test.tsx} (93%) create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/Messages.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/PayloadBody.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/Section.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/SpanError.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/payload.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/content/payload.ts rename ui/litellm-dashboard/src/components/lens/traces/detail/{ => conversation}/TraceConversation.integration.test.tsx (96%) rename ui/litellm-dashboard/src/components/lens/traces/detail/{ => conversation}/TraceConversation.tsx (96%) rename ui/litellm-dashboard/src/components/lens/traces/detail/{ => conversation}/conversation.test.ts (98%) rename ui/litellm-dashboard/src/components/lens/traces/detail/{ => conversation}/conversation.ts (96%) create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/run/PagingBanner.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/run/RunBody.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/run/RunHeader.tsx rename ui/litellm-dashboard/src/components/lens/traces/detail/{TraceDrawer.test.tsx => run/RunView.test.tsx} (92%) create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/run/useRunTree.ts create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/span/AttributesTab.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/span/DetailGroup.tsx rename ui/litellm-dashboard/src/components/lens/traces/detail/{ => span}/DetailPane.integration.test.tsx (82%) create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/span/DetailPane.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/span/GroupPane.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/span/PaneHeader.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/span/RequestTab.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/span/SpanPane.tsx rename ui/litellm-dashboard/src/components/lens/traces/detail/{ => tree}/SpanHoverCard.tsx (95%) create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/tree/SpanTree.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/tree/TreeRows.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/tree/timeline.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/traces/detail/tree/timeline.ts delete mode 100644 ui/litellm-dashboard/src/components/lens/traces/ui/PaneBar.tsx create mode 100644 ui/litellm-dashboard/src/components/ui/Resizable.tsx diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 37027f215fe..9aa5764548c 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -47,6 +47,7 @@ "react-json-view-lite": "2.5.0", "react-markdown": "9.1.0", "react-reconciler": "0.33.0", + "react-resizable-panels": "4.14.1", "react-syntax-highlighter": "15.6.6", "recharts": "3.9.2", "remark-gfm": "4.0.1", @@ -10649,6 +10650,16 @@ } } }, + "node_modules/react-resizable-panels": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.14.1.tgz", + "integrity": "sha512-OB1bXDNTLcGgTTbaX6Dn5efZhlMboSnkfr1w4xpsJTHPEbRJpACBhXpgRCXozgPO1ztgFeZRUe5J3mFNCVKm5g==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/react-syntax-highlighter": { "version": "15.6.6", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 388b6c7eab9..81b89337325 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -64,6 +64,7 @@ "react-json-view-lite": "2.5.0", "react-markdown": "9.1.0", "react-reconciler": "0.33.0", + "react-resizable-panels": "4.14.1", "react-syntax-highlighter": "15.6.6", "recharts": "3.9.2", "remark-gfm": "4.0.1", diff --git a/ui/litellm-dashboard/src/app/globals.css b/ui/litellm-dashboard/src/app/globals.css index cad79f64ea0..cfa85817ea6 100644 --- a/ui/litellm-dashboard/src/app/globals.css +++ b/ui/litellm-dashboard/src/app/globals.css @@ -208,9 +208,12 @@ --trace-tab-active: oklch(0.95 0.025 205); --trace-tab-hover: oklch(0.93 0.02 215); --trace-tag: oklch(0.95 0.02 205); - --trace-chain: oklch(0.56 0.17 255); - --trace-llm: oklch(0.6 0.13 215); - --trace-tool: oklch(0.64 0.14 165); + --trace-chain: #2a78d6; + --trace-llm: #eb6834; + --trace-tool: #1baf7a; + --trace-chain-soft: color-mix(in oklab, #2a78d6 12%, transparent); + --trace-llm-soft: color-mix(in oklab, #eb6834 12%, transparent); + --trace-tool-soft: color-mix(in oklab, #1baf7a 14%, transparent); --trace-glyph: oklch(0.99 0 0); --trace-human: oklch(0.5 0.15 260); --trace-human-glyph: oklch(0.95 0.04 210); @@ -282,9 +285,12 @@ --trace-tab-active: oklch(0.28 0.03 215); --trace-tab-hover: oklch(0.32 0.03 220); --trace-tag: oklch(0.28 0.03 215); - --trace-chain: oklch(0.6 0.17 255); - --trace-llm: oklch(0.64 0.13 215); - --trace-tool: oklch(0.68 0.14 165); + --trace-chain: #3987e5; + --trace-llm: #d95926; + --trace-tool: #199e70; + --trace-chain-soft: color-mix(in oklab, #3987e5 24%, transparent); + --trace-llm-soft: color-mix(in oklab, #d95926 24%, transparent); + --trace-tool-soft: color-mix(in oklab, #199e70 24%, transparent); --trace-glyph: oklch(0.99 0 0); --trace-human: oklch(0.56 0.15 260); --trace-human-glyph: oklch(0.95 0.04 210); @@ -320,6 +326,9 @@ --color-trace-chain: var(--trace-chain); --color-trace-llm: var(--trace-llm); --color-trace-tool: var(--trace-tool); + --color-trace-chain-soft: var(--trace-chain-soft); + --color-trace-llm-soft: var(--trace-llm-soft); + --color-trace-tool-soft: var(--trace-tool-soft); --color-trace-glyph: var(--trace-glyph); --color-trace-human: var(--trace-human); --color-trace-human-glyph: var(--trace-human-glyph); diff --git a/ui/litellm-dashboard/src/components/lens/investigations/Evidence.tsx b/ui/litellm-dashboard/src/components/lens/investigations/Evidence.tsx index 05017e76b68..fa4b716691c 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/Evidence.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/Evidence.tsx @@ -5,7 +5,7 @@ import { useQuery } from "@tanstack/react-query"; import { Inspector } from "@/components/shared/Inspector"; import { Button } from "@/components/ui/button"; -import { RunView } from "@/components/lens/traces/detail/TraceDrawer"; +import { RunView } from "@/components/lens/traces/detail/run/RunView"; import { useLocalRunSelection } from "@/components/lens/traces/routing"; import { lensQueries } from "../data/queries"; diff --git a/ui/litellm-dashboard/src/components/lens/investigations/FindingDetails.integration.test.tsx b/ui/litellm-dashboard/src/components/lens/investigations/FindingDetails.integration.test.tsx index 93942cb8fa9..544a7272e14 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/FindingDetails.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/FindingDetails.integration.test.tsx @@ -11,7 +11,7 @@ import type { OwnedFinding } from "../model/inbox"; import type { Finding, Lens } from "../model/types"; import { FindingPanel, ownedFindingKey } from "./FindingDetails"; -vi.mock("@/components/lens/traces/detail/TraceDrawer", () => ({ +vi.mock("@/components/lens/traces/detail/run/RunView", () => ({ RunView: ({ traceId, selection }: { traceId: string; selection: RunSelection }) => (
{traceId} at {selection.spanId} diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/AttributesDetail.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/AttributesDetail.tsx deleted file mode 100644 index 4461a3ca429..00000000000 --- a/ui/litellm-dashboard/src/components/lens/traces/detail/AttributesDetail.tsx +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { type KeyValue, KeyValueRows } from "./KeyValueRows"; -import { Card } from "./MessageCard"; -import type { Span } from "../types"; - -interface AttributesDetailProps { - traceId: string; - span: Span; - attributes: Record | undefined; - isLoading: boolean; -} - -export function DetailGroup({ title, children }: { title: string; children: React.ReactNode }) { - return ( -
-

{title}

- {children} -
- ); -} - -/** Ids, then the raw OTEL attributes, as dot-bulleted key / value rows. */ -export function AttributesDetail({ traceId, span, attributes, isLoading }: AttributesDetailProps) { - const ids: KeyValue[] = [ - ["trace_id", traceId], - ["span_id", span.span_id], - ["parent_span_id", span.parent_span_id ?? "—"], - ]; - const attributeEntries: KeyValue[] = Object.entries(attributes ?? {}).sort(([a], [b]) => a.localeCompare(b)); - return ( -
- - - - {attributeEntries.length > 0 && ( - - - - )} - {isLoading &&
Loading attributes…
} -
- ); -} diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/DetailContent.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/DetailContent.tsx deleted file mode 100644 index 70e62de65d5..00000000000 --- a/ui/litellm-dashboard/src/components/lens/traces/detail/DetailContent.tsx +++ /dev/null @@ -1,259 +0,0 @@ -"use client"; -import { useTracesApi } from "../api"; - -import { useQuery, type UseQueryOptions } from "@tanstack/react-query"; -import { useState } from "react"; -import { AlertTriangle } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/cva.config"; - -import { type KeyValue, KeyValueRows, objectEntries } from "./KeyValueRows"; -import { Card, MessageCard, Section, ToolResultCard } from "./MessageCard"; -import { - classifyTraceReadFailure, - isRetryableTraceRead, - traceReadRetry, - traceReadRetryDelay, -} from "../list/traceReadFailure"; -import type { ErrorSource } from "../tree"; -import type { Span, SpanDetail, SpanErrorPage, TraceMessage, UIContent, UIMessage } from "../types"; -import { errorSource, parseJson, parseMessages, prettyPayload } from "../utils"; - -const ERROR_SOURCE_LABEL: Record = { tool: "Tool", model: "Model", litellm: "LiteLLM" }; -const TRACEBACK_MARKER = "Traceback (most recent call last):"; -const STATUS_TEXT = "px-3 py-2 text-sm text-muted-foreground"; -const PAYLOAD_PRE = "font-mono text-sm leading-normal break-words whitespace-pre-wrap text-foreground"; - -/** Exporters record `repr(exc)` + traceback with no separator; keep the exception line. */ -export const errorHeadline = (error: string): string => - (error.split(TRACEBACK_MARKER, 1)[0].split("\n")[0] ?? "").trim() || error.trim(); - -/** `ValueError('x not found')` → "ValueError"; plain text → "error". */ -const errorReason = (headline: string): string => /^([A-Za-z_][\w.]*)\(/.exec(headline)?.[1] ?? "error"; - -/** Shared lazy fetch of one span's full input / output / attributes. */ -export function useSpanDetail(accessToken: string, traceId: string, spanId: string | null, traceRef?: string) { - const traces = useTracesApi(accessToken); - const queryOptions: UseQueryOptions = { - queryKey: ["agentTraceSpan", traceId, traceRef, spanId, accessToken], - queryFn: () => traces.span(traceId, spanId as string, traceRef), - enabled: spanId !== null, - staleTime: Infinity, - }; - return useQuery(queryOptions); -} - -export function ErrorBlock({ span }: { span: Span }) { - const source = errorSource(span); - if (!source) return null; - const headline = errorHeadline(span.error ?? "") || "Span reported an error status."; - return ( -
-
- - {ERROR_SOURCE_LABEL[source]} · {errorReason(headline)} -
-
{headline}
-
- ); -} - -function FieldsCard({ entries }: { entries: readonly KeyValue[] }) { - return ( - - - - ); -} - -function TextCard({ text }: { text: string }) { - return ( - -
{text}
-
- ); -} - -function PlainPayload({ value }: { value: string }) { - const entries = objectEntries(parseJson(value)); - if (entries && entries.length > 0) return ; - return ; -} - -function Messages({ messages, model }: { messages: TraceMessage[]; model: string | null }) { - return ( - <> - {messages.map((message, i) => ( - - ))} - - ); -} - -const toTraceMessage = (message: UIMessage): TraceMessage => ({ - ...message, - tool_calls: message.tool_calls?.map((call) => ({ - name: call.name, - args: parseJson(call.arguments) ?? call.arguments, - })), -}); - -interface PayloadProps { - value: string; - span: Span; - role: "input" | "output"; -} - -const isToolResult = ({ span, role }: Omit): boolean => - span.type === "tool" && role === "output"; - -function ToolResult({ value, span }: Omit) { - return ; -} - -const singleText = (content: UIContent): string | null => - content.kind === "messages" && content.messages.length === 1 && !content.messages[0].tool_calls?.length - ? content.messages[0].content - : null; - -function UIPayload({ content, ...props }: PayloadProps & { content: UIContent }) { - const toolText = isToolResult(props) ? singleText(content) : null; - if (toolText !== null) return ; - if (content.kind === "messages") { - return ; - } - if (isToolResult(props)) return ; - if (content.kind === "fields" && content.fields.length > 0) { - return [field.key, field.value])} />; - } - return ; -} - -function Payload(props: PayloadProps) { - const messages = parseMessages(props.value); - if (messages) return ; - if (isToolResult(props)) return ; - return ; -} - -function SpanPayload({ content, ...props }: PayloadProps & { content: UIContent | undefined }) { - return content ? : ; -} - -interface DetailContentProps { - accessToken: string; - traceId: string; - traceRef?: string; - span: Span; -} - -function DiagnosticContent({ accessToken, traceId, traceRef, span }: DetailContentProps) { - const traces = useTracesApi(accessToken); - const [opened, setOpened] = useState(false); - const [cursor, setCursor] = useState(null); - const queryOptions: UseQueryOptions = { - queryKey: ["agentTraceSpanError", traceId, traceRef, span.span_id, accessToken, cursor], - queryFn: () => traces.spanError(traceId, span.span_id, { traceRef, cursor }), - enabled: opened, - staleTime: Infinity, - gcTime: 0, - retry: traceReadRetry, - retryDelay: traceReadRetryDelay, - }; - const query = useQuery(queryOptions); - const failure = query.error ? classifyTraceReadFailure(query.error) : null; - return ( -
- {span.error_truncated &&

Error preview truncated

} - {!opened && ( - - )} - {opened && query.isPending &&

Loading diagnostic…

} - {failure && ( -
- Could not load diagnostic: {failure.message} - {isRetryableTraceRead(failure) ? ( - - ) : ( - - )} -
- )} - {opened && query.data && ( - <> - -

- {cursor ? "Continuation" : "Beginning"} of stored diagnostic ({query.data.total_chars.toLocaleString()}{" "} - characters) -

- {query.data.next_cursor && ( - - )} - {cursor && ( - - )} - - )} -
- ); -} - -/** Content tab: the error first (if any), then collapsible Input and Output rendered as chat cards. */ -export function DetailContent({ accessToken, traceId, traceRef, span }: DetailContentProps) { - const detailQuery = useSpanDetail(accessToken, traceId, span.span_id, traceRef); - const detail = detailQuery.data; - const empty = detail && !detail.input && !detail.output; - const inputMessages = - detail?.input_ui?.kind === "messages" - ? detail.input_ui.messages.length - : parseMessages(detail?.input ?? "")?.length; - - return ( -
- - {span.error && ( - - )} - {detailQuery.isLoading &&
Loading span…
} - {detailQuery.isError &&
Could not load span: {detailQuery.error.message}
} - {detail?.input ? ( -
- -
- ) : null} - {detail?.output ? ( -
- -
- ) : null} - {empty && span.status !== "error" && ( -
No content recorded for this span.
- )} -
- ); -} diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/DetailPane.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/DetailPane.tsx deleted file mode 100644 index e030d393ab3..00000000000 --- a/ui/litellm-dashboard/src/components/lens/traces/detail/DetailPane.tsx +++ /dev/null @@ -1,240 +0,0 @@ -"use client"; - -import { PanelRightClose } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; - -import { AttributesDetail } from "./AttributesDetail"; -import CopyButton from "@/components/shared/CopyButton"; -import { DetailContent, errorHeadline, useSpanDetail } from "./DetailContent"; -import { IdChip } from "../ui/IdChip"; -import { PaneBar } from "../ui/PaneBar"; -import { RequestDetail } from "./RequestDetail"; -import { SpanIcon } from "../ui/SpanIcon"; -import { useTracesApi } from "../api"; -import { SPAN_TABS, type SpanTab } from "../routing"; -import type { GroupRowData, TreeRow } from "../tree"; -import type { Span, SpanType, Trace } from "../types"; -import { fmtMs, fmtTok } from "../utils"; - -interface SpanTabProps { - spanTab: SpanTab; - onSpanTabChange: (tab: SpanTab) => void; -} - -interface DetailPaneProps extends SpanTabProps { - trace: Trace; - row: TreeRow | undefined; - accessToken: string; - onClose: () => void; -} - -const TAB_LABELS: Record = { content: "Content", request: "Request", attributes: "Attributes" }; - -function PaneHeader({ - type, - model, - failed, - title, - idValue, - onClose, -}: { - type: SpanType; - model: string | null; - failed: boolean; - title: React.ReactNode; - idValue?: string; - onClose: () => void; -}) { - return ( - -
- - - -

{title}

- {idValue && ( - - - - )} -
- -
- ); -} - -function PaneFooter({ children }: { children: React.ReactNode }) { - return
{children}
; -} - -function Meta({ label, value }: { label: string; value: string }) { - return ( - - {label} - {value} - - ); -} - -function SpanPane({ - trace, - span, - accessToken, - spanTab: tab, - onSpanTabChange, - onClose, -}: SpanTabProps & { - trace: Trace; - span: Span; - accessToken: string; - onClose: () => void; -}) { - const handoff = useTracesApi(accessToken).handoff(trace.summary.trace_id, span.span_id, trace.summary.trace_ref); - const traceId = trace.summary.trace_id; - const detailQuery = useSpanDetail( - accessToken, - traceId, - tab === "attributes" ? span.span_id : null, - trace.summary.trace_ref, - ); - const tokens = span.input_tokens + span.output_tokens; - return ( - - ); -} - -function GroupMetric({ label, value }: { label: string; value: string }) { - return ( -
-
{label}
-
{value}
-
- ); -} - -/** ×N group: rollup of every invocation plus the first failure's message. */ -function GroupPane({ - trace, - row, - accessToken, - onClose, -}: { - trace: Trace; - row: GroupRowData; - accessToken: string; - onClose: () => void; -}) { - const tokens = row.members.reduce((sum, m) => sum + m.input_tokens + m.output_tokens, 0); - const firstFailure = row.members.find((m) => m.status === "error" && m.error); - const sample = (firstFailure ?? row.members[0]).span_id; - const handoff = useTracesApi(accessToken).handoff(trace.summary.trace_id, sample, trace.summary.trace_ref); - return ( - - ); -} - -/** Right pane of the run view: switches on the selected tree row. */ -export function DetailPane({ trace, row, accessToken, spanTab, onSpanTabChange, onClose }: DetailPaneProps) { - if (!row || row.kind === "load-more") { - return ( -
- Select a span to inspect it. -
- ); - } - if (row.kind === "group") - return ; - return ( - - ); -} diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/KeyValueRows.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/KeyValueRows.tsx deleted file mode 100644 index 6eb8217df3b..00000000000 --- a/ui/litellm-dashboard/src/components/lens/traces/detail/KeyValueRows.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { cn } from "@/lib/cva.config"; - -import { FoldChevron } from "../ui/Collapse"; - -const LONG_VALUE_CHARS = 90; -const ID_KEY = /(^|_)id$/i; - -export type KeyValue = readonly [key: string, value: string]; - -export const isLongValue = (value: string): boolean => value.length > LONG_VALUE_CHARS || value.includes("\n"); - -export const displayValue = (value: unknown): string => { - if (typeof value === "string") return value; - if (value === null || value === undefined) return String(value); - return JSON.stringify(value); -}; - -export const objectEntries = (value: unknown): KeyValue[] | null => { - if (typeof value !== "object" || value === null || Array.isArray(value)) return null; - return Object.entries(value).map(([key, v]): KeyValue => [key, displayValue(v)]); -}; - -const ROW = "flex min-w-0 items-baseline gap-3 border-b border-border/60 py-2 last:border-0"; -const KEY = "w-24 shrink-0 break-words text-xs leading-5 font-normal"; - -function Lead({ children }: { children: React.ReactNode }) { - return {children}; -} - -function KeyValueRow({ entry, mono }: { entry: KeyValue; mono: boolean }) { - const [key, value] = entry; - const [open, setOpen] = useState(false); - const valueClass = cn( - "min-w-0 flex-1 break-words text-left text-sm leading-5 text-foreground", - mono || ID_KEY.test(key) ? "font-mono" : "font-sans", - ); - - if (!isLongValue(value)) { - return ( -
  • - {key} - - {value} - -
  • - ); - } - - const head = ( - <> - - - - {key} - - ); - const toggle = { - type: "button", - "aria-expanded": open, - "aria-label": `${open ? "Collapse" : "Expand"} ${key}`, - onClick: () => setOpen((prev) => !prev), - } as const; - - return ( -
  • - {open ? ( - <> - -
    {value}
    - - ) : ( - - )} -
  • - ); -} - -export function KeyValueRows({ - entries, - mono = false, - className, -}: { - entries: readonly KeyValue[]; - mono?: boolean; - className?: string; -}) { - return ( -
      - {entries.map((entry) => ( - - ))} -
    - ); -} diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/MessageCard.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/MessageCard.tsx deleted file mode 100644 index f27023dd1e1..00000000000 --- a/ui/litellm-dashboard/src/components/lens/traces/detail/MessageCard.tsx +++ /dev/null @@ -1,221 +0,0 @@ -"use client"; - -import { Wrench } from "lucide-react"; -import { useState } from "react"; -import ReactMarkdown, { type Components } from "react-markdown"; -import remarkGfm from "remark-gfm"; - -import { cn } from "@/lib/cva.config"; - -import { FoldChevron } from "../ui/Collapse"; -import CopyButton from "@/components/shared/CopyButton"; -import { displayValue, type KeyValue, KeyValueRows, objectEntries } from "./KeyValueRows"; -import type { TraceMessage, TraceToolCall } from "../types"; - -const ROLE_LABEL: Record = { user: "User", system: "System", assistant: "Assistant", tool: "Tool" }; - -const BLOCK_GAP = "mb-[1lh] last:mb-0"; - -const markdownComponents: Components = { - p: ({ node, ...props }) =>

    , - ul: ({ node, ...props }) =>