@@ -110,20 +110,24 @@ function ResearchProgress({ part }: { part: any }) {
110110 ) ;
111111}
112112
113+ type TtfbEntry = { turn : number ; ttfbMs : number } ;
114+
113115function DebugPanel ( {
114116 chatId,
115117 model,
116118 status,
117119 session,
118120 dashboardUrl,
119121 messageCount,
122+ ttfbHistory,
120123} : {
121124 chatId : string ;
122125 model : string ;
123126 status : string ;
124127 session ?: { runId : string ; publicAccessToken : string ; lastEventId ?: string } ;
125128 dashboardUrl ?: string ;
126129 messageCount : number ;
130+ ttfbHistory : TtfbEntry [ ] ;
127131} ) {
128132 const [ open , setOpen ] = useState ( false ) ;
129133
@@ -132,6 +136,12 @@ function DebugPanel({
132136 ? `${ dashboardUrl } /runs/${ session . runId } `
133137 : undefined ;
134138
139+ const latestTtfb = ttfbHistory . length > 0 ? ttfbHistory [ ttfbHistory . length - 1 ] ! : undefined ;
140+ const avgTtfb =
141+ ttfbHistory . length > 0
142+ ? Math . round ( ttfbHistory . reduce ( ( sum , e ) => sum + e . ttfbMs , 0 ) / ttfbHistory . length )
143+ : undefined ;
144+
135145 return (
136146 < div className = "shrink-0 border-t border-gray-200 bg-gray-50 text-xs text-gray-500" >
137147 < button
@@ -150,6 +160,9 @@ function DebugPanel({
150160 } `}
151161 />
152162 < span > { status } </ span >
163+ { latestTtfb && (
164+ < span className = "font-mono text-blue-600" > TTFB { latestTtfb . ttfbMs . toLocaleString ( ) } ms</ span >
165+ ) }
153166 { session ?. runId && (
154167 < span className = "font-mono" > { session . runId . slice ( 0 , 16 ) } ...</ span >
155168 ) }
@@ -170,6 +183,22 @@ function DebugPanel({
170183 ) : (
171184 < Row label = "Session" value = "none" />
172185 ) }
186+ { ttfbHistory . length > 0 && (
187+ < >
188+ < div className = "mt-2 border-t border-gray-200 pt-2" >
189+ < span className = "font-medium text-gray-600" > TTFB</ span >
190+ { avgTtfb !== undefined && (
191+ < span className = "ml-2 text-gray-400" > avg { avgTtfb . toLocaleString ( ) } ms</ span >
192+ ) }
193+ </ div >
194+ { ttfbHistory . map ( ( entry ) => (
195+ < div key = { entry . turn } className = "flex items-center gap-2" >
196+ < span className = "w-24 shrink-0 text-gray-400" > Turn { entry . turn } </ span >
197+ < span className = "font-mono" > { entry . ttfbMs . toLocaleString ( ) } ms</ span >
198+ </ div >
199+ ) ) }
200+ </ >
201+ ) }
173202 </ div >
174203 ) }
175204 </ div >
@@ -236,6 +265,11 @@ export function Chat({
236265 const [ input , setInput ] = useState ( "" ) ;
237266 const hasCalledFirstMessage = useRef ( false ) ;
238267
268+ // TTFB tracking
269+ const sendTimestamp = useRef < number | null > ( null ) ;
270+ const turnCounter = useRef ( 0 ) ;
271+ const [ ttfbHistory , setTtfbHistory ] = useState < TtfbEntry [ ] > ( [ ] ) ;
272+
239273 const { messages, sendMessage, stop, status, error } = useChat ( {
240274 id : chatId ,
241275 messages : initialMessages ,
@@ -257,6 +291,19 @@ export function Chat({
257291 }
258292 } , [ messages , chatId , onFirstMessage ] ) ;
259293
294+ // TTFB detection: record when first assistant content appears after send
295+ useEffect ( ( ) => {
296+ if ( status !== "streaming" ) return ;
297+ if ( sendTimestamp . current === null ) return ;
298+ const lastMsg = messages [ messages . length - 1 ] ;
299+ if ( lastMsg ?. role === "assistant" ) {
300+ const ttfbMs = Date . now ( ) - sendTimestamp . current ;
301+ const turn = turnCounter . current ;
302+ sendTimestamp . current = null ;
303+ setTtfbHistory ( ( prev ) => [ ...prev , { turn, ttfbMs } ] ) ;
304+ }
305+ } , [ status , messages ] ) ;
306+
260307 // Pending message to send after the current turn completes
261308 const [ pendingMessage , setPendingMessage ] = useState < string | null > ( null ) ;
262309
@@ -277,6 +324,8 @@ export function Chat({
277324 if ( pendingMessage ) {
278325 const text = pendingMessage ;
279326 setPendingMessage ( null ) ;
327+ turnCounter . current ++ ;
328+ sendTimestamp . current = Date . now ( ) ;
280329 sendMessage ( { text } , { metadata : { model } } ) ;
281330 }
282331 } , [ status , messages , chatId , onMessagesChange , sendMessage , pendingMessage , model ] ) ;
@@ -423,6 +472,7 @@ export function Chat({
423472 session = { session }
424473 dashboardUrl = { dashboardUrl }
425474 messageCount = { messages . length }
475+ ttfbHistory = { ttfbHistory }
426476 />
427477
428478 < form
@@ -432,6 +482,8 @@ export function Chat({
432482 if ( status === "streaming" ) {
433483 setPendingMessage ( input ) ;
434484 } else {
485+ turnCounter . current ++ ;
486+ sendTimestamp . current = Date . now ( ) ;
435487 sendMessage ( { text : input } , { metadata : { model } } ) ;
436488 }
437489 setInput ( "" ) ;
0 commit comments