@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,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
|
+
×
|
|
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> >5% improvement
|
|
58
|
+
</span>
|
|
59
|
+
<span>
|
|
60
|
+
<span className="text-yellow-400 font-bold">**</span> >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;
|