-
Notifications
You must be signed in to change notification settings - Fork 154
improve llm spans view #1061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
improve llm spans view #1061
Changes from 4 commits
461327f
cce4e66
1927ffe
ba36867
b201b15
5352847
fe7fa54
10df28d
f9302a7
6a21c39
f22031a
598b5aa
de9a3f7
512f3fa
687d9da
f2f7bca
5328f3e
a67d6f6
29141b1
689c00a
4306b36
e47e204
1afb6c8
ef0c432
947f19b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,10 @@ | ||
| import { VirtualItem } from "@tanstack/react-virtual"; | ||
| import {CircleDollarSign, Coins} from "lucide-react"; | ||
| import React, { memo, useLayoutEffect, useMemo, useRef, useState } from "react"; | ||
|
|
||
| import { TraceViewSpan } from "@/components/traces/trace-view/trace-view-store.tsx"; | ||
| import { TimelineData } from "@/components/traces/trace-view/trace-view-store-utils.ts"; | ||
| import {getLLMMetrics, getSpanDisplayName} from "@/components/traces/trace-view/utils.ts"; | ||
| import { SPAN_TYPE_TO_COLOR } from "@/lib/traces/utils"; | ||
| import { cn, getDurationString } from "@/lib/utils"; | ||
|
|
||
|
|
@@ -25,6 +27,7 @@ const TimelineElement = ({ | |
| const textRef = useRef<HTMLSpanElement>(null); | ||
| const blockRef = useRef<HTMLDivElement>(null); | ||
| const [textPosition, setTextPosition] = useState<"inside" | "outside">("inside"); | ||
| const [isHovered, setIsHovered] = useState(false); | ||
|
|
||
| const isSelected = useMemo(() => selectedSpan?.spanId === span.span.spanId, [span.span.spanId, selectedSpan?.spanId]); | ||
|
|
||
|
|
@@ -34,6 +37,8 @@ const TimelineElement = ({ | |
| } | ||
| }; | ||
|
|
||
| const llmMetrics = getLLMMetrics(span.span); | ||
|
|
||
| useLayoutEffect(() => { | ||
| if (!blockRef.current || !textRef.current) return; | ||
|
|
||
|
|
@@ -56,11 +61,24 @@ const TimelineElement = ({ | |
| }; | ||
| }, [span.span.name, span.events.length, span.width]); | ||
|
|
||
| const SpanText = useMemo(() => { | ||
| const spanTextElement = useMemo(() => { | ||
| const displayName = isHovered && span.span.spanType === "LLM" ? span.span.name : getSpanDisplayName(span.span); | ||
|
|
||
| const textContent = ( | ||
| <> | ||
| {span.span.name}{" "} | ||
| {displayName}{" "} | ||
| <span className="text-white/70">{getDurationString(span.span.startTime, span.span.endTime)}</span> | ||
| {llmMetrics && ( | ||
| <> | ||
| <div className={'text-white/70 inline-flex items-center gap-1 ml-2'}> | ||
| <Coins className="min-w-3" size={12} /> | ||
| {llmMetrics.totalTokens} | ||
| </div> | ||
| <div className={'text-white/70 inline-flex items-center gap-1 ml-4'} style={{marginLeft: 4}}> | ||
|
||
| <CircleDollarSign className="min-w-3" size={12} /> | ||
| {llmMetrics.cost}</div> | ||
| </> | ||
| )} | ||
| </> | ||
| ); | ||
|
|
||
|
|
@@ -104,13 +122,15 @@ const TimelineElement = ({ | |
| {textContent} | ||
| </span> | ||
| ); | ||
| }, [span.span.name, span.span.startTime, span.span.endTime, span.left, span.events.length, textPosition]); | ||
| }, [span.span.name, span.span.startTime, span.span.endTime, span.span.spanType, span.left, span.events.length, textPosition, isHovered]); | ||
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return ( | ||
| <div | ||
| key={virtualRow.index} | ||
| data-index={virtualRow.index} | ||
| onClick={handleSpanSelect} | ||
| onMouseEnter={() => setIsHovered(true)} | ||
| onMouseLeave={() => setIsHovered(false)} | ||
| className={cn( | ||
| "absolute top-0 left-0 w-full h-8 flex items-center px-4 hover:bg-muted cursor-pointer transition duration-200" | ||
| )} | ||
|
|
@@ -123,14 +143,14 @@ const TimelineElement = ({ | |
| <span | ||
| title={span.span.name} | ||
| ref={textRef} | ||
| className="text-xs font-medium text-black truncate absolute" | ||
| className={"text-xs font-medium text-black truncate absolute"} | ||
| style={{ | ||
| right: `calc(100% - ${span.left}% + 16px)`, | ||
| textAlign: "right", | ||
| maxWidth: "250px", | ||
| }} | ||
| > | ||
| {span.span.name}{" "} | ||
| {isHovered && span.span.spanType === "LLM" ? span.span.name : getSpanDisplayName(span.span)}{" "} | ||
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| <span className="text-secondary-foreground">{getDurationString(span.span.startTime, span.span.endTime)}</span> | ||
| </span> | ||
| )} | ||
|
|
@@ -156,9 +176,9 @@ const TimelineElement = ({ | |
| }} | ||
| /> | ||
| ))} | ||
| {textPosition === "inside" && SpanText} | ||
| {textPosition === "inside" && spanTextElement} | ||
| </div> | ||
| {textPosition === "outside" && SpanText} | ||
| {textPosition === "outside" && spanTextElement} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -236,3 +236,23 @@ export const findSpanToSelect = ( | |
| // Priority 3: First span as fallback | ||
| return spans?.[0]; | ||
| }; | ||
|
|
||
|
|
||
|
|
||
| export const getSpanDisplayName = (span:TraceViewSpan) => { | ||
| const modelName = span.model ?? span.attributes["gen_ai.request.model"]; | ||
| return span.spanType === "LLM" ? modelName : span.name; | ||
| }; | ||
shoqqan marked this conversation as resolved.
Show resolved
Hide resolved
olzhik11 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| export const getLLMMetrics = (span:TraceViewSpan)=>{ | ||
| if (span.spanType !== "LLM") return null; | ||
|
|
||
| const cost = span.attributes["gen_ai.usage.cost"].toFixed(3); | ||
|
||
| const totalTokens = | ||
| span.attributes["gen_ai.usage.input_tokens"] + span.attributes["gen_ai.usage.output_tokens"]; | ||
shoqqan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if (!cost || !totalTokens) return null; | ||
|
|
||
| return { cost, totalTokens }; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.