@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.
- package/components/MobileEvalDashboard.tsx +267 -0
- package/components/comparison/DeltaAnalysisTable.tsx +137 -0
- package/components/comparison/ProfileComparisonCard.tsx +176 -0
- package/components/comparison/RecognitionABMode.tsx +385 -0
- package/components/comparison/RecognitionMetricsPanel.tsx +135 -0
- package/components/comparison/WinnerIndicator.tsx +64 -0
- package/components/comparison/index.ts +5 -0
- package/components/mobile/BottomSheet.tsx +233 -0
- package/components/mobile/DimensionBreakdown.tsx +210 -0
- package/components/mobile/DocsView.tsx +363 -0
- package/components/mobile/LogsView.tsx +481 -0
- package/components/mobile/PsychodynamicQuadrant.tsx +261 -0
- package/components/mobile/QuickTestView.tsx +1098 -0
- package/components/mobile/RecognitionTypeChart.tsx +124 -0
- package/components/mobile/RecognitionView.tsx +809 -0
- package/components/mobile/RunDetailView.tsx +261 -0
- package/components/mobile/RunHistoryView.tsx +367 -0
- package/components/mobile/ScoreRadial.tsx +211 -0
- package/components/mobile/StreamingLogPanel.tsx +230 -0
- package/components/mobile/SynthesisStrategyChart.tsx +140 -0
- package/config/interaction-eval-scenarios.yaml +832 -0
- package/config/learner-agents.yaml +248 -0
- package/docs/research/ABLATION-DIALOGUE-ROUNDS.md +52 -0
- package/docs/research/ABLATION-MODEL-SELECTION.md +53 -0
- package/docs/research/ADVANCED-EVAL-ANALYSIS.md +60 -0
- package/docs/research/ANOVA-RESULTS-2026-01-14.md +257 -0
- package/docs/research/COMPREHENSIVE-EVALUATION-PLAN.md +586 -0
- package/docs/research/COST-ANALYSIS.md +56 -0
- package/docs/research/CRITICAL-REVIEW-RECOGNITION-TUTORING.md +340 -0
- package/docs/research/DYNAMIC-VS-SCRIPTED-ANALYSIS.md +291 -0
- package/docs/research/EVAL-SYSTEM-ANALYSIS.md +306 -0
- package/docs/research/FACTORIAL-RESULTS-2026-01-14.md +301 -0
- package/docs/research/IMPLEMENTATION-PLAN-CRITIQUE-RESPONSE.md +1988 -0
- package/docs/research/LONGITUDINAL-DYADIC-EVALUATION.md +282 -0
- package/docs/research/MULTI-JUDGE-VALIDATION-2026-01-14.md +147 -0
- package/docs/research/PAPER-EXTENSION-DYADIC.md +204 -0
- package/docs/research/PAPER-UNIFIED.md +659 -0
- package/docs/research/PAPER-UNIFIED.pdf +0 -0
- package/docs/research/PROMPT-IMPROVEMENTS-2026-01-14.md +356 -0
- package/docs/research/SESSION-NOTES-2026-01-11-RECOGNITION-EVAL.md +419 -0
- package/docs/research/apa.csl +2133 -0
- package/docs/research/archive/PAPER-DRAFT-RECOGNITION-TUTORING.md +1637 -0
- package/docs/research/archive/paper-multiagent-tutor.tex +978 -0
- package/docs/research/paper-draft/full-paper.md +136 -0
- package/docs/research/paper-draft/images/pasted-image-2026-01-24T03-47-47-846Z-d76a7ae2.png +0 -0
- package/docs/research/paper-draft/references.bib +515 -0
- package/docs/research/transcript-baseline.md +139 -0
- package/docs/research/transcript-recognition-multiagent.md +187 -0
- package/hooks/useEvalData.ts +625 -0
- package/index.js +27 -0
- package/package.json +73 -0
- package/routes/evalRoutes.js +3002 -0
- package/scripts/advanced-eval-analysis.js +351 -0
- package/scripts/analyze-eval-costs.js +378 -0
- package/scripts/analyze-eval-results.js +513 -0
- package/scripts/analyze-interaction-evals.js +368 -0
- package/server-init.js +45 -0
- package/server.js +162 -0
- package/services/benchmarkService.js +1892 -0
- package/services/evaluationRunner.js +739 -0
- package/services/evaluationStore.js +1121 -0
- package/services/learnerConfigLoader.js +385 -0
- package/services/learnerTutorInteractionEngine.js +857 -0
- package/services/memory/learnerMemoryService.js +1227 -0
- package/services/memory/learnerWritingPad.js +577 -0
- package/services/memory/tutorWritingPad.js +674 -0
- package/services/promptRecommendationService.js +493 -0
- 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;
|