@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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScoreRadial Component
|
|
3
|
+
*
|
|
4
|
+
* A premium circular gauge displaying the overall evaluation score with pass/fail indication.
|
|
5
|
+
* Features gradient strokes, glow effects, and smooth animations.
|
|
6
|
+
* Mobile-optimized with clear visual feedback.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useEffect, useState } from 'react';
|
|
10
|
+
|
|
11
|
+
interface ScoreRadialProps {
|
|
12
|
+
score: number | null;
|
|
13
|
+
passed: boolean;
|
|
14
|
+
size?: number;
|
|
15
|
+
animate?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const ScoreRadial: React.FC<ScoreRadialProps> = ({
|
|
19
|
+
score,
|
|
20
|
+
passed,
|
|
21
|
+
size = 120,
|
|
22
|
+
animate = true
|
|
23
|
+
}) => {
|
|
24
|
+
const [animatedScore, setAnimatedScore] = useState(animate ? 0 : (score ?? 0));
|
|
25
|
+
const percentage = score !== null ? animatedScore : 0;
|
|
26
|
+
const radius = (size - 16) / 2; // Account for stroke width
|
|
27
|
+
const circumference = 2 * Math.PI * radius;
|
|
28
|
+
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
|
29
|
+
|
|
30
|
+
// Unique ID for gradient definitions
|
|
31
|
+
const gradientId = `score-gradient-${passed ? 'pass' : 'fail'}`;
|
|
32
|
+
const glowId = `score-glow-${passed ? 'pass' : 'fail'}`;
|
|
33
|
+
|
|
34
|
+
// Animate score on mount/change
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!animate || score === null) {
|
|
37
|
+
setAnimatedScore(score ?? 0);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const duration = 1000;
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
const startValue = animatedScore;
|
|
44
|
+
const endValue = score;
|
|
45
|
+
|
|
46
|
+
const animateValue = () => {
|
|
47
|
+
const elapsed = Date.now() - startTime;
|
|
48
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
49
|
+
// Ease out cubic
|
|
50
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
51
|
+
const current = startValue + (endValue - startValue) * eased;
|
|
52
|
+
|
|
53
|
+
setAnimatedScore(current);
|
|
54
|
+
|
|
55
|
+
if (progress < 1) {
|
|
56
|
+
requestAnimationFrame(animateValue);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
requestAnimationFrame(animateValue);
|
|
61
|
+
}, [score, animate]);
|
|
62
|
+
|
|
63
|
+
// Get quality label based on score
|
|
64
|
+
const getQualityLabel = (s: number): string => {
|
|
65
|
+
if (s >= 80) return 'Excellent';
|
|
66
|
+
if (s >= 60) return 'Good';
|
|
67
|
+
if (s >= 40) return 'Fair';
|
|
68
|
+
return 'Needs Work';
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className="relative flex items-center justify-center"
|
|
74
|
+
style={{ width: size, height: size }}
|
|
75
|
+
>
|
|
76
|
+
{/* Outer glow effect */}
|
|
77
|
+
{score !== null && (
|
|
78
|
+
<div
|
|
79
|
+
className={`absolute inset-0 rounded-full transition-opacity duration-1000 ${
|
|
80
|
+
passed ? 'bg-green-500/10' : 'bg-red-500/10'
|
|
81
|
+
} ${percentage > 0 ? 'opacity-100' : 'opacity-0'}`}
|
|
82
|
+
style={{
|
|
83
|
+
boxShadow: passed
|
|
84
|
+
? '0 0 40px rgba(34, 197, 94, 0.2), inset 0 0 20px rgba(34, 197, 94, 0.05)'
|
|
85
|
+
: '0 0 40px rgba(230, 57, 70, 0.2), inset 0 0 20px rgba(230, 57, 70, 0.05)'
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<svg className="transform -rotate-90" width={size} height={size}>
|
|
91
|
+
{/* Gradient definitions */}
|
|
92
|
+
<defs>
|
|
93
|
+
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="100%">
|
|
94
|
+
{passed ? (
|
|
95
|
+
<>
|
|
96
|
+
<stop offset="0%" stopColor="#22c55e" />
|
|
97
|
+
<stop offset="50%" stopColor="#4ade80" />
|
|
98
|
+
<stop offset="100%" stopColor="#22c55e" />
|
|
99
|
+
</>
|
|
100
|
+
) : (
|
|
101
|
+
<>
|
|
102
|
+
<stop offset="0%" stopColor="#E63946" />
|
|
103
|
+
<stop offset="50%" stopColor="#f87171" />
|
|
104
|
+
<stop offset="100%" stopColor="#c1121f" />
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
</linearGradient>
|
|
108
|
+
<filter id={glowId} x="-50%" y="-50%" width="200%" height="200%">
|
|
109
|
+
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
|
|
110
|
+
<feMerge>
|
|
111
|
+
<feMergeNode in="coloredBlur" />
|
|
112
|
+
<feMergeNode in="SourceGraphic" />
|
|
113
|
+
</feMerge>
|
|
114
|
+
</filter>
|
|
115
|
+
</defs>
|
|
116
|
+
|
|
117
|
+
{/* Background track - glass effect */}
|
|
118
|
+
<circle
|
|
119
|
+
cx={size / 2}
|
|
120
|
+
cy={size / 2}
|
|
121
|
+
r={radius}
|
|
122
|
+
stroke="rgba(31, 41, 55, 0.8)"
|
|
123
|
+
strokeWidth="10"
|
|
124
|
+
fill="none"
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
{/* Inner subtle ring */}
|
|
128
|
+
<circle
|
|
129
|
+
cx={size / 2}
|
|
130
|
+
cy={size / 2}
|
|
131
|
+
r={radius - 6}
|
|
132
|
+
stroke="rgba(255, 255, 255, 0.03)"
|
|
133
|
+
strokeWidth="1"
|
|
134
|
+
fill="none"
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
{/* Progress arc with gradient */}
|
|
138
|
+
{score !== null && (
|
|
139
|
+
<circle
|
|
140
|
+
cx={size / 2}
|
|
141
|
+
cy={size / 2}
|
|
142
|
+
r={radius}
|
|
143
|
+
stroke={`url(#${gradientId})`}
|
|
144
|
+
strokeWidth="10"
|
|
145
|
+
fill="none"
|
|
146
|
+
strokeLinecap="round"
|
|
147
|
+
strokeDasharray={circumference}
|
|
148
|
+
strokeDashoffset={strokeDashoffset}
|
|
149
|
+
filter={percentage >= 60 ? `url(#${glowId})` : undefined}
|
|
150
|
+
className="transition-all duration-100"
|
|
151
|
+
/>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Decorative end cap glow */}
|
|
155
|
+
{score !== null && percentage > 5 && (
|
|
156
|
+
<circle
|
|
157
|
+
cx={size / 2 + radius * Math.cos((2 * Math.PI * percentage) / 100 - Math.PI / 2)}
|
|
158
|
+
cy={size / 2 + radius * Math.sin((2 * Math.PI * percentage) / 100 - Math.PI / 2)}
|
|
159
|
+
r="5"
|
|
160
|
+
fill={passed ? '#4ade80' : '#f87171'}
|
|
161
|
+
opacity="0.6"
|
|
162
|
+
className="animate-pulse"
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
</svg>
|
|
166
|
+
|
|
167
|
+
{/* Center content */}
|
|
168
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
169
|
+
{score !== null ? (
|
|
170
|
+
<>
|
|
171
|
+
{/* Score value */}
|
|
172
|
+
<div className="flex items-baseline gap-0.5">
|
|
173
|
+
<span className="text-4xl font-bold text-white tabular-nums">
|
|
174
|
+
{Math.round(animatedScore)}
|
|
175
|
+
</span>
|
|
176
|
+
<span className="text-lg text-gray-500 font-medium">%</span>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Pass/Fail badge */}
|
|
180
|
+
<div
|
|
181
|
+
className={`mt-1 px-3 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider
|
|
182
|
+
${passed
|
|
183
|
+
? 'bg-green-500/20 text-green-400 border border-green-500/30'
|
|
184
|
+
: 'bg-red-500/20 text-red-400 border border-red-500/30'
|
|
185
|
+
}`}
|
|
186
|
+
>
|
|
187
|
+
{passed ? 'Pass' : 'Fail'}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* Quality label */}
|
|
191
|
+
{size >= 140 && (
|
|
192
|
+
<span className="mt-2 text-[10px] text-gray-500 font-medium">
|
|
193
|
+
{getQualityLabel(score)}
|
|
194
|
+
</span>
|
|
195
|
+
)}
|
|
196
|
+
</>
|
|
197
|
+
) : (
|
|
198
|
+
<div className="flex flex-col items-center gap-1">
|
|
199
|
+
<svg className="w-8 h-8 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
200
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
|
|
201
|
+
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" />
|
|
202
|
+
</svg>
|
|
203
|
+
<span className="text-gray-500 text-xs font-medium">No score</span>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export default ScoreRadial;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamingLogPanel Component
|
|
3
|
+
*
|
|
4
|
+
* Displays real-time streaming logs during test execution.
|
|
5
|
+
* Premium glass morphism styling with visual log type indicators.
|
|
6
|
+
* Expandable panel that auto-scrolls to latest content.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useRef, useEffect, useState } from 'react';
|
|
10
|
+
import type { StreamLog } from '../../hooks/useEvalData';
|
|
11
|
+
import haptics from '../../utils/haptics';
|
|
12
|
+
|
|
13
|
+
interface StreamingLogPanelProps {
|
|
14
|
+
logs: StreamLog[];
|
|
15
|
+
isRunning: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Log type configurations with styling
|
|
19
|
+
interface LogTypeConfig {
|
|
20
|
+
color: string;
|
|
21
|
+
bgColor: string;
|
|
22
|
+
icon: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const LogTypeIcons: Record<StreamLog['type'] | 'info', LogTypeConfig> = {
|
|
26
|
+
success: {
|
|
27
|
+
color: 'text-green-400',
|
|
28
|
+
bgColor: 'bg-green-500/10',
|
|
29
|
+
icon: (
|
|
30
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
31
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
warning: {
|
|
36
|
+
color: 'text-yellow-400',
|
|
37
|
+
bgColor: 'bg-yellow-500/10',
|
|
38
|
+
icon: (
|
|
39
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
40
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
41
|
+
</svg>
|
|
42
|
+
)
|
|
43
|
+
},
|
|
44
|
+
error: {
|
|
45
|
+
color: 'text-red-400',
|
|
46
|
+
bgColor: 'bg-red-500/10',
|
|
47
|
+
icon: (
|
|
48
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
|
|
49
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
50
|
+
</svg>
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
progress: {
|
|
54
|
+
color: 'text-blue-400',
|
|
55
|
+
bgColor: 'bg-blue-500/10',
|
|
56
|
+
icon: (
|
|
57
|
+
<svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
58
|
+
<path strokeLinecap="round" strokeLinejoin="round" 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" />
|
|
59
|
+
</svg>
|
|
60
|
+
)
|
|
61
|
+
},
|
|
62
|
+
info: {
|
|
63
|
+
color: 'text-gray-400',
|
|
64
|
+
bgColor: 'bg-gray-500/10',
|
|
65
|
+
icon: (
|
|
66
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
67
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
68
|
+
</svg>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const StreamingLogPanel: React.FC<StreamingLogPanelProps> = ({
|
|
74
|
+
logs,
|
|
75
|
+
isRunning
|
|
76
|
+
}) => {
|
|
77
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
78
|
+
const bottomRef = useRef<HTMLDivElement>(null);
|
|
79
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
80
|
+
|
|
81
|
+
// Auto-scroll to bottom when new logs arrive
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (bottomRef.current && containerRef.current) {
|
|
84
|
+
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
|
85
|
+
}
|
|
86
|
+
}, [logs]);
|
|
87
|
+
|
|
88
|
+
// Auto-expand when running starts
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (isRunning) {
|
|
91
|
+
setIsExpanded(true);
|
|
92
|
+
}
|
|
93
|
+
}, [isRunning]);
|
|
94
|
+
|
|
95
|
+
const getLogConfig = (type: StreamLog['type']): LogTypeConfig => {
|
|
96
|
+
return LogTypeIcons[type] || LogTypeIcons.info;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Count log types for summary
|
|
100
|
+
const logCounts = logs.reduce((acc, log) => {
|
|
101
|
+
acc[log.type] = (acc[log.type] || 0) + 1;
|
|
102
|
+
return acc;
|
|
103
|
+
}, {} as Record<string, number>);
|
|
104
|
+
|
|
105
|
+
if (logs.length === 0 && !isRunning) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="border-t border-white/5 bg-gray-900/30 backdrop-blur-sm">
|
|
111
|
+
{/* Toggle header - Glass styling */}
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={() => {
|
|
115
|
+
haptics.light();
|
|
116
|
+
setIsExpanded(!isExpanded);
|
|
117
|
+
}}
|
|
118
|
+
className="w-full flex items-center justify-between p-3.5
|
|
119
|
+
hover:bg-white/5 active:scale-[0.995] transition-all duration-150"
|
|
120
|
+
>
|
|
121
|
+
<div className="flex items-center gap-3">
|
|
122
|
+
{/* Live indicator */}
|
|
123
|
+
{isRunning && (
|
|
124
|
+
<div className="relative">
|
|
125
|
+
<span className="absolute inset-0 w-2.5 h-2.5 bg-green-500 rounded-full animate-ping opacity-75" />
|
|
126
|
+
<span className="relative w-2.5 h-2.5 bg-green-500 rounded-full block" />
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Title */}
|
|
131
|
+
<span className="text-sm font-medium text-gray-300">
|
|
132
|
+
{isRunning ? 'Live Output' : 'Output'}
|
|
133
|
+
</span>
|
|
134
|
+
|
|
135
|
+
{/* Log type counts - Mini badges */}
|
|
136
|
+
<div className="flex items-center gap-1.5">
|
|
137
|
+
{logCounts.error && logCounts.error > 0 && (
|
|
138
|
+
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-red-500/20 text-red-400 border border-red-500/30">
|
|
139
|
+
{logCounts.error} error{logCounts.error > 1 ? 's' : ''}
|
|
140
|
+
</span>
|
|
141
|
+
)}
|
|
142
|
+
{logCounts.warning && logCounts.warning > 0 && (
|
|
143
|
+
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-500/30">
|
|
144
|
+
{logCounts.warning} warn
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-white/5 text-gray-500 border border-white/5">
|
|
148
|
+
{logs.length} total
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* Expand indicator */}
|
|
154
|
+
<div className={`w-7 h-7 rounded-full bg-white/5 flex items-center justify-center
|
|
155
|
+
transition-all duration-200 ${isExpanded ? 'bg-white/10' : ''}`}>
|
|
156
|
+
<svg
|
|
157
|
+
className={`w-4 h-4 text-gray-500 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
|
|
158
|
+
fill="none"
|
|
159
|
+
viewBox="0 0 24 24"
|
|
160
|
+
stroke="currentColor"
|
|
161
|
+
>
|
|
162
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
|
163
|
+
</svg>
|
|
164
|
+
</div>
|
|
165
|
+
</button>
|
|
166
|
+
|
|
167
|
+
{/* Log content - Terminal style with glass */}
|
|
168
|
+
<div
|
|
169
|
+
ref={containerRef}
|
|
170
|
+
className={`overflow-hidden transition-all duration-300 ease-out
|
|
171
|
+
${isExpanded ? 'max-h-80' : 'max-h-0'}`}
|
|
172
|
+
>
|
|
173
|
+
<div className="h-full overflow-y-auto bg-black/40 backdrop-blur-sm p-3 font-mono text-xs leading-relaxed
|
|
174
|
+
border-t border-white/5 scrollbar-hide">
|
|
175
|
+
|
|
176
|
+
{/* Log entries */}
|
|
177
|
+
{logs.map((log, i) => {
|
|
178
|
+
const config = getLogConfig(log.type);
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
key={i}
|
|
182
|
+
className={`flex items-start gap-2 py-1 px-2 -mx-2 rounded
|
|
183
|
+
${config.bgColor} ${config.color}
|
|
184
|
+
animate-fade-in`}
|
|
185
|
+
style={{ animationDelay: `${Math.min(i * 20, 200)}ms` }}
|
|
186
|
+
>
|
|
187
|
+
{/* Icon */}
|
|
188
|
+
<span className="flex-shrink-0 mt-0.5 w-4 flex items-center justify-center">
|
|
189
|
+
{config.icon}
|
|
190
|
+
</span>
|
|
191
|
+
|
|
192
|
+
{/* Message */}
|
|
193
|
+
<span className="whitespace-pre-wrap break-words flex-1">
|
|
194
|
+
{log.message}
|
|
195
|
+
</span>
|
|
196
|
+
|
|
197
|
+
{/* Timestamp for progress logs */}
|
|
198
|
+
{log.type === 'progress' && (
|
|
199
|
+
<span className="text-[10px] text-gray-600 flex-shrink-0 tabular-nums">
|
|
200
|
+
{new Date().toLocaleTimeString(undefined, {
|
|
201
|
+
hour: '2-digit',
|
|
202
|
+
minute: '2-digit',
|
|
203
|
+
second: '2-digit'
|
|
204
|
+
})}
|
|
205
|
+
</span>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
})}
|
|
210
|
+
|
|
211
|
+
{/* Typing indicator while running */}
|
|
212
|
+
{isRunning && (
|
|
213
|
+
<div className="flex items-center gap-1.5 mt-3 pt-2 border-t border-white/5">
|
|
214
|
+
<div className="flex items-center gap-1">
|
|
215
|
+
<span className="w-1.5 h-1.5 rounded-full bg-brand-red animate-pulse" />
|
|
216
|
+
<span className="w-1.5 h-1.5 rounded-full bg-brand-red animate-pulse" style={{ animationDelay: '0.2s' }} />
|
|
217
|
+
<span className="w-1.5 h-1.5 rounded-full bg-brand-red animate-pulse" style={{ animationDelay: '0.4s' }} />
|
|
218
|
+
</div>
|
|
219
|
+
<span className="text-[10px] text-gray-600 ml-1">Processing...</span>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
<div ref={bottomRef} />
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export default StreamingLogPanel;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynthesisStrategyChart Component
|
|
3
|
+
*
|
|
4
|
+
* Horizontal bars showing distribution of synthesis strategies:
|
|
5
|
+
* - Ghost Dominates (red) - Superego/authority wins
|
|
6
|
+
* - Learner Dominates (blue) - Learner needs prioritized
|
|
7
|
+
* - Dialectical Synthesis (gold/green) - True mutual recognition
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
|
|
12
|
+
interface SynthesisStrategyCounts {
|
|
13
|
+
ghost_dominates: number;
|
|
14
|
+
learner_dominates: number;
|
|
15
|
+
dialectical_synthesis: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SynthesisStrategyChartProps {
|
|
19
|
+
counts: SynthesisStrategyCounts;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const SynthesisStrategyChart: React.FC<SynthesisStrategyChartProps> = ({
|
|
23
|
+
counts,
|
|
24
|
+
}) => {
|
|
25
|
+
const total =
|
|
26
|
+
counts.ghost_dominates + counts.learner_dominates + counts.dialectical_synthesis;
|
|
27
|
+
|
|
28
|
+
if (total === 0) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
|
|
31
|
+
<div className="text-xs text-gray-400 mb-3">Synthesis Strategies</div>
|
|
32
|
+
<div className="text-sm text-gray-500 text-center py-4">
|
|
33
|
+
No synthesis data recorded
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const strategies = [
|
|
40
|
+
{
|
|
41
|
+
key: 'dialectical_synthesis',
|
|
42
|
+
label: 'Dialectical Synthesis',
|
|
43
|
+
count: counts.dialectical_synthesis,
|
|
44
|
+
percentage: (counts.dialectical_synthesis / total) * 100,
|
|
45
|
+
gradient: 'from-yellow-500 to-green-500',
|
|
46
|
+
bgColor: 'bg-yellow-500/20',
|
|
47
|
+
borderColor: 'border-yellow-500/30',
|
|
48
|
+
textColor: 'text-yellow-400',
|
|
49
|
+
icon: '⚡',
|
|
50
|
+
description: 'Mutual recognition achieved',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: 'learner_dominates',
|
|
54
|
+
label: 'Learner Dominates',
|
|
55
|
+
count: counts.learner_dominates,
|
|
56
|
+
percentage: (counts.learner_dominates / total) * 100,
|
|
57
|
+
gradient: 'from-blue-500 to-blue-400',
|
|
58
|
+
bgColor: 'bg-blue-500/20',
|
|
59
|
+
borderColor: 'border-blue-500/30',
|
|
60
|
+
textColor: 'text-blue-400',
|
|
61
|
+
icon: '🎯',
|
|
62
|
+
description: 'Learner needs prioritized',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'ghost_dominates',
|
|
66
|
+
label: 'Ghost Dominates',
|
|
67
|
+
count: counts.ghost_dominates,
|
|
68
|
+
percentage: (counts.ghost_dominates / total) * 100,
|
|
69
|
+
gradient: 'from-red-500 to-red-400',
|
|
70
|
+
bgColor: 'bg-red-500/20',
|
|
71
|
+
borderColor: 'border-red-500/30',
|
|
72
|
+
textColor: 'text-red-400',
|
|
73
|
+
icon: '👻',
|
|
74
|
+
description: 'Authority/superego wins',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Find max for scaling
|
|
79
|
+
const maxCount = Math.max(
|
|
80
|
+
counts.ghost_dominates,
|
|
81
|
+
counts.learner_dominates,
|
|
82
|
+
counts.dialectical_synthesis
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
|
|
87
|
+
<div className="flex items-center justify-between mb-4">
|
|
88
|
+
<div className="text-xs text-gray-400">Synthesis Strategies</div>
|
|
89
|
+
<div className="text-xs text-gray-500">{total} total</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div className="space-y-3">
|
|
93
|
+
{strategies.map((strategy) => (
|
|
94
|
+
<div key={strategy.key} className="space-y-1.5">
|
|
95
|
+
{/* Label row */}
|
|
96
|
+
<div className="flex items-center justify-between">
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
<span className="text-sm">{strategy.icon}</span>
|
|
99
|
+
<span className="text-xs text-gray-300">{strategy.label}</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="flex items-center gap-2">
|
|
102
|
+
<span className={`text-xs ${strategy.textColor} font-medium`}>
|
|
103
|
+
{strategy.count}
|
|
104
|
+
</span>
|
|
105
|
+
<span className="text-xs text-gray-500">
|
|
106
|
+
({strategy.percentage.toFixed(0)}%)
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Progress bar */}
|
|
112
|
+
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
|
113
|
+
<div
|
|
114
|
+
className={`h-full bg-gradient-to-r ${strategy.gradient} rounded-full transition-all duration-500`}
|
|
115
|
+
style={{ width: `${maxCount > 0 ? (strategy.count / maxCount) * 100 : 0}%` }}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Description */}
|
|
120
|
+
<div className="text-[10px] text-gray-600">{strategy.description}</div>
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
{/* Ideal indicator */}
|
|
126
|
+
{counts.dialectical_synthesis > 0 && (
|
|
127
|
+
<div className="mt-4 pt-3 border-t border-white/5">
|
|
128
|
+
<div className="flex items-center gap-2 text-xs">
|
|
129
|
+
<span className="text-yellow-400">
|
|
130
|
+
{((counts.dialectical_synthesis / total) * 100).toFixed(0)}%
|
|
131
|
+
</span>
|
|
132
|
+
<span className="text-gray-500">of moments achieve dialectical synthesis</span>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default SynthesisStrategyChart;
|