@machinespirits/eval 0.1.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 (68) hide show
  1. package/components/MobileEvalDashboard.tsx +267 -0
  2. package/components/comparison/DeltaAnalysisTable.tsx +137 -0
  3. package/components/comparison/ProfileComparisonCard.tsx +176 -0
  4. package/components/comparison/RecognitionABMode.tsx +385 -0
  5. package/components/comparison/RecognitionMetricsPanel.tsx +135 -0
  6. package/components/comparison/WinnerIndicator.tsx +64 -0
  7. package/components/comparison/index.ts +5 -0
  8. package/components/mobile/BottomSheet.tsx +233 -0
  9. package/components/mobile/DimensionBreakdown.tsx +210 -0
  10. package/components/mobile/DocsView.tsx +363 -0
  11. package/components/mobile/LogsView.tsx +481 -0
  12. package/components/mobile/PsychodynamicQuadrant.tsx +261 -0
  13. package/components/mobile/QuickTestView.tsx +1098 -0
  14. package/components/mobile/RecognitionTypeChart.tsx +124 -0
  15. package/components/mobile/RecognitionView.tsx +809 -0
  16. package/components/mobile/RunDetailView.tsx +261 -0
  17. package/components/mobile/RunHistoryView.tsx +367 -0
  18. package/components/mobile/ScoreRadial.tsx +211 -0
  19. package/components/mobile/StreamingLogPanel.tsx +230 -0
  20. package/components/mobile/SynthesisStrategyChart.tsx +140 -0
  21. package/config/interaction-eval-scenarios.yaml +832 -0
  22. package/config/learner-agents.yaml +248 -0
  23. package/docs/research/ABLATION-DIALOGUE-ROUNDS.md +52 -0
  24. package/docs/research/ABLATION-MODEL-SELECTION.md +53 -0
  25. package/docs/research/ADVANCED-EVAL-ANALYSIS.md +60 -0
  26. package/docs/research/ANOVA-RESULTS-2026-01-14.md +257 -0
  27. package/docs/research/COMPREHENSIVE-EVALUATION-PLAN.md +586 -0
  28. package/docs/research/COST-ANALYSIS.md +56 -0
  29. package/docs/research/CRITICAL-REVIEW-RECOGNITION-TUTORING.md +340 -0
  30. package/docs/research/DYNAMIC-VS-SCRIPTED-ANALYSIS.md +291 -0
  31. package/docs/research/EVAL-SYSTEM-ANALYSIS.md +306 -0
  32. package/docs/research/FACTORIAL-RESULTS-2026-01-14.md +301 -0
  33. package/docs/research/IMPLEMENTATION-PLAN-CRITIQUE-RESPONSE.md +1988 -0
  34. package/docs/research/LONGITUDINAL-DYADIC-EVALUATION.md +282 -0
  35. package/docs/research/MULTI-JUDGE-VALIDATION-2026-01-14.md +147 -0
  36. package/docs/research/PAPER-EXTENSION-DYADIC.md +204 -0
  37. package/docs/research/PAPER-UNIFIED.md +659 -0
  38. package/docs/research/PAPER-UNIFIED.pdf +0 -0
  39. package/docs/research/PROMPT-IMPROVEMENTS-2026-01-14.md +356 -0
  40. package/docs/research/SESSION-NOTES-2026-01-11-RECOGNITION-EVAL.md +419 -0
  41. package/docs/research/apa.csl +2133 -0
  42. package/docs/research/archive/PAPER-DRAFT-RECOGNITION-TUTORING.md +1637 -0
  43. package/docs/research/archive/paper-multiagent-tutor.tex +978 -0
  44. package/docs/research/paper-draft/full-paper.md +136 -0
  45. package/docs/research/paper-draft/images/pasted-image-2026-01-24T03-47-47-846Z-d76a7ae2.png +0 -0
  46. package/docs/research/paper-draft/references.bib +515 -0
  47. package/docs/research/transcript-baseline.md +139 -0
  48. package/docs/research/transcript-recognition-multiagent.md +187 -0
  49. package/hooks/useEvalData.ts +625 -0
  50. package/index.js +27 -0
  51. package/package.json +73 -0
  52. package/routes/evalRoutes.js +3002 -0
  53. package/scripts/advanced-eval-analysis.js +351 -0
  54. package/scripts/analyze-eval-costs.js +378 -0
  55. package/scripts/analyze-eval-results.js +513 -0
  56. package/scripts/analyze-interaction-evals.js +368 -0
  57. package/server-init.js +45 -0
  58. package/server.js +162 -0
  59. package/services/benchmarkService.js +1892 -0
  60. package/services/evaluationRunner.js +739 -0
  61. package/services/evaluationStore.js +1121 -0
  62. package/services/learnerConfigLoader.js +385 -0
  63. package/services/learnerTutorInteractionEngine.js +857 -0
  64. package/services/memory/learnerMemoryService.js +1227 -0
  65. package/services/memory/learnerWritingPad.js +577 -0
  66. package/services/memory/tutorWritingPad.js +674 -0
  67. package/services/promptRecommendationService.js +493 -0
  68. package/services/rubricEvaluator.js +826 -0
@@ -0,0 +1,1098 @@
1
+ /**
2
+ * QuickTestView Component
3
+ *
4
+ * Test execution view with profile/scenario selection,
5
+ * streaming output, and result display.
6
+ * Supports both Quick (single) and Matrix (comparison) modes.
7
+ *
8
+ * Mobile-first features:
9
+ * - Quick Actions: Re-run last test, favorites for one-tap access
10
+ * - Compact mode: Minimal UI while test runs, glanceable status
11
+ * - Haptic feedback on completion
12
+ */
13
+
14
+ import React, { useState, useMemo, useEffect, useCallback } from 'react';
15
+ import type { EvalProfile, EvalScenario, EvalQuickTestResult } from '../../types';
16
+ import type { StreamLog, MatrixResult } from '../../hooks/useEvalData';
17
+ import { ScoreRadial } from './ScoreRadial';
18
+ import { DimensionBreakdown } from './DimensionBreakdown';
19
+ import { StreamingLogPanel } from './StreamingLogPanel';
20
+ import haptics from '../../utils/haptics';
21
+
22
+ type TestMode = 'quick' | 'matrix';
23
+
24
+ // Saved test configuration for quick re-run
25
+ interface SavedTestConfig {
26
+ profile: string;
27
+ scenario: string;
28
+ scenarioName?: string;
29
+ timestamp: number;
30
+ }
31
+
32
+ // Favorite preset
33
+ interface FavoritePreset {
34
+ id: string;
35
+ name: string;
36
+ profile: string;
37
+ scenario: string;
38
+ }
39
+
40
+ // LocalStorage keys
41
+ const STORAGE_KEYS = {
42
+ lastTest: 'eval-last-test',
43
+ favorites: 'eval-favorites',
44
+ compactMode: 'eval-compact-mode'
45
+ };
46
+
47
+ // Load from localStorage
48
+ function loadLastTest(): SavedTestConfig | null {
49
+ try {
50
+ const data = localStorage.getItem(STORAGE_KEYS.lastTest);
51
+ return data ? JSON.parse(data) : null;
52
+ } catch { return null; }
53
+ }
54
+
55
+ function loadFavorites(): FavoritePreset[] {
56
+ try {
57
+ const data = localStorage.getItem(STORAGE_KEYS.favorites);
58
+ return data ? JSON.parse(data) : [];
59
+ } catch { return []; }
60
+ }
61
+
62
+ function saveFavorites(favorites: FavoritePreset[]) {
63
+ try {
64
+ localStorage.setItem(STORAGE_KEYS.favorites, JSON.stringify(favorites));
65
+ } catch { /* Storage full */ }
66
+ }
67
+
68
+ function saveLastTest(config: SavedTestConfig) {
69
+ try {
70
+ localStorage.setItem(STORAGE_KEYS.lastTest, JSON.stringify(config));
71
+ } catch { /* Storage full */ }
72
+ }
73
+
74
+ interface QuickTestViewProps {
75
+ profiles: EvalProfile[];
76
+ scenarios: EvalScenario[];
77
+ onRunTest: (scenario: string, profile: string) => void;
78
+ onRunMatrix: (profiles: string[], scenarios: string[]) => void;
79
+ isRunning: boolean;
80
+ isMatrixRunning: boolean;
81
+ isLoadingData?: boolean;
82
+ result: EvalQuickTestResult | null;
83
+ matrixResult: MatrixResult | null;
84
+ streamLogs: StreamLog[];
85
+ onClearResult: () => void;
86
+ onClearMatrixResult: () => void;
87
+ }
88
+
89
+ export const QuickTestView: React.FC<QuickTestViewProps> = ({
90
+ profiles,
91
+ scenarios,
92
+ onRunTest,
93
+ onRunMatrix,
94
+ isRunning,
95
+ isMatrixRunning,
96
+ isLoadingData = false,
97
+ result,
98
+ matrixResult,
99
+ streamLogs,
100
+ onClearResult,
101
+ onClearMatrixResult
102
+ }) => {
103
+ const [mode, setMode] = useState<TestMode>('quick');
104
+ const [selectedProfile, setSelectedProfile] = useState<string>('');
105
+ const [selectedProfiles, setSelectedProfiles] = useState<Set<string>>(new Set());
106
+ const [selectedScenario, setSelectedScenario] = useState<string>('');
107
+ const [showSuggestions, setShowSuggestions] = useState(false);
108
+
109
+ // Quick Actions state
110
+ const [lastTest, setLastTest] = useState<SavedTestConfig | null>(null);
111
+ const [favorites, setFavorites] = useState<FavoritePreset[]>([]);
112
+ const [showQuickActions, setShowQuickActions] = useState(true);
113
+ const [compactMode, setCompactMode] = useState(false);
114
+ const [showFavoriteModal, setShowFavoriteModal] = useState(false);
115
+ const [newFavoriteName, setNewFavoriteName] = useState('');
116
+ const prevIsRunning = React.useRef(false);
117
+
118
+ // Load saved data on mount
119
+ useEffect(() => {
120
+ setLastTest(loadLastTest());
121
+ setFavorites(loadFavorites());
122
+ }, []);
123
+
124
+ // Haptic feedback when test completes + title notification
125
+ useEffect(() => {
126
+ const wasRunning = prevIsRunning.current;
127
+ const nowComplete = !isRunning && !isMatrixRunning;
128
+
129
+ if (wasRunning && nowComplete) {
130
+ // Strong haptic on completion
131
+ haptics.heavy();
132
+
133
+ // Update document title to notify user
134
+ if (result) {
135
+ const score = result.overallScore?.toFixed(1) || '?';
136
+ document.title = result.passed ? `✓ ${score} - Eval` : `✗ ${score} - Eval`;
137
+
138
+ // Reset title after 5 seconds
139
+ setTimeout(() => {
140
+ document.title = 'Eval Dashboard';
141
+ }, 5000);
142
+ }
143
+ }
144
+
145
+ prevIsRunning.current = isRunning || isMatrixRunning;
146
+ }, [isRunning, isMatrixRunning, result]);
147
+
148
+ // Group scenarios by category
149
+ const scenariosByCategory = useMemo(() => {
150
+ const grouped: Record<string, EvalScenario[]> = {};
151
+ scenarios.forEach((s) => {
152
+ const category = s.category || 'General';
153
+ if (!grouped[category]) grouped[category] = [];
154
+ grouped[category].push(s);
155
+ });
156
+ return grouped;
157
+ }, [scenarios]);
158
+
159
+ // Set default profile when profiles load
160
+ useEffect(() => {
161
+ if (profiles.length > 0 && !selectedProfile) {
162
+ setSelectedProfile(profiles[0].name);
163
+ }
164
+ }, [profiles, selectedProfile]);
165
+
166
+ const handleRunTest = useCallback(() => {
167
+ if (!selectedScenario || !selectedProfile) return;
168
+ haptics.button();
169
+
170
+ // Save as last test
171
+ const scenario = scenarios.find(s => s.id === selectedScenario);
172
+ const config: SavedTestConfig = {
173
+ profile: selectedProfile,
174
+ scenario: selectedScenario,
175
+ scenarioName: scenario?.name,
176
+ timestamp: Date.now()
177
+ };
178
+ saveLastTest(config);
179
+ setLastTest(config);
180
+
181
+ // Enter compact mode automatically
182
+ setCompactMode(true);
183
+
184
+ onRunTest(selectedScenario, selectedProfile);
185
+ }, [selectedScenario, selectedProfile, scenarios, onRunTest]);
186
+
187
+ const handleRunMatrix = useCallback(() => {
188
+ if (selectedProfiles.size < 2) return;
189
+ haptics.button();
190
+ setCompactMode(true);
191
+ onRunMatrix(Array.from(selectedProfiles), selectedScenario ? [selectedScenario] : []);
192
+ }, [selectedProfiles, selectedScenario, onRunMatrix]);
193
+
194
+ const handleClearResult = useCallback(() => {
195
+ haptics.light();
196
+ if (mode === 'matrix') {
197
+ onClearMatrixResult();
198
+ } else {
199
+ onClearResult();
200
+ }
201
+ setShowSuggestions(false);
202
+ setCompactMode(false);
203
+ }, [mode, onClearMatrixResult, onClearResult]);
204
+
205
+ // Quick Actions
206
+ const handleRerunLast = useCallback(() => {
207
+ if (!lastTest) return;
208
+ haptics.button();
209
+
210
+ // Set selections
211
+ setSelectedProfile(lastTest.profile);
212
+ setSelectedScenario(lastTest.scenario);
213
+ setMode('quick');
214
+ setCompactMode(true);
215
+
216
+ // Run immediately
217
+ onRunTest(lastTest.scenario, lastTest.profile);
218
+ }, [lastTest, onRunTest]);
219
+
220
+ const handleRunFavorite = useCallback((fav: FavoritePreset) => {
221
+ haptics.button();
222
+ setSelectedProfile(fav.profile);
223
+ setSelectedScenario(fav.scenario);
224
+ setMode('quick');
225
+ setCompactMode(true);
226
+ onRunTest(fav.scenario, fav.profile);
227
+ }, [onRunTest]);
228
+
229
+ const handleAddFavorite = useCallback(() => {
230
+ if (!selectedProfile || !selectedScenario || !newFavoriteName.trim()) return;
231
+
232
+ const newFav: FavoritePreset = {
233
+ id: `fav-${Date.now()}`,
234
+ name: newFavoriteName.trim(),
235
+ profile: selectedProfile,
236
+ scenario: selectedScenario
237
+ };
238
+
239
+ const updated = [...favorites, newFav];
240
+ setFavorites(updated);
241
+ saveFavorites(updated);
242
+ setShowFavoriteModal(false);
243
+ setNewFavoriteName('');
244
+ haptics.success();
245
+ }, [selectedProfile, selectedScenario, newFavoriteName, favorites]);
246
+
247
+ const handleRemoveFavorite = useCallback((favId: string) => {
248
+ haptics.light();
249
+ const updated = favorites.filter(f => f.id !== favId);
250
+ setFavorites(updated);
251
+ saveFavorites(updated);
252
+ }, [favorites]);
253
+
254
+ const toggleProfileSelection = (profileName: string) => {
255
+ haptics.light();
256
+ const newSet = new Set(selectedProfiles);
257
+ if (newSet.has(profileName)) {
258
+ newSet.delete(profileName);
259
+ } else {
260
+ newSet.add(profileName);
261
+ }
262
+ setSelectedProfiles(newSet);
263
+ };
264
+
265
+ const handleModeChange = (newMode: TestMode) => {
266
+ haptics.light();
267
+ setMode(newMode);
268
+ // Clear results when switching modes
269
+ if (newMode === 'quick') {
270
+ onClearMatrixResult();
271
+ } else {
272
+ onClearResult();
273
+ }
274
+ };
275
+
276
+ const selectedProfileData = profiles.find((p) => p.name === selectedProfile);
277
+ const anyRunning = isRunning || isMatrixRunning;
278
+ const hasQuickActions = lastTest || favorites.length > 0;
279
+ const currentScenario = scenarios.find(s => s.id === selectedScenario);
280
+
281
+ // Compact Running Mode - shows only essential status with premium styling
282
+ if (compactMode && anyRunning) {
283
+ const latestLog = streamLogs[streamLogs.length - 1];
284
+ const progressLog = streamLogs.filter(l => l.type === 'progress').pop();
285
+
286
+ return (
287
+ <div className="flex flex-col h-full bg-[#0a0a0a]">
288
+ {/* Compact header with pulsing indicator - Glass style */}
289
+ <div className="flex-shrink-0 p-4 border-b border-white/5 bg-gray-900/50 backdrop-blur-sm">
290
+ <div className="flex items-center justify-between">
291
+ <div className="flex items-center gap-3">
292
+ <div className="relative">
293
+ <div className="w-3 h-3 bg-green-500 rounded-full animate-pulse" />
294
+ <div className="absolute inset-0 w-3 h-3 bg-green-500 rounded-full animate-ping opacity-75" />
295
+ </div>
296
+ <div>
297
+ <div className="text-sm font-semibold text-white">
298
+ {lastTest?.scenarioName || 'Running Test...'}
299
+ </div>
300
+ <div className="text-xs text-gray-500">{selectedProfile}</div>
301
+ </div>
302
+ </div>
303
+ <button
304
+ type="button"
305
+ onClick={() => setCompactMode(false)}
306
+ className="text-xs text-gray-400 px-3 py-1.5 bg-white/5 border border-white/10 rounded-lg
307
+ hover:bg-white/10 transition-colors"
308
+ >
309
+ Expand
310
+ </button>
311
+ </div>
312
+
313
+ {/* Progress bar - Gradient glow */}
314
+ {progressLog && (
315
+ <div className="mt-4">
316
+ <div className="h-1.5 bg-gray-800/50 rounded-full overflow-hidden">
317
+ <div
318
+ className="h-full bg-gradient-to-r from-[#E63946] to-[#d62839] rounded-full
319
+ shadow-[0_0_10px_rgba(230,57,70,0.5)] transition-all duration-300"
320
+ style={{
321
+ width: progressLog.message.includes('%')
322
+ ? progressLog.message.match(/(\d+)%/)?.[1] + '%'
323
+ : '50%'
324
+ }}
325
+ />
326
+ </div>
327
+ </div>
328
+ )}
329
+ </div>
330
+
331
+ {/* Latest status line - Centered with animation */}
332
+ <div className="flex-1 flex items-center justify-center p-6">
333
+ <div className="text-center">
334
+ {/* Animated status icon */}
335
+ <div className="relative inline-block mb-6">
336
+ <div className="absolute inset-0 rounded-full bg-gradient-to-r from-[#E63946]/20 to-[#d62839]/20 animate-spin"
337
+ style={{ animationDuration: '4s' }} />
338
+ <div className="relative w-24 h-24 rounded-full bg-gray-900/80 backdrop-blur-sm border border-white/10
339
+ flex items-center justify-center">
340
+ <span className="text-4xl">
341
+ {latestLog?.type === 'success' ? '✓' :
342
+ latestLog?.type === 'error' ? '✗' :
343
+ latestLog?.type === 'warning' ? '⚠' : '◐'}
344
+ </span>
345
+ </div>
346
+ </div>
347
+ <div className="text-sm text-gray-300 font-medium max-w-xs mx-auto line-clamp-2">
348
+ {latestLog?.message || 'Starting evaluation...'}
349
+ </div>
350
+ <div className="flex justify-center gap-1.5 mt-6">
351
+ <span className="w-2 h-2 rounded-full bg-[#E63946]/60 animate-pulse" />
352
+ <span className="w-2 h-2 rounded-full bg-[#E63946]/60 animate-pulse" style={{ animationDelay: '0.2s' }} />
353
+ <span className="w-2 h-2 rounded-full bg-[#E63946]/60 animate-pulse" style={{ animationDelay: '0.4s' }} />
354
+ </div>
355
+ </div>
356
+ </div>
357
+ </div>
358
+ );
359
+ }
360
+
361
+ return (
362
+ <div className="flex flex-col h-full overflow-hidden">
363
+ {/* Quick Actions Bar - Glass morphism with premium feel */}
364
+ {showQuickActions && hasQuickActions && !anyRunning && !result && !matrixResult && (
365
+ <div className="flex-shrink-0 border-b border-white/5 bg-gradient-to-r from-gray-900/80 via-gray-800/60 to-gray-900/80 backdrop-blur-xl">
366
+ <div className="flex items-center gap-3 p-3 overflow-x-auto scrollbar-hide">
367
+ {/* Re-run Last - Hero button with glow */}
368
+ {lastTest && (
369
+ <button
370
+ type="button"
371
+ onClick={handleRerunLast}
372
+ className="flex-shrink-0 flex items-center gap-2.5 px-5 py-3
373
+ bg-gradient-to-r from-[#E63946] to-[#d62839] text-white
374
+ rounded-xl text-sm font-semibold
375
+ shadow-lg shadow-[#E63946]/25
376
+ active:scale-[0.98] active:shadow-md
377
+ transition-all duration-150"
378
+ style={{ minHeight: '48px' }}
379
+ >
380
+ <svg className="w-4 h-4 animate-[spin_3s_linear_infinite]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
381
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
382
+ </svg>
383
+ <span>Re-run</span>
384
+ </button>
385
+ )}
386
+
387
+ {/* Favorites - Refined glass cards */}
388
+ {favorites.map((fav) => (
389
+ <button
390
+ key={fav.id}
391
+ type="button"
392
+ onClick={() => handleRunFavorite(fav)}
393
+ className="flex-shrink-0 flex items-center gap-2.5 px-4 py-3
394
+ bg-white/5 backdrop-blur-sm border border-white/10
395
+ text-white rounded-xl text-sm font-medium
396
+ hover:bg-white/10 hover:border-white/20
397
+ active:scale-[0.98]
398
+ transition-all duration-150 group"
399
+ style={{ minHeight: '48px' }}
400
+ >
401
+ <span className="text-amber-400 text-base">★</span>
402
+ <span className="max-w-[100px] truncate">{fav.name}</span>
403
+ <button
404
+ type="button"
405
+ onClick={(e) => {
406
+ e.stopPropagation();
407
+ handleRemoveFavorite(fav.id);
408
+ }}
409
+ className="ml-1 w-5 h-5 flex items-center justify-center rounded-full
410
+ text-gray-500 hover:text-white hover:bg-red-500/80
411
+ opacity-0 group-hover:opacity-100 transition-all"
412
+ >
413
+ ×
414
+ </button>
415
+ </button>
416
+ ))}
417
+
418
+ {/* Collapse - Minimal */}
419
+ <button
420
+ type="button"
421
+ onClick={() => setShowQuickActions(false)}
422
+ className="flex-shrink-0 w-8 h-8 flex items-center justify-center
423
+ rounded-full text-gray-500 hover:text-white hover:bg-white/10
424
+ transition-all"
425
+ >
426
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
427
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
428
+ </svg>
429
+ </button>
430
+ </div>
431
+ </div>
432
+ )}
433
+
434
+ {/* Show Quick Actions toggle if hidden */}
435
+ {!showQuickActions && hasQuickActions && !anyRunning && !result && !matrixResult && (
436
+ <button
437
+ type="button"
438
+ onClick={() => setShowQuickActions(true)}
439
+ className="flex-shrink-0 flex items-center justify-center gap-2 py-2 text-xs text-gray-500 border-b border-gray-800"
440
+ >
441
+ <span>↓</span> Quick Actions <span>↓</span>
442
+ </button>
443
+ )}
444
+
445
+ {/* Configuration Section - Glass panel */}
446
+ <div className="flex-shrink-0 p-4 space-y-5 border-b border-white/5 bg-gradient-to-b from-gray-900/30 to-transparent">
447
+ {/* Mode Toggle - Premium segmented control */}
448
+ <div className="relative flex bg-gray-900/80 backdrop-blur-sm rounded-xl p-1 border border-white/5">
449
+ {/* Animated sliding background */}
450
+ <div
451
+ className="absolute top-1 bottom-1 w-[calc(50%-4px)] bg-gradient-to-r from-[#E63946] to-[#d62839] rounded-lg shadow-lg shadow-[#E63946]/20 transition-all duration-300 ease-out"
452
+ style={{ left: mode === 'quick' ? '4px' : 'calc(50% + 0px)' }}
453
+ />
454
+ <button
455
+ type="button"
456
+ onClick={() => handleModeChange('quick')}
457
+ className={`relative flex-1 py-2.5 px-4 rounded-lg text-sm font-semibold transition-all duration-300
458
+ ${mode === 'quick'
459
+ ? 'text-white'
460
+ : 'text-gray-400 hover:text-gray-200'
461
+ }`}
462
+ >
463
+ Quick Test
464
+ </button>
465
+ <button
466
+ type="button"
467
+ onClick={() => handleModeChange('matrix')}
468
+ className={`relative flex-1 py-2.5 px-4 rounded-lg text-sm font-semibold transition-all duration-300
469
+ ${mode === 'matrix'
470
+ ? 'text-white'
471
+ : 'text-gray-400 hover:text-gray-200'
472
+ }`}
473
+ >
474
+ Matrix Compare
475
+ </button>
476
+ </div>
477
+
478
+ {/* Profile Selection - Premium cards */}
479
+ <div>
480
+ <label className="block text-xs text-gray-500 mb-3 uppercase tracking-wider font-medium">
481
+ {mode === 'matrix' ? 'Compare Profiles (2+)' : 'Tutor Profile'}
482
+ </label>
483
+ {isLoadingData && profiles.length === 0 ? (
484
+ <div className="flex gap-3 pb-2">
485
+ {[1, 2, 3].map(i => (
486
+ <div key={i} className="h-16 w-28 bg-gray-800/50 rounded-xl animate-pulse" />
487
+ ))}
488
+ </div>
489
+ ) : profiles.length === 0 ? (
490
+ <div className="text-sm text-gray-500 py-4 text-center bg-gray-900/30 rounded-xl border border-white/5">
491
+ No profiles available
492
+ </div>
493
+ ) : (
494
+ <div className="relative">
495
+ {/* Fade edges */}
496
+ <div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-[#0a0a0a] to-transparent z-10 pointer-events-none" />
497
+ <div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#0a0a0a] to-transparent z-10 pointer-events-none" />
498
+
499
+ <div className="flex gap-3 overflow-x-auto pb-3 px-1 scrollbar-hide -mx-1">
500
+ {profiles.map((p, idx) => {
501
+ const isSelected = mode === 'matrix'
502
+ ? selectedProfiles.has(p.name)
503
+ : selectedProfile === p.name;
504
+
505
+ // Profile color based on type
506
+ const colors = [
507
+ { bg: 'bg-blue-500/20', border: 'border-blue-500/30', dot: 'bg-blue-400' },
508
+ { bg: 'bg-purple-500/20', border: 'border-purple-500/30', dot: 'bg-purple-400' },
509
+ { bg: 'bg-green-500/20', border: 'border-green-500/30', dot: 'bg-green-400' },
510
+ { bg: 'bg-amber-500/20', border: 'border-amber-500/30', dot: 'bg-amber-400' },
511
+ { bg: 'bg-cyan-500/20', border: 'border-cyan-500/30', dot: 'bg-cyan-400' },
512
+ { bg: 'bg-pink-500/20', border: 'border-pink-500/30', dot: 'bg-pink-400' },
513
+ ];
514
+ const color = colors[idx % colors.length];
515
+
516
+ return (
517
+ <button
518
+ key={p.name}
519
+ type="button"
520
+ onClick={() => {
521
+ if (mode === 'matrix') {
522
+ toggleProfileSelection(p.name);
523
+ } else {
524
+ haptics.light();
525
+ setSelectedProfile(p.name);
526
+ }
527
+ }}
528
+ className={`flex-shrink-0 p-3 rounded-xl text-left transition-all duration-200 active:scale-[0.97]
529
+ ${isSelected
530
+ ? 'bg-gradient-to-br from-[#E63946]/20 to-[#d62839]/10 border-2 border-[#E63946]/50 shadow-lg shadow-[#E63946]/10'
531
+ : `${color.bg} border ${color.border} hover:border-white/20`
532
+ }`}
533
+ style={{ minWidth: '120px' }}
534
+ >
535
+ <div className="flex items-center gap-2 mb-1.5">
536
+ <div className={`w-2.5 h-2.5 rounded-full ${isSelected ? 'bg-[#E63946]' : color.dot}`} />
537
+ <span className={`text-sm font-semibold truncate ${isSelected ? 'text-white' : 'text-gray-200'}`}>
538
+ {p.name}
539
+ </span>
540
+ {mode === 'matrix' && isSelected && (
541
+ <svg className="w-4 h-4 text-[#E63946] ml-auto flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
542
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
543
+ </svg>
544
+ )}
545
+ </div>
546
+ <p className={`text-[10px] line-clamp-1 ${isSelected ? 'text-gray-300' : 'text-gray-500'}`}>
547
+ {p.description?.split(' ').slice(0, 4).join(' ')}...
548
+ </p>
549
+ </button>
550
+ );
551
+ })}
552
+ </div>
553
+ </div>
554
+ )}
555
+ {mode === 'matrix' && (
556
+ <div className="flex items-center gap-2 mt-2">
557
+ <div className={`text-xs font-medium px-2.5 py-1 rounded-full
558
+ ${selectedProfiles.size >= 2
559
+ ? 'bg-green-500/20 text-green-400 border border-green-500/30'
560
+ : 'bg-gray-800/50 text-gray-500 border border-white/5'}`}>
561
+ {selectedProfiles.size} selected
562
+ </div>
563
+ {selectedProfiles.size < 2 && (
564
+ <span className="text-xs text-gray-600">Select at least 2</span>
565
+ )}
566
+ </div>
567
+ )}
568
+ </div>
569
+
570
+ {/* Scenario Selection - Enhanced */}
571
+ <div>
572
+ <label className="block text-xs text-gray-500 mb-3 uppercase tracking-wider font-medium">
573
+ {mode === 'matrix' ? 'Scenario (or all)' : 'Test Scenario'}
574
+ </label>
575
+ {isLoadingData && scenarios.length === 0 ? (
576
+ <div className="h-16 bg-gray-800/50 rounded-xl animate-pulse" />
577
+ ) : (
578
+ <div className="relative group">
579
+ <select
580
+ value={selectedScenario}
581
+ onChange={(e) => {
582
+ haptics.light();
583
+ setSelectedScenario(e.target.value);
584
+ }}
585
+ disabled={scenarios.length === 0}
586
+ className="w-full appearance-none bg-gray-900/60 backdrop-blur-sm
587
+ border border-white/10 rounded-xl p-4 pr-14 text-base text-white
588
+ focus:outline-none focus:border-[#E63946]/50 focus:ring-2 focus:ring-[#E63946]/20
589
+ disabled:opacity-50 transition-all duration-200
590
+ hover:border-white/20 hover:bg-gray-900/80"
591
+ style={{ fontSize: '16px' }} // Prevent iOS zoom
592
+ >
593
+ <option value="">
594
+ {scenarios.length === 0
595
+ ? 'No scenarios available'
596
+ : mode === 'matrix'
597
+ ? '◎ All scenarios'
598
+ : '○ Select scenario...'}
599
+ </option>
600
+ {Object.entries(scenariosByCategory).map(([category, items]) => (
601
+ <optgroup key={category} label={`── ${category} ──`}>
602
+ {items.map((s) => (
603
+ <option key={s.id} value={s.id}>
604
+ {s.name} {s.isMultiTurn ? `• ${s.turnCount} turns` : ''}
605
+ </option>
606
+ ))}
607
+ </optgroup>
608
+ ))}
609
+ </select>
610
+ {/* Custom dropdown arrow with animation */}
611
+ <div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none
612
+ w-8 h-8 rounded-lg bg-white/5 flex items-center justify-center
613
+ group-hover:bg-white/10 transition-colors">
614
+ <svg className="w-4 h-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
615
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
616
+ </svg>
617
+ </div>
618
+ </div>
619
+ )}
620
+ {/* Selected scenario preview */}
621
+ {selectedScenario && currentScenario && (
622
+ <div className="mt-3 p-3 bg-white/5 rounded-xl border border-white/5">
623
+ <div className="flex items-start gap-3">
624
+ <div className="w-8 h-8 rounded-lg bg-[#E63946]/20 flex items-center justify-center flex-shrink-0">
625
+ <span className="text-sm">🎯</span>
626
+ </div>
627
+ <div className="min-w-0 flex-1">
628
+ <p className="text-sm font-medium text-white truncate">{currentScenario.name}</p>
629
+ <p className="text-xs text-gray-500 mt-0.5">
630
+ {currentScenario.category || 'General'}
631
+ {currentScenario.isMultiTurn && ` • ${currentScenario.turnCount} turns`}
632
+ </p>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ )}
637
+ </div>
638
+
639
+ {/* Run Button Row */}
640
+ <div className="flex gap-3 pt-1">
641
+ <button
642
+ type="button"
643
+ onClick={mode === 'matrix' ? handleRunMatrix : handleRunTest}
644
+ disabled={
645
+ anyRunning ||
646
+ isLoadingData ||
647
+ (mode === 'quick' && (!selectedScenario || !selectedProfile)) ||
648
+ (mode === 'matrix' && selectedProfiles.size < 2)
649
+ }
650
+ className="flex-1 bg-gradient-to-r from-[#E63946] to-[#d62839] text-white py-4 rounded-2xl text-lg font-bold
651
+ disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none disabled:from-gray-700 disabled:to-gray-600
652
+ active:scale-[0.98] transition-all duration-200
653
+ shadow-xl shadow-[#E63946]/30 hover:shadow-2xl hover:shadow-[#E63946]/40
654
+ flex items-center justify-center gap-3
655
+ relative overflow-hidden group"
656
+ style={{ minHeight: '60px' }}
657
+ >
658
+ {/* Shimmer effect when enabled */}
659
+ {!anyRunning && (selectedScenario || mode === 'matrix') && (
660
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent
661
+ translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000" />
662
+ )}
663
+ {anyRunning ? (
664
+ <>
665
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
666
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
667
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
668
+ </svg>
669
+ <span>{isMatrixRunning ? 'Running Matrix...' : 'Running...'}</span>
670
+ </>
671
+ ) : mode === 'matrix' ? (
672
+ <>
673
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
674
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
675
+ </svg>
676
+ <span>Compare {selectedProfiles.size} Profiles</span>
677
+ </>
678
+ ) : (
679
+ <>
680
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
681
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
682
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
683
+ </svg>
684
+ <span>Run Test</span>
685
+ </>
686
+ )}
687
+ </button>
688
+
689
+ {/* Save as Favorite button - only for quick mode with valid selection */}
690
+ {mode === 'quick' && selectedProfile && selectedScenario && !anyRunning && (
691
+ <button
692
+ type="button"
693
+ onClick={() => {
694
+ haptics.light();
695
+ setShowFavoriteModal(true);
696
+ setNewFavoriteName(currentScenario?.name?.slice(0, 20) || '');
697
+ }}
698
+ className="flex-shrink-0 w-14 bg-gray-900/80 backdrop-blur-sm border border-white/10
699
+ text-amber-400 rounded-xl
700
+ flex items-center justify-center
701
+ active:scale-[0.95] hover:border-amber-400/30 hover:bg-amber-400/5
702
+ transition-all duration-200"
703
+ style={{ minHeight: '58px' }}
704
+ title="Save as favorite"
705
+ >
706
+ <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
707
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
708
+ </svg>
709
+ </button>
710
+ )}
711
+ </div>
712
+
713
+ {/* Save as Favorite Modal - Glass morphism */}
714
+ {showFavoriteModal && (
715
+ <div className="fixed inset-0 z-50 flex items-end justify-center bg-black/70 backdrop-blur-sm"
716
+ onClick={() => setShowFavoriteModal(false)}>
717
+ <div
718
+ className="w-full max-w-lg bg-gray-900/95 backdrop-blur-xl border-t border-white/10
719
+ rounded-t-3xl p-5 space-y-5 animate-slide-up"
720
+ onClick={(e) => e.stopPropagation()}
721
+ >
722
+ <div className="w-10 h-1 bg-white/20 rounded-full mx-auto" />
723
+ <h3 className="text-xl font-bold text-white text-center">Save as Favorite</h3>
724
+
725
+ <div className="space-y-4">
726
+ <div className="flex items-center gap-3 p-3 bg-white/5 rounded-xl border border-white/5">
727
+ <div className="w-10 h-10 rounded-full bg-[#E63946]/20 flex items-center justify-center">
728
+ <span className="text-lg">⚙️</span>
729
+ </div>
730
+ <div className="flex-1 min-w-0">
731
+ <div className="text-xs text-gray-500 uppercase tracking-wide">Configuration</div>
732
+ <div className="text-sm text-white truncate">{selectedProfile} → {currentScenario?.name}</div>
733
+ </div>
734
+ </div>
735
+ <input
736
+ type="text"
737
+ value={newFavoriteName}
738
+ onChange={(e) => setNewFavoriteName(e.target.value)}
739
+ placeholder="Give it a name..."
740
+ className="w-full bg-white/5 border border-white/10 rounded-xl p-4 text-white
741
+ placeholder:text-gray-500
742
+ focus:outline-none focus:border-[#E63946]/50 focus:ring-2 focus:ring-[#E63946]/20
743
+ transition-all duration-200"
744
+ style={{ fontSize: '16px' }}
745
+ autoFocus
746
+ />
747
+ </div>
748
+
749
+ <div className="flex gap-3 pt-2">
750
+ <button
751
+ type="button"
752
+ onClick={() => setShowFavoriteModal(false)}
753
+ className="flex-1 py-3.5 bg-white/5 border border-white/10 text-gray-300 rounded-xl font-medium
754
+ active:scale-[0.98] transition-all duration-200"
755
+ >
756
+ Cancel
757
+ </button>
758
+ <button
759
+ type="button"
760
+ onClick={handleAddFavorite}
761
+ disabled={!newFavoriteName.trim()}
762
+ className="flex-1 py-3.5 bg-gradient-to-r from-[#E63946] to-[#d62839] text-white rounded-xl font-semibold
763
+ shadow-lg shadow-[#E63946]/25
764
+ disabled:opacity-40 disabled:shadow-none
765
+ active:scale-[0.98] transition-all duration-200"
766
+ >
767
+ Save Favorite
768
+ </button>
769
+ </div>
770
+ </div>
771
+ </div>
772
+ )}
773
+ </div>
774
+
775
+ {/* Results Section */}
776
+ <div className="flex-1 overflow-y-auto">
777
+ {/* Streaming Logs - Always show when running or has logs */}
778
+ <StreamingLogPanel logs={streamLogs} isRunning={anyRunning} />
779
+
780
+ {/* Matrix Result Display */}
781
+ {mode === 'matrix' && matrixResult && !isMatrixRunning && (
782
+ <div className="p-4 space-y-4">
783
+ {/* Header */}
784
+ <div className="flex items-start justify-between">
785
+ <div>
786
+ <h3 className="text-lg font-semibold text-white">Matrix Results</h3>
787
+ <p className="text-sm text-gray-400">
788
+ {matrixResult.profiles.length} profiles × {matrixResult.scenariosRun} scenarios
789
+ </p>
790
+ </div>
791
+ <button
792
+ type="button"
793
+ onClick={handleClearResult}
794
+ className="p-2 text-gray-400 hover:text-white transition-colors"
795
+ aria-label="Clear result"
796
+ >
797
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
798
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
799
+ </svg>
800
+ </button>
801
+ </div>
802
+
803
+ {/* Rankings */}
804
+ <div className="bg-gray-800/50 rounded-lg p-4">
805
+ <h4 className="text-sm font-medium text-gray-300 mb-3">Rankings</h4>
806
+ <div className="space-y-2">
807
+ {matrixResult.rankings
808
+ .sort((a, b) => a.rank - b.rank)
809
+ .map((ranking, i) => (
810
+ <div
811
+ key={ranking.profile}
812
+ className="flex items-center justify-between p-3 bg-gray-900/50 rounded-lg"
813
+ >
814
+ <div className="flex items-center gap-3">
815
+ <span className={`text-lg font-bold ${i === 0 ? 'text-yellow-400' : i === 1 ? 'text-gray-300' : i === 2 ? 'text-amber-600' : 'text-gray-500'}`}>
816
+ #{ranking.rank}
817
+ </span>
818
+ <span className="text-white font-medium">{ranking.profile}</span>
819
+ </div>
820
+ <span className={`text-lg font-semibold ${ranking.avgScore >= 7 ? 'text-green-400' : ranking.avgScore >= 5 ? 'text-yellow-400' : 'text-red-400'}`}>
821
+ {ranking.avgScore.toFixed(1)}
822
+ </span>
823
+ </div>
824
+ ))}
825
+ </div>
826
+ </div>
827
+
828
+ {/* Dimension Averages by Profile */}
829
+ {Object.keys(matrixResult.dimensionAverages).length > 0 && (
830
+ <div className="bg-gray-800/50 rounded-lg p-4">
831
+ <h4 className="text-sm font-medium text-gray-300 mb-3">Dimension Scores</h4>
832
+ <div className="space-y-4">
833
+ {Object.entries(matrixResult.dimensionAverages).map(([profile, dims]) => (
834
+ <div key={profile}>
835
+ <p className="text-xs text-gray-400 mb-2">{profile}</p>
836
+ <DimensionBreakdown scores={dims as Record<string, number>} />
837
+ </div>
838
+ ))}
839
+ </div>
840
+ </div>
841
+ )}
842
+ </div>
843
+ )}
844
+
845
+ {/* Quick Test Result Display */}
846
+ {mode === 'quick' && result && !isRunning && (
847
+ <div className="p-4 space-y-6">
848
+ {/* Header with action buttons */}
849
+ <div className="flex items-start justify-between">
850
+ <div className="flex-1 min-w-0">
851
+ <h3 className="text-lg font-semibold text-white truncate">{result.scenarioName}</h3>
852
+ <p className="text-sm text-gray-400">{result.profile}</p>
853
+ </div>
854
+ <div className="flex items-center gap-1 flex-shrink-0 ml-2">
855
+ {/* Run Again */}
856
+ <button
857
+ type="button"
858
+ onClick={handleRerunLast}
859
+ className="flex items-center gap-1.5 px-3 py-1.5 bg-[#E63946] text-white rounded-lg text-sm font-medium active:bg-[#c1121f] transition-colors"
860
+ style={{ minHeight: '36px' }}
861
+ >
862
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
863
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
864
+ </svg>
865
+ Again
866
+ </button>
867
+ {/* Clear */}
868
+ <button
869
+ type="button"
870
+ onClick={handleClearResult}
871
+ className="p-2 text-gray-400 hover:text-white transition-colors"
872
+ aria-label="Clear result"
873
+ >
874
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
875
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
876
+ </svg>
877
+ </button>
878
+ </div>
879
+ </div>
880
+
881
+ {/* Score Display */}
882
+ <div className="flex items-center justify-center">
883
+ <ScoreRadial score={result.overallScore} passed={result.passed} size={140} />
884
+ </div>
885
+
886
+ {/* Metrics Row */}
887
+ <div className="grid grid-cols-3 gap-3">
888
+ <div className="bg-gray-800/50 rounded-lg p-3 text-center">
889
+ <div className="text-lg font-semibold text-white">
890
+ {result.latencyMs ? `${(result.latencyMs / 1000).toFixed(1)}s` : '-'}
891
+ </div>
892
+ <div className="text-xs text-gray-400">Latency</div>
893
+ </div>
894
+ <div className="bg-gray-800/50 rounded-lg p-3 text-center">
895
+ <div className="text-lg font-semibold text-white">
896
+ {result.totalTokens?.toLocaleString() || '-'}
897
+ </div>
898
+ <div className="text-xs text-gray-400">Tokens</div>
899
+ </div>
900
+ <div className="bg-gray-800/50 rounded-lg p-3 text-center">
901
+ <div className="text-lg font-semibold text-white">
902
+ {result.dialogueRounds || result.apiCalls || '-'}
903
+ </div>
904
+ <div className="text-xs text-gray-400">Rounds</div>
905
+ </div>
906
+ </div>
907
+
908
+ {/* Dimension Scores */}
909
+ {result.scores && (
910
+ <div className="bg-gray-800/30 rounded-lg p-4">
911
+ <h4 className="text-sm font-medium text-gray-300 mb-3">Dimension Scores</h4>
912
+ <DimensionBreakdown scores={result.scores} />
913
+ </div>
914
+ )}
915
+
916
+ {/* Validation */}
917
+ {result.validation && (
918
+ <div className="bg-gray-800/30 rounded-lg p-4">
919
+ <h4 className="text-sm font-medium text-gray-300 mb-2">Validation</h4>
920
+ <div className="flex gap-4">
921
+ <div className="flex items-center gap-2">
922
+ <span className={result.validation.passesRequired ? 'text-green-400' : 'text-red-400'}>
923
+ {result.validation.passesRequired ? '✓' : '✗'}
924
+ </span>
925
+ <span className="text-xs text-gray-400">Required</span>
926
+ </div>
927
+ <div className="flex items-center gap-2">
928
+ <span className={result.validation.passesForbidden ? 'text-green-400' : 'text-red-400'}>
929
+ {result.validation.passesForbidden ? '✓' : '✗'}
930
+ </span>
931
+ <span className="text-xs text-gray-400">Forbidden</span>
932
+ </div>
933
+ </div>
934
+ {result.validation.requiredMissing.length > 0 && (
935
+ <div className="mt-2 text-xs text-red-400">
936
+ Missing: {result.validation.requiredMissing.join(', ')}
937
+ </div>
938
+ )}
939
+ {result.validation.forbiddenFound.length > 0 && (
940
+ <div className="mt-2 text-xs text-red-400">
941
+ Found forbidden: {result.validation.forbiddenFound.join(', ')}
942
+ </div>
943
+ )}
944
+ </div>
945
+ )}
946
+
947
+ {/* Suggestions Toggle */}
948
+ {result.suggestions && result.suggestions.length > 0 && (
949
+ <div className="bg-gray-800/30 rounded-lg overflow-hidden">
950
+ <button
951
+ type="button"
952
+ onClick={() => {
953
+ haptics.light();
954
+ setShowSuggestions(!showSuggestions);
955
+ }}
956
+ className="w-full flex items-center justify-between p-4 hover:bg-gray-800/50 transition-colors"
957
+ >
958
+ <span className="text-sm font-medium text-gray-300">
959
+ Suggestions ({result.suggestions.length})
960
+ </span>
961
+ <svg
962
+ className={`w-4 h-4 text-gray-400 transition-transform ${showSuggestions ? 'rotate-180' : ''}`}
963
+ fill="none"
964
+ viewBox="0 0 24 24"
965
+ stroke="currentColor"
966
+ >
967
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
968
+ </svg>
969
+ </button>
970
+
971
+ {showSuggestions && (
972
+ <div className="border-t border-gray-800 p-4 space-y-3">
973
+ {result.suggestions.map((suggestion, i) => (
974
+ <div key={i} className="bg-gray-900/50 rounded-lg p-3">
975
+ <div className="flex items-start gap-2">
976
+ <span
977
+ className={`text-xs px-2 py-0.5 rounded font-medium flex-shrink-0
978
+ ${suggestion.priority === 'high'
979
+ ? 'bg-red-900/50 text-red-400'
980
+ : suggestion.priority === 'medium'
981
+ ? 'bg-yellow-900/50 text-yellow-400'
982
+ : 'bg-gray-700 text-gray-400'
983
+ }`}
984
+ >
985
+ {suggestion.type}
986
+ </span>
987
+ <div className="flex-1 min-w-0">
988
+ <div className="text-sm font-medium text-white">
989
+ {suggestion.title || suggestion.headline}
990
+ </div>
991
+ <div className="text-xs text-gray-400 mt-1 line-clamp-3">
992
+ {suggestion.message || suggestion.body}
993
+ </div>
994
+ </div>
995
+ </div>
996
+ </div>
997
+ ))}
998
+ </div>
999
+ )}
1000
+ </div>
1001
+ )}
1002
+
1003
+ {/* Evaluator Reasoning */}
1004
+ {result.evaluationReasoning && (
1005
+ <div className="bg-gray-800/30 rounded-lg p-4">
1006
+ <h4 className="text-sm font-medium text-gray-300 mb-2">Evaluator Reasoning</h4>
1007
+ <p className="text-xs text-gray-400 leading-relaxed">
1008
+ {result.evaluationReasoning}
1009
+ </p>
1010
+ {result.evaluatorModel && (
1011
+ <p className="text-[10px] text-gray-600 mt-2">
1012
+ Model: {result.evaluatorModel}
1013
+ </p>
1014
+ )}
1015
+ </div>
1016
+ )}
1017
+ </div>
1018
+ )}
1019
+
1020
+ {/* Empty State - Premium visual with helpful guidance */}
1021
+ {!result && !matrixResult && !anyRunning && streamLogs.length === 0 && (
1022
+ <div className="flex flex-col items-center justify-center py-16 px-6">
1023
+ {/* Animated icon stack */}
1024
+ <div className="relative mb-8">
1025
+ {/* Outer pulsing ring */}
1026
+ <div className="absolute -inset-4 rounded-full bg-gradient-to-r from-[#E63946]/10 via-transparent to-[#E63946]/10 animate-pulse" />
1027
+ {/* Middle rotating ring */}
1028
+ <div className="absolute -inset-2 rounded-full border border-dashed border-white/10 animate-spin"
1029
+ style={{ animationDuration: '20s' }} />
1030
+ {/* Icon container */}
1031
+ <div className="relative w-24 h-24 rounded-2xl bg-gradient-to-br from-gray-900/80 to-gray-800/50 backdrop-blur-sm
1032
+ border border-white/10 flex items-center justify-center shadow-2xl">
1033
+ <div className="text-4xl">
1034
+ {mode === 'matrix' ? '⚖️' : '🧪'}
1035
+ </div>
1036
+ </div>
1037
+ {/* Floating accent dots */}
1038
+ <div className="absolute -top-1 -right-1 w-3 h-3 rounded-full bg-[#E63946] animate-bounce"
1039
+ style={{ animationDelay: '0s', animationDuration: '2s' }} />
1040
+ <div className="absolute -bottom-1 -left-1 w-2 h-2 rounded-full bg-blue-400 animate-bounce"
1041
+ style={{ animationDelay: '0.5s', animationDuration: '2.5s' }} />
1042
+ </div>
1043
+
1044
+ {/* Text content */}
1045
+ <h3 className="text-lg font-semibold text-white mb-2">
1046
+ {mode === 'matrix' ? 'Compare Profiles' : 'Ready to Test'}
1047
+ </h3>
1048
+ <p className="text-sm text-gray-400 text-center max-w-xs mb-6">
1049
+ {mode === 'matrix'
1050
+ ? 'Select 2 or more tutor profiles above to run a side-by-side comparison'
1051
+ : 'Choose a tutor profile and scenario to evaluate AI tutor performance'}
1052
+ </p>
1053
+
1054
+ {/* Visual checklist */}
1055
+ <div className="flex flex-col gap-2 w-full max-w-xs">
1056
+ <div className={`flex items-center gap-3 p-3 rounded-xl transition-all duration-300
1057
+ ${(mode === 'matrix' ? selectedProfiles.size >= 2 : selectedProfile)
1058
+ ? 'bg-green-500/10 border border-green-500/20'
1059
+ : 'bg-white/5 border border-white/5'}`}>
1060
+ <div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs
1061
+ ${(mode === 'matrix' ? selectedProfiles.size >= 2 : selectedProfile)
1062
+ ? 'bg-green-500/20 text-green-400'
1063
+ : 'bg-white/10 text-gray-500'}`}>
1064
+ {(mode === 'matrix' ? selectedProfiles.size >= 2 : selectedProfile) ? '✓' : '1'}
1065
+ </div>
1066
+ <span className={`text-sm ${(mode === 'matrix' ? selectedProfiles.size >= 2 : selectedProfile) ? 'text-green-400' : 'text-gray-400'}`}>
1067
+ {mode === 'matrix' ? 'Select profiles to compare' : 'Select a tutor profile'}
1068
+ </span>
1069
+ </div>
1070
+ <div className={`flex items-center gap-3 p-3 rounded-xl transition-all duration-300
1071
+ ${selectedScenario
1072
+ ? 'bg-green-500/10 border border-green-500/20'
1073
+ : 'bg-white/5 border border-white/5'}`}>
1074
+ <div className={`w-6 h-6 rounded-full flex items-center justify-center text-xs
1075
+ ${selectedScenario
1076
+ ? 'bg-green-500/20 text-green-400'
1077
+ : 'bg-white/10 text-gray-500'}`}>
1078
+ {selectedScenario ? '✓' : '2'}
1079
+ </div>
1080
+ <span className={`text-sm ${selectedScenario ? 'text-green-400' : 'text-gray-400'}`}>
1081
+ Choose a test scenario
1082
+ </span>
1083
+ </div>
1084
+ <div className="flex items-center gap-3 p-3 rounded-xl bg-white/5 border border-white/5">
1085
+ <div className="w-6 h-6 rounded-full flex items-center justify-center text-xs bg-white/10 text-gray-500">
1086
+ 3
1087
+ </div>
1088
+ <span className="text-sm text-gray-400">Tap Run Test to begin</span>
1089
+ </div>
1090
+ </div>
1091
+ </div>
1092
+ )}
1093
+ </div>
1094
+ </div>
1095
+ );
1096
+ };
1097
+
1098
+ export default QuickTestView;