@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,267 @@
1
+ /**
2
+ * Mobile Evaluation Dashboard
3
+ *
4
+ * A mobile-first evaluation dashboard for testing and reviewing AI tutor performance.
5
+ * Features bottom tab navigation, touch-optimized interactions, and offline support.
6
+ *
7
+ * Tabs:
8
+ * - Test: Run quick tests with streaming output
9
+ * - History: Browse past evaluation runs
10
+ * - Logs: View dialogue transcripts
11
+ * - Docs: Read evaluation documentation
12
+ */
13
+
14
+ import React, { useState, useCallback, useRef } from 'react';
15
+ import { useEvalData } from '../hooks/useEvalData';
16
+ import haptics from '../utils/haptics';
17
+ import { QuickTestView } from './mobile/QuickTestView';
18
+ import { RunHistoryView } from './mobile/RunHistoryView';
19
+ import { LogsView } from './mobile/LogsView';
20
+ import { DocsView } from './mobile/DocsView';
21
+ import { BottomSheet } from './mobile/BottomSheet';
22
+ import { RunDetailView } from './mobile/RunDetailView';
23
+ import type { RunDetails } from '../hooks/useEvalData';
24
+
25
+ // Tab configuration
26
+ type ViewMode = 'test' | 'history' | 'logs' | 'docs';
27
+
28
+ interface TabConfig {
29
+ id: ViewMode;
30
+ label: string;
31
+ icon: React.ReactNode;
32
+ }
33
+
34
+ // Icons as inline SVG for bundle efficiency
35
+ const PlayIcon = () => (
36
+ <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
37
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
38
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
39
+ </svg>
40
+ );
41
+
42
+ const ClockIcon = () => (
43
+ <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
44
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
45
+ </svg>
46
+ );
47
+
48
+ const FileTextIcon = () => (
49
+ <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
50
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
51
+ </svg>
52
+ );
53
+
54
+ const BookIcon = () => (
55
+ <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
56
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
57
+ </svg>
58
+ );
59
+
60
+ const tabs: TabConfig[] = [
61
+ { id: 'test', label: 'Test', icon: <PlayIcon /> },
62
+ { id: 'history', label: 'History', icon: <ClockIcon /> },
63
+ { id: 'logs', label: 'Logs', icon: <FileTextIcon /> },
64
+ { id: 'docs', label: 'Docs', icon: <BookIcon /> }
65
+ ];
66
+
67
+ export const MobileEvalDashboard: React.FC = () => {
68
+ // View state
69
+ const [activeTab, setActiveTab] = useState<ViewMode>('test');
70
+
71
+ // Bottom sheet state
72
+ const [showRunDetail, setShowRunDetail] = useState(false);
73
+ const [selectedRunDetails, setSelectedRunDetails] = useState<RunDetails | null>(null);
74
+
75
+ // Swipe navigation state
76
+ const touchStartX = useRef<number | null>(null);
77
+ const touchEndX = useRef<number | null>(null);
78
+ const MIN_SWIPE_DISTANCE = 50;
79
+
80
+ // Data hook
81
+ const evalData = useEvalData();
82
+
83
+ // Tab switching with haptic feedback
84
+ const handleTabChange = useCallback((tab: ViewMode) => {
85
+ if (tab !== activeTab) {
86
+ haptics.light();
87
+ setActiveTab(tab);
88
+ }
89
+ }, [activeTab]);
90
+
91
+ // Swipe navigation between tabs
92
+ const handleTouchStart = useCallback((e: React.TouchEvent) => {
93
+ touchEndX.current = null;
94
+ touchStartX.current = e.touches[0].clientX;
95
+ }, []);
96
+
97
+ const handleTouchMove = useCallback((e: React.TouchEvent) => {
98
+ touchEndX.current = e.touches[0].clientX;
99
+ }, []);
100
+
101
+ const handleTouchEnd = useCallback(() => {
102
+ if (!touchStartX.current || !touchEndX.current) return;
103
+
104
+ const distance = touchStartX.current - touchEndX.current;
105
+ const tabOrder: ViewMode[] = ['test', 'history', 'logs', 'docs'];
106
+ const currentIndex = tabOrder.indexOf(activeTab);
107
+
108
+ if (Math.abs(distance) > MIN_SWIPE_DISTANCE) {
109
+ if (distance > 0 && currentIndex < tabOrder.length - 1) {
110
+ // Swipe left - go to next tab
111
+ haptics.light();
112
+ setActiveTab(tabOrder[currentIndex + 1]);
113
+ } else if (distance < 0 && currentIndex > 0) {
114
+ // Swipe right - go to previous tab
115
+ haptics.light();
116
+ setActiveTab(tabOrder[currentIndex - 1]);
117
+ }
118
+ }
119
+
120
+ touchStartX.current = null;
121
+ touchEndX.current = null;
122
+ }, [activeTab]);
123
+
124
+ // Handle run selection from history
125
+ const handleSelectRun = useCallback(async (runId: string) => {
126
+ haptics.light();
127
+ const details = await evalData.loadRunDetails(runId);
128
+ if (details) {
129
+ setSelectedRunDetails(details);
130
+ setShowRunDetail(true);
131
+ }
132
+ }, [evalData]);
133
+
134
+ // Handle navigation to logs from run detail
135
+ const handleViewDialogue = useCallback((logDate: string) => {
136
+ setShowRunDetail(false);
137
+ setActiveTab('logs');
138
+ // The LogsView will handle loading the specific date
139
+ }, []);
140
+
141
+ return (
142
+ <div
143
+ className="mobile-eval-dashboard flex flex-col h-screen bg-[#0a0a0a] text-white overflow-hidden"
144
+ style={{ paddingTop: 'env(safe-area-inset-top)' }}
145
+ >
146
+ {/* Header */}
147
+ <header className="flex-shrink-0 px-4 py-3 border-b border-gray-800">
148
+ <h1 className="text-lg font-semibold">Evaluation</h1>
149
+ {evalData.error && (
150
+ <div className="mt-2 px-3 py-2 bg-red-900/30 border border-red-800 rounded-lg text-sm text-red-400 flex items-center justify-between">
151
+ <span>{evalData.error}</span>
152
+ <button
153
+ type="button"
154
+ onClick={evalData.clearError}
155
+ className="ml-2 text-red-400 hover:text-red-300"
156
+ >
157
+ &times;
158
+ </button>
159
+ </div>
160
+ )}
161
+ </header>
162
+
163
+ {/* Main content area with swipe support */}
164
+ <main
165
+ className="flex-1 overflow-hidden"
166
+ onTouchStart={handleTouchStart}
167
+ onTouchMove={handleTouchMove}
168
+ onTouchEnd={handleTouchEnd}
169
+ >
170
+ {activeTab === 'test' && (
171
+ <QuickTestView
172
+ profiles={evalData.profiles}
173
+ scenarios={evalData.scenarios}
174
+ onRunTest={evalData.runQuickTest}
175
+ onRunMatrix={evalData.runMatrixTest}
176
+ isRunning={evalData.isTestRunning}
177
+ isMatrixRunning={evalData.isMatrixRunning}
178
+ isLoadingData={evalData.isInitialLoading}
179
+ result={evalData.testResult}
180
+ matrixResult={evalData.matrixResult}
181
+ streamLogs={evalData.streamLogs}
182
+ onClearResult={evalData.clearTestResult}
183
+ onClearMatrixResult={evalData.clearMatrixResult}
184
+ />
185
+ )}
186
+
187
+ {activeTab === 'history' && (
188
+ <RunHistoryView
189
+ runs={evalData.runs}
190
+ isLoading={evalData.isLoading}
191
+ onSelectRun={handleSelectRun}
192
+ onRefresh={evalData.loadRuns}
193
+ />
194
+ )}
195
+
196
+ {activeTab === 'logs' && (
197
+ <LogsView
198
+ logDates={evalData.logDates}
199
+ isLoading={evalData.isLoading}
200
+ onLoadDates={evalData.loadLogDates}
201
+ onLoadDialogues={evalData.loadDialogues}
202
+ onLoadDialogueById={evalData.loadDialogueById}
203
+ />
204
+ )}
205
+
206
+ {activeTab === 'docs' && (
207
+ <DocsView
208
+ docs={evalData.docs}
209
+ isLoading={evalData.isLoading}
210
+ onLoadDocs={evalData.loadDocs}
211
+ onLoadDocContent={evalData.loadDocContent}
212
+ />
213
+ )}
214
+ </main>
215
+
216
+ {/* Bottom Tab Bar - Glass morphism */}
217
+ <nav
218
+ className="flex-shrink-0 bg-gray-900/80 backdrop-blur-xl border-t border-white/5"
219
+ style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
220
+ >
221
+ <div className="flex justify-around items-center h-14" role="tablist">
222
+ {tabs.map((tab) => (
223
+ <button
224
+ key={tab.id}
225
+ type="button"
226
+ role="tab"
227
+ aria-selected={activeTab === tab.id}
228
+ onClick={() => handleTabChange(tab.id)}
229
+ className={`relative flex flex-col items-center justify-center w-full h-full transition-all duration-200
230
+ ${activeTab === tab.id ? 'text-[#E63946]' : 'text-gray-500 hover:text-gray-300'}`}
231
+ style={{ minHeight: '48px' }}
232
+ >
233
+ {/* Active indicator glow */}
234
+ {activeTab === tab.id && (
235
+ <div className="absolute top-0 left-1/2 -translate-x-1/2 w-8 h-0.5 bg-gradient-to-r from-transparent via-[#E63946] to-transparent" />
236
+ )}
237
+ <div className={`transition-transform duration-200 ${activeTab === tab.id ? 'scale-110' : ''}`}>
238
+ {tab.icon}
239
+ </div>
240
+ <span className={`text-[10px] mt-1 font-medium transition-all duration-200
241
+ ${activeTab === tab.id ? 'text-[#E63946]' : ''}`}>
242
+ {tab.label}
243
+ </span>
244
+ </button>
245
+ ))}
246
+ </div>
247
+ </nav>
248
+
249
+ {/* Run Detail Bottom Sheet */}
250
+ <BottomSheet
251
+ isOpen={showRunDetail}
252
+ onClose={() => setShowRunDetail(false)}
253
+ title="Run Details"
254
+ >
255
+ {selectedRunDetails && (
256
+ <RunDetailView
257
+ details={selectedRunDetails}
258
+ onViewDialogue={handleViewDialogue}
259
+ onClose={() => setShowRunDetail(false)}
260
+ />
261
+ )}
262
+ </BottomSheet>
263
+ </div>
264
+ );
265
+ };
266
+
267
+ export default MobileEvalDashboard;
@@ -0,0 +1,137 @@
1
+ /**
2
+ * DeltaAnalysisTable Component
3
+ *
4
+ * Dimension-by-dimension comparison table showing:
5
+ * - Baseline score
6
+ * - Recognition score
7
+ * - Delta (difference)
8
+ * - Winner badge per dimension
9
+ */
10
+
11
+ import React from 'react';
12
+ import { WinnerIndicator } from './WinnerIndicator';
13
+
14
+ interface DeltaEntry {
15
+ dimension: string;
16
+ baseline: number | null;
17
+ recognition: number | null;
18
+ delta: number;
19
+ deltaPercent: number;
20
+ significance: '' | '*' | '**';
21
+ winner: 'baseline' | 'recognition' | null;
22
+ }
23
+
24
+ interface DeltaAnalysisTableProps {
25
+ deltaAnalysis: DeltaEntry[];
26
+ }
27
+
28
+ const dimensionLabels: Record<string, string> = {
29
+ relevance: 'Relevance',
30
+ specificity: 'Specificity',
31
+ pedagogical: 'Pedagogical',
32
+ personalization: 'Personalization',
33
+ actionability: 'Actionability',
34
+ tone: 'Tone',
35
+ };
36
+
37
+ export const DeltaAnalysisTable: React.FC<DeltaAnalysisTableProps> = ({
38
+ deltaAnalysis,
39
+ }) => {
40
+ if (!deltaAnalysis || deltaAnalysis.length === 0) {
41
+ return (
42
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
43
+ <div className="text-xs text-gray-400 mb-3">Delta Analysis</div>
44
+ <div className="text-sm text-gray-500 text-center py-4">
45
+ No comparison data available
46
+ </div>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
53
+ <div className="flex items-center justify-between mb-4">
54
+ <div className="text-xs text-gray-400">Delta Analysis</div>
55
+ <div className="flex items-center gap-3 text-[10px] text-gray-500">
56
+ <span>
57
+ <span className="text-yellow-400 font-bold">*</span> &gt;5% improvement
58
+ </span>
59
+ <span>
60
+ <span className="text-yellow-400 font-bold">**</span> &gt;10% improvement
61
+ </span>
62
+ </div>
63
+ </div>
64
+
65
+ <div className="overflow-x-auto">
66
+ <table className="w-full text-xs">
67
+ <thead>
68
+ <tr className="text-gray-500 border-b border-white/5">
69
+ <th className="text-left py-2 pr-4">Dimension</th>
70
+ <th className="text-right py-2 px-2">Baseline</th>
71
+ <th className="text-right py-2 px-2">Recognition</th>
72
+ <th className="text-right py-2 px-2">Delta</th>
73
+ <th className="text-center py-2 pl-4">Winner</th>
74
+ </tr>
75
+ </thead>
76
+ <tbody>
77
+ {deltaAnalysis.map((entry) => {
78
+ const deltaColor =
79
+ entry.delta > 0
80
+ ? 'text-green-400'
81
+ : entry.delta < 0
82
+ ? 'text-red-400'
83
+ : 'text-gray-400';
84
+
85
+ return (
86
+ <tr
87
+ key={entry.dimension}
88
+ className="border-b border-white/5 last:border-0 hover:bg-white/5 transition-colors"
89
+ >
90
+ <td className="py-2 pr-4">
91
+ <span className="text-gray-300">
92
+ {dimensionLabels[entry.dimension] || entry.dimension}
93
+ </span>
94
+ </td>
95
+ <td className="text-right py-2 px-2 text-blue-400 font-mono">
96
+ {entry.baseline != null ? entry.baseline.toFixed(2) : '—'}
97
+ </td>
98
+ <td className="text-right py-2 px-2 text-yellow-400 font-mono">
99
+ {entry.recognition != null ? entry.recognition.toFixed(2) : '—'}
100
+ </td>
101
+ <td className={`text-right py-2 px-2 font-mono ${deltaColor}`}>
102
+ {entry.delta > 0 ? '+' : ''}
103
+ {entry.delta.toFixed(2)}
104
+ {entry.significance && (
105
+ <span className="text-yellow-300 ml-0.5">{entry.significance}</span>
106
+ )}
107
+ </td>
108
+ <td className="text-center py-2 pl-4">
109
+ <WinnerIndicator
110
+ winner={entry.winner}
111
+ significance={entry.significance}
112
+ size="sm"
113
+ showLabel={false}
114
+ />
115
+ </td>
116
+ </tr>
117
+ );
118
+ })}
119
+ </tbody>
120
+ </table>
121
+ </div>
122
+
123
+ {/* Summary row */}
124
+ <div className="mt-3 pt-3 border-t border-white/5 flex items-center justify-between">
125
+ <div className="text-[10px] text-gray-500">
126
+ {deltaAnalysis.filter((d) => d.winner === 'recognition').length} dimensions favor
127
+ recognition
128
+ </div>
129
+ <div className="text-[10px] text-gray-500">
130
+ {deltaAnalysis.filter((d) => d.winner === 'baseline').length} dimensions favor baseline
131
+ </div>
132
+ </div>
133
+ </div>
134
+ );
135
+ };
136
+
137
+ export default DeltaAnalysisTable;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * ProfileComparisonCard Component
3
+ *
4
+ * Single profile result display with:
5
+ * - Profile badge (baseline vs recognition)
6
+ * - Overall score with delta indicator
7
+ * - Mini radar chart of dimension scores
8
+ * - Test stats (latency, success rate)
9
+ */
10
+
11
+ import React from 'react';
12
+
13
+ interface DimensionAverages {
14
+ relevance: number | null;
15
+ specificity: number | null;
16
+ pedagogical: number | null;
17
+ personalization: number | null;
18
+ actionability: number | null;
19
+ tone: number | null;
20
+ }
21
+
22
+ interface ProfileComparisonCardProps {
23
+ profile: 'baseline' | 'recognition';
24
+ overallScore: number | null;
25
+ delta?: number | null;
26
+ dimensionAverages: DimensionAverages;
27
+ testCount: number;
28
+ successCount: number;
29
+ avgLatency: number;
30
+ isWinner?: boolean;
31
+ }
32
+
33
+ // Mini radar chart component
34
+ const MiniRadarChart: React.FC<{
35
+ scores: DimensionAverages;
36
+ color: string;
37
+ }> = ({ scores, color }) => {
38
+ const dimensions = ['relevance', 'specificity', 'pedagogical', 'personalization', 'actionability', 'tone'] as const;
39
+ const size = 80;
40
+ const center = size / 2;
41
+ const radius = 30;
42
+
43
+ // Calculate points for polygon
44
+ const points = dimensions
45
+ .map((dim, i) => {
46
+ const angle = (Math.PI * 2 * i) / dimensions.length - Math.PI / 2;
47
+ const value = (scores[dim] ?? 0) / 5; // Normalize to 0-1 (assuming 5-point scale)
48
+ const x = center + Math.cos(angle) * radius * value;
49
+ const y = center + Math.sin(angle) * radius * value;
50
+ return `${x},${y}`;
51
+ })
52
+ .join(' ');
53
+
54
+ // Background polygon (full scale)
55
+ const bgPoints = dimensions
56
+ .map((_, i) => {
57
+ const angle = (Math.PI * 2 * i) / dimensions.length - Math.PI / 2;
58
+ const x = center + Math.cos(angle) * radius;
59
+ const y = center + Math.sin(angle) * radius;
60
+ return `${x},${y}`;
61
+ })
62
+ .join(' ');
63
+
64
+ return (
65
+ <svg width={size} height={size} className="mx-auto">
66
+ {/* Background grid */}
67
+ <polygon points={bgPoints} fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="1" />
68
+ <polygon
69
+ points={bgPoints}
70
+ fill="none"
71
+ stroke="rgba(255,255,255,0.05)"
72
+ strokeWidth="1"
73
+ transform={`scale(0.66) translate(${center * 0.5}, ${center * 0.5})`}
74
+ />
75
+ <polygon
76
+ points={bgPoints}
77
+ fill="none"
78
+ stroke="rgba(255,255,255,0.05)"
79
+ strokeWidth="1"
80
+ transform={`scale(0.33) translate(${center * 2}, ${center * 2})`}
81
+ />
82
+
83
+ {/* Data polygon */}
84
+ <polygon points={points} fill={`${color}20`} stroke={color} strokeWidth="2" />
85
+
86
+ {/* Center dot */}
87
+ <circle cx={center} cy={center} r="2" fill={color} />
88
+ </svg>
89
+ );
90
+ };
91
+
92
+ export const ProfileComparisonCard: React.FC<ProfileComparisonCardProps> = ({
93
+ profile,
94
+ overallScore,
95
+ delta,
96
+ dimensionAverages,
97
+ testCount,
98
+ successCount,
99
+ avgLatency,
100
+ isWinner = false,
101
+ }) => {
102
+ const isRecognition = profile === 'recognition';
103
+
104
+ // Profile-specific styling
105
+ const profileStyles = isRecognition
106
+ ? {
107
+ border: isWinner ? 'border-yellow-500/40' : 'border-yellow-500/20',
108
+ badge: 'bg-gradient-to-r from-yellow-500/20 to-green-500/20 text-yellow-400 border-yellow-500/30',
109
+ color: '#facc15', // yellow-400
110
+ icon: '⚡',
111
+ label: 'Recognition',
112
+ }
113
+ : {
114
+ border: isWinner ? 'border-blue-500/40' : 'border-blue-500/20',
115
+ badge: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
116
+ color: '#60a5fa', // blue-400
117
+ icon: '🎯',
118
+ label: 'Baseline',
119
+ };
120
+
121
+ const successRate = testCount > 0 ? (successCount / testCount) * 100 : 0;
122
+
123
+ return (
124
+ <div
125
+ className={`bg-gray-900/60 backdrop-blur-sm border rounded-xl p-4 ${profileStyles.border} ${isWinner ? 'ring-2 ring-yellow-500/20' : ''}`}
126
+ >
127
+ {/* Header with badge */}
128
+ <div className="flex items-center justify-between mb-3">
129
+ <span
130
+ className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs border ${profileStyles.badge}`}
131
+ >
132
+ <span>{profileStyles.icon}</span>
133
+ <span className="font-medium">{profileStyles.label}</span>
134
+ </span>
135
+
136
+ {isWinner && (
137
+ <span className="text-yellow-400 text-sm">🏆</span>
138
+ )}
139
+ </div>
140
+
141
+ {/* Score and radar */}
142
+ <div className="flex items-center gap-4 mb-4">
143
+ {/* Overall score */}
144
+ <div className="text-center">
145
+ <div className="text-3xl font-bold" style={{ color: profileStyles.color }}>
146
+ {overallScore != null ? overallScore.toFixed(1) : '—'}
147
+ </div>
148
+ {delta != null && (
149
+ <div
150
+ className={`text-xs ${delta > 0 ? 'text-green-400' : delta < 0 ? 'text-red-400' : 'text-gray-400'}`}
151
+ >
152
+ {delta > 0 ? '+' : ''}
153
+ {delta.toFixed(1)}
154
+ </div>
155
+ )}
156
+ <div className="text-[10px] text-gray-500">Overall</div>
157
+ </div>
158
+
159
+ {/* Mini radar */}
160
+ <div className="flex-1">
161
+ <MiniRadarChart scores={dimensionAverages} color={profileStyles.color} />
162
+ </div>
163
+ </div>
164
+
165
+ {/* Stats row */}
166
+ <div className="flex items-center justify-between text-[10px] text-gray-500 pt-2 border-t border-white/5">
167
+ <span>
168
+ {successCount}/{testCount} tests ({successRate.toFixed(0)}%)
169
+ </span>
170
+ <span>{avgLatency.toFixed(0)}ms avg</span>
171
+ </div>
172
+ </div>
173
+ );
174
+ };
175
+
176
+ export default ProfileComparisonCard;