@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,809 @@
1
+ /**
2
+ * RecognitionView Component
3
+ *
4
+ * Recognition metrics dashboard for the evaluation system.
5
+ * Visualizes recognition engine data: moments, memory layers, learner events.
6
+ */
7
+
8
+ import React, { useState, useEffect, useCallback } from 'react';
9
+ import haptics from '../../utils/haptics';
10
+ import { PsychodynamicQuadrant } from './PsychodynamicQuadrant';
11
+ import { RecognitionTypeChart } from './RecognitionTypeChart';
12
+ import { SynthesisStrategyChart } from './SynthesisStrategyChart';
13
+
14
+ // Sub-view type
15
+ type SubView = 'overview' | 'moments' | 'memory' | 'events' | 'quadrant';
16
+
17
+ interface LearnerOption {
18
+ id: string;
19
+ name: string;
20
+ }
21
+
22
+ interface RecognitionStats {
23
+ total_recognition_moments: number;
24
+ dialectical_depth: number;
25
+ mutual_transformation_score: number;
26
+ pedagogical_attunement: number;
27
+ // Extended stats for visualizations
28
+ recognition_types?: {
29
+ pedagogical: number;
30
+ metacognitive: number;
31
+ existential: number;
32
+ };
33
+ synthesis_strategies?: {
34
+ ghost_dominates: number;
35
+ learner_dominates: number;
36
+ dialectical_synthesis: number;
37
+ };
38
+ average_compliance?: number;
39
+ average_recognition_seeking?: number;
40
+ }
41
+
42
+ interface RecognitionMoment {
43
+ id: string;
44
+ writing_pad_id: string;
45
+ session_id: string;
46
+ created_at: string;
47
+ ghostDemand: {
48
+ voice?: string;
49
+ principle?: string;
50
+ intensity?: number;
51
+ };
52
+ learnerNeed: {
53
+ need?: string;
54
+ intensity?: number;
55
+ };
56
+ synthesis_strategy: string;
57
+ transformative: boolean;
58
+ persistence_layer: 'conscious' | 'preconscious' | 'unconscious';
59
+ consolidated_at?: string;
60
+ }
61
+
62
+ interface WritingPad {
63
+ id: string;
64
+ learnerId: string;
65
+ createdAt: string;
66
+ updatedAt: string;
67
+ metrics: {
68
+ totalRecognitionMoments: number;
69
+ dialecticalDepth: number;
70
+ mutualTransformationScore: number;
71
+ pedagogicalAttunement: number;
72
+ };
73
+ conscious: {
74
+ workingThoughts: string[];
75
+ ephemeralNotes: Record<string, unknown>;
76
+ lastCleared: string;
77
+ };
78
+ preconscious: {
79
+ recentPatterns: Array<{ pattern: string; confidence: number }>;
80
+ provisionalRules: string[];
81
+ fadeThreshold: number;
82
+ };
83
+ unconscious: {
84
+ permanentTraces: Array<{ type: string; content: string }>;
85
+ learnerArchetype: {
86
+ preferredLearningStyle: string | null;
87
+ commonStruggles: string[];
88
+ breakthroughPatterns: string[];
89
+ };
90
+ conflictPatterns: string[];
91
+ superegoTraces: string[];
92
+ };
93
+ }
94
+
95
+ interface LearnerEvent {
96
+ id: string;
97
+ learner_id: string;
98
+ event_type: 'resistance' | 'breakthrough' | 'demand';
99
+ created_at: string;
100
+ interpretation?: string;
101
+ strength?: number;
102
+ details?: Record<string, unknown>;
103
+ }
104
+
105
+ export const RecognitionView: React.FC = () => {
106
+ const [activeSubView, setActiveSubView] = useState<SubView>('overview');
107
+ const [learners, setLearners] = useState<LearnerOption[]>([]);
108
+ const [selectedLearnerId, setSelectedLearnerId] = useState<string>('');
109
+ const [stats, setStats] = useState<RecognitionStats | null>(null);
110
+ const [moments, setMoments] = useState<RecognitionMoment[]>([]);
111
+ const [writingPad, setWritingPad] = useState<WritingPad | null>(null);
112
+ const [events, setEvents] = useState<LearnerEvent[]>([]);
113
+ const [isLoading, setIsLoading] = useState(false);
114
+ const [error, setError] = useState<string | null>(null);
115
+ const [expandedMomentId, setExpandedMomentId] = useState<string | null>(null);
116
+ const [filterLayer, setFilterLayer] = useState<string>('all');
117
+
118
+ // Load learners with recognition data
119
+ const loadLearners = useCallback(async () => {
120
+ try {
121
+ const res = await fetch('/api/tutor/recognition-moments?limit=100');
122
+ if (!res.ok) throw new Error('Failed to load recognition data');
123
+ const data = await res.json();
124
+
125
+ // Handle various response formats defensively
126
+ let momentsList: RecognitionMoment[] = [];
127
+ if (Array.isArray(data)) {
128
+ momentsList = data;
129
+ } else if (data && Array.isArray(data.moments)) {
130
+ momentsList = data.moments;
131
+ }
132
+
133
+ const learnerMap = new Map<string, string>();
134
+ for (const moment of momentsList) {
135
+ if (moment && moment.learner_id && !learnerMap.has(moment.learner_id)) {
136
+ learnerMap.set(moment.learner_id, moment.learner_name || moment.learner_id);
137
+ }
138
+ }
139
+
140
+ const learnerList = Array.from(learnerMap.entries()).map(([id, name]) => ({
141
+ id,
142
+ name,
143
+ }));
144
+ setLearners(learnerList);
145
+
146
+ if (learnerList.length > 0 && !selectedLearnerId) {
147
+ setSelectedLearnerId(learnerList[0].id);
148
+ }
149
+ } catch (err) {
150
+ console.error('Failed to load learners:', err);
151
+ setError(err instanceof Error ? err.message : 'Failed to load learners');
152
+ }
153
+ }, [selectedLearnerId]);
154
+
155
+ // Load all data for selected learner
156
+ const loadLearnerData = useCallback(async () => {
157
+ if (!selectedLearnerId) return;
158
+
159
+ setIsLoading(true);
160
+ setError(null);
161
+
162
+ try {
163
+ const [statsRes, momentsRes, padRes, eventsRes] = await Promise.all([
164
+ fetch(`/api/tutor/writing-pad/${selectedLearnerId}/stats`),
165
+ fetch(`/api/tutor/writing-pad/${selectedLearnerId}/moments?limit=50`),
166
+ fetch(`/api/tutor/writing-pad/${selectedLearnerId}`),
167
+ fetch(`/api/tutor/learner-events/${selectedLearnerId}?limit=50`),
168
+ ]);
169
+
170
+ if (statsRes.ok) {
171
+ const data = await statsRes.json();
172
+ setStats(data);
173
+ }
174
+
175
+ if (momentsRes.ok) {
176
+ const data = await momentsRes.json();
177
+ const momentsList = Array.isArray(data) ? data : (Array.isArray(data?.moments) ? data.moments : []);
178
+ setMoments(momentsList);
179
+ }
180
+
181
+ if (padRes.ok) {
182
+ const data = await padRes.json();
183
+ setWritingPad(data?.writingPad || data || null);
184
+ }
185
+
186
+ if (eventsRes.ok) {
187
+ const data = await eventsRes.json();
188
+ const eventsList = Array.isArray(data) ? data : (Array.isArray(data?.events) ? data.events : []);
189
+ setEvents(eventsList);
190
+ }
191
+ } catch (err) {
192
+ console.error('Failed to load learner data:', err);
193
+ setError(err instanceof Error ? err.message : 'Failed to load data');
194
+ } finally {
195
+ setIsLoading(false);
196
+ }
197
+ }, [selectedLearnerId]);
198
+
199
+ useEffect(() => {
200
+ loadLearners();
201
+ }, [loadLearners]);
202
+
203
+ useEffect(() => {
204
+ if (selectedLearnerId) {
205
+ loadLearnerData();
206
+ }
207
+ }, [selectedLearnerId, loadLearnerData]);
208
+
209
+ const handleRefresh = () => {
210
+ haptics.medium();
211
+ loadLearners();
212
+ if (selectedLearnerId) {
213
+ loadLearnerData();
214
+ }
215
+ };
216
+
217
+ const handleSubViewChange = (view: SubView) => {
218
+ haptics.light();
219
+ setActiveSubView(view);
220
+ };
221
+
222
+ const toggleMomentExpand = (id: string) => {
223
+ haptics.light();
224
+ setExpandedMomentId(expandedMomentId === id ? null : id);
225
+ };
226
+
227
+ const subViews: { id: SubView; label: string; icon: string }[] = [
228
+ { id: 'overview', label: 'Overview', icon: '📊' },
229
+ { id: 'moments', label: 'Moments', icon: '💡' },
230
+ { id: 'memory', label: 'Memory', icon: '🧠' },
231
+ { id: 'events', label: 'Events', icon: '📅' },
232
+ { id: 'quadrant', label: 'Quadrant', icon: '🎯' },
233
+ ];
234
+
235
+ const filteredMoments = filterLayer === 'all'
236
+ ? moments
237
+ : moments.filter(m => m.persistence_layer === filterLayer);
238
+
239
+ const getLayerColor = (layer: string) => {
240
+ switch (layer) {
241
+ case 'conscious': return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30';
242
+ case 'preconscious': return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
243
+ case 'unconscious': return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
244
+ default: return 'bg-gray-500/20 text-gray-400 border-gray-500/30';
245
+ }
246
+ };
247
+
248
+ const getEventColor = (type: string, interpretation?: string) => {
249
+ if (type === 'breakthrough') return 'border-l-yellow-500 bg-yellow-500/10';
250
+ if (type === 'demand') return 'border-l-blue-500 bg-blue-500/10';
251
+ if (type === 'resistance') {
252
+ if (interpretation === 'productive') return 'border-l-green-500 bg-green-500/10';
253
+ if (interpretation === 'confused') return 'border-l-yellow-500 bg-yellow-500/10';
254
+ return 'border-l-red-500 bg-red-500/10';
255
+ }
256
+ return 'border-l-gray-500 bg-gray-500/10';
257
+ };
258
+
259
+ return (
260
+ <div className="space-y-4">
261
+ {/* Header with Learner Selector */}
262
+ <div className="flex items-center justify-between gap-4">
263
+ <div className="flex-1">
264
+ <select
265
+ value={selectedLearnerId}
266
+ onChange={(e) => {
267
+ haptics.light();
268
+ setSelectedLearnerId(e.target.value);
269
+ }}
270
+ className="w-full bg-gray-900/60 backdrop-blur-sm border border-white/10 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-[#E63946]/50"
271
+ >
272
+ {learners.length === 0 ? (
273
+ <option value="">No learners with recognition data</option>
274
+ ) : (
275
+ <>
276
+ <option value="">Select learner...</option>
277
+ {learners.map((l) => (
278
+ <option key={l.id} value={l.id}>
279
+ {l.name}
280
+ </option>
281
+ ))}
282
+ </>
283
+ )}
284
+ </select>
285
+ </div>
286
+ <button
287
+ type="button"
288
+ onClick={handleRefresh}
289
+ disabled={isLoading}
290
+ className="px-3 py-2 bg-gray-900/60 backdrop-blur-sm border border-white/10 rounded-lg text-sm text-white hover:bg-gray-800/60 active:scale-[0.98] transition-all disabled:opacity-50"
291
+ >
292
+ {isLoading ? '...' : '🔄'}
293
+ </button>
294
+ </div>
295
+
296
+ {/* Sub-Navigation */}
297
+ <div className="flex gap-1 p-1 bg-gray-900/40 backdrop-blur-sm rounded-xl border border-white/5">
298
+ {subViews.map((sv) => (
299
+ <button
300
+ key={sv.id}
301
+ type="button"
302
+ onClick={() => handleSubViewChange(sv.id)}
303
+ className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg text-xs font-medium transition-all active:scale-[0.98] ${
304
+ activeSubView === sv.id
305
+ ? 'bg-[#E63946]/20 text-[#E63946] border border-[#E63946]/30'
306
+ : 'text-gray-400 hover:text-white hover:bg-white/5'
307
+ }`}
308
+ >
309
+ <span>{sv.icon}</span>
310
+ <span className="hidden sm:inline">{sv.label}</span>
311
+ </button>
312
+ ))}
313
+ </div>
314
+
315
+ {/* Error State */}
316
+ {error && (
317
+ <div className="bg-red-900/20 border border-red-500/30 rounded-xl p-4 text-sm text-red-400">
318
+ {error}
319
+ </div>
320
+ )}
321
+
322
+ {/* Content Area */}
323
+ {!selectedLearnerId ? (
324
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
325
+ <div className="text-4xl mb-3">🔍</div>
326
+ <h3 className="text-white font-medium mb-2">Select a Learner</h3>
327
+ <p className="text-sm text-gray-400">
328
+ Choose a learner from the dropdown to view their recognition metrics.
329
+ </p>
330
+ </div>
331
+ ) : (
332
+ <>
333
+ {/* Overview Sub-View */}
334
+ {activeSubView === 'overview' && (
335
+ <div className="space-y-4">
336
+ {stats ? (
337
+ <>
338
+ {/* Core metrics grid */}
339
+ <div className="grid grid-cols-2 gap-3">
340
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
341
+ <div className="text-xs text-gray-400 mb-1">Recognition Moments</div>
342
+ <div className="text-2xl font-bold text-white">
343
+ {stats.total_recognition_moments || 0}
344
+ </div>
345
+ </div>
346
+
347
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
348
+ <div className="text-xs text-gray-400 mb-1">Dialectical Depth</div>
349
+ <div className="text-2xl font-bold text-white">
350
+ {((stats.dialectical_depth || 0) * 100).toFixed(0)}%
351
+ </div>
352
+ <div className="mt-2 h-1.5 bg-gray-800 rounded-full overflow-hidden">
353
+ <div
354
+ className="h-full bg-gradient-to-r from-[#E63946] to-[#E63946]/60 rounded-full transition-all"
355
+ style={{ width: `${(stats.dialectical_depth || 0) * 100}%` }}
356
+ />
357
+ </div>
358
+ </div>
359
+
360
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
361
+ <div className="text-xs text-gray-400 mb-1">Mutual Transform</div>
362
+ <div className="text-2xl font-bold text-white">
363
+ {((stats.mutual_transformation_score || 0) * 100).toFixed(0)}%
364
+ </div>
365
+ <div className="mt-2 h-1.5 bg-gray-800 rounded-full overflow-hidden">
366
+ <div
367
+ className="h-full bg-gradient-to-r from-blue-500 to-blue-500/60 rounded-full transition-all"
368
+ style={{ width: `${(stats.mutual_transformation_score || 0) * 100}%` }}
369
+ />
370
+ </div>
371
+ </div>
372
+
373
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
374
+ <div className="text-xs text-gray-400 mb-1">Pedagogical Attunement</div>
375
+ <div className="text-2xl font-bold text-white">
376
+ {((stats.pedagogical_attunement || 0) * 100).toFixed(0)}%
377
+ </div>
378
+ <div className="mt-2 h-1.5 bg-gray-800 rounded-full overflow-hidden">
379
+ <div
380
+ className="h-full bg-gradient-to-r from-green-500 to-green-500/60 rounded-full transition-all"
381
+ style={{ width: `${(stats.pedagogical_attunement || 0) * 100}%` }}
382
+ />
383
+ </div>
384
+ </div>
385
+ </div>
386
+
387
+ {/* Recognition Type Distribution */}
388
+ <RecognitionTypeChart
389
+ counts={stats.recognition_types || { pedagogical: 0, metacognitive: 0, existential: 0 }}
390
+ />
391
+
392
+ {/* Synthesis Strategy Distribution */}
393
+ <SynthesisStrategyChart
394
+ counts={stats.synthesis_strategies || { ghost_dominates: 0, learner_dominates: 0, dialectical_synthesis: 0 }}
395
+ />
396
+ </>
397
+ ) : isLoading ? (
398
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
399
+ <div className="animate-pulse text-gray-400">Loading stats...</div>
400
+ </div>
401
+ ) : (
402
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
403
+ <div className="text-gray-400">No stats available for this learner</div>
404
+ </div>
405
+ )}
406
+ </div>
407
+ )}
408
+
409
+ {/* Moments Sub-View */}
410
+ {activeSubView === 'moments' && (
411
+ <div className="space-y-3">
412
+ {/* Filter Chips */}
413
+ <div className="flex gap-2 flex-wrap">
414
+ {['all', 'conscious', 'preconscious', 'unconscious'].map((layer) => (
415
+ <button
416
+ key={layer}
417
+ type="button"
418
+ onClick={() => {
419
+ haptics.light();
420
+ setFilterLayer(layer);
421
+ }}
422
+ className={`px-3 py-1 rounded-full text-xs font-medium transition-all active:scale-[0.98] ${
423
+ filterLayer === layer
424
+ ? 'bg-[#E63946]/20 text-[#E63946] border border-[#E63946]/30'
425
+ : 'bg-gray-800/50 text-gray-400 border border-white/5 hover:text-white'
426
+ }`}
427
+ >
428
+ {layer === 'all' ? 'All' : layer.charAt(0).toUpperCase() + layer.slice(1)}
429
+ </button>
430
+ ))}
431
+ </div>
432
+
433
+ {/* Moments List */}
434
+ {filteredMoments.length > 0 ? (
435
+ <div className="space-y-2">
436
+ {filteredMoments.map((moment) => (
437
+ <div
438
+ key={moment.id}
439
+ className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl overflow-hidden"
440
+ >
441
+ <button
442
+ type="button"
443
+ onClick={() => toggleMomentExpand(moment.id)}
444
+ className="w-full p-4 text-left active:scale-[0.99] transition-all"
445
+ >
446
+ <div className="flex items-start justify-between gap-3">
447
+ <div className="flex-1 min-w-0">
448
+ <div className="flex items-center gap-2 mb-1">
449
+ <span className={`px-2 py-0.5 rounded-full text-xs border ${getLayerColor(moment.persistence_layer)}`}>
450
+ {moment.persistence_layer}
451
+ </span>
452
+ {moment.transformative && (
453
+ <span className="px-2 py-0.5 rounded-full text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30">
454
+ ✨ Transformative
455
+ </span>
456
+ )}
457
+ </div>
458
+ <p className="text-sm text-white truncate">
459
+ {moment.synthesis_strategy || 'No synthesis recorded'}
460
+ </p>
461
+ <p className="text-xs text-gray-500 mt-1">
462
+ {new Date(moment.created_at).toLocaleString()}
463
+ </p>
464
+ </div>
465
+ <span className="text-gray-500 text-lg">
466
+ {expandedMomentId === moment.id ? '−' : '+'}
467
+ </span>
468
+ </div>
469
+ </button>
470
+
471
+ {/* Expanded Content */}
472
+ {expandedMomentId === moment.id && (
473
+ <div className="px-4 pb-4 pt-2 border-t border-white/5 space-y-3">
474
+ {/* Ghost Demand (Thesis) */}
475
+ <div className="bg-gray-800/50 rounded-lg p-3">
476
+ <div className="text-xs text-gray-400 mb-1 flex items-center gap-1">
477
+ <span>👻</span> Ghost Demand (Superego)
478
+ </div>
479
+ <p className="text-sm text-white">
480
+ {moment.ghostDemand?.voice || 'No voice recorded'}
481
+ </p>
482
+ {moment.ghostDemand?.principle && (
483
+ <p className="text-xs text-gray-400 mt-1">
484
+ Principle: {moment.ghostDemand.principle}
485
+ </p>
486
+ )}
487
+ </div>
488
+
489
+ {/* Learner Need (Antithesis) */}
490
+ <div className="bg-gray-800/50 rounded-lg p-3">
491
+ <div className="text-xs text-gray-400 mb-1 flex items-center gap-1">
492
+ <span>🎯</span> Learner Need (Antithesis)
493
+ </div>
494
+ <p className="text-sm text-white">
495
+ {moment.learnerNeed?.need || 'No need recorded'}
496
+ </p>
497
+ {moment.learnerNeed?.intensity != null && (
498
+ <div className="mt-2">
499
+ <div className="flex items-center justify-between text-xs text-gray-400 mb-1">
500
+ <span>Intensity</span>
501
+ <span>{(moment.learnerNeed.intensity * 100).toFixed(0)}%</span>
502
+ </div>
503
+ <div className="h-1 bg-gray-700 rounded-full overflow-hidden">
504
+ <div
505
+ className="h-full bg-blue-500 rounded-full"
506
+ style={{ width: `${moment.learnerNeed.intensity * 100}%` }}
507
+ />
508
+ </div>
509
+ </div>
510
+ )}
511
+ </div>
512
+
513
+ {/* Synthesis */}
514
+ <div className="bg-[#E63946]/10 rounded-lg p-3 border border-[#E63946]/20">
515
+ <div className="text-xs text-[#E63946] mb-1 flex items-center gap-1">
516
+ <span>⚡</span> Synthesis
517
+ </div>
518
+ <p className="text-sm text-white">
519
+ {moment.synthesis_strategy || 'No synthesis recorded'}
520
+ </p>
521
+ </div>
522
+ </div>
523
+ )}
524
+ </div>
525
+ ))}
526
+ </div>
527
+ ) : isLoading ? (
528
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
529
+ <div className="animate-pulse text-gray-400">Loading moments...</div>
530
+ </div>
531
+ ) : (
532
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
533
+ <div className="text-4xl mb-3">💡</div>
534
+ <h3 className="text-white font-medium mb-2">No Moments Yet</h3>
535
+ <p className="text-sm text-gray-400">
536
+ Recognition moments will appear here as the learner interacts with the tutor.
537
+ </p>
538
+ </div>
539
+ )}
540
+ </div>
541
+ )}
542
+
543
+ {/* Memory Sub-View */}
544
+ {activeSubView === 'memory' && (
545
+ <div className="space-y-3">
546
+ {writingPad ? (
547
+ <>
548
+ {/* Conscious Layer */}
549
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-yellow-500/20 rounded-xl overflow-hidden">
550
+ <div className="px-4 py-3 border-b border-yellow-500/20 flex items-center gap-2">
551
+ <span className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse" />
552
+ <span className="text-sm font-medium text-yellow-400">Conscious</span>
553
+ <span className="text-xs text-gray-500 ml-auto">Working Memory</span>
554
+ </div>
555
+ <div className="p-4 space-y-2">
556
+ <div className="text-sm text-gray-300">
557
+ <span className="text-gray-500">Working Thoughts:</span>{' '}
558
+ {writingPad.conscious.workingThoughts.length || 0}
559
+ </div>
560
+ <div className="text-sm text-gray-300">
561
+ <span className="text-gray-500">Ephemeral Notes:</span>{' '}
562
+ {Object.keys(writingPad.conscious.ephemeralNotes || {}).length || 0}
563
+ </div>
564
+ <div className="text-xs text-gray-500">
565
+ Last cleared: {new Date(writingPad.conscious.lastCleared).toLocaleString()}
566
+ </div>
567
+ </div>
568
+ </div>
569
+
570
+ {/* Preconscious Layer */}
571
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-blue-500/20 rounded-xl overflow-hidden">
572
+ <div className="px-4 py-3 border-b border-blue-500/20 flex items-center gap-2">
573
+ <span className="w-2 h-2 rounded-full bg-blue-500" />
574
+ <span className="text-sm font-medium text-blue-400">Preconscious</span>
575
+ <span className="text-xs text-gray-500 ml-auto">Recent Patterns</span>
576
+ </div>
577
+ <div className="p-4 space-y-3">
578
+ {writingPad.preconscious.recentPatterns.length > 0 ? (
579
+ writingPad.preconscious.recentPatterns.slice(0, 5).map((p, i) => (
580
+ <div key={i} className="space-y-1">
581
+ <div className="flex items-center justify-between">
582
+ <span className="text-sm text-gray-300 truncate flex-1">
583
+ {typeof p === 'string' ? p : p.pattern || 'Pattern'}
584
+ </span>
585
+ <span className="text-xs text-gray-500 ml-2">
586
+ {typeof p === 'object' && p.confidence
587
+ ? `${(p.confidence * 100).toFixed(0)}%`
588
+ : '—'}
589
+ </span>
590
+ </div>
591
+ {typeof p === 'object' && p.confidence != null && (
592
+ <div className="h-1 bg-gray-700 rounded-full overflow-hidden">
593
+ <div
594
+ className="h-full bg-blue-500 rounded-full"
595
+ style={{ width: `${p.confidence * 100}%` }}
596
+ />
597
+ </div>
598
+ )}
599
+ </div>
600
+ ))
601
+ ) : (
602
+ <p className="text-sm text-gray-500">No patterns recorded yet</p>
603
+ )}
604
+ <div className="text-xs text-gray-500 pt-2 border-t border-white/5">
605
+ Fade threshold: {writingPad.preconscious.fadeThreshold} interactions
606
+ </div>
607
+ </div>
608
+ </div>
609
+
610
+ {/* Unconscious Layer */}
611
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-purple-500/20 rounded-xl overflow-hidden">
612
+ <div className="px-4 py-3 border-b border-purple-500/20 flex items-center gap-2">
613
+ <span className="w-2 h-2 rounded-full bg-purple-500" />
614
+ <span className="text-sm font-medium text-purple-400">Unconscious</span>
615
+ <span className="text-xs text-gray-500 ml-auto">Permanent Traces</span>
616
+ </div>
617
+ <div className="p-4 space-y-3">
618
+ <div className="text-sm text-gray-300">
619
+ <span className="text-gray-500">Permanent Traces:</span>{' '}
620
+ {writingPad.unconscious.permanentTraces.length || 0}
621
+ </div>
622
+
623
+ {/* Learner Archetype */}
624
+ <div className="bg-gray-800/50 rounded-lg p-3">
625
+ <div className="text-xs text-gray-400 mb-2">Learner Archetype</div>
626
+ <div className="space-y-1 text-sm">
627
+ <div className="text-gray-300">
628
+ <span className="text-gray-500">Style:</span>{' '}
629
+ {writingPad.unconscious.learnerArchetype.preferredLearningStyle || 'Unknown'}
630
+ </div>
631
+ <div className="text-gray-300">
632
+ <span className="text-gray-500">Struggles:</span>{' '}
633
+ {writingPad.unconscious.learnerArchetype.commonStruggles.length || 0}
634
+ </div>
635
+ <div className="text-gray-300">
636
+ <span className="text-gray-500">Breakthroughs:</span>{' '}
637
+ {writingPad.unconscious.learnerArchetype.breakthroughPatterns.length || 0}
638
+ </div>
639
+ </div>
640
+ </div>
641
+
642
+ <div className="text-sm text-gray-300">
643
+ <span className="text-gray-500">Conflict Patterns:</span>{' '}
644
+ {writingPad.unconscious.conflictPatterns.length || 0}
645
+ </div>
646
+ <div className="text-sm text-gray-300">
647
+ <span className="text-gray-500">Superego Traces:</span>{' '}
648
+ {writingPad.unconscious.superegoTraces.length || 0}
649
+ </div>
650
+ </div>
651
+ </div>
652
+ </>
653
+ ) : isLoading ? (
654
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
655
+ <div className="animate-pulse text-gray-400">Loading memory state...</div>
656
+ </div>
657
+ ) : (
658
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
659
+ <div className="text-4xl mb-3">🧠</div>
660
+ <h3 className="text-white font-medium mb-2">No Memory Data</h3>
661
+ <p className="text-sm text-gray-400">
662
+ Writing pad data will appear here once the learner has interactions.
663
+ </p>
664
+ </div>
665
+ )}
666
+ </div>
667
+ )}
668
+
669
+ {/* Events Sub-View */}
670
+ {activeSubView === 'events' && (
671
+ <div className="space-y-2">
672
+ {events.length > 0 ? (
673
+ events.map((event) => (
674
+ <div
675
+ key={event.id}
676
+ className={`border-l-4 rounded-r-xl p-4 ${getEventColor(event.event_type, event.interpretation)}`}
677
+ >
678
+ <div className="flex items-start justify-between gap-3">
679
+ <div className="flex-1">
680
+ <div className="flex items-center gap-2 mb-1">
681
+ <span className="text-sm font-medium text-white capitalize">
682
+ {event.event_type === 'breakthrough' && '✨ '}
683
+ {event.event_type === 'resistance' && '🛡️ '}
684
+ {event.event_type === 'demand' && '📢 '}
685
+ {event.event_type}
686
+ </span>
687
+ {event.interpretation && (
688
+ <span className="text-xs text-gray-400">
689
+ ({event.interpretation})
690
+ </span>
691
+ )}
692
+ </div>
693
+ {event.strength != null && (
694
+ <div className="flex items-center gap-2 mt-2">
695
+ <span className="text-xs text-gray-500">Strength:</span>
696
+ <div className="flex-1 h-1.5 bg-gray-700 rounded-full overflow-hidden">
697
+ <div
698
+ className="h-full bg-white/50 rounded-full"
699
+ style={{ width: `${event.strength * 100}%` }}
700
+ />
701
+ </div>
702
+ <span className="text-xs text-gray-400">
703
+ {(event.strength * 100).toFixed(0)}%
704
+ </span>
705
+ </div>
706
+ )}
707
+ </div>
708
+ <span className="text-xs text-gray-500 whitespace-nowrap">
709
+ {new Date(event.created_at).toLocaleTimeString()}
710
+ </span>
711
+ </div>
712
+ </div>
713
+ ))
714
+ ) : isLoading ? (
715
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
716
+ <div className="animate-pulse text-gray-400">Loading events...</div>
717
+ </div>
718
+ ) : (
719
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
720
+ <div className="text-4xl mb-3">📅</div>
721
+ <h3 className="text-white font-medium mb-2">No Events Yet</h3>
722
+ <p className="text-sm text-gray-400">
723
+ Learner events (resistance, breakthroughs, demands) will appear here.
724
+ </p>
725
+ </div>
726
+ )}
727
+ </div>
728
+ )}
729
+
730
+ {/* Quadrant Sub-View */}
731
+ {activeSubView === 'quadrant' && (
732
+ <div className="space-y-4">
733
+ {stats ? (
734
+ <>
735
+ {/* Psychodynamic Quadrant Chart */}
736
+ <PsychodynamicQuadrant
737
+ superegoCompliance={stats.average_compliance ?? 0.5}
738
+ recognitionSeeking={stats.average_recognition_seeking ?? 0.5}
739
+ />
740
+
741
+ {/* Parameter explanation */}
742
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-4">
743
+ <div className="text-xs text-gray-400 mb-3">About the Quadrants</div>
744
+ <div className="space-y-3 text-xs">
745
+ <div className="flex items-start gap-2">
746
+ <span className="text-green-400">●</span>
747
+ <div>
748
+ <span className="text-white font-medium">Dialogical Recognition</span>
749
+ <span className="text-gray-500"> (high compliance + high seeking)</span>
750
+ <p className="text-gray-400 mt-0.5">
751
+ Ideal state: tutor balances authority with genuine responsiveness to learner needs.
752
+ </p>
753
+ </div>
754
+ </div>
755
+ <div className="flex items-start gap-2">
756
+ <span className="text-blue-400">●</span>
757
+ <div>
758
+ <span className="text-white font-medium">Permissive Responsive</span>
759
+ <span className="text-gray-500"> (low compliance + high seeking)</span>
760
+ <p className="text-gray-400 mt-0.5">
761
+ Highly learner-centered but may lack pedagogical structure.
762
+ </p>
763
+ </div>
764
+ </div>
765
+ <div className="flex items-start gap-2">
766
+ <span className="text-red-400">●</span>
767
+ <div>
768
+ <span className="text-white font-medium">Traditional Authoritarian</span>
769
+ <span className="text-gray-500"> (high compliance + low seeking)</span>
770
+ <p className="text-gray-400 mt-0.5">
771
+ Authority-driven instruction with less learner recognition.
772
+ </p>
773
+ </div>
774
+ </div>
775
+ <div className="flex items-start gap-2">
776
+ <span className="text-gray-500">●</span>
777
+ <div>
778
+ <span className="text-white font-medium">Disengaged</span>
779
+ <span className="text-gray-500"> (low compliance + low seeking)</span>
780
+ <p className="text-gray-400 mt-0.5">
781
+ Minimal tension or engagement - may indicate passive interactions.
782
+ </p>
783
+ </div>
784
+ </div>
785
+ </div>
786
+ </div>
787
+ </>
788
+ ) : isLoading ? (
789
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
790
+ <div className="animate-pulse text-gray-400">Loading quadrant data...</div>
791
+ </div>
792
+ ) : (
793
+ <div className="bg-gray-900/60 backdrop-blur-sm border border-white/5 rounded-xl p-8 text-center">
794
+ <div className="text-4xl mb-3">🎯</div>
795
+ <h3 className="text-white font-medium mb-2">No Quadrant Data</h3>
796
+ <p className="text-sm text-gray-400">
797
+ Psychodynamic parameters will appear here after recognition moments are recorded.
798
+ </p>
799
+ </div>
800
+ )}
801
+ </div>
802
+ )}
803
+ </>
804
+ )}
805
+ </div>
806
+ );
807
+ };
808
+
809
+ export default RecognitionView;