@polderlabs/bizar 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/bizar-dash/dist/assets/{MobileChat-BVojK0n5.js → MobileChat-TCputYzr.js} +1 -1
  2. package/bizar-dash/dist/assets/{MobileChat-BVojK0n5.js.map → MobileChat-TCputYzr.js.map} +1 -1
  3. package/bizar-dash/dist/assets/{MobileSettings-C85ApWKZ.js → MobileSettings-BxcbL5XT.js} +1 -1
  4. package/bizar-dash/dist/assets/{MobileSettings-C85ApWKZ.js.map → MobileSettings-BxcbL5XT.js.map} +1 -1
  5. package/bizar-dash/dist/assets/{icons-Clz0NR6Y.js → icons-DRDXfbBP.js} +154 -134
  6. package/bizar-dash/dist/assets/icons-DRDXfbBP.js.map +1 -0
  7. package/bizar-dash/dist/assets/main-D5Ditnrd.js +19 -0
  8. package/bizar-dash/dist/assets/main-D5Ditnrd.js.map +1 -0
  9. package/bizar-dash/dist/assets/{main-DTkNlLrw.css → main-xFpWMd32.css} +1 -1
  10. package/bizar-dash/dist/assets/{mobile-D2pc-iNh.js → mobile--17fkfrl.js} +1 -1
  11. package/bizar-dash/dist/assets/{mobile-D2pc-iNh.js.map → mobile--17fkfrl.js.map} +1 -1
  12. package/bizar-dash/dist/assets/{mobile-IaZ47uKC.js → mobile-j3rOZK6v.js} +2 -2
  13. package/bizar-dash/dist/assets/{mobile-IaZ47uKC.js.map → mobile-j3rOZK6v.js.map} +1 -1
  14. package/bizar-dash/dist/assets/{useSlashCommands-DjEwHl4n.js → useSlashCommands-BG-DhEck.js} +2 -2
  15. package/bizar-dash/dist/assets/{useSlashCommands-DjEwHl4n.js.map → useSlashCommands-BG-DhEck.js.map} +1 -1
  16. package/bizar-dash/dist/index.html +5 -5
  17. package/bizar-dash/dist/mobile.html +3 -3
  18. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  19. package/bizar-dash/src/server/api.mjs +4 -0
  20. package/bizar-dash/src/server/routes/plugins.mjs +11 -3
  21. package/bizar-dash/src/server/routes/tailscale.mjs +46 -0
  22. package/bizar-dash/src/server/routes/voice.mjs +42 -6
  23. package/bizar-dash/src/server/server.mjs +13 -0
  24. package/bizar-dash/src/server/voice-store.mjs +27 -0
  25. package/bizar-dash/src/server/workers/transcription-worker.mjs +213 -0
  26. package/bizar-dash/src/web/App.tsx +9 -0
  27. package/bizar-dash/src/web/components/EvalDiff.tsx +157 -0
  28. package/bizar-dash/src/web/components/EvalRunCard.tsx +78 -0
  29. package/bizar-dash/src/web/components/PluginCard.tsx +64 -0
  30. package/bizar-dash/src/web/components/PluginPermissions.tsx +36 -0
  31. package/bizar-dash/src/web/components/TailscaleSettings.tsx +161 -0
  32. package/bizar-dash/src/web/components/Toggle.tsx +31 -0
  33. package/bizar-dash/src/web/components/Topbar.tsx +6 -0
  34. package/bizar-dash/src/web/styles/main.css +545 -0
  35. package/bizar-dash/src/web/views/Eval.tsx +172 -0
  36. package/bizar-dash/src/web/views/EvalReport.tsx +349 -0
  37. package/bizar-dash/src/web/views/Plugins.tsx +128 -0
  38. package/bizar-dash/src/web/views/Settings.tsx +2 -0
  39. package/bizar-dash/tests/cli-tailscale.test.mjs +113 -0
  40. package/bizar-dash/tests/components/plugin-permissions.test.tsx +95 -0
  41. package/bizar-dash/tests/eval-web-ui.test.tsx +220 -0
  42. package/bizar-dash/tests/voice-transcribe-worker.test.mjs +343 -0
  43. package/cli/bin.mjs +14 -0
  44. package/cli/commands/dash.mjs +19 -1
  45. package/cli/commands/service.mjs +7 -0
  46. package/cli/commands/tailscale.mjs +251 -0
  47. package/package.json +1 -1
  48. package/bizar-dash/dist/assets/icons-Clz0NR6Y.js.map +0 -1
  49. package/bizar-dash/dist/assets/main-8feQWXiF.js +0 -19
  50. package/bizar-dash/dist/assets/main-8feQWXiF.js.map +0 -1
@@ -9065,3 +9065,548 @@ select:focus-visible,
9065
9065
  color: var(--accent, #8b5cf6);
9066
9066
  margin-left: 6px;
9067
9067
  }
9068
+
9069
+ /* ─── Toggle switch ──────────────────────────────────────────────── */
9070
+ .toggle {
9071
+ position: relative;
9072
+ display: inline-flex;
9073
+ align-items: center;
9074
+ cursor: pointer;
9075
+ width: 40px;
9076
+ height: 22px;
9077
+ flex-shrink: 0;
9078
+ }
9079
+
9080
+ .toggle input {
9081
+ opacity: 0;
9082
+ width: 0;
9083
+ height: 0;
9084
+ position: absolute;
9085
+ }
9086
+
9087
+ .toggle-slider {
9088
+ position: absolute;
9089
+ inset: 0;
9090
+ background: var(--bg-elev-3);
9091
+ border-radius: 11px;
9092
+ transition: background 200ms ease;
9093
+ }
9094
+
9095
+ .toggle-slider::before {
9096
+ content: '';
9097
+ position: absolute;
9098
+ left: 2px;
9099
+ top: 2px;
9100
+ width: 18px;
9101
+ height: 18px;
9102
+ background: white;
9103
+ border-radius: 50%;
9104
+ transition: transform 200ms ease;
9105
+ }
9106
+
9107
+ .toggle input:checked + .toggle-slider {
9108
+ background: var(--accent);
9109
+ }
9110
+
9111
+ .toggle input:checked + .toggle-slider::before {
9112
+ transform: translateX(18px);
9113
+ }
9114
+
9115
+ .toggle input:focus-visible + .toggle-slider {
9116
+ outline: 2px solid var(--accent);
9117
+ outline-offset: 2px;
9118
+ }
9119
+
9120
+ /* ─── Plugins view ────────────────────────────────────────────────── */
9121
+ .view-plugins {
9122
+ max-width: 1400px;
9123
+ }
9124
+
9125
+ .plugins-toolbar {
9126
+ display: flex;
9127
+ align-items: center;
9128
+ gap: var(--space-3);
9129
+ margin-bottom: var(--space-4);
9130
+ }
9131
+
9132
+ .plugin-grid {
9133
+ display: grid;
9134
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
9135
+ gap: var(--space-4);
9136
+ }
9137
+
9138
+ .plugin-description {
9139
+ font-size: 13px;
9140
+ color: var(--text-dim);
9141
+ margin: 0;
9142
+ line-height: 1.5;
9143
+ }
9144
+
9145
+ .plugin-card-head {
9146
+ display: flex;
9147
+ align-items: flex-start;
9148
+ justify-content: space-between;
9149
+ gap: var(--space-3);
9150
+ }
9151
+
9152
+ .plugin-card-actions {
9153
+ display: flex;
9154
+ gap: var(--space-2);
9155
+ flex-wrap: wrap;
9156
+ margin-top: var(--space-2);
9157
+ }
9158
+
9159
+ .plugin-card-stats {
9160
+ font-size: 11px;
9161
+ color: var(--text-dim);
9162
+ margin-top: var(--space-2);
9163
+ }
9164
+
9165
+ /* ─── Plugin permissions ──────────────────────────────────────────── */
9166
+ .plugin-permissions {
9167
+ display: flex;
9168
+ flex-wrap: wrap;
9169
+ gap: 6px;
9170
+ margin: var(--space-3) 0;
9171
+ }
9172
+
9173
+ .permission-chip {
9174
+ display: inline-flex;
9175
+ align-items: center;
9176
+ gap: 4px;
9177
+ padding: 3px 8px;
9178
+ border-radius: 12px;
9179
+ font-size: 11px;
9180
+ font-weight: 500;
9181
+ }
9182
+
9183
+ .permission-chip.is-blue {
9184
+ background: rgba(59, 130, 246, 0.15);
9185
+ color: #58a6ff;
9186
+ }
9187
+
9188
+ .permission-chip.is-yellow {
9189
+ background: rgba(245, 158, 11, 0.15);
9190
+ color: #d29922;
9191
+ }
9192
+
9193
+ .permission-chip.is-red {
9194
+ background: rgba(239, 68, 68, 0.15);
9195
+ color: #f85149;
9196
+ }
9197
+
9198
+ .permission-chip.is-gray {
9199
+ background: rgba(139, 148, 158, 0.15);
9200
+ color: #8b949e;
9201
+ }
9202
+
9203
+ /* ─── v5.2.0 — Eval framework web UI ─────────────────────────────────────── */
9204
+
9205
+ .eval-grid {
9206
+ display: grid;
9207
+ grid-template-columns: minmax(280px, 1fr) minmax(0, 2fr);
9208
+ gap: var(--space-6, 24px);
9209
+ margin-top: var(--space-6, 24px);
9210
+ align-items: flex-start;
9211
+ }
9212
+
9213
+ @media (max-width: 900px) {
9214
+ .eval-grid {
9215
+ grid-template-columns: minmax(0, 1fr);
9216
+ }
9217
+ }
9218
+
9219
+ .eval-runs-list {
9220
+ display: flex;
9221
+ flex-direction: column;
9222
+ gap: var(--space-3, 12px);
9223
+ min-width: 0;
9224
+ }
9225
+
9226
+ .eval-run-card-wrap {
9227
+ position: relative;
9228
+ display: flex;
9229
+ flex-direction: column;
9230
+ gap: 4px;
9231
+ }
9232
+
9233
+ .eval-run-card {
9234
+ cursor: pointer;
9235
+ }
9236
+
9237
+ .eval-run-card-selected {
9238
+ border-color: var(--accent, #8b5cf6);
9239
+ box-shadow: 0 0 0 1px var(--accent, #8b5cf6);
9240
+ }
9241
+
9242
+ .eval-run-head {
9243
+ display: flex;
9244
+ align-items: center;
9245
+ justify-content: space-between;
9246
+ gap: var(--space-3, 12px);
9247
+ min-width: 0;
9248
+ }
9249
+
9250
+ .eval-run-title {
9251
+ font-family: ui-monospace, 'JetBrains Mono', monospace;
9252
+ font-size: 13px;
9253
+ font-weight: 600;
9254
+ color: var(--text-strong);
9255
+ margin: 0;
9256
+ overflow: hidden;
9257
+ text-overflow: ellipsis;
9258
+ white-space: nowrap;
9259
+ min-width: 0;
9260
+ }
9261
+
9262
+ .eval-run-meta {
9263
+ font-size: 12px;
9264
+ color: var(--text-dim);
9265
+ }
9266
+
9267
+ .eval-status {
9268
+ display: inline-flex;
9269
+ align-items: center;
9270
+ padding: 4px 10px;
9271
+ border-radius: 12px;
9272
+ font-size: 12px;
9273
+ font-weight: 600;
9274
+ white-space: nowrap;
9275
+ flex-shrink: 0;
9276
+ }
9277
+
9278
+ .eval-status.is-pass { background: rgba(63, 185, 80, 0.15); color: #3fb950; }
9279
+ .eval-status.is-warn { background: rgba(210, 153, 34, 0.15); color: #d29922; }
9280
+ .eval-status.is-fail { background: rgba(239, 68, 68, 0.15); color: #f85149; }
9281
+
9282
+ .eval-baseline-btn {
9283
+ align-self: flex-end;
9284
+ display: inline-flex;
9285
+ align-items: center;
9286
+ gap: 4px;
9287
+ background: transparent;
9288
+ border: 1px solid var(--border);
9289
+ border-radius: 999px;
9290
+ padding: 2px 8px;
9291
+ font-size: 11px;
9292
+ color: var(--text-dim);
9293
+ cursor: pointer;
9294
+ transition: border-color var(--motion-base) var(--ease), color var(--motion-base) var(--ease);
9295
+ }
9296
+
9297
+ .eval-baseline-btn:hover {
9298
+ border-color: var(--accent, #8b5cf6);
9299
+ color: var(--accent, #8b5cf6);
9300
+ }
9301
+
9302
+ .eval-detail {
9303
+ min-width: 0;
9304
+ display: flex;
9305
+ flex-direction: column;
9306
+ gap: var(--space-4, 16px);
9307
+ }
9308
+
9309
+ .eval-detail-loading,
9310
+ .eval-runs-loading {
9311
+ display: inline-flex;
9312
+ align-items: center;
9313
+ gap: var(--space-2, 8px);
9314
+ padding: var(--space-4, 16px);
9315
+ color: var(--text-dim);
9316
+ }
9317
+
9318
+ .eval-detail-empty {
9319
+ padding: var(--space-6, 24px);
9320
+ }
9321
+
9322
+ .eval-run-detail {
9323
+ display: flex;
9324
+ flex-direction: column;
9325
+ gap: var(--space-4, 16px);
9326
+ }
9327
+
9328
+ .eval-run-detail-header-row {
9329
+ display: flex;
9330
+ align-items: center;
9331
+ justify-content: space-between;
9332
+ gap: var(--space-3, 12px);
9333
+ }
9334
+
9335
+ .eval-run-detail-summary {
9336
+ display: flex;
9337
+ align-items: center;
9338
+ gap: var(--space-3, 12px);
9339
+ flex-wrap: wrap;
9340
+ }
9341
+
9342
+ .eval-rate {
9343
+ font-weight: 600;
9344
+ }
9345
+
9346
+ .eval-rate.is-pass { color: #3fb950; }
9347
+ .eval-rate.is-fail { color: #f85149; }
9348
+
9349
+ .eval-fixtures-list {
9350
+ list-style: none;
9351
+ margin: 0;
9352
+ padding: 0;
9353
+ display: flex;
9354
+ flex-direction: column;
9355
+ gap: 4px;
9356
+ }
9357
+
9358
+ .eval-fixture-row {
9359
+ display: grid;
9360
+ grid-template-columns: 24px 1fr auto;
9361
+ align-items: center;
9362
+ gap: var(--space-3, 12px);
9363
+ padding: 6px 0;
9364
+ border-bottom: 1px solid var(--border);
9365
+ font-size: 13px;
9366
+ }
9367
+
9368
+ .eval-fixture-row:last-child {
9369
+ border-bottom: none;
9370
+ }
9371
+
9372
+ .eval-fixture-pill {
9373
+ display: inline-flex;
9374
+ align-items: center;
9375
+ justify-content: center;
9376
+ width: 22px;
9377
+ height: 22px;
9378
+ border-radius: 50%;
9379
+ font-weight: 700;
9380
+ font-size: 12px;
9381
+ }
9382
+
9383
+ .eval-fixture-pill.is-pass {
9384
+ background: rgba(63, 185, 80, 0.15);
9385
+ color: #3fb950;
9386
+ }
9387
+
9388
+ .eval-fixture-pill.is-fail {
9389
+ background: rgba(239, 68, 68, 0.15);
9390
+ color: #f85149;
9391
+ }
9392
+
9393
+ .eval-fixture-id {
9394
+ font-size: 12px;
9395
+ overflow: hidden;
9396
+ text-overflow: ellipsis;
9397
+ white-space: nowrap;
9398
+ }
9399
+
9400
+ .eval-fixture-fails {
9401
+ font-size: 12px;
9402
+ grid-column: 2 / 3;
9403
+ }
9404
+
9405
+ .eval-fixture-latency {
9406
+ font-size: 11px;
9407
+ font-family: ui-monospace, 'JetBrains Mono', monospace;
9408
+ white-space: nowrap;
9409
+ }
9410
+
9411
+ .eval-diff {
9412
+ display: flex;
9413
+ flex-direction: column;
9414
+ gap: var(--space-3, 12px);
9415
+ }
9416
+
9417
+ .eval-diff-title {
9418
+ margin: 0;
9419
+ font-size: 14px;
9420
+ font-weight: 600;
9421
+ }
9422
+
9423
+ .eval-diff-subtitle {
9424
+ font-size: 12px;
9425
+ }
9426
+
9427
+ .eval-diff-stats {
9428
+ display: flex;
9429
+ align-items: center;
9430
+ gap: var(--space-4, 16px);
9431
+ flex-wrap: wrap;
9432
+ margin: 4px 0 8px;
9433
+ }
9434
+
9435
+ .eval-diff-stat {
9436
+ display: inline-flex;
9437
+ align-items: center;
9438
+ gap: 4px;
9439
+ font-size: 13px;
9440
+ font-weight: 600;
9441
+ }
9442
+
9443
+ .eval-diff-stat-icon {
9444
+ display: inline-flex;
9445
+ align-items: center;
9446
+ justify-content: center;
9447
+ width: 18px;
9448
+ height: 18px;
9449
+ border-radius: 50%;
9450
+ font-size: 12px;
9451
+ font-weight: 700;
9452
+ }
9453
+
9454
+ .eval-diff-stat.improved { color: #3fb950; }
9455
+ .eval-diff-stat.improved .eval-diff-stat-icon { background: rgba(63, 185, 80, 0.15); }
9456
+ .eval-diff-stat.regressed { color: #f85149; }
9457
+ .eval-diff-stat.regressed .eval-diff-stat-icon { background: rgba(239, 68, 68, 0.15); }
9458
+ .eval-diff-stat.unchanged { color: var(--text-dim); }
9459
+
9460
+ .eval-diff-list {
9461
+ list-style: none;
9462
+ margin: 0;
9463
+ padding: 0;
9464
+ display: flex;
9465
+ flex-direction: column;
9466
+ gap: 4px;
9467
+ }
9468
+
9469
+ .eval-diff-row {
9470
+ display: grid;
9471
+ grid-template-columns: minmax(0, 1.4fr) auto minmax(0, 1fr);
9472
+ align-items: center;
9473
+ gap: var(--space-3, 12px);
9474
+ padding: 4px 0;
9475
+ border-bottom: 1px solid var(--border);
9476
+ font-size: 13px;
9477
+ }
9478
+
9479
+ .eval-diff-row:last-child {
9480
+ border-bottom: none;
9481
+ }
9482
+
9483
+ .eval-diff-row.is-improved { color: var(--text); }
9484
+ .eval-diff-row.is-improved .eval-diff-fixture { color: #3fb950; }
9485
+ .eval-diff-row.is-regressed { color: var(--text); }
9486
+ .eval-diff-row.is-regressed .eval-diff-fixture { color: #f85149; }
9487
+
9488
+ .eval-diff-fixture {
9489
+ font-size: 12px;
9490
+ font-weight: 600;
9491
+ overflow: hidden;
9492
+ text-overflow: ellipsis;
9493
+ white-space: nowrap;
9494
+ }
9495
+
9496
+ .eval-diff-arrow {
9497
+ color: var(--text-dim);
9498
+ }
9499
+
9500
+ .eval-diff-arrow-small {
9501
+ color: var(--text-dim);
9502
+ margin: 0 4px;
9503
+ }
9504
+
9505
+ .eval-diff-states {
9506
+ display: inline-flex;
9507
+ align-items: center;
9508
+ gap: 4px;
9509
+ font-size: 12px;
9510
+ }
9511
+
9512
+ .eval-diff-state {
9513
+ display: inline-flex;
9514
+ align-items: center;
9515
+ padding: 2px 6px;
9516
+ border-radius: 4px;
9517
+ font-family: ui-monospace, 'JetBrains Mono', monospace;
9518
+ font-size: 11px;
9519
+ }
9520
+
9521
+ .eval-diff-state.is-pass {
9522
+ background: rgba(63, 185, 80, 0.15);
9523
+ color: #3fb950;
9524
+ }
9525
+
9526
+ .eval-diff-state.is-fail {
9527
+ background: rgba(239, 68, 68, 0.15);
9528
+ color: #f85149;
9529
+ }
9530
+
9531
+ .eval-diff-empty {
9532
+ margin: 0;
9533
+ font-size: 13px;
9534
+ }
9535
+
9536
+ .eval-diff-card {
9537
+ display: flex;
9538
+ flex-direction: column;
9539
+ gap: var(--space-3, 12px);
9540
+ }
9541
+
9542
+ .eval-diff-card-head {
9543
+ display: flex;
9544
+ align-items: center;
9545
+ justify-content: space-between;
9546
+ gap: var(--space-3, 12px);
9547
+ }
9548
+
9549
+ .eval-diff-card-title {
9550
+ display: inline-flex;
9551
+ align-items: center;
9552
+ gap: 6px;
9553
+ font-weight: 600;
9554
+ font-size: 13px;
9555
+ }
9556
+
9557
+ .eval-diff-missing {
9558
+ font-size: 13px;
9559
+ padding: var(--space-3, 12px);
9560
+ }
9561
+
9562
+ .eval-baseline-hint {
9563
+ background: rgba(139, 92, 246, 0.05);
9564
+ border-color: rgba(139, 92, 246, 0.25);
9565
+ }
9566
+
9567
+ /* ─── Eval overview (Eval.tsx) ──────────────────────────────────────────── */
9568
+
9569
+ .eval-summary-grid {
9570
+ display: grid;
9571
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
9572
+ gap: var(--space-4, 16px);
9573
+ margin-bottom: var(--space-6, 24px);
9574
+ }
9575
+
9576
+ .eval-summary-card {
9577
+ min-height: 120px;
9578
+ }
9579
+
9580
+ .eval-summary-value {
9581
+ font-size: 28px;
9582
+ font-weight: 700;
9583
+ letter-spacing: -0.01em;
9584
+ color: var(--text-strong);
9585
+ line-height: 1;
9586
+ margin: 4px 0;
9587
+ }
9588
+
9589
+ .eval-summary-cta {
9590
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.08), rgba(59, 130, 246, 0.04));
9591
+ border-color: rgba(139, 92, 246, 0.25);
9592
+ justify-content: space-between;
9593
+ }
9594
+
9595
+ .eval-summary-cta-btn {
9596
+ align-self: flex-start;
9597
+ margin-top: auto;
9598
+ }
9599
+
9600
+ .eval-section-title {
9601
+ font-size: 14px;
9602
+ font-weight: 600;
9603
+ text-transform: uppercase;
9604
+ letter-spacing: 0.04em;
9605
+ color: var(--text-dim);
9606
+ margin: 0 0 var(--space-3, 12px);
9607
+ }
9608
+
9609
+ .eval-recent-section {
9610
+ display: flex;
9611
+ flex-direction: column;
9612
+ }
@@ -0,0 +1,172 @@
1
+ // src/views/Eval.tsx — v5.2.0
2
+ //
3
+ // Main "Eval" tab view. Acts as the entry point to the eval framework
4
+ // UI: shows the recent runs summary at the top and links the user
5
+ // into EvalReport for the full per-fixture breakdown and regression
6
+ // diffing.
7
+ //
8
+ // The detailed comparison/diff lives in EvalReport.tsx; this view is
9
+ // a lighter landing page that mirrors the existing Doctor-style
10
+ // overview pattern (counts at top, recent history, call to action).
11
+
12
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
13
+ import { ClipboardCheck, ArrowRight, RefreshCw, ExternalLink } from 'lucide-react';
14
+ import { Card, CardTitle, CardMeta } from '../components/Card';
15
+ import { Button } from '../components/Button';
16
+ import { EmptyState } from '../components/EmptyState';
17
+ import { Spinner } from '../components/Spinner';
18
+ import { useToast } from '../components/Toast';
19
+ import { EvalRunCard, type EvalRunSummary } from '../components/EvalRunCard';
20
+ import { api } from '../lib/api';
21
+ import { cn } from '../lib/utils';
22
+ import type { Settings, Snapshot } from '../lib/types';
23
+
24
+ type Props = {
25
+ snapshot: Snapshot;
26
+ settings: Settings;
27
+ activeTab: string;
28
+ setActiveTab: (id: string) => void;
29
+ refreshSnapshot: () => Promise<void>;
30
+ };
31
+
32
+ const RECENT_RUNS_LIMIT = 8;
33
+
34
+ function EvalInner({ setActiveTab }: Props) {
35
+ const toast = useToast();
36
+ const [runs, setRuns] = useState<EvalRunSummary[]>([]);
37
+ const [loading, setLoading] = useState(true);
38
+ const cancelledRef = useRef(false);
39
+
40
+ const load = useCallback(async () => {
41
+ setLoading(true);
42
+ try {
43
+ const r = await api.get<{ runs: EvalRunSummary[] }>(`/eval/runs?limit=${RECENT_RUNS_LIMIT}`);
44
+ if (!cancelledRef.current) setRuns(r.runs || []);
45
+ } catch (err) {
46
+ if (!cancelledRef.current) {
47
+ toast.error(`Failed to load eval runs: ${(err as Error).message}`);
48
+ }
49
+ } finally {
50
+ if (!cancelledRef.current) setLoading(false);
51
+ }
52
+ }, [toast]);
53
+
54
+ useEffect(() => {
55
+ cancelledRef.current = false;
56
+ load();
57
+ return () => { cancelledRef.current = true; };
58
+ }, [load]);
59
+
60
+ const recent = runs.slice(0, RECENT_RUNS_LIMIT);
61
+ const totals = runs.reduce(
62
+ (acc, r) => {
63
+ acc.total += r.total;
64
+ acc.passed += r.passed;
65
+ acc.failed += r.failed;
66
+ if (r.failed === 0) acc.fullPass += 1;
67
+ return acc;
68
+ },
69
+ { total: 0, passed: 0, failed: 0, fullPass: 0 },
70
+ );
71
+ const overallRate = totals.total > 0 ? ((totals.passed / totals.total) * 100).toFixed(1) : '—';
72
+
73
+ return (
74
+ <div className="view view-eval" data-testid="eval-overview">
75
+ <div className="view-header">
76
+ <div className="view-header-text">
77
+ <h1 className="view-title">
78
+ <ClipboardCheck size={20} aria-hidden /> Eval
79
+ </h1>
80
+ <p className="view-subtitle muted">
81
+ Track golden-fixture runs, catch regressions, and compare run history.
82
+ </p>
83
+ </div>
84
+ <div className="view-actions">
85
+ <Button variant="secondary" size="sm" onClick={load} disabled={loading}>
86
+ {loading ? <Spinner size="sm" /> : <RefreshCw size={12} />}
87
+ {loading ? 'Refreshing…' : 'Refresh'}
88
+ </Button>
89
+ <Button
90
+ variant="primary"
91
+ size="sm"
92
+ onClick={() => setActiveTab('evalReport')}
93
+ title="Open the full eval report"
94
+ >
95
+ <ExternalLink size={12} /> Open Eval Report
96
+ </Button>
97
+ </div>
98
+ </div>
99
+
100
+ <div className="eval-summary-grid">
101
+ <Card className="eval-summary-card">
102
+ <CardTitle>Recent runs</CardTitle>
103
+ <div className="eval-summary-value">{runs.length}</div>
104
+ <CardMeta className="muted">
105
+ Latest {RECENT_RUNS_LIMIT} runs across all suites.
106
+ </CardMeta>
107
+ </Card>
108
+ <Card className="eval-summary-card">
109
+ <CardTitle>Fixtures run</CardTitle>
110
+ <div className="eval-summary-value">{totals.total}</div>
111
+ <CardMeta className="muted">
112
+ <span className={cn('eval-rate', totals.failed === 0 ? 'is-pass' : 'is-fail')}>
113
+ {overallRate}% pass rate
114
+ </span>
115
+ </CardMeta>
116
+ </Card>
117
+ <Card className="eval-summary-card">
118
+ <CardTitle>Clean runs</CardTitle>
119
+ <div className="eval-summary-value">{totals.fullPass}</div>
120
+ <CardMeta className="muted">
121
+ Runs where every fixture passed.
122
+ </CardMeta>
123
+ </Card>
124
+ <Card className="eval-summary-card eval-summary-cta">
125
+ <CardTitle>Compare runs</CardTitle>
126
+ <CardMeta>
127
+ Open the detailed report to diff two runs side-by-side and
128
+ spot regressions fixture by fixture.
129
+ </CardMeta>
130
+ <Button
131
+ variant="primary"
132
+ size="sm"
133
+ onClick={() => setActiveTab('evalReport')}
134
+ className="eval-summary-cta-btn"
135
+ >
136
+ Open report <ArrowRight size={12} />
137
+ </Button>
138
+ </Card>
139
+ </div>
140
+
141
+ <div className="eval-recent-section">
142
+ <h2 className="eval-section-title">Recent runs</h2>
143
+ {loading && recent.length === 0 ? (
144
+ <div className="eval-runs-loading">
145
+ <Spinner size="sm" />
146
+ <span className="muted">Loading runs…</span>
147
+ </div>
148
+ ) : recent.length === 0 ? (
149
+ <EmptyState
150
+ icon={<ClipboardCheck size={28} />}
151
+ title="No eval runs yet"
152
+ message={
153
+ <>
154
+ Run a suite via the CLI:{' '}
155
+ <code className="mono">bizar eval run ./path/to/fixtures</code>.
156
+ Results will appear here.
157
+ </>
158
+ }
159
+ />
160
+ ) : (
161
+ <div className="eval-runs-list">
162
+ {recent.map((r) => (
163
+ <EvalRunCard key={r.id} run={r} />
164
+ ))}
165
+ </div>
166
+ )}
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ export const Eval = React.memo(EvalInner);