@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,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogsView Component
|
|
3
|
+
*
|
|
4
|
+
* Browse and view dialogue transcripts by date.
|
|
5
|
+
* Supports date selection, pagination, and expandable dialogue entries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
9
|
+
import type { EvalDialogue, EvalDialogueEntry } from '../../types';
|
|
10
|
+
import haptics from '../../utils/haptics';
|
|
11
|
+
|
|
12
|
+
interface LogsViewProps {
|
|
13
|
+
logDates: string[];
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
onLoadDates: () => Promise<void>;
|
|
16
|
+
onLoadDialogues: (date: string, offset?: number, limit?: number) => Promise<{
|
|
17
|
+
dialogues: EvalDialogue[];
|
|
18
|
+
total: number;
|
|
19
|
+
hasMore: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
onLoadDialogueById: (dialogueId: string) => Promise<EvalDialogue | null>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Format time from ISO string
|
|
25
|
+
function formatTime(dateStr: string): string {
|
|
26
|
+
const date = new Date(dateStr);
|
|
27
|
+
return date.toLocaleTimeString(undefined, {
|
|
28
|
+
hour: 'numeric',
|
|
29
|
+
minute: '2-digit',
|
|
30
|
+
second: '2-digit'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Format date for display
|
|
35
|
+
function formatDateLabel(dateStr: string): string {
|
|
36
|
+
const date = new Date(dateStr + 'T00:00:00');
|
|
37
|
+
const today = new Date();
|
|
38
|
+
const yesterday = new Date(today);
|
|
39
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
40
|
+
|
|
41
|
+
if (dateStr === today.toISOString().split('T')[0]) return 'Today';
|
|
42
|
+
if (dateStr === yesterday.toISOString().split('T')[0]) return 'Yesterday';
|
|
43
|
+
|
|
44
|
+
return date.toLocaleDateString(undefined, {
|
|
45
|
+
weekday: 'short',
|
|
46
|
+
month: 'short',
|
|
47
|
+
day: 'numeric'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Agent icon SVG components for consistent styling
|
|
52
|
+
const AgentIcons = {
|
|
53
|
+
ego: (
|
|
54
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
55
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
56
|
+
</svg>
|
|
57
|
+
),
|
|
58
|
+
superego: (
|
|
59
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
60
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
|
|
61
|
+
</svg>
|
|
62
|
+
),
|
|
63
|
+
user: (
|
|
64
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
65
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
66
|
+
</svg>
|
|
67
|
+
),
|
|
68
|
+
default: (
|
|
69
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
70
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
71
|
+
</svg>
|
|
72
|
+
)
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Get agent badge color - Premium styling with SVG icons
|
|
76
|
+
function getAgentBadge(agent: string): { bg: string; text: string; icon: React.ReactNode } {
|
|
77
|
+
switch (agent) {
|
|
78
|
+
case 'ego':
|
|
79
|
+
return { bg: 'bg-blue-500/20 border border-blue-500/30', text: 'text-blue-400', icon: AgentIcons.ego };
|
|
80
|
+
case 'superego':
|
|
81
|
+
return { bg: 'bg-green-500/20 border border-green-500/30', text: 'text-green-400', icon: AgentIcons.superego };
|
|
82
|
+
case 'user':
|
|
83
|
+
return { bg: 'bg-purple-500/20 border border-purple-500/30', text: 'text-purple-400', icon: AgentIcons.user };
|
|
84
|
+
default:
|
|
85
|
+
return { bg: 'bg-gray-500/20 border border-gray-500/30', text: 'text-gray-400', icon: AgentIcons.default };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Dialogue Entry Component - Premium glass styling
|
|
90
|
+
const DialogueEntryItem: React.FC<{ entry: EvalDialogueEntry; index: number }> = ({ entry, index }) => {
|
|
91
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
92
|
+
const agentBadge = getAgentBadge(entry.agent);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="border-b border-white/5 last:border-b-0">
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => {
|
|
99
|
+
haptics.light();
|
|
100
|
+
setIsExpanded(!isExpanded);
|
|
101
|
+
}}
|
|
102
|
+
className="w-full p-3 text-left active:bg-white/5 transition-all duration-150"
|
|
103
|
+
>
|
|
104
|
+
<div className="flex items-start gap-3">
|
|
105
|
+
{/* Step number - Gradient ring */}
|
|
106
|
+
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-800/80 border border-white/10
|
|
107
|
+
text-gray-400 text-xs font-semibold flex items-center justify-center">
|
|
108
|
+
{index + 1}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div className="flex-1 min-w-0">
|
|
112
|
+
{/* Agent and action */}
|
|
113
|
+
<div className="flex items-center gap-2 mb-1.5">
|
|
114
|
+
<span className={`inline-flex items-center gap-1.5 text-[10px] px-2.5 py-1 rounded-lg font-semibold uppercase tracking-wide
|
|
115
|
+
${agentBadge.bg} ${agentBadge.text}`}>
|
|
116
|
+
{agentBadge.icon}
|
|
117
|
+
<span>{entry.agent}</span>
|
|
118
|
+
</span>
|
|
119
|
+
{entry.action && (
|
|
120
|
+
<span className="text-xs text-gray-500 font-medium">{entry.action}</span>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
{/* Summary info */}
|
|
125
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-500">
|
|
126
|
+
{entry.latencyMs && (
|
|
127
|
+
<span className="flex items-center gap-1">
|
|
128
|
+
<span className="w-1 h-1 rounded-full bg-gray-600" />
|
|
129
|
+
{entry.latencyMs}ms
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
{entry.model && (
|
|
133
|
+
<span className="flex items-center gap-1">
|
|
134
|
+
<span className="w-1 h-1 rounded-full bg-gray-600" />
|
|
135
|
+
{entry.model}
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
{entry.suggestions && entry.suggestions.length > 0 && (
|
|
139
|
+
<span className="text-blue-400 font-medium">{entry.suggestions.length} suggestions</span>
|
|
140
|
+
)}
|
|
141
|
+
{entry.verdict && (
|
|
142
|
+
<span className={`font-medium ${entry.verdict.approved ? 'text-green-400' : 'text-yellow-400'}`}>
|
|
143
|
+
{entry.verdict.approved ? '✓ Approved' : '⟳ Revise'}
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Expand indicator */}
|
|
150
|
+
<div className={`w-6 h-6 rounded-full bg-white/5 flex items-center justify-center
|
|
151
|
+
transition-all duration-200 ${isExpanded ? 'bg-white/10' : ''}`}>
|
|
152
|
+
<svg
|
|
153
|
+
className={`w-3 h-3 text-gray-500 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
|
|
154
|
+
fill="none"
|
|
155
|
+
viewBox="0 0 24 24"
|
|
156
|
+
stroke="currentColor"
|
|
157
|
+
>
|
|
158
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
159
|
+
</svg>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</button>
|
|
163
|
+
|
|
164
|
+
{/* Expanded content - Glass panels */}
|
|
165
|
+
{isExpanded && (
|
|
166
|
+
<div className="px-3 pb-4 pl-14 space-y-3">
|
|
167
|
+
{/* Suggestions */}
|
|
168
|
+
{entry.suggestions && entry.suggestions.length > 0 && (
|
|
169
|
+
<div className="space-y-2">
|
|
170
|
+
<h5 className="text-xs text-gray-400 font-semibold uppercase tracking-wide">Suggestions</h5>
|
|
171
|
+
{entry.suggestions.map((s, i) => (
|
|
172
|
+
<div key={i} className="bg-white/5 backdrop-blur-sm border border-white/5 rounded-lg p-3">
|
|
173
|
+
<div className="flex items-start gap-2">
|
|
174
|
+
<span className={`text-[10px] px-2 py-0.5 rounded-md font-medium
|
|
175
|
+
${s.priority === 'high' ? 'bg-red-500/20 border border-red-500/30 text-red-400' :
|
|
176
|
+
s.priority === 'medium' ? 'bg-yellow-500/20 border border-yellow-500/30 text-yellow-400' :
|
|
177
|
+
'bg-gray-500/20 border border-gray-500/30 text-gray-400'}`}>
|
|
178
|
+
{s.type}
|
|
179
|
+
</span>
|
|
180
|
+
<div className="flex-1">
|
|
181
|
+
<div className="text-xs text-white font-medium">{s.title}</div>
|
|
182
|
+
<div className="text-[10px] text-gray-500 mt-1 line-clamp-2">{s.message}</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Verdict */}
|
|
191
|
+
{entry.verdict && (
|
|
192
|
+
<div className="bg-white/5 backdrop-blur-sm border border-white/5 rounded-lg p-3 space-y-1">
|
|
193
|
+
<h5 className="text-xs text-gray-400 font-semibold uppercase tracking-wide">Verdict</h5>
|
|
194
|
+
<div className={`text-sm font-medium ${entry.verdict.approved ? 'text-green-400' : 'text-yellow-400'}`}>
|
|
195
|
+
{entry.verdict.approved ? '✓ Approved' : '⟳ Requires Revision'}
|
|
196
|
+
{entry.verdict.confidence !== undefined && (
|
|
197
|
+
<span className="text-gray-500 text-xs ml-2 font-normal">
|
|
198
|
+
({(entry.verdict.confidence * 100).toFixed(0)}% confidence)
|
|
199
|
+
</span>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
{entry.verdict.feedback && (
|
|
203
|
+
<p className="text-xs text-gray-500 mt-1">{entry.verdict.feedback}</p>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* Pre-analysis */}
|
|
209
|
+
{entry.preAnalysis && entry.preAnalysis.isPreAnalysis && (
|
|
210
|
+
<div className="bg-white/5 backdrop-blur-sm border border-white/5 rounded-lg p-3 space-y-1">
|
|
211
|
+
<h5 className="text-xs text-gray-400 font-semibold uppercase tracking-wide">Pre-Analysis</h5>
|
|
212
|
+
{entry.preAnalysis.overallCaution && (
|
|
213
|
+
<p className="text-xs text-gray-500">{entry.preAnalysis.overallCaution}</p>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Token usage */}
|
|
219
|
+
{(entry.inputTokens || entry.outputTokens) && (
|
|
220
|
+
<div className="flex gap-4 text-[10px] text-gray-500">
|
|
221
|
+
{entry.inputTokens && (
|
|
222
|
+
<span className="flex items-center gap-1.5">
|
|
223
|
+
<span className="text-gray-600">↓</span>
|
|
224
|
+
In: {entry.inputTokens.toLocaleString()}
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
227
|
+
{entry.outputTokens && (
|
|
228
|
+
<span className="flex items-center gap-1.5">
|
|
229
|
+
<span className="text-gray-600">↑</span>
|
|
230
|
+
Out: {entry.outputTokens.toLocaleString()}
|
|
231
|
+
</span>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const LogsView: React.FC<LogsViewProps> = ({
|
|
242
|
+
logDates,
|
|
243
|
+
isLoading,
|
|
244
|
+
onLoadDates,
|
|
245
|
+
onLoadDialogues
|
|
246
|
+
}) => {
|
|
247
|
+
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
|
248
|
+
const [dialogues, setDialogues] = useState<EvalDialogue[]>([]);
|
|
249
|
+
const [expandedDialogue, setExpandedDialogue] = useState<string | null>(null);
|
|
250
|
+
const [hasMore, setHasMore] = useState(false);
|
|
251
|
+
const [offset, setOffset] = useState(0);
|
|
252
|
+
const [isLoadingDialogues, setIsLoadingDialogues] = useState(false);
|
|
253
|
+
const LIMIT = 10;
|
|
254
|
+
|
|
255
|
+
// Load dates on mount
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (logDates.length === 0) {
|
|
258
|
+
onLoadDates();
|
|
259
|
+
}
|
|
260
|
+
}, [logDates.length, onLoadDates]);
|
|
261
|
+
|
|
262
|
+
// Auto-select most recent date
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (logDates.length > 0 && !selectedDate) {
|
|
265
|
+
setSelectedDate(logDates[0]);
|
|
266
|
+
}
|
|
267
|
+
}, [logDates, selectedDate]);
|
|
268
|
+
|
|
269
|
+
// Load dialogues when date changes
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (selectedDate) {
|
|
272
|
+
setIsLoadingDialogues(true);
|
|
273
|
+
setDialogues([]);
|
|
274
|
+
setOffset(0);
|
|
275
|
+
onLoadDialogues(selectedDate, 0, LIMIT)
|
|
276
|
+
.then(({ dialogues: newDialogues, hasMore: more }) => {
|
|
277
|
+
setDialogues(newDialogues);
|
|
278
|
+
setHasMore(more);
|
|
279
|
+
})
|
|
280
|
+
.finally(() => setIsLoadingDialogues(false));
|
|
281
|
+
}
|
|
282
|
+
}, [selectedDate, onLoadDialogues]);
|
|
283
|
+
|
|
284
|
+
// Load more dialogues
|
|
285
|
+
const handleLoadMore = useCallback(async () => {
|
|
286
|
+
if (!selectedDate || isLoadingDialogues || !hasMore) return;
|
|
287
|
+
|
|
288
|
+
setIsLoadingDialogues(true);
|
|
289
|
+
const newOffset = offset + LIMIT;
|
|
290
|
+
const { dialogues: newDialogues, hasMore: more } = await onLoadDialogues(selectedDate, newOffset, LIMIT);
|
|
291
|
+
setDialogues((prev) => [...prev, ...newDialogues]);
|
|
292
|
+
setHasMore(more);
|
|
293
|
+
setOffset(newOffset);
|
|
294
|
+
setIsLoadingDialogues(false);
|
|
295
|
+
}, [selectedDate, isLoadingDialogues, hasMore, offset, onLoadDialogues]);
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
299
|
+
{/* Date selector - Glass bar with fade edges */}
|
|
300
|
+
<div className="flex-shrink-0 p-3 border-b border-white/5 bg-gray-900/30 backdrop-blur-sm">
|
|
301
|
+
<div className="relative">
|
|
302
|
+
{/* Left fade edge */}
|
|
303
|
+
<div className="absolute left-0 top-0 bottom-0 w-4 bg-gradient-to-r from-gray-900/80 to-transparent z-10 pointer-events-none" />
|
|
304
|
+
{/* Right fade edge */}
|
|
305
|
+
<div className="absolute right-0 top-0 bottom-0 w-4 bg-gradient-to-l from-gray-900/80 to-transparent z-10 pointer-events-none" />
|
|
306
|
+
<div className="flex gap-2 overflow-x-auto pb-1 px-1 scrollbar-hide">
|
|
307
|
+
{logDates.map((date) => (
|
|
308
|
+
<button
|
|
309
|
+
key={date}
|
|
310
|
+
type="button"
|
|
311
|
+
onClick={() => {
|
|
312
|
+
haptics.light();
|
|
313
|
+
setSelectedDate(date);
|
|
314
|
+
setExpandedDialogue(null);
|
|
315
|
+
}}
|
|
316
|
+
className={`flex-shrink-0 px-4 py-2.5 rounded-xl text-sm font-medium
|
|
317
|
+
transition-all duration-200 active:scale-[0.97]
|
|
318
|
+
${selectedDate === date
|
|
319
|
+
? 'bg-gradient-to-r from-[#E63946] to-[#d62839] text-white shadow-md shadow-[#E63946]/20'
|
|
320
|
+
: 'bg-gray-800/80 text-gray-300 hover:bg-gray-700/80 border border-white/5'
|
|
321
|
+
}`}
|
|
322
|
+
>
|
|
323
|
+
{formatDateLabel(date)}
|
|
324
|
+
</button>
|
|
325
|
+
))}
|
|
326
|
+
{logDates.length === 0 && !isLoading && (
|
|
327
|
+
<span className="text-sm text-gray-500 px-2">No logs available</span>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Dialogues list */}
|
|
334
|
+
<div className="flex-1 overflow-y-auto">
|
|
335
|
+
{/* Loading state - Premium animated */}
|
|
336
|
+
{isLoadingDialogues && dialogues.length === 0 && (
|
|
337
|
+
<div className="flex items-center justify-center h-48">
|
|
338
|
+
<div className="flex flex-col items-center gap-4">
|
|
339
|
+
<div className="relative">
|
|
340
|
+
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-[#E63946]/20 to-[#d62839]/20 animate-spin"
|
|
341
|
+
style={{ animationDuration: '3s' }} />
|
|
342
|
+
<div className="relative w-12 h-12 rounded-full bg-gray-900/80 backdrop-blur-sm border border-white/10
|
|
343
|
+
flex items-center justify-center">
|
|
344
|
+
<svg className="w-6 h-6 text-[#E63946] animate-spin" fill="none" viewBox="0 0 24 24">
|
|
345
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
346
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
347
|
+
</svg>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
<span className="text-sm text-gray-400 font-medium">Loading dialogues...</span>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
|
|
355
|
+
{/* Empty state - Enhanced */}
|
|
356
|
+
{!isLoadingDialogues && dialogues.length === 0 && selectedDate && (
|
|
357
|
+
<div className="flex flex-col items-center justify-center h-48 text-gray-500">
|
|
358
|
+
<div className="relative mb-4">
|
|
359
|
+
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-gray-600/20 via-transparent to-gray-600/20 animate-spin"
|
|
360
|
+
style={{ animationDuration: '8s' }} />
|
|
361
|
+
<div className="relative w-16 h-16 rounded-full bg-gray-900/50 backdrop-blur-sm border border-white/5
|
|
362
|
+
flex items-center justify-center">
|
|
363
|
+
<svg className="w-8 h-8 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
364
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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" />
|
|
365
|
+
</svg>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
<p className="text-sm font-medium text-gray-400">No dialogues for this date</p>
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{/* Dialogue cards - Glass styling */}
|
|
373
|
+
{dialogues.length > 0 && (
|
|
374
|
+
<div className="p-3 space-y-2">
|
|
375
|
+
{dialogues.map((dialogue) => {
|
|
376
|
+
const isExpanded = expandedDialogue === dialogue.dialogueId;
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div key={dialogue.dialogueId} className="bg-gray-900/60 backdrop-blur-sm border border-white/5
|
|
380
|
+
rounded-xl overflow-hidden">
|
|
381
|
+
{/* Dialogue header */}
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
onClick={() => {
|
|
385
|
+
haptics.light();
|
|
386
|
+
setExpandedDialogue(isExpanded ? null : dialogue.dialogueId);
|
|
387
|
+
}}
|
|
388
|
+
className="w-full p-4 text-left active:bg-white/5 transition-all duration-150"
|
|
389
|
+
>
|
|
390
|
+
<div className="flex items-start justify-between gap-3">
|
|
391
|
+
<div>
|
|
392
|
+
<div className="text-sm font-semibold text-white">
|
|
393
|
+
{formatTime(dialogue.startTime)}
|
|
394
|
+
</div>
|
|
395
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-500 mt-1.5">
|
|
396
|
+
<span className="flex items-center gap-1">
|
|
397
|
+
<span className="w-1 h-1 rounded-full bg-gray-600" />
|
|
398
|
+
{dialogue.entryCount} entries
|
|
399
|
+
</span>
|
|
400
|
+
{dialogue.summary && (
|
|
401
|
+
<>
|
|
402
|
+
<span className="flex items-center gap-1">
|
|
403
|
+
<span className="w-1 h-1 rounded-full bg-blue-500" />
|
|
404
|
+
<span className="text-blue-400">{dialogue.summary.totalSuggestions} suggestions</span>
|
|
405
|
+
</span>
|
|
406
|
+
{dialogue.summary.approvedCount > 0 && (
|
|
407
|
+
<span className="flex items-center gap-1">
|
|
408
|
+
<span className="w-1 h-1 rounded-full bg-green-500" />
|
|
409
|
+
<span className="text-green-400">{dialogue.summary.approvedCount} approved</span>
|
|
410
|
+
</span>
|
|
411
|
+
)}
|
|
412
|
+
</>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
<div className="flex items-center gap-3">
|
|
417
|
+
{dialogue.summary && (
|
|
418
|
+
<span className="text-xs text-gray-500 font-medium">
|
|
419
|
+
{(dialogue.summary.totalLatencyMs / 1000).toFixed(1)}s
|
|
420
|
+
</span>
|
|
421
|
+
)}
|
|
422
|
+
<div className={`w-7 h-7 rounded-full bg-white/5 flex items-center justify-center
|
|
423
|
+
transition-all duration-200 ${isExpanded ? 'bg-white/10' : ''}`}>
|
|
424
|
+
<svg
|
|
425
|
+
className={`w-3.5 h-3.5 text-gray-500 transition-transform duration-200 ${isExpanded ? 'rotate-180' : ''}`}
|
|
426
|
+
fill="none"
|
|
427
|
+
viewBox="0 0 24 24"
|
|
428
|
+
stroke="currentColor"
|
|
429
|
+
>
|
|
430
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
431
|
+
</svg>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
</button>
|
|
436
|
+
|
|
437
|
+
{/* Expanded dialogue entries */}
|
|
438
|
+
{isExpanded && dialogue.entries && (
|
|
439
|
+
<div className="border-t border-white/5 bg-black/20">
|
|
440
|
+
{dialogue.entries.map((entry, i) => (
|
|
441
|
+
<DialogueEntryItem key={i} entry={entry} index={i} />
|
|
442
|
+
))}
|
|
443
|
+
</div>
|
|
444
|
+
)}
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
})}
|
|
448
|
+
|
|
449
|
+
{/* Load more button - Glass styling */}
|
|
450
|
+
{hasMore && (
|
|
451
|
+
<button
|
|
452
|
+
type="button"
|
|
453
|
+
onClick={handleLoadMore}
|
|
454
|
+
disabled={isLoadingDialogues}
|
|
455
|
+
className="w-full p-4 text-sm font-medium text-gray-400 hover:text-white
|
|
456
|
+
bg-white/5 backdrop-blur-sm border border-white/5 rounded-xl
|
|
457
|
+
transition-all duration-200 disabled:opacity-50
|
|
458
|
+
active:scale-[0.99]"
|
|
459
|
+
>
|
|
460
|
+
{isLoadingDialogues ? (
|
|
461
|
+
<span className="flex items-center justify-center gap-2">
|
|
462
|
+
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
463
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
464
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
465
|
+
</svg>
|
|
466
|
+
Loading...
|
|
467
|
+
</span>
|
|
468
|
+
) : 'Load more dialogues'}
|
|
469
|
+
</button>
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
)}
|
|
473
|
+
|
|
474
|
+
{/* Bottom padding */}
|
|
475
|
+
<div className="h-4" />
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export default LogsView;
|