@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,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;
|