1+ import { X } from "lucide-react" ;
2+ import { useEffect , useRef , useState } from "react" ;
13import { Bar , BarChart , Cell , Tooltip , XAxis , YAxis } from "recharts" ;
2- import type { TopToolCall } from "#/db" ;
4+ import type { ToolErrorEntry , TopToolCall } from "#/db" ;
5+ import { getToolErrorsFn } from "#/lib/serverFns" ;
36import { ChartCard } from "../ChartCard" ;
47
58const AXIS_TICK = {
@@ -17,7 +20,113 @@ function shortToolName(name: string): string {
1720 return parts . length > 1 ? parts [ parts . length - 1 ] : name ;
1821}
1922
23+ /**
24+ * Strip `<tool_use_error>...</tool_use_error>` wrapper if present, then trim.
25+ */
26+ function cleanErrorText ( raw : string ) : string {
27+ return raw
28+ . replace ( / ^ < t o o l _ u s e _ e r r o r > \s * / i, "" )
29+ . replace ( / \s * < \/ t o o l _ u s e _ e r r o r > $ / i, "" )
30+ . trim ( ) ;
31+ }
32+
33+ // ─── ErrorModal ───────────────────────────────────────────────────────────────
34+
35+ function ErrorModal ( {
36+ toolName,
37+ onClose,
38+ } : {
39+ toolName : string ;
40+ onClose : ( ) => void ;
41+ } ) {
42+ const [ errors , setErrors ] = useState < ToolErrorEntry [ ] | null > ( null ) ;
43+ const dialogRef = useRef < HTMLDivElement > ( null ) ;
44+
45+ useEffect ( ( ) => {
46+ dialogRef . current ?. focus ( ) ;
47+ getToolErrorsFn ( { data : toolName } )
48+ . then ( setErrors )
49+ . catch ( ( ) => setErrors ( [ ] ) ) ;
50+ } , [ toolName ] ) ;
51+
52+ const displayName = shortToolName ( toolName ) ;
53+
54+ return (
55+ // biome-ignore lint/a11y/useKeyWithClickEvents: Escape handled by inner div
56+ // biome-ignore lint/a11y/noStaticElementInteractions: modal backdrop pattern
57+ < div
58+ className = "fixed inset-0 z-50 bg-background/70 backdrop-blur-sm flex items-center justify-center p-4"
59+ onClick = { onClose }
60+ >
61+ < div
62+ ref = { dialogRef }
63+ tabIndex = { - 1 }
64+ role = "dialog"
65+ aria-modal = "true"
66+ aria-label = { `Errors for ${ displayName } ` }
67+ className = "relative bg-card border border-border shadow-lg w-full max-w-md max-h-[70vh] flex flex-col focus:outline-none"
68+ onClick = { ( e ) => e . stopPropagation ( ) }
69+ onKeyDown = { ( e ) => {
70+ if ( e . key === "Escape" ) onClose ( ) ;
71+ } }
72+ >
73+ { /* Header */ }
74+ < div className = "flex items-center justify-between px-4 py-3 border-b border-border shrink-0" >
75+ < div >
76+ < div className = "text-xs font-medium text-foreground font-mono" >
77+ { displayName }
78+ </ div >
79+ { toolName !== displayName && (
80+ < div className = "text-[9px] text-muted-foreground font-mono truncate max-w-[300px]" >
81+ { toolName }
82+ </ div >
83+ ) }
84+ </ div >
85+ < button
86+ type = "button"
87+ onClick = { onClose }
88+ aria-label = "Close"
89+ className = "text-muted-foreground hover:text-foreground transition-colors p-1 -mr-1"
90+ >
91+ < X className = "w-3.5 h-3.5" />
92+ </ button >
93+ </ div >
94+
95+ { /* Body */ }
96+ < div className = "overflow-y-auto flex-1 p-4" >
97+ { errors === null ? (
98+ < div className = "text-[10px] text-muted-foreground animate-pulse" >
99+ Loading…
100+ </ div >
101+ ) : errors . length === 0 ? (
102+ < div className = "text-[10px] text-muted-foreground" >
103+ No error details found.
104+ </ div >
105+ ) : (
106+ < div className = "space-y-2" >
107+ { errors . map ( ( e , i ) => (
108+ // biome-ignore lint/suspicious/noArrayIndexKey: stable order from DB, no reorder
109+ < div key = { i } className = "flex gap-3 items-start" >
110+ < span className = "shrink-0 text-[9px] tabular-nums text-muted-foreground pt-0.5 min-w-[24px] text-right" >
111+ { e . count } ×
112+ </ span >
113+ < span className = "text-[10px] font-mono text-foreground/80 leading-relaxed break-all" >
114+ { cleanErrorText ( e . text ) }
115+ </ span >
116+ </ div >
117+ ) ) }
118+ </ div >
119+ ) }
120+ </ div >
121+ </ div >
122+ </ div >
123+ ) ;
124+ }
125+
126+ // ─── TopToolsChart ────────────────────────────────────────────────────────────
127+
20128export function TopToolsChart ( { data } : { data : TopToolCall [ ] } ) {
129+ const [ selectedTool , setSelectedTool ] = useState < string | null > ( null ) ;
21130 const empty = data . length === 0 ;
22131 const rows = data . map ( ( d ) => ( {
23132 name : shortToolName ( d . name ) ,
@@ -30,67 +139,89 @@ export function TopToolsChart({ data }: { data: TopToolCall[] }) {
30139 const height = empty ? 140 : Math . max ( 140 , 26 * rows . length + 20 ) ;
31140
32141 return (
33- < ChartCard
34- title = "Top tool calls"
35- subtitle = { empty ? undefined : `Top ${ data . length } by count` }
36- height = { height }
37- empty = { empty ? "No tool events yet" : undefined }
38- >
39- < BarChart
40- data = { rows }
41- layout = "vertical"
42- margin = { { top : 4 , right : 12 , bottom : 0 , left : 8 } }
142+ < >
143+ < ChartCard
144+ title = "Top tool calls"
145+ subtitle = { empty ? undefined : `Top ${ data . length } by count` }
146+ height = { height }
147+ empty = { empty ? "No tool events yet" : undefined }
43148 >
44- < XAxis
45- type = "number"
46- tick = { AXIS_TICK }
47- axisLine = { false }
48- tickLine = { false }
49- />
50- < YAxis
51- type = "category"
52- dataKey = "name"
53- tick = { AXIS_TICK }
54- axisLine = { false }
55- tickLine = { false }
56- width = { 84 }
57- />
58- < Tooltip
59- cursor = { { fill : "color-mix(in oklch, var(--data) 8%, transparent)" } }
60- content = { ( { active, payload } ) => {
61- if ( ! active || ! payload ?. length ) return null ;
62- const r = payload [ 0 ] . payload as ( typeof rows ) [ number ] ;
63- return (
64- < div className = "text-[9px] tabular-nums bg-background/95 border border-border px-2 py-1 rounded shadow-sm text-foreground/80 space-y-0.5" >
65- < div className = "text-foreground" > { r . fullName } </ div >
66- < div > { r . count } calls</ div >
67- < div className = "text-muted-foreground" >
68- { ( r . errorRate * 100 ) . toFixed ( 1 ) } % errors
149+ < BarChart
150+ data = { rows }
151+ layout = "vertical"
152+ margin = { { top : 4 , right : 12 , bottom : 0 , left : 8 } }
153+ >
154+ < XAxis
155+ type = "number"
156+ tick = { AXIS_TICK }
157+ axisLine = { false }
158+ tickLine = { false }
159+ />
160+ < YAxis
161+ type = "category"
162+ dataKey = "name"
163+ tick = { AXIS_TICK }
164+ axisLine = { false }
165+ tickLine = { false }
166+ width = { 84 }
167+ />
168+ < Tooltip
169+ cursor = { {
170+ fill : "color-mix(in oklch, var(--data) 8%, transparent)" ,
171+ } }
172+ content = { ( { active, payload } ) => {
173+ if ( ! active || ! payload ?. length ) return null ;
174+ const r = payload [ 0 ] . payload as ( typeof rows ) [ number ] ;
175+ return (
176+ < div className = "text-[9px] tabular-nums bg-background/95 border border-border px-2 py-1 rounded shadow-sm text-foreground/80 space-y-0.5" >
177+ < div className = "text-foreground" > { r . fullName } </ div >
178+ < div > { r . count } calls</ div >
179+ < div className = "text-muted-foreground" >
180+ { ( r . errorRate * 100 ) . toFixed ( 1 ) } % errors
181+ { r . errorRate > 0 && (
182+ < span className = "ml-1 opacity-60" >
183+ — click for details
184+ </ span >
185+ ) }
186+ </ div >
69187 </ div >
70- </ div >
71- ) ;
72- } }
188+ ) ;
189+ } }
190+ />
191+ < Bar
192+ dataKey = "count"
193+ radius = { [ 0 , 2 , 2 , 0 ] }
194+ isAnimationActive = { false }
195+ onClick = { ( barData ) => {
196+ const r = barData as unknown as ( typeof rows ) [ number ] ;
197+ if ( r . errorRate > 0 ) setSelectedTool ( r . fullName ) ;
198+ } }
199+ style = { { cursor : "pointer" } }
200+ >
201+ { rows . map ( ( r ) => {
202+ // Defensive clamp: errorRate is contract-bounded to [0,1] but
203+ // guard against bad upstream data so opacity stays valid.
204+ const er = Math . max ( 0 , Math . min ( 1 , r . errorRate ) ) ;
205+ // Min opacity 0.7 keeps low-error bars readable on the dark
206+ // theme background (--destructive is too muddy at <0.7).
207+ return (
208+ < Cell
209+ key = { r . fullName }
210+ fill = { er > 0 ? "var(--chart-error)" : "var(--data)" }
211+ fillOpacity = { er > 0 ? 0.7 + er * 0.3 : 0.85 }
212+ />
213+ ) ;
214+ } ) }
215+ </ Bar >
216+ </ BarChart >
217+ </ ChartCard >
218+
219+ { selectedTool && (
220+ < ErrorModal
221+ toolName = { selectedTool }
222+ onClose = { ( ) => setSelectedTool ( null ) }
73223 />
74- { /* Single bar per tool. Cell fill ramps from --data (no errors)
75- toward --destructive as errorRate rises so high-error tools
76- visually pop instead of fading away. */ }
77- < Bar dataKey = "count" radius = { [ 0 , 2 , 2 , 0 ] } isAnimationActive = { false } >
78- { rows . map ( ( r ) => {
79- // Defensive clamp: errorRate is contract-bounded to [0,1] but
80- // guard against bad upstream data so opacity stays valid.
81- const er = Math . max ( 0 , Math . min ( 1 , r . errorRate ) ) ;
82- // Min opacity 0.7 keeps low-error bars readable on the dark
83- // theme background (--destructive is too muddy at <0.7).
84- return (
85- < Cell
86- key = { r . fullName }
87- fill = { er > 0 ? "var(--chart-error)" : "var(--data)" }
88- fillOpacity = { er > 0 ? 0.7 + er * 0.3 : 0.85 }
89- />
90- ) ;
91- } ) }
92- </ Bar >
93- </ BarChart >
94- </ ChartCard >
224+ ) }
225+ </ >
95226 ) ;
96227}
0 commit comments