1- import { FileText } from "lucide-react" ;
2- import { useCallback , useState } from "react" ;
1+ import { CircleAlert , FileDown , type LucideIcon , Signal } from "lucide-react" ;
2+ import { useCallback , useMemo , useState } from "react" ;
33
44import { FileUpload } from "@/components/file-upload" ;
55import { ModeToggle } from "@/components/mode-toggle" ;
66import { TraceView } from "@/components/trace-view" ;
7- import { type TraceFile , parseTraceLog } from "@/lib/trace" ;
7+ import { formatDuration } from "@/components/trace-view/span-utils" ;
8+ import {
9+ type TraceFile ,
10+ extractSpansFromEntries ,
11+ parseTraceLog ,
12+ } from "@/lib/trace" ;
13+
14+ function useTraceStats ( file : TraceFile | null ) {
15+ return useMemo ( ( ) => {
16+ if ( ! file ) return null ;
17+ const spans = extractSpansFromEntries ( file . entries ) ;
18+ if ( spans . length === 0 ) {
19+ return { spans : 0 , entries : file . entries . length , duration : 0 , errors : 0 } ;
20+ }
21+ const min = Math . min ( ...spans . map ( ( s ) => s . startTime ) ) ;
22+ const max = Math . max ( ...spans . map ( ( s ) => s . endTime ) ) ;
23+ return {
24+ spans : spans . length ,
25+ entries : file . entries . length ,
26+ duration : max - min ,
27+ errors : spans . filter ( ( s ) => s . status === "ERROR" ) . length ,
28+ } ;
29+ } , [ file ] ) ;
30+ }
31+
32+ function Stat ( {
33+ label,
34+ value,
35+ tone = "default" ,
36+ icon : Icon ,
37+ } : {
38+ label : string ;
39+ value : string ;
40+ tone ?: "default" | "error" ;
41+ icon ?: LucideIcon ;
42+ } ) {
43+ const active = tone === "error" && value !== "0" ;
44+ return (
45+ < div className = "flex flex-col leading-none" >
46+ < span className = "text-muted-foreground font-mono text-[9px] tracking-[0.18em] uppercase" >
47+ { label }
48+ </ span >
49+ < span
50+ className = { `mt-1 flex items-center gap-1.5 font-mono text-sm font-semibold tabular-nums ${
51+ active ? "text-kind-error" : "text-foreground"
52+ } `}
53+ >
54+ { Icon && (
55+ < Icon
56+ className = { `h-3.5 w-3.5 ${ active ? "text-kind-error" : "text-muted-foreground/50" } ` }
57+ aria-hidden
58+ />
59+ ) }
60+ { value }
61+ </ span >
62+ </ div >
63+ ) ;
64+ }
865
966function App ( ) {
1067 const [ file , setFile ] = useState < TraceFile | null > ( null ) ;
1168 const [ isDragging , setIsDragging ] = useState ( false ) ;
69+ const stats = useTraceStats ( file ) ;
1270
1371 const handleFileLoaded = useCallback ( ( loadedFile : TraceFile ) => {
1472 setFile ( loadedFile ) ;
1573 setIsDragging ( false ) ;
1674 } , [ ] ) ;
1775
18- const handleClearFile = useCallback ( ( ) => {
19- setFile ( null ) ;
20- } , [ ] ) ;
76+ const handleClearFile = useCallback ( ( ) => setFile ( null ) , [ ] ) ;
2177
2278 const handleDrop = useCallback (
2379 ( e : React . DragEvent ) => {
@@ -27,26 +83,21 @@ function App() {
2783
2884 const droppedFile = e . dataTransfer . files [ 0 ] ;
2985 if ( ! droppedFile ) return ;
30-
31- // Only handle JSONL files
3286 if ( ! droppedFile . name . endsWith ( ".jsonl" ) ) {
3387 console . error ( "Only .jsonl files are supported" ) ;
3488 return ;
3589 }
3690
3791 const reader = new FileReader ( ) ;
38- reader . onload = ( e ) => {
92+ reader . onload = ( ev ) => {
3993 try {
40- const content = e . target ?. result as string ;
41- const traceFile = parseTraceLog ( droppedFile . name , content ) ;
42- handleFileLoaded ( traceFile ) ;
94+ const content = ev . target ?. result as string ;
95+ handleFileLoaded ( parseTraceLog ( droppedFile . name , content ) ) ;
4396 } catch ( err ) {
4497 console . error ( "Failed to parse trace file:" , err ) ;
4598 }
4699 } ;
47- reader . onerror = ( ) => {
48- console . error ( "Failed to read file" ) ;
49- } ;
100+ reader . onerror = ( ) => console . error ( "Failed to read file" ) ;
50101 reader . readAsText ( droppedFile ) ;
51102 } ,
52103 [ handleFileLoaded ] ,
@@ -61,81 +112,89 @@ function App() {
61112 const handleDragLeave = useCallback ( ( e : React . DragEvent ) => {
62113 e . preventDefault ( ) ;
63114 e . stopPropagation ( ) ;
64- // Only set dragging to false if we're leaving the main container
65- if ( e . currentTarget === e . target ) {
66- setIsDragging ( false ) ;
67- }
115+ if ( e . currentTarget === e . target ) setIsDragging ( false ) ;
68116 } , [ ] ) ;
69117
70118 return (
71119 < div
72- className = "relative flex h-screen flex-col bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-950 dark:to-slate-900 "
120+ className = "instrument-backdrop relative flex h-screen flex-col overflow-hidden "
73121 onDrop = { handleDrop }
74122 onDragOver = { handleDragOver }
75123 onDragLeave = { handleDragLeave }
76124 >
77- { /* Drag Overlay */ }
125+ { /* Drag overlay */ }
78126 { isDragging && (
79- < div className = "pointer-events-none absolute inset-0 z-50 flex items-center justify-center bg-blue-500/10 backdrop-blur-sm" >
80- < div className = "rounded-lg border-2 border-dashed border-blue-500 bg-white/90 px-12 py-8 text-center shadow-lg dark:bg-slate-900/90 " >
81- < FileText className = "mx-auto mb-4 h-16 w-16 text-blue-500 " />
82- < p className = "text-xl font-semibold text-slate-900 dark:text-slate-50 " >
83- Drop trace file to load
127+ < div className = "bg-primary/10 pointer-events-none absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm" >
128+ < div className = "border-primary bg-card/90 rounded-xl border-2 border-dashed px-14 py-10 text-center shadow-2xl " >
129+ < FileDown className = "text-primary mx-auto mb-4 h-14 w-14 " />
130+ < p className = "text-foreground text-lg font-semibold " >
131+ Drop trace to load
84132 </ p >
85- < p className = "mt-2 text-sm text-slate-600 dark: text-slate-400 " >
86- .jsonl files only
133+ < p className = "text-muted-foreground mt-1 font-mono text-xs " >
134+ .jsonl signal logs only
87135 </ p >
88136 </ div >
89137 </ div >
90138 ) }
91139
92- { /* Header */ }
93- < div className = "flex-shrink-0 border-b border-slate-200 dark:border-slate-800" >
94- < div className = "px-4 py-4" >
95- < div className = "flex min-h-[40px] items-center justify-between gap-4" >
96- < div className = "flex items-center gap-4" >
97- < h1
98- className = { `text-xl font-semibold text-slate-900 dark:text-slate-50 ${
99- file ? "cursor-pointer" : ""
100- } `}
101- onClick = { file ? handleClearFile : undefined }
102- >
103- 🌀 Duron
104- </ h1 >
105- { file && (
106- < >
107- < div className = "h-6 w-px bg-slate-300 dark:bg-slate-700" />
108- < div className = "flex items-center gap-2" >
109- < FileText className = "h-4 w-4 text-blue-500" />
110- < div className = "flex items-center gap-2 text-sm" >
111- < span className = "font-medium text-slate-900 dark:text-slate-50" >
112- { file . name }
113- </ span >
114- < span className = "text-slate-500 dark:text-slate-400" >
115- { file . entries . length } entries
116- </ span >
117- </ div >
118- </ div >
119- </ >
120- ) }
121- </ div >
122- < div className = "flex items-center gap-2" >
123- < ModeToggle />
124- </ div >
140+ { /* Instrument top bar */ }
141+ < header className = "border-border bg-surface/60 relative z-10 flex-shrink-0 border-b backdrop-blur" >
142+ < div className = "flex min-h-[52px] items-center justify-between gap-4 px-4 py-2" >
143+ < div className = "flex items-center gap-4" >
144+ < button
145+ type = "button"
146+ onClick = { file ? handleClearFile : undefined }
147+ className = { `flex items-center gap-2 ${ file ? "cursor-pointer" : "cursor-default" } ` }
148+ >
149+ < span className = "text-lg leading-none" aria-hidden >
150+ 🌀
151+ </ span >
152+ < span className = "text-foreground font-mono text-sm font-bold tracking-[0.16em] uppercase" >
153+ Duron
154+ </ span >
155+ </ button >
156+
157+ { file && (
158+ < >
159+ < div className = "bg-border h-7 w-px" />
160+ < div className = "flex items-center gap-2" >
161+ < Signal className = "text-primary h-3.5 w-3.5" aria-hidden />
162+ < span className = "text-foreground font-mono text-xs" >
163+ { file . name }
164+ </ span >
165+ </ div >
166+ </ >
167+ ) }
168+ </ div >
169+
170+ < div className = "flex items-center gap-5" >
171+ { stats && (
172+ < div className = "hidden items-center gap-5 sm:flex" >
173+ < Stat label = "Spans" value = { String ( stats . spans ) } />
174+ < Stat label = "Events" value = { String ( stats . entries ) } />
175+ < Stat label = "Elapsed" value = { formatDuration ( stats . duration ) } />
176+ < Stat
177+ label = "Errors"
178+ value = { String ( stats . errors ) }
179+ tone = "error"
180+ icon = { CircleAlert }
181+ />
182+ </ div >
183+ ) }
184+ < ModeToggle />
125185 </ div >
126186 </ div >
127- </ div >
187+ </ header >
128188
129- { /* Main Content */ }
189+ { /* Body */ }
130190 { ! file ? (
131- < div className = "flex flex-1 items-center justify-center overflow-auto p-4" >
132- < div className = "w-full max-w-2xl space-y-6" >
133- { /* File Upload */ }
134- < FileUpload onFileLoaded = { handleFileLoaded } />
135- </ div >
191+ < div className = "relative z-10 flex flex-1 items-center justify-center overflow-auto p-4" >
192+ < FileUpload onFileLoaded = { handleFileLoaded } />
136193 </ div >
137194 ) : (
138- < TraceView file = { file } />
195+ < div className = "relative z-10 flex flex-1 overflow-hidden" >
196+ < TraceView file = { file } />
197+ </ div >
139198 ) }
140199 </ div >
141200 ) ;
0 commit comments