-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat(ai-conversations): use conversations endpoint #106367
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
Merged
obostjancic
merged 10 commits into
master
from
ogi/tet-1720-call-new-conversation-endpoint
Jan 20, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
9f0d9ab
feat(ai-conversations): use conversations endpoint
obostjancic 4ea2037
padding fix
obostjancic 48db8b0
cleanup
obostjancic 474bba9
deslop
obostjancic 6c426ad
fixes
obostjancic 7f81bbd
Merge branch 'master' into ogi/tet-1720-call-new-conversation-endpoint
obostjancic adbc716
remove unused
obostjancic 63c9c48
fixes
obostjancic 1354f58
review fixes
obostjancic 86a4117
Merge branch 'master' into ogi/tet-1720-call-new-conversation-endpoint
obostjancic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,31 +68,37 @@ export function AISpanList({ | |
| nodes, | ||
| selectedNodeKey, | ||
| onSelectNode, | ||
| compressGaps = false, | ||
| }: { | ||
| nodes: AITraceSpanNode[]; | ||
| onSelectNode: (node: AITraceSpanNode) => void; | ||
| selectedNodeKey: string | null; | ||
| compressGaps?: boolean; | ||
| }) { | ||
| const nodesByTransaction = useMemo(() => { | ||
| const result: Map<TransactionNode | EapSpanNode, AITraceSpanNode[]> = new Map(); | ||
| const result: Map< | ||
| TransactionNode | EapSpanNode | AITraceSpanNode, | ||
| AITraceSpanNode[] | ||
| > = new Map(); | ||
| // Use a placeholder key for nodes without a transaction (e.g., conversation view) | ||
| let orphanGroup: AITraceSpanNode | null = null; | ||
|
|
||
| for (const node of nodes) { | ||
| // TODO: We should consider using BaseNode.expand to control toggle state, | ||
| // instead of grouping by transactions for toggling by transactions only. | ||
| // This would allow us to avoid using type guards/checks like below, outside of the BaseNode classes. | ||
| const isNodeTransaction = | ||
| isTransactionNode(node) || (isEAPSpanNode(node) && node.value.is_transaction); | ||
| const transaction = isNodeTransaction ? node : node.findClosestParentTransaction(); | ||
| if (!transaction) { | ||
| continue; | ||
| } | ||
| const transactionNodes = result.get(transaction) || []; | ||
| result.set(transaction, [...transactionNodes, node]); | ||
| const groupKey = transaction ?? (orphanGroup ??= node); | ||
| const transactionNodes = result.get(groupKey) || []; | ||
| result.set(groupKey, [...transactionNodes, node]); | ||
| } | ||
| return result; | ||
| }, [nodes]); | ||
|
|
||
| return ( | ||
| <Stack padding="2xs" gap="xs" overflow="hidden"> | ||
| <Stack gap="xs"> | ||
| {Array.from(nodesByTransaction.entries()).map(([transaction, transactionNodes]) => ( | ||
| <TransactionWrapper | ||
| key={transaction.id} | ||
|
|
@@ -101,6 +107,7 @@ export function AISpanList({ | |
| nodes={transactionNodes} | ||
| onSelectNode={onSelectNode} | ||
| selectedNodeKey={selectedNodeKey} | ||
| compressGaps={compressGaps} | ||
| /> | ||
| ))} | ||
| </Stack> | ||
|
|
@@ -113,17 +120,24 @@ function TransactionWrapper({ | |
| onSelectNode, | ||
| selectedNodeKey, | ||
| transaction, | ||
| compressGaps = false, | ||
| }: { | ||
| canCollapse: boolean; | ||
| nodes: AITraceSpanNode[]; | ||
| onSelectNode: (node: AITraceSpanNode) => void; | ||
| selectedNodeKey: string | null; | ||
| transaction: TransactionNode | EapSpanNode; | ||
| transaction: TransactionNode | EapSpanNode | AITraceSpanNode; | ||
| compressGaps?: boolean; | ||
| }) { | ||
| const [isExpanded, setIsExpanded] = useState(true); | ||
| const theme = useTheme(); | ||
| const colors = [...theme.chart.getColorPalette(5), theme.colors.red400]; | ||
| const timeBounds = getNodeTimeBounds(nodes); | ||
|
|
||
| const compressedBounds = useMemo( | ||
| () => (compressGaps ? getCompressedTimeBounds(nodes) : null), | ||
| [compressGaps, nodes] | ||
| ); | ||
| const timeBounds = compressedBounds ?? getNodeTimeBounds(nodes); | ||
|
|
||
| const nodeAiRunParentsMap = useMemo<Record<string, AITraceSpanNode>>(() => { | ||
| const parents: Record<string, AITraceSpanNode> = {}; | ||
|
|
@@ -142,21 +156,25 @@ function TransactionWrapper({ | |
| setIsExpanded(prevValue => !prevValue); | ||
| }; | ||
|
|
||
| const title = | ||
| 'transaction' in transaction.value | ||
| ? transaction.value.transaction | ||
| : transaction.value.description; | ||
|
|
||
| const showHeader = canCollapse || !!title; | ||
|
|
||
| return ( | ||
| <Fragment> | ||
| <TransactionButton type="button" disabled={!canCollapse} onClick={handleCollapse}> | ||
| {canCollapse ? ( | ||
| <StyledIconChevron direction={isExpanded ? 'down' : 'right'} /> | ||
| ) : null} | ||
| <Tooltip | ||
| title={transaction.value.transaction} | ||
| showOnlyOnOverflow | ||
| skipWrapper | ||
| delay={500} | ||
| > | ||
| <span>{transaction.value.transaction}</span> | ||
| </Tooltip> | ||
| </TransactionButton> | ||
| {showHeader && ( | ||
| <TransactionButton type="button" disabled={!canCollapse} onClick={handleCollapse}> | ||
| {canCollapse ? ( | ||
| <StyledIconChevron direction={isExpanded ? 'down' : 'right'} /> | ||
| ) : null} | ||
| <Tooltip title={title} showOnlyOnOverflow skipWrapper delay={500}> | ||
| <span>{title}</span> | ||
| </Tooltip> | ||
| </TransactionButton> | ||
| )} | ||
| {isExpanded && | ||
| nodes.map(node => { | ||
| const aiRunNode = nodeAiRunParentsMap[node.id]; | ||
|
|
@@ -174,6 +192,7 @@ function TransactionWrapper({ | |
| onClick={() => onSelectNode(node)} | ||
| isSelected={uniqueKey === selectedNodeKey} | ||
| colors={colors} | ||
| compressedStartByNodeId={compressedBounds?.compressedStartByNodeId} | ||
| /> | ||
| ); | ||
| })} | ||
|
|
@@ -188,18 +207,24 @@ const TraceListItem = memo(function TraceListItem({ | |
| colors, | ||
| traceBounds, | ||
| indent, | ||
| compressedStartByNodeId, | ||
| }: { | ||
| colors: readonly string[]; | ||
| indent: number; | ||
| isSelected: boolean; | ||
| node: AITraceSpanNode; | ||
| onClick: () => void; | ||
| traceBounds: TraceBounds; | ||
| compressedStartByNodeId?: Map<string, number>; | ||
| }) { | ||
| const hasErrors = hasError(node); | ||
| const {icon, title, subtitle, color} = getNodeInfo(node, colors); | ||
| const safeColor = color || colors[0] || '#9ca3af'; | ||
| const relativeTiming = calculateRelativeTiming(node, traceBounds); | ||
| const relativeTiming = calculateRelativeTiming( | ||
| node, | ||
| traceBounds, | ||
| compressedStartByNodeId | ||
| ); | ||
| const duration = getNodeTimeBounds(node).duration; | ||
|
|
||
| return ( | ||
|
|
@@ -266,9 +291,97 @@ interface TraceBounds { | |
| startTime: number; | ||
| } | ||
|
|
||
| interface CompressedTimeBounds extends TraceBounds { | ||
| compressedStartByNodeId: Map<string, number>; | ||
| } | ||
|
|
||
| const MAX_GAP_SECONDS = 30; | ||
| const COMPRESSED_GAP_SECONDS = 1; | ||
|
|
||
| /** | ||
| * Compresses large time gaps between spans to make the timeline more readable. | ||
| * Gaps larger than MAX_GAP_SECONDS are compressed to COMPRESSED_GAP_SECONDS. | ||
| * | ||
| * This function handles nested/overlapping spans by tracking "segments" - continuous | ||
| * time ranges where spans are active. When a gap is detected between segments, | ||
| * it's compressed if larger than MAX_GAP_SECONDS. | ||
| * | ||
| * Returns a Map of node IDs to their compressed start times, which allows O(1) | ||
| * lookup when rendering each span's position on the timeline. | ||
| */ | ||
| function getCompressedTimeBounds(nodes: AITraceSpanNode[]): CompressedTimeBounds { | ||
|
Comment on lines
293
to
+312
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I got a few problems with this function:
Something like this could better suit your needs: function getCompressedTimeBounds(nodes: AITraceSpanNode[]): CompressedTimeBounds {
const emptyResult: CompressedTimeBounds = {
startTime: 0,
endTime: 0,
duration: 0,
compressedStartByNodeId: new Map(),
};
if (nodes.length === 0) {
return emptyResult;
}
const sortedNodes = [...nodes]
.filter(n => n.startTimestamp && n.endTimestamp)
.sort((a, b) => (a.startTimestamp ?? 0) - (b.startTimestamp ?? 0));
if (sortedNodes.length === 0) {
return emptyResult;
}
const compressedStartByNodeId = new Map<string, number>();
// Track current segment bounds
const firstNode = sortedNodes[0]!;
let segmentRealStart = firstNode.startTimestamp!;
let segmentRealEnd = firstNode.endTimestamp!;
let segmentCompressedStart = 0;
compressedStartByNodeId.set(firstNode.id, 0);
for (let i = 1; i < sortedNodes.length; i++) {
const node = sortedNodes[i]!;
const nodeStart = node.startTimestamp!;
const nodeEnd = node.endTimestamp!;
if (nodeStart > segmentRealEnd) {
// Gap detected - finish current segment and start new one
const gap = nodeStart - segmentRealEnd;
const compressedGap = gap > MAX_GAP_SECONDS ? COMPRESSED_GAP_SECONDS : gap;
const segmentDuration = segmentRealEnd - segmentRealStart;
// Advance compressed time by segment duration + gap
segmentCompressedStart += segmentDuration + compressedGap;
// Start new segment
segmentRealStart = nodeStart;
segmentRealEnd = nodeEnd;
} else {
// Overlapping - extend current segment
segmentRealEnd = Math.max(segmentRealEnd, nodeEnd);
}
// Calculate this node's compressed start
const offsetInSegment = nodeStart - segmentRealStart;
compressedStartByNodeId.set(node.id, segmentCompressedStart + offsetInSegment);
}
// Total duration is the compressed start of last segment + its duration
const totalDuration = segmentCompressedStart + (segmentRealEnd - segmentRealStart);
return {
startTime: 0,
endTime: totalDuration,
duration: totalDuration,
compressedStartByNodeId,
};
} |
||
| const emptyResult: CompressedTimeBounds = { | ||
| startTime: 0, | ||
| endTime: 0, | ||
| duration: 0, | ||
| compressedStartByNodeId: new Map(), | ||
| }; | ||
|
|
||
| if (nodes.length === 0) { | ||
| return emptyResult; | ||
| } | ||
|
|
||
| const sortedNodes = [...nodes] | ||
| .filter(n => n.startTimestamp && n.endTimestamp) | ||
| .sort((a, b) => (a.startTimestamp ?? 0) - (b.startTimestamp ?? 0)); | ||
|
|
||
| if (sortedNodes.length === 0) { | ||
| return emptyResult; | ||
| } | ||
|
|
||
| const compressedStartByNodeId = new Map<string, number>(); | ||
|
|
||
| // Track current segment bounds - a segment is a continuous time range | ||
| // where at least one span is active (handles overlapping/nested spans) | ||
| const firstNode = sortedNodes[0]!; | ||
| let segmentRealStart = firstNode.startTimestamp!; | ||
| let segmentRealEnd = firstNode.endTimestamp!; | ||
| let segmentCompressedStart = 0; | ||
|
|
||
| compressedStartByNodeId.set(firstNode.id, 0); | ||
|
|
||
| for (let i = 1; i < sortedNodes.length; i++) { | ||
| const node = sortedNodes[i]!; | ||
| const nodeStart = node.startTimestamp!; | ||
| const nodeEnd = node.endTimestamp!; | ||
|
|
||
| if (nodeStart > segmentRealEnd) { | ||
| // Gap detected - finish current segment and start new one | ||
| const gap = nodeStart - segmentRealEnd; | ||
| const compressedGap = gap > MAX_GAP_SECONDS ? COMPRESSED_GAP_SECONDS : gap; | ||
| const segmentDuration = segmentRealEnd - segmentRealStart; | ||
|
|
||
| // Advance compressed time by segment duration + gap | ||
| segmentCompressedStart += segmentDuration + compressedGap; | ||
|
|
||
| // Start new segment | ||
| segmentRealStart = nodeStart; | ||
| segmentRealEnd = nodeEnd; | ||
| } else { | ||
| // Overlapping/nested span - extend current segment if needed | ||
| segmentRealEnd = Math.max(segmentRealEnd, nodeEnd); | ||
| } | ||
|
|
||
| // Calculate this node's compressed start relative to the current segment | ||
| const offsetInSegment = nodeStart - segmentRealStart; | ||
| compressedStartByNodeId.set(node.id, segmentCompressedStart + offsetInSegment); | ||
| } | ||
|
|
||
| // Total duration is the compressed start of last segment + its duration | ||
| const totalDuration = segmentCompressedStart + (segmentRealEnd - segmentRealStart); | ||
|
|
||
| return { | ||
| startTime: 0, | ||
| endTime: totalDuration, | ||
| duration: totalDuration, | ||
| compressedStartByNodeId, | ||
| }; | ||
| } | ||
|
|
||
| function calculateRelativeTiming( | ||
| node: AITraceSpanNode, | ||
| traceBounds: TraceBounds | ||
| traceBounds: TraceBounds, | ||
| compressedStartByNodeId?: Map<string, number> | ||
| ): {leftPercent: number; widthPercent: number} { | ||
| if (!node.value) return {leftPercent: 0, widthPercent: 0}; | ||
|
|
||
|
|
@@ -283,11 +396,19 @@ function calculateRelativeTiming( | |
|
|
||
| if (traceBounds.duration === 0) return {leftPercent: 0, widthPercent: 0}; | ||
|
|
||
| const relativeStart = | ||
| Math.max(0, (startTime - traceBounds.startTime) / traceBounds.duration) * 100; | ||
| const spanDuration = ((endTime - startTime) / traceBounds.duration) * 100; | ||
| // Look up the pre-computed compressed start time for this node. | ||
| // The span duration stays the same - only gaps between spans are compressed. | ||
| const compressedStart = compressedStartByNodeId?.get(node.id); | ||
| const effectiveStart = | ||
| compressedStart === undefined ? startTime - traceBounds.startTime : compressedStart; | ||
| const effectiveEnd = | ||
| compressedStart === undefined | ||
| ? endTime - traceBounds.startTime | ||
| : compressedStart + (endTime - startTime); | ||
|
|
||
| const relativeStart = Math.max(0, effectiveStart / traceBounds.duration) * 100; | ||
| const spanDuration = ((effectiveEnd - effectiveStart) / traceBounds.duration) * 100; | ||
|
|
||
| // Minimum width of 2% for very short spans | ||
| const minWidth = 2; | ||
| const adjustedWidth = Math.max(spanDuration, minWidth); | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.

Uh oh!
There was an error while loading. Please reload this page.