@polderlabs/bizar 5.0.2 → 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 (88) hide show
  1. package/bizar-dash/dist/assets/MobileChat-TCputYzr.js +1 -0
  2. package/bizar-dash/dist/assets/MobileChat-TCputYzr.js.map +1 -0
  3. package/bizar-dash/dist/assets/MobileSettings-BxcbL5XT.js +1 -0
  4. package/bizar-dash/dist/assets/MobileSettings-BxcbL5XT.js.map +1 -0
  5. package/bizar-dash/dist/assets/{icons-Bo0iH9EC.js → icons-DRDXfbBP.js} +151 -136
  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/markdown-C6mXtQxD.js +1 -0
  11. package/bizar-dash/dist/assets/markdown-C6mXtQxD.js.map +1 -0
  12. package/bizar-dash/dist/assets/mobile--17fkfrl.js +1 -0
  13. package/bizar-dash/dist/assets/{mobile-DD-FZrTC.js.map → mobile--17fkfrl.js.map} +1 -1
  14. package/bizar-dash/dist/assets/mobile-j3rOZK6v.js +2 -0
  15. package/bizar-dash/dist/assets/mobile-j3rOZK6v.js.map +1 -0
  16. package/bizar-dash/dist/assets/{react-vendor-DZRUXSPQ.js → react-vendor-Dn4wqh4Z.js} +8 -8
  17. package/bizar-dash/dist/assets/react-vendor-Dn4wqh4Z.js.map +1 -0
  18. package/bizar-dash/dist/assets/useSlashCommands-BG-DhEck.js +2 -0
  19. package/bizar-dash/dist/assets/useSlashCommands-BG-DhEck.js.map +1 -0
  20. package/bizar-dash/dist/assets/vendor-CeHGtduv.js +29 -0
  21. package/bizar-dash/dist/assets/vendor-CeHGtduv.js.map +1 -0
  22. package/bizar-dash/dist/index.html +8 -6
  23. package/bizar-dash/dist/mobile.html +4 -5
  24. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  25. package/bizar-dash/src/server/api.mjs +4 -0
  26. package/bizar-dash/src/server/eval.mjs +93 -3
  27. package/bizar-dash/src/server/metrics.mjs +75 -0
  28. package/bizar-dash/src/server/otel.mjs +141 -14
  29. package/bizar-dash/src/server/plugins/registry.mjs +145 -43
  30. package/bizar-dash/src/server/routes/chat.mjs +46 -9
  31. package/bizar-dash/src/server/routes/opencode-session-detail.mjs +120 -17
  32. package/bizar-dash/src/server/routes/plugins.mjs +131 -16
  33. package/bizar-dash/src/server/routes/tailscale.mjs +46 -0
  34. package/bizar-dash/src/server/routes/voice.mjs +42 -6
  35. package/bizar-dash/src/server/routes/workspaces.mjs +156 -61
  36. package/bizar-dash/src/server/server.mjs +57 -15
  37. package/bizar-dash/src/server/voice-store.mjs +27 -0
  38. package/bizar-dash/src/server/workers/transcription-worker.mjs +213 -0
  39. package/bizar-dash/src/web/App.tsx +9 -0
  40. package/bizar-dash/src/web/MobileApp.tsx +18 -10
  41. package/bizar-dash/src/web/components/EvalDiff.tsx +157 -0
  42. package/bizar-dash/src/web/components/EvalRunCard.tsx +78 -0
  43. package/bizar-dash/src/web/components/PluginCard.tsx +64 -0
  44. package/bizar-dash/src/web/components/PluginPermissions.tsx +36 -0
  45. package/bizar-dash/src/web/components/TailscaleSettings.tsx +161 -0
  46. package/bizar-dash/src/web/components/Toggle.tsx +31 -0
  47. package/bizar-dash/src/web/components/Topbar.tsx +6 -0
  48. package/bizar-dash/src/web/mobile/MobileChat.tsx +138 -0
  49. package/bizar-dash/src/web/mobile/MobileSettings.tsx +196 -0
  50. package/bizar-dash/src/web/styles/main.css +545 -0
  51. package/bizar-dash/src/web/views/Eval.tsx +172 -0
  52. package/bizar-dash/src/web/views/EvalReport.tsx +349 -0
  53. package/bizar-dash/src/web/views/Plugins.tsx +128 -0
  54. package/bizar-dash/src/web/views/Settings.tsx +2 -0
  55. package/bizar-dash/tests/bundle-analysis.test.mjs +5 -4
  56. package/bizar-dash/tests/cli-tailscale.test.mjs +113 -0
  57. package/bizar-dash/tests/components/plugin-permissions.test.tsx +95 -0
  58. package/bizar-dash/tests/eval/fixtures-extra.test.mjs +285 -0
  59. package/bizar-dash/tests/eval-web-ui.test.tsx +220 -0
  60. package/bizar-dash/tests/otel-spans.test.mjs +253 -0
  61. package/bizar-dash/tests/plugins-registry-fallback.test.mjs +259 -0
  62. package/bizar-dash/tests/voice-transcribe-worker.test.mjs +343 -0
  63. package/cli/bin.mjs +14 -0
  64. package/cli/commands/dash.mjs +19 -1
  65. package/cli/commands/service.mjs +7 -0
  66. package/cli/commands/tailscale.mjs +251 -0
  67. package/package.json +1 -1
  68. package/templates/eval-fixtures/citation.json +16 -0
  69. package/templates/eval-fixtures/code-review.json +16 -0
  70. package/templates/eval-fixtures/concise-output.json +16 -0
  71. package/templates/eval-fixtures/context-window.json +16 -0
  72. package/templates/eval-fixtures/error-recovery.json +16 -0
  73. package/templates/eval-fixtures/json-output.json +20 -0
  74. package/templates/eval-fixtures/multi-language.json +16 -0
  75. package/templates/eval-fixtures/safe-paths.json +16 -0
  76. package/templates/eval-fixtures/tool-call-multi-step.json +18 -0
  77. package/templates/eval-fixtures/unicode-handling.json +16 -0
  78. package/bizar-dash/dist/assets/icons-Bo0iH9EC.js.map +0 -1
  79. package/bizar-dash/dist/assets/index-DmpSFPJY.js +0 -9
  80. package/bizar-dash/dist/assets/index-DmpSFPJY.js.map +0 -1
  81. package/bizar-dash/dist/assets/main-C1cpttnv.js +0 -19
  82. package/bizar-dash/dist/assets/main-C1cpttnv.js.map +0 -1
  83. package/bizar-dash/dist/assets/markdown-DIquRulQ.js +0 -29
  84. package/bizar-dash/dist/assets/markdown-DIquRulQ.js.map +0 -1
  85. package/bizar-dash/dist/assets/mobile-BtxQJftK.js +0 -2
  86. package/bizar-dash/dist/assets/mobile-BtxQJftK.js.map +0 -1
  87. package/bizar-dash/dist/assets/mobile-DD-FZrTC.js +0 -1
  88. package/bizar-dash/dist/assets/react-vendor-DZRUXSPQ.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);