dvalle08 commited on
Commit
b90c86e
·
1 Parent(s): c4ca11e

Update UI styles and metrics handling: Add .vscode to .gitignore, enhance button styles in index.html, and introduce new metrics for voice generation in main.js. Refactor metric calculations and improve layout for better responsiveness.

Browse files
Files changed (3) hide show
  1. .gitignore +1 -1
  2. src/ui/index.html +341 -68
  3. src/ui/main.js +107 -10
.gitignore CHANGED
@@ -9,7 +9,7 @@ __pycache__/
9
  *.egg-info/
10
  dist/
11
  build/
12
-
13
  # IDE
14
  .cursor/
15
  .cursorignore
 
9
  *.egg-info/
10
  dist/
11
  build/
12
+ .vscode/
13
  # IDE
14
  .cursor/
15
  .cursorignore
src/ui/index.html CHANGED
@@ -123,7 +123,7 @@
123
  display: flex;
124
  gap: 6px;
125
  }
126
- button {
127
  padding: 7px 14px;
128
  border-radius: 20px;
129
  border: 1px solid var(--border);
@@ -137,28 +137,28 @@
137
  align-items: center;
138
  gap: 5px;
139
  }
140
- button:hover:not(:disabled) {
141
  background: var(--bg-card);
142
  border-color: var(--border-light);
143
  }
144
- button:disabled {
145
  opacity: 0.35;
146
  cursor: not-allowed;
147
  }
148
- button.btn-primary {
149
  background: var(--accent);
150
  border-color: var(--accent);
151
  color: #fff;
152
  }
153
- button.btn-primary:hover:not(:disabled) {
154
  background: var(--accent-light);
155
  border-color: var(--accent-light);
156
  }
157
- button.btn-danger {
158
  color: var(--critical);
159
  border-color: rgba(239, 68, 68, 0.3);
160
  }
161
- button.btn-danger:hover:not(:disabled) {
162
  background: rgba(239, 68, 68, 0.1);
163
  border-color: rgba(239, 68, 68, 0.5);
164
  }
@@ -168,7 +168,8 @@
168
  display: flex;
169
  flex-direction: column;
170
  height: calc(100vh - 57px);
171
- overflow: hidden;
 
172
  }
173
 
174
  /* Waveform hero */
@@ -195,53 +196,263 @@
195
  /* Live metrics row */
196
  .live-metrics-row {
197
  flex-shrink: 0;
198
- display: grid;
199
- grid-template-columns: repeat(3, minmax(0, 1fr));
200
- gap: 8px;
201
  padding: 0 24px 12px;
202
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  .metric-card {
204
  background: var(--bg-card);
205
  border: 1px solid var(--border);
206
  border-radius: var(--radius-sm);
207
  padding: 10px 12px;
208
  }
209
- .metric-card-label {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  display: flex;
211
  justify-content: space-between;
212
- align-items: baseline;
213
- margin-bottom: 6px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  }
215
- .metric-card-name {
216
  font-size: 10px;
217
- color: var(--text-muted);
 
 
 
218
  font-weight: 600;
219
- text-transform: uppercase;
220
- letter-spacing: 0.4px;
 
 
 
 
 
 
 
 
 
 
 
221
  }
222
  .metric-card-value {
223
- font-size: 14px;
224
  font-weight: 700;
 
225
  font-variant-numeric: tabular-nums;
226
- color: var(--accent);
227
  transition: color 0.3s;
228
  }
 
 
 
 
 
 
229
  .metric-card-value.warning { color: var(--warning); }
230
  .metric-card-value.critical { color: var(--critical); }
 
 
 
 
231
  .metric-card-track {
232
  height: 4px;
233
- background: var(--bg-primary);
234
- border-radius: 2px;
235
  overflow: hidden;
236
  }
237
  .metric-card-fill {
238
  height: 100%;
239
- border-radius: 2px;
240
- background: var(--accent);
241
- transition: width 0.4s cubic-bezier(0.22, 1, 0.36, 1), background 0.3s;
242
  }
243
  .metric-card-fill.warning { background: var(--warning); }
244
  .metric-card-fill.critical { background: var(--critical); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  /* Averages compact row */
247
  .averages-row {
@@ -332,55 +543,117 @@
332
 
333
  <!-- Live metric cards -->
334
  <div class="live-metrics-row">
335
- <div class="metric-card">
336
- <div class="metric-card-label">
337
- <span class="metric-card-name">EOU Delay</span>
338
- <span class="metric-card-value" id="live-eou">--</span>
339
- </div>
340
- <div class="metric-card-track">
341
- <div class="metric-card-fill" id="live-eou-bar" style="width: 0%"></div>
342
- </div>
343
- </div>
344
- <div class="metric-card">
345
- <div class="metric-card-label">
346
- <span class="metric-card-name">STT Finalization</span>
347
- <span class="metric-card-value" id="live-stt-finalization">--</span>
348
- </div>
349
- <div class="metric-card-track">
350
- <div class="metric-card-fill" id="live-stt-finalization-bar" style="width: 0%"></div>
351
- </div>
352
  </div>
353
- <div class="metric-card">
354
- <div class="metric-card-label">
355
- <span class="metric-card-name">LLM TTFT</span>
356
- <span class="metric-card-value" id="live-llm-ttft">--</span>
357
- </div>
358
- <div class="metric-card-track">
359
- <div class="metric-card-fill" id="live-llm-ttft-bar" style="width: 0%"></div>
360
- </div>
361
- </div>
362
- <div class="metric-card">
363
- <div class="metric-card-label">
364
- <span class="metric-card-name">LLM→TTS Handoff</span>
365
- <span class="metric-card-value" id="live-llm-handoff">--</span>
 
 
 
 
 
 
 
 
 
366
  </div>
367
- <div class="metric-card-track">
368
- <div class="metric-card-fill" id="live-llm-handoff-bar" style="width: 0%"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  </div>
370
- </div>
371
- <div class="metric-card">
372
- <div class="metric-card-label">
373
- <span class="metric-card-name">TTS TTFB</span>
374
- <span class="metric-card-value" id="live-tts-ttfb">--</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  </div>
376
- <div class="metric-card-track">
377
- <div class="metric-card-fill" id="live-tts-ttfb-bar" style="width: 0%"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  </div>
379
  </div>
380
- <div class="metric-card">
381
- <div class="metric-card-label">
382
- <span class="metric-card-name">Total Latency</span>
383
- <span class="metric-card-value" id="live-total">--</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  </div>
385
  <div class="metric-card-track">
386
  <div class="metric-card-fill" id="live-total-bar" style="width: 0%"></div>
 
123
  display: flex;
124
  gap: 6px;
125
  }
126
+ .controls button {
127
  padding: 7px 14px;
128
  border-radius: 20px;
129
  border: 1px solid var(--border);
 
137
  align-items: center;
138
  gap: 5px;
139
  }
140
+ .controls button:hover:not(:disabled) {
141
  background: var(--bg-card);
142
  border-color: var(--border-light);
143
  }
144
+ .controls button:disabled {
145
  opacity: 0.35;
146
  cursor: not-allowed;
147
  }
148
+ .controls button.btn-primary {
149
  background: var(--accent);
150
  border-color: var(--accent);
151
  color: #fff;
152
  }
153
+ .controls button.btn-primary:hover:not(:disabled) {
154
  background: var(--accent-light);
155
  border-color: var(--accent-light);
156
  }
157
+ .controls button.btn-danger {
158
  color: var(--critical);
159
  border-color: rgba(239, 68, 68, 0.3);
160
  }
161
+ .controls button.btn-danger:hover:not(:disabled) {
162
  background: rgba(239, 68, 68, 0.1);
163
  border-color: rgba(239, 68, 68, 0.5);
164
  }
 
168
  display: flex;
169
  flex-direction: column;
170
  height: calc(100vh - 57px);
171
+ overflow-x: hidden;
172
+ overflow-y: auto;
173
  }
174
 
175
  /* Waveform hero */
 
196
  /* Live metrics row */
197
  .live-metrics-row {
198
  flex-shrink: 0;
 
 
 
199
  padding: 0 24px 12px;
200
  }
201
+ .pipeline-header {
202
+ display: flex;
203
+ align-items: baseline;
204
+ gap: 10px;
205
+ margin-bottom: 10px;
206
+ }
207
+ .pipeline-title {
208
+ font-size: 11px;
209
+ font-weight: 700;
210
+ text-transform: uppercase;
211
+ letter-spacing: 0.08em;
212
+ color: var(--text-muted);
213
+ }
214
+ .pipeline-subtitle {
215
+ font-size: 11px;
216
+ color: var(--text-muted);
217
+ }
218
+ .pipeline-stage-row {
219
+ display: grid;
220
+ grid-template-columns: repeat(4, minmax(0, 1fr));
221
+ gap: 10px;
222
+ }
223
  .metric-card {
224
  background: var(--bg-card);
225
  border: 1px solid var(--border);
226
  border-radius: var(--radius-sm);
227
  padding: 10px 12px;
228
  }
229
+ .stage-card {
230
+ position: relative;
231
+ }
232
+ .stage-card::after {
233
+ content: "";
234
+ position: absolute;
235
+ top: 50%;
236
+ right: -14px;
237
+ width: 18px;
238
+ height: 2px;
239
+ border-radius: 999px;
240
+ background: linear-gradient(
241
+ 90deg,
242
+ rgba(108, 143, 255, 0.12) 0%,
243
+ rgba(108, 143, 255, 0.75) 45%,
244
+ rgba(167, 139, 250, 0.65) 100%
245
+ );
246
+ box-shadow: 0 0 8px rgba(108, 143, 255, 0.22);
247
+ transform: translateY(-50%);
248
+ pointer-events: none;
249
+ z-index: 1;
250
+ }
251
+ .stage-card:last-child::after {
252
+ display: none;
253
+ }
254
+ .metric-card-top {
255
  display: flex;
256
  justify-content: space-between;
257
+ align-items: center;
258
+ margin-bottom: 8px;
259
+ }
260
+ .metric-step {
261
+ width: 22px;
262
+ height: 22px;
263
+ border-radius: 6px;
264
+ display: inline-grid;
265
+ place-items: center;
266
+ font-size: 11px;
267
+ font-weight: 700;
268
+ color: var(--accent-light);
269
+ background: var(--accent-glow);
270
+ border: 1px solid rgba(108, 143, 255, 0.35);
271
+ flex-shrink: 0;
272
+ }
273
+ .metric-tech-wrap {
274
+ display: inline-flex;
275
+ align-items: center;
276
+ gap: 6px;
277
  }
278
+ .metric-tech {
279
  font-size: 10px;
280
+ color: var(--text-secondary);
281
+ }
282
+ .metric-card-human {
283
+ font-size: 13px;
284
  font-weight: 600;
285
+ margin-bottom: 2px;
286
+ }
287
+ .metric-card-desc {
288
+ font-size: 11px;
289
+ color: var(--text-muted);
290
+ margin-bottom: 8px;
291
+ min-height: 28px;
292
+ }
293
+ .metric-card-value-row {
294
+ display: flex;
295
+ align-items: baseline;
296
+ gap: 8px;
297
+ margin-bottom: 8px;
298
  }
299
  .metric-card-value {
300
+ font-size: 22px;
301
  font-weight: 700;
302
+ line-height: 1;
303
  font-variant-numeric: tabular-nums;
304
+ color: var(--accent-light);
305
  transition: color 0.3s;
306
  }
307
+ .metric-card-value.loading {
308
+ font-size: 13px;
309
+ font-weight: 600;
310
+ line-height: 1.2;
311
+ color: var(--text-muted);
312
+ }
313
  .metric-card-value.warning { color: var(--warning); }
314
  .metric-card-value.critical { color: var(--critical); }
315
+ .metric-card-avg {
316
+ font-size: 10px;
317
+ color: var(--text-secondary);
318
+ }
319
  .metric-card-track {
320
  height: 4px;
321
+ background: var(--border);
322
+ border-radius: 999px;
323
  overflow: hidden;
324
  }
325
  .metric-card-fill {
326
  height: 100%;
327
+ border-radius: 999px;
328
+ background: var(--accent-gradient);
329
+ transition: width 0.5s ease, background 0.3s;
330
  }
331
  .metric-card-fill.warning { background: var(--warning); }
332
  .metric-card-fill.critical { background: var(--critical); }
333
+ .pipeline-total-card {
334
+ margin-top: 10px;
335
+ background: linear-gradient(135deg, rgba(108, 143, 255, 0.08), rgba(167, 139, 250, 0.06));
336
+ border: 1px solid rgba(108, 143, 255, 0.28);
337
+ border-radius: var(--radius-sm);
338
+ padding: 12px 14px;
339
+ }
340
+ .pipeline-total-top {
341
+ display: flex;
342
+ justify-content: space-between;
343
+ align-items: flex-end;
344
+ gap: 10px;
345
+ margin-bottom: 10px;
346
+ }
347
+ .pipeline-total-label {
348
+ font-size: 14px;
349
+ font-weight: 600;
350
+ }
351
+ .pipeline-total-tech {
352
+ font-size: 10px;
353
+ color: var(--text-secondary);
354
+ display: inline-flex;
355
+ align-items: center;
356
+ gap: 6px;
357
+ }
358
+ .pipeline-total-value-wrap {
359
+ display: flex;
360
+ flex-direction: column;
361
+ align-items: flex-end;
362
+ gap: 2px;
363
+ }
364
+ .pipeline-total-value {
365
+ font-size: 28px;
366
+ }
367
+ .pipeline-total-avg {
368
+ font-size: 11px;
369
+ color: var(--text-secondary);
370
+ }
371
+
372
+ .tooltip {
373
+ position: relative;
374
+ display: inline-flex;
375
+ align-items: center;
376
+ }
377
+ .tooltip-trigger {
378
+ width: 16px;
379
+ height: 16px;
380
+ padding: 0;
381
+ border-radius: 50%;
382
+ border: 1px solid var(--border-light);
383
+ background: rgba(255, 255, 255, 0.02);
384
+ color: var(--text-secondary);
385
+ cursor: pointer;
386
+ font-size: 10px;
387
+ line-height: 1;
388
+ display: inline-grid;
389
+ place-items: center;
390
+ }
391
+ .tooltip-trigger:hover {
392
+ border-color: rgba(108, 143, 255, 0.4);
393
+ color: var(--text-primary);
394
+ }
395
+ .tooltip-content {
396
+ position: absolute;
397
+ right: 0;
398
+ bottom: calc(100% + 8px);
399
+ width: 220px;
400
+ padding: 8px;
401
+ border-radius: 8px;
402
+ border: 1px solid var(--border-light);
403
+ background: #111725;
404
+ color: var(--text-primary);
405
+ font-size: 11px;
406
+ line-height: 1.35;
407
+ opacity: 0;
408
+ pointer-events: none;
409
+ transform: translateY(4px);
410
+ transition: opacity 0.2s ease, transform 0.2s ease;
411
+ z-index: 20;
412
+ }
413
+ .tooltip:hover .tooltip-content,
414
+ .tooltip:focus-within .tooltip-content {
415
+ opacity: 1;
416
+ pointer-events: auto;
417
+ transform: translateY(0);
418
+ }
419
+
420
+ @media (max-width: 1100px) {
421
+ .pipeline-stage-row {
422
+ grid-template-columns: repeat(2, minmax(0, 1fr));
423
+ }
424
+ .stage-card::after {
425
+ display: none;
426
+ }
427
+ .stage-card:nth-child(odd)::after {
428
+ display: block;
429
+ }
430
+ .stage-card:last-child::after {
431
+ display: none;
432
+ }
433
+ }
434
+ @media (max-width: 700px) {
435
+ .waveform-section {
436
+ min-height: 160px;
437
+ padding: 14px 14px 10px;
438
+ }
439
+ .live-metrics-row {
440
+ padding: 0 14px 10px;
441
+ }
442
+ .pipeline-stage-row {
443
+ grid-template-columns: 1fr;
444
+ }
445
+ .pipeline-total-top {
446
+ align-items: flex-start;
447
+ flex-direction: column;
448
+ }
449
+ .pipeline-total-value-wrap {
450
+ align-items: flex-start;
451
+ }
452
+ .stage-card::after {
453
+ display: none !important;
454
+ }
455
+ }
456
 
457
  /* Averages compact row */
458
  .averages-row {
 
543
 
544
  <!-- Live metric cards -->
545
  <div class="live-metrics-row">
546
+ <div class="pipeline-header">
547
+ <span class="pipeline-title">Voice Agent Pipeline</span>
548
+ <span class="pipeline-subtitle">4 stages from voice input to response</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  </div>
550
+
551
+ <div class="pipeline-stage-row">
552
+ <div class="metric-card stage-card">
553
+ <div class="metric-card-top">
554
+ <span class="metric-step">1</span>
555
+ <div class="metric-tech-wrap">
556
+ <span class="metric-tech">EOU Delay</span>
557
+ <div class="tooltip">
558
+ <button type="button" class="tooltip-trigger" aria-label="What is EOU Delay?">i</button>
559
+ <span class="tooltip-content" role="tooltip">Time from user speech end to end-of-utterance detection.</span>
560
+ </div>
561
+ </div>
562
+ </div>
563
+ <div class="metric-card-human">Silence Detection</div>
564
+ <div class="metric-card-desc">Detecting when you stop speaking</div>
565
+ <div class="metric-card-value-row">
566
+ <span class="metric-card-value" id="live-eou">--</span>
567
+ <span class="metric-card-avg" id="live-eou-avg"></span>
568
+ </div>
569
+ <div class="metric-card-track">
570
+ <div class="metric-card-fill" id="live-eou-bar" style="width: 0%"></div>
571
+ </div>
572
  </div>
573
+
574
+ <div class="metric-card stage-card">
575
+ <div class="metric-card-top">
576
+ <span class="metric-step">2</span>
577
+ <div class="metric-tech-wrap">
578
+ <span class="metric-tech">STT Finalization</span>
579
+ <div class="tooltip">
580
+ <button type="button" class="tooltip-trigger" aria-label="What is STT Finalization?">i</button>
581
+ <span class="tooltip-content" role="tooltip">Time to finalize the transcript after speech end.</span>
582
+ </div>
583
+ </div>
584
+ </div>
585
+ <div class="metric-card-human">Speech to Text</div>
586
+ <div class="metric-card-desc">Finalizing transcription</div>
587
+ <div class="metric-card-value-row">
588
+ <span class="metric-card-value" id="live-stt-finalization">--</span>
589
+ <span class="metric-card-avg" id="live-stt-finalization-avg"></span>
590
+ </div>
591
+ <div class="metric-card-track">
592
+ <div class="metric-card-fill" id="live-stt-finalization-bar" style="width: 0%"></div>
593
+ </div>
594
  </div>
595
+
596
+ <div class="metric-card stage-card">
597
+ <div class="metric-card-top">
598
+ <span class="metric-step">3</span>
599
+ <div class="metric-tech-wrap">
600
+ <span class="metric-tech">LLM TTFT</span>
601
+ <div class="tooltip">
602
+ <button type="button" class="tooltip-trigger" aria-label="What is LLM TTFT?">i</button>
603
+ <span class="tooltip-content" role="tooltip">Time for the LLM to produce the first token.</span>
604
+ </div>
605
+ </div>
606
+ </div>
607
+ <div class="metric-card-human">Thinking</div>
608
+ <div class="metric-card-desc">Generating first token</div>
609
+ <div class="metric-card-value-row">
610
+ <span class="metric-card-value" id="live-llm-ttft">--</span>
611
+ <span class="metric-card-avg" id="live-llm-ttft-avg"></span>
612
+ </div>
613
+ <div class="metric-card-track">
614
+ <div class="metric-card-fill" id="live-llm-ttft-bar" style="width: 0%"></div>
615
+ </div>
616
  </div>
617
+
618
+ <div class="metric-card stage-card">
619
+ <div class="metric-card-top">
620
+ <span class="metric-step">4</span>
621
+ <div class="metric-tech-wrap">
622
+ <span class="metric-tech">Voice Generation</span>
623
+ <div class="tooltip">
624
+ <button type="button" class="tooltip-trigger" aria-label="What is Voice Generation?">i</button>
625
+ <span class="tooltip-content" role="tooltip">Sum of LLM to TTS handoff latency and TTS first-byte latency.</span>
626
+ </div>
627
+ </div>
628
+ </div>
629
+ <div class="metric-card-human">Voice Generation</div>
630
+ <div class="metric-card-desc">Synthesizing assistant voice</div>
631
+ <div class="metric-card-value-row">
632
+ <span class="metric-card-value" id="live-voice-generation">--</span>
633
+ <span class="metric-card-avg" id="live-voice-generation-avg"></span>
634
+ </div>
635
+ <div class="metric-card-track">
636
+ <div class="metric-card-fill" id="live-voice-generation-bar" style="width: 0%"></div>
637
+ </div>
638
  </div>
639
  </div>
640
+
641
+ <div class="pipeline-total-card">
642
+ <div class="pipeline-total-top">
643
+ <div>
644
+ <div class="pipeline-total-label">Total Round-Trip</div>
645
+ <div class="pipeline-total-tech">
646
+ <span>End-to-End Latency</span>
647
+ <div class="tooltip">
648
+ <button type="button" class="tooltip-trigger" aria-label="What is End-to-End Latency?">i</button>
649
+ <span class="tooltip-content" role="tooltip">Total latency from end of user speech to assistant speech start.</span>
650
+ </div>
651
+ </div>
652
+ </div>
653
+ <div class="pipeline-total-value-wrap">
654
+ <span class="metric-card-value pipeline-total-value" id="live-total">--</span>
655
+ <span class="pipeline-total-avg" id="live-total-avg"></span>
656
+ </div>
657
  </div>
658
  <div class="metric-card-track">
659
  <div class="metric-card-fill" id="live-total-bar" style="width: 0%"></div>
src/ui/main.js CHANGED
@@ -26,17 +26,29 @@ let averages = {
26
  llmTtft: [],
27
  llmToTtsHandoff: [],
28
  ttsTtfb: [],
 
29
  totalLatency: [],
30
  };
31
  const LIVE_METRIC_IDS = [
32
  "eou",
33
  "stt-finalization",
34
  "llm-ttft",
35
- "llm-handoff",
36
- "tts-ttfb",
37
  "total",
38
  ];
39
  let activeLiveSpeechId = null;
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  // Initialize canvas sizing on load
42
  window.addEventListener('DOMContentLoaded', () => {
@@ -278,12 +290,14 @@ function resetMetrics() {
278
  metricsHistory = [];
279
  turnCount = 0;
280
  activeLiveSpeechId = null;
 
281
  averages = {
282
  eouDelay: [],
283
  sttFinalization: [],
284
  llmTtft: [],
285
  llmToTtsHandoff: [],
286
  ttsTtfb: [],
 
287
  totalLatency: [],
288
  };
289
 
@@ -299,6 +313,8 @@ function resetMetrics() {
299
  ].forEach((id) => {
300
  document.getElementById(id).innerHTML = '-- <span class="unit">s</span>';
301
  });
 
 
302
  }
303
 
304
  function handleLiveTurnBoundary(metricsData) {
@@ -308,12 +324,14 @@ function handleLiveTurnBoundary(metricsData) {
308
  if (!speechId) {
309
  clearAllLiveMetrics();
310
  activeLiveSpeechId = null;
 
311
  return;
312
  }
313
 
314
  if (speechId === activeLiveSpeechId) return;
315
  activeLiveSpeechId = speechId;
316
- clearAllLiveMetrics();
 
317
  }
318
 
319
  async function toggleMute() {
@@ -361,6 +379,12 @@ function getTpsClass(value, warningThreshold, criticalThreshold) {
361
  return "";
362
  }
363
 
 
 
 
 
 
 
364
  function setLiveMetric(metricId, value, maxValue, warningThreshold, criticalThreshold, options) {
365
  const bar = document.getElementById(`live-${metricId}-bar`);
366
  const label = document.getElementById(`live-${metricId}`);
@@ -375,66 +399,135 @@ function setLiveMetric(metricId, value, maxValue, warningThreshold, criticalThre
375
  const suffix = (options && options.suffix) || "s";
376
  const decimals = (options && options.decimals !== undefined) ? options.decimals : 2;
377
  label.textContent = decimals > 0 ? `${value.toFixed(decimals)}${suffix}` : `${Math.round(value)} ${suffix}`;
378
- label.className = "metric-card-value" + (cls ? ` ${cls}` : "");
379
  bar.style.width = `${percent}%`;
380
  bar.className = "metric-card-fill" + (cls ? ` ${cls}` : "");
381
  }
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  function clearLiveMetric(metricId) {
384
  const label = document.getElementById(`live-${metricId}`);
385
  const bar = document.getElementById(`live-${metricId}-bar`);
 
386
  label.textContent = "--";
387
- label.className = "metric-card-value";
388
  bar.style.width = "0%";
389
  bar.className = "metric-card-fill";
 
390
  }
391
 
392
  function clearAllLiveMetrics() {
393
  LIVE_METRIC_IDS.forEach((id) => clearLiveMetric(id));
394
  }
395
 
 
 
 
 
 
396
  function isFiniteNumber(value) {
397
  return typeof value === "number" && Number.isFinite(value);
398
  }
399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  function updateLiveMetrics(turn) {
401
  const metrics = turn.metrics || {};
402
  const latencies = turn.latencies || {};
 
 
 
 
 
 
 
403
 
404
  const eouDelay = latencies.eou_delay ?? latencies.vad_detection_delay;
405
  if (isFiniteNumber(eouDelay)) {
 
406
  setLiveMetric("eou", eouDelay, 4.0, 0.8, 1.2);
407
  }
408
 
409
  const sttFinalizationDelay = latencies.stt_finalization_delay;
410
  if (isFiniteNumber(sttFinalizationDelay)) {
 
411
  setLiveMetric("stt-finalization", sttFinalizationDelay, 3.0, 0.4, 0.8);
412
  }
413
 
414
  const llmTtft = metrics.llm?.ttft;
415
  if (isFiniteNumber(llmTtft)) {
 
416
  setLiveMetric("llm-ttft", llmTtft, 4.0, 0.5, 1.0);
417
  }
418
 
419
  const llmToTtsHandoff = latencies.llm_to_tts_handoff_latency;
420
  if (isFiniteNumber(llmToTtsHandoff)) {
421
- setLiveMetric("llm-handoff", llmToTtsHandoff, 8.0, 1.5, 3.0);
422
  }
423
 
424
  const ttsTtfb = metrics.tts?.ttfb;
425
  if (isFiniteNumber(ttsTtfb)) {
426
- setLiveMetric("tts-ttfb", ttsTtfb, 4.0, 0.3, 0.6);
427
  }
428
 
429
  const totalLatency = latencies.total_latency;
430
  if (isFiniteNumber(totalLatency)) {
431
- setLiveMetric("total", totalLatency, 8.0, 1.5, 3.0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  }
433
  }
434
 
435
  function updateAverages() {
436
- const avg = (arr) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : null;
437
-
438
  const avgEou = avg(averages.eouDelay);
439
  const avgSttFinalization = avg(averages.sttFinalization);
440
  const avgLlmTtft = avg(averages.llmTtft);
@@ -454,6 +547,7 @@ function updateAverages() {
454
  setAverageValue("avg-llm-handoff", avgLlmToTtsHandoff);
455
  setAverageValue("avg-tts-ttfb", avgTtsTtfb);
456
  setAverageValue("avg-total", avgTotalLatency);
 
457
  }
458
 
459
  function renderTurn(turn) {
@@ -479,6 +573,9 @@ function renderTurn(turn) {
479
 
480
  const ttsTtfb = metrics.tts?.ttfb;
481
  if (isFiniteNumber(ttsTtfb) && ttsTtfb > 0) averages.ttsTtfb.push(ttsTtfb);
 
 
 
482
 
483
  const totalLatency = latencies.total_latency;
484
  if (isFiniteNumber(totalLatency) && totalLatency > 0) {
 
26
  llmTtft: [],
27
  llmToTtsHandoff: [],
28
  ttsTtfb: [],
29
+ voiceGeneration: [],
30
  totalLatency: [],
31
  };
32
  const LIVE_METRIC_IDS = [
33
  "eou",
34
  "stt-finalization",
35
  "llm-ttft",
36
+ "voice-generation",
 
37
  "total",
38
  ];
39
  let activeLiveSpeechId = null;
40
+ let liveTurnValues = createEmptyLiveTurnValues();
41
+
42
+ function createEmptyLiveTurnValues() {
43
+ return {
44
+ eouDelay: null,
45
+ sttFinalizationDelay: null,
46
+ llmTtft: null,
47
+ llmToTtsHandoff: null,
48
+ ttsTtfb: null,
49
+ totalLatency: null,
50
+ };
51
+ }
52
 
53
  // Initialize canvas sizing on load
54
  window.addEventListener('DOMContentLoaded', () => {
 
290
  metricsHistory = [];
291
  turnCount = 0;
292
  activeLiveSpeechId = null;
293
+ liveTurnValues = createEmptyLiveTurnValues();
294
  averages = {
295
  eouDelay: [],
296
  sttFinalization: [],
297
  llmTtft: [],
298
  llmToTtsHandoff: [],
299
  ttsTtfb: [],
300
+ voiceGeneration: [],
301
  totalLatency: [],
302
  };
303
 
 
313
  ].forEach((id) => {
314
  document.getElementById(id).innerHTML = '-- <span class="unit">s</span>';
315
  });
316
+
317
+ updateLiveMetricAverages();
318
  }
319
 
320
  function handleLiveTurnBoundary(metricsData) {
 
324
  if (!speechId) {
325
  clearAllLiveMetrics();
326
  activeLiveSpeechId = null;
327
+ liveTurnValues = createEmptyLiveTurnValues();
328
  return;
329
  }
330
 
331
  if (speechId === activeLiveSpeechId) return;
332
  activeLiveSpeechId = speechId;
333
+ liveTurnValues = createEmptyLiveTurnValues();
334
+ setAllLiveMetricsLoading();
335
  }
336
 
337
  async function toggleMute() {
 
379
  return "";
380
  }
381
 
382
+ function getLiveMetricValueBaseClass(metricId) {
383
+ return metricId === "total"
384
+ ? "metric-card-value pipeline-total-value"
385
+ : "metric-card-value";
386
+ }
387
+
388
  function setLiveMetric(metricId, value, maxValue, warningThreshold, criticalThreshold, options) {
389
  const bar = document.getElementById(`live-${metricId}-bar`);
390
  const label = document.getElementById(`live-${metricId}`);
 
399
  const suffix = (options && options.suffix) || "s";
400
  const decimals = (options && options.decimals !== undefined) ? options.decimals : 2;
401
  label.textContent = decimals > 0 ? `${value.toFixed(decimals)}${suffix}` : `${Math.round(value)} ${suffix}`;
402
+ label.className = getLiveMetricValueBaseClass(metricId) + (cls ? ` ${cls}` : "");
403
  bar.style.width = `${percent}%`;
404
  bar.className = "metric-card-fill" + (cls ? ` ${cls}` : "");
405
  }
406
 
407
+ function setLiveMetricAverage(metricId, value) {
408
+ const averageLabel = document.getElementById(`live-${metricId}-avg`);
409
+ if (!averageLabel) return;
410
+ averageLabel.textContent = value !== null ? `avg ${value.toFixed(2)}s` : "";
411
+ }
412
+
413
+ function setLiveMetricLoading(metricId) {
414
+ const label = document.getElementById(`live-${metricId}`);
415
+ const bar = document.getElementById(`live-${metricId}-bar`);
416
+ label.textContent = "coming...";
417
+ label.className = `${getLiveMetricValueBaseClass(metricId)} loading`;
418
+ bar.style.width = "0%";
419
+ bar.className = "metric-card-fill";
420
+ }
421
+
422
  function clearLiveMetric(metricId) {
423
  const label = document.getElementById(`live-${metricId}`);
424
  const bar = document.getElementById(`live-${metricId}-bar`);
425
+ const averageLabel = document.getElementById(`live-${metricId}-avg`);
426
  label.textContent = "--";
427
+ label.className = getLiveMetricValueBaseClass(metricId);
428
  bar.style.width = "0%";
429
  bar.className = "metric-card-fill";
430
+ if (averageLabel) averageLabel.textContent = "";
431
  }
432
 
433
  function clearAllLiveMetrics() {
434
  LIVE_METRIC_IDS.forEach((id) => clearLiveMetric(id));
435
  }
436
 
437
+ function setAllLiveMetricsLoading() {
438
+ LIVE_METRIC_IDS.forEach((id) => setLiveMetricLoading(id));
439
+ updateLiveMetricAverages();
440
+ }
441
+
442
  function isFiniteNumber(value) {
443
  return typeof value === "number" && Number.isFinite(value);
444
  }
445
 
446
+ function avg(values) {
447
+ if (!values.length) return null;
448
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
449
+ }
450
+
451
+ function updateLiveMetricAverages() {
452
+ setLiveMetricAverage("eou", avg(averages.eouDelay));
453
+ setLiveMetricAverage("stt-finalization", avg(averages.sttFinalization));
454
+ setLiveMetricAverage("llm-ttft", avg(averages.llmTtft));
455
+ setLiveMetricAverage("voice-generation", avg(averages.voiceGeneration));
456
+ setLiveMetricAverage("total", avg(averages.totalLatency));
457
+ }
458
+
459
  function updateLiveMetrics(turn) {
460
  const metrics = turn.metrics || {};
461
  const latencies = turn.latencies || {};
462
+ const speechId = turn.speech_id;
463
+
464
+ if (speechId && speechId !== activeLiveSpeechId) {
465
+ activeLiveSpeechId = speechId;
466
+ liveTurnValues = createEmptyLiveTurnValues();
467
+ setAllLiveMetricsLoading();
468
+ }
469
 
470
  const eouDelay = latencies.eou_delay ?? latencies.vad_detection_delay;
471
  if (isFiniteNumber(eouDelay)) {
472
+ liveTurnValues.eouDelay = eouDelay;
473
  setLiveMetric("eou", eouDelay, 4.0, 0.8, 1.2);
474
  }
475
 
476
  const sttFinalizationDelay = latencies.stt_finalization_delay;
477
  if (isFiniteNumber(sttFinalizationDelay)) {
478
+ liveTurnValues.sttFinalizationDelay = sttFinalizationDelay;
479
  setLiveMetric("stt-finalization", sttFinalizationDelay, 3.0, 0.4, 0.8);
480
  }
481
 
482
  const llmTtft = metrics.llm?.ttft;
483
  if (isFiniteNumber(llmTtft)) {
484
+ liveTurnValues.llmTtft = llmTtft;
485
  setLiveMetric("llm-ttft", llmTtft, 4.0, 0.5, 1.0);
486
  }
487
 
488
  const llmToTtsHandoff = latencies.llm_to_tts_handoff_latency;
489
  if (isFiniteNumber(llmToTtsHandoff)) {
490
+ liveTurnValues.llmToTtsHandoff = llmToTtsHandoff;
491
  }
492
 
493
  const ttsTtfb = metrics.tts?.ttfb;
494
  if (isFiniteNumber(ttsTtfb)) {
495
+ liveTurnValues.ttsTtfb = ttsTtfb;
496
  }
497
 
498
  const totalLatency = latencies.total_latency;
499
  if (isFiniteNumber(totalLatency)) {
500
+ liveTurnValues.totalLatency = totalLatency;
501
+ }
502
+
503
+ if (isFiniteNumber(liveTurnValues.llmToTtsHandoff) && isFiniteNumber(liveTurnValues.ttsTtfb)) {
504
+ const voiceGeneration = liveTurnValues.llmToTtsHandoff + liveTurnValues.ttsTtfb;
505
+ setLiveMetric("voice-generation", voiceGeneration, 8.0, 1.0, 2.2);
506
+ }
507
+
508
+ const hasAllStages = (
509
+ isFiniteNumber(liveTurnValues.eouDelay) &&
510
+ isFiniteNumber(liveTurnValues.sttFinalizationDelay) &&
511
+ isFiniteNumber(liveTurnValues.llmTtft) &&
512
+ isFiniteNumber(liveTurnValues.llmToTtsHandoff) &&
513
+ isFiniteNumber(liveTurnValues.ttsTtfb)
514
+ );
515
+
516
+ if (hasAllStages) {
517
+ const computedTotal =
518
+ liveTurnValues.eouDelay +
519
+ liveTurnValues.sttFinalizationDelay +
520
+ liveTurnValues.llmTtft +
521
+ liveTurnValues.llmToTtsHandoff +
522
+ liveTurnValues.ttsTtfb;
523
+ const totalValue = isFiniteNumber(liveTurnValues.totalLatency)
524
+ ? liveTurnValues.totalLatency
525
+ : computedTotal;
526
+ setLiveMetric("total", totalValue, 8.0, 1.5, 3.0);
527
  }
528
  }
529
 
530
  function updateAverages() {
 
 
531
  const avgEou = avg(averages.eouDelay);
532
  const avgSttFinalization = avg(averages.sttFinalization);
533
  const avgLlmTtft = avg(averages.llmTtft);
 
547
  setAverageValue("avg-llm-handoff", avgLlmToTtsHandoff);
548
  setAverageValue("avg-tts-ttfb", avgTtsTtfb);
549
  setAverageValue("avg-total", avgTotalLatency);
550
+ updateLiveMetricAverages();
551
  }
552
 
553
  function renderTurn(turn) {
 
573
 
574
  const ttsTtfb = metrics.tts?.ttfb;
575
  if (isFiniteNumber(ttsTtfb) && ttsTtfb > 0) averages.ttsTtfb.push(ttsTtfb);
576
+ if (isFiniteNumber(llmToTtsHandoff) && llmToTtsHandoff > 0 && isFiniteNumber(ttsTtfb) && ttsTtfb > 0) {
577
+ averages.voiceGeneration.push(llmToTtsHandoff + ttsTtfb);
578
+ }
579
 
580
  const totalLatency = latencies.total_latency;
581
  if (isFiniteNumber(totalLatency) && totalLatency > 0) {