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 }) =>