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