neoagent 3.1.1-beta.9 → 3.2.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 (150) hide show
  1. package/.env.example +2 -2
  2. package/README.md +5 -1
  3. package/docs/architecture.md +1 -1
  4. package/docs/benchmarking.md +68 -87
  5. package/docs/capabilities.md +1 -1
  6. package/docs/clients-and-devices.md +3 -4
  7. package/docs/configuration.md +1 -1
  8. package/docs/devices.md +1 -2
  9. package/docs/health.md +24 -0
  10. package/docs/index.md +2 -2
  11. package/docs/integrations-architecture.md +6 -0
  12. package/docs/integrations.md +15 -1
  13. package/docs/models.md +1 -1
  14. package/docs/persistence.md +1 -1
  15. package/docs/why-neoagent.md +1 -1
  16. package/flutter_app/README.md +0 -5
  17. package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
  19. package/flutter_app/lib/main.dart +0 -6
  20. package/flutter_app/lib/main_app_shell.dart +0 -2
  21. package/flutter_app/lib/main_controller.dart +2 -867
  22. package/flutter_app/lib/main_devices.dart +0 -73
  23. package/flutter_app/lib/main_integrations.dart +339 -187
  24. package/flutter_app/lib/main_launcher.dart +2 -212
  25. package/flutter_app/lib/main_models.dart +0 -259
  26. package/flutter_app/lib/main_navigation.dart +2 -14
  27. package/flutter_app/lib/main_operations.dart +322 -364
  28. package/flutter_app/lib/main_runtime.dart +9 -208
  29. package/flutter_app/lib/main_settings.dart +17 -199
  30. package/flutter_app/lib/main_shared.dart +20 -591
  31. package/flutter_app/lib/main_timeline.dart +26 -122
  32. package/flutter_app/lib/main_voice_assistant.dart +0 -5
  33. package/flutter_app/lib/src/backend_client.dart +0 -102
  34. package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
  35. package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
  36. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
  37. package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
  38. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  39. package/flutter_app/macos/Podfile.lock +56 -5
  40. package/flutter_app/pubspec.lock +0 -7
  41. package/flutter_app/pubspec.yaml +0 -2
  42. package/flutter_app/web/index.html +174 -0
  43. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
  44. package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
  45. package/landing/index.html +0 -2
  46. package/lib/manager.js +1 -1
  47. package/lib/schema_migrations.js +75 -0
  48. package/package.json +1 -4
  49. package/server/db/database.js +1 -133
  50. package/server/http/middleware.js +2 -16
  51. package/server/http/routes.js +0 -2
  52. package/server/public/.last_build_id +1 -1
  53. package/server/public/assets/NOTICES +0 -25
  54. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  55. package/server/public/flutter_bootstrap.js +1 -1
  56. package/server/public/index.html +174 -0
  57. package/server/public/main.dart.js +88081 -90036
  58. package/server/routes/auth.js +11 -2
  59. package/server/routes/settings.js +0 -5
  60. package/server/routes/voice_assistant.js +1 -128
  61. package/server/services/account/erasure.js +0 -3
  62. package/server/services/ai/deliverables/artifact_helpers.js +1 -1
  63. package/server/services/ai/loop/conversation_loop.js +6 -2
  64. package/server/services/ai/loop/tool_dispatch.js +0 -3
  65. package/server/services/ai/messagingFallback.js +1 -1
  66. package/server/services/ai/settings.js +0 -6
  67. package/server/services/ai/toolEvidence.js +1 -4
  68. package/server/services/ai/tools.js +0 -234
  69. package/server/services/desktop/auth.js +0 -3
  70. package/server/services/desktop/registry.js +2 -50
  71. package/server/services/integrations/neoarchive/provider.js +616 -0
  72. package/server/services/integrations/neorecall/client.js +140 -0
  73. package/server/services/integrations/neorecall/constants.js +87 -0
  74. package/server/services/integrations/neorecall/provider.js +163 -0
  75. package/server/services/integrations/registry.js +4 -2
  76. package/server/services/manager.js +0 -22
  77. package/server/services/memory/manager.js +44 -19
  78. package/server/services/memory/retrieval_reasoning.js +36 -14
  79. package/server/services/messaging/whatsapp.js +18 -0
  80. package/server/services/security/tool_categories.js +0 -5
  81. package/server/services/social_video/service.js +2 -20
  82. package/server/services/tasks/adapters/index.js +1 -1
  83. package/server/services/tasks/integration_runtime.js +3 -67
  84. package/server/services/tasks/runtime.js +1 -6
  85. package/server/services/tasks/task_repository.js +6 -13
  86. package/server/services/timeline/service.js +0 -224
  87. package/server/services/voice/message.js +0 -24
  88. package/server/services/voice/providers.js +42 -1
  89. package/server/services/voice/runtimeManager.js +9 -5
  90. package/server/services/voice/turnRunner.js +0 -22
  91. package/server/services/wearable/service.js +0 -5
  92. package/docs/recordings-and-health.md +0 -41
  93. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
  94. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
  95. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
  96. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
  97. package/flutter_app/lib/main_recordings.dart +0 -920
  98. package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
  99. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
  100. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
  101. package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
  102. package/flutter_app/lib/src/recording_bridge.dart +0 -232
  103. package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
  104. package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
  105. package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
  106. package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
  107. package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
  108. package/flutter_app/lib/src/recording_payloads.dart +0 -95
  109. package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
  110. package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
  111. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
  112. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
  113. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
  114. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
  115. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
  116. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
  117. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
  118. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
  119. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
  120. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
  121. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
  122. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
  123. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
  124. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
  125. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
  126. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
  127. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
  128. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
  129. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
  130. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
  131. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
  132. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
  133. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
  134. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
  135. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
  136. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
  137. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
  138. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
  139. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
  140. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
  141. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
  142. package/server/routes/recordings.js +0 -335
  143. package/server/routes/screenHistory.js +0 -190
  144. package/server/services/ai/recordingInsights.js +0 -105
  145. package/server/services/integrations/neomail/provider.js +0 -993
  146. package/server/services/recordings/deepgram.js +0 -58
  147. package/server/services/recordings/manager.js +0 -1273
  148. package/server/services/tasks/adapters/neomail_email_received.js +0 -46
  149. package/server/services/voice/screenshotContext.js +0 -73
  150. package/server/services/voice/turnCoordinator.js +0 -51
@@ -1,920 +0,0 @@
1
- part of 'main.dart';
2
-
3
- class RecordingsPanel extends StatefulWidget {
4
- const RecordingsPanel({super.key, required this.controller});
5
-
6
- final NeoAgentController controller;
7
-
8
- @override
9
- State<RecordingsPanel> createState() => _RecordingsPanelState();
10
- }
11
-
12
- class _RecordingsPanelState extends State<RecordingsPanel> {
13
- Future<void> _deleteSegment(
14
- BuildContext context,
15
- RecordingSessionItem session,
16
- RecordingTranscriptSegment segment,
17
- ) async {
18
- await _confirmDelete(
19
- context,
20
- title: 'Delete segment?',
21
- message:
22
- 'Remove the transcript segment at ${segment.timestampLabel} from "${session.title}"?',
23
- onConfirm: () =>
24
- widget.controller.deleteRecordingSegment(session.id, segment.id),
25
- );
26
- }
27
-
28
- Future<void> _deleteRecording(
29
- BuildContext context,
30
- RecordingSessionItem session,
31
- ) async {
32
- await _confirmDelete(
33
- context,
34
- title: 'Delete recording?',
35
- message:
36
- 'Remove the full recording "${session.title}", including audio chunks and transcript data?',
37
- onConfirm: () => widget.controller.deleteRecordingSession(session.id),
38
- );
39
- }
40
-
41
- @override
42
- Widget build(BuildContext context) {
43
- final runtime = widget.controller.recordingRuntime;
44
- final isStarting = widget.controller.isStartingRecording;
45
- final isStopping = widget.controller.isStoppingRecording;
46
- final statusLabel = isStarting
47
- ? 'Starting'
48
- : isStopping
49
- ? 'Stopping'
50
- : runtime.active
51
- ? (runtime.paused ? 'Paused' : 'Recording')
52
- : 'Ready';
53
- final statusColor = isStarting || isStopping
54
- ? _accent
55
- : runtime.active
56
- ? (runtime.paused ? _warning : _danger)
57
- : _success;
58
-
59
- return ListView(
60
- padding: _pagePadding(context),
61
- children: <Widget>[
62
- const _PageTitle(
63
- title: 'Recordings',
64
- subtitle:
65
- 'Capture sessions, monitor device state, and review transcripts with clearer status and playback controls.',
66
- ),
67
- Card(
68
- child: Padding(
69
- padding: const EdgeInsets.all(22),
70
- child: Column(
71
- crossAxisAlignment: CrossAxisAlignment.start,
72
- children: <Widget>[
73
- Row(
74
- crossAxisAlignment: CrossAxisAlignment.start,
75
- children: <Widget>[
76
- Expanded(
77
- child: Column(
78
- crossAxisAlignment: CrossAxisAlignment.start,
79
- children: <Widget>[
80
- Text(
81
- 'Capture controls',
82
- style: _displayTitleStyle(24),
83
- ),
84
- const SizedBox(height: 6),
85
- Text(
86
- runtime.platformLabel?.trim().isNotEmpty == true
87
- ? runtime.platformLabel!
88
- : 'Browser screen + microphone recorder',
89
- style: TextStyle(
90
- color: _textSecondary,
91
- fontSize: 15,
92
- height: 1.45,
93
- ),
94
- ),
95
- ],
96
- ),
97
- ),
98
- const SizedBox(width: 16),
99
- _StatusPill(label: statusLabel, color: statusColor),
100
- ],
101
- ),
102
- const SizedBox(height: 16),
103
- AnimatedSwitcher(
104
- duration: const Duration(milliseconds: 220),
105
- switchInCurve: Curves.easeOutCubic,
106
- switchOutCurve: Curves.easeInCubic,
107
- child: isStarting
108
- ? _GlassSurface(
109
- key: const ValueKey<String>('starting'),
110
- width: double.infinity,
111
- padding: const EdgeInsets.symmetric(
112
- horizontal: 14,
113
- vertical: 12,
114
- ),
115
- borderRadius: BorderRadius.circular(16),
116
- blurSigma: 10,
117
- fillColor: _bgCard.withValues(alpha: 0.88),
118
- borderColor: _borderLight,
119
- child: Row(
120
- children: <Widget>[
121
- const SizedBox.square(
122
- dimension: 16,
123
- child: CircularProgressIndicator(
124
- strokeWidth: 2,
125
- ),
126
- ),
127
- const SizedBox(width: 12),
128
- Expanded(
129
- child: Text(
130
- 'Starting recording. This can take a few seconds while the session and permissions are prepared.',
131
- style: TextStyle(
132
- color: _textSecondary,
133
- height: 1.4,
134
- ),
135
- ),
136
- ),
137
- ],
138
- ),
139
- )
140
- : isStopping
141
- ? _GlassSurface(
142
- key: const ValueKey<String>('stopping'),
143
- width: double.infinity,
144
- padding: const EdgeInsets.symmetric(
145
- horizontal: 14,
146
- vertical: 12,
147
- ),
148
- borderRadius: BorderRadius.circular(16),
149
- blurSigma: 10,
150
- fillColor: _bgCard.withValues(alpha: 0.88),
151
- borderColor: _borderLight,
152
- child: Text(
153
- 'Finalizing recording...',
154
- style: TextStyle(
155
- color: _textSecondary,
156
- height: 1.4,
157
- ),
158
- ),
159
- )
160
- : const SizedBox.shrink(),
161
- ),
162
- if (isStarting || isStopping) const SizedBox(height: 14),
163
- const SizedBox(height: 18),
164
- Wrap(
165
- spacing: 12,
166
- runSpacing: 12,
167
- children: <Widget>[
168
- if (runtime.supportsScreenAndMic)
169
- FilledButton.icon(
170
- onPressed:
171
- widget.controller.isStartingRecording ||
172
- runtime.active
173
- ? null
174
- : widget.controller.startWebRecording,
175
- icon: Icon(Icons.desktop_windows_outlined),
176
- label: Text('Screen + mic'),
177
- ),
178
- if (runtime.supportsScreenAndMic)
179
- OutlinedButton.icon(
180
- onPressed:
181
- widget.controller.isStartingRecording ||
182
- runtime.active
183
- ? null
184
- : widget.controller.startWebMicrophoneRecording,
185
- icon: Icon(Icons.graphic_eq_outlined),
186
- label: Text('Mic only'),
187
- ),
188
- if (runtime.supportsBackgroundMic)
189
- FilledButton.icon(
190
- onPressed:
191
- widget.controller.isStartingRecording ||
192
- runtime.active
193
- ? null
194
- : widget.controller.startBackgroundRecording,
195
- icon: Icon(Icons.mic_none_outlined),
196
- label: Text('Background mic'),
197
- ),
198
- if (runtime.supportsSystemAudio)
199
- FilledButton.icon(
200
- onPressed: widget.controller.canStartDesktopRecording
201
- ? widget.controller.startDesktopRecording
202
- : null,
203
- style: FilledButton.styleFrom(
204
- backgroundColor: _accentAlt,
205
- foregroundColor: Colors.white,
206
- ),
207
- icon: Icon(Icons.surround_sound_outlined),
208
- label: Text('Desktop studio'),
209
- ),
210
- if (runtime.supportsBackgroundMic && runtime.active)
211
- OutlinedButton.icon(
212
- onPressed: runtime.paused
213
- ? widget.controller.resumeBackgroundRecording
214
- : widget.controller.pauseBackgroundRecording,
215
- icon: Icon(
216
- runtime.paused ? Icons.play_arrow : Icons.pause,
217
- ),
218
- label: Text(runtime.paused ? 'Resume' : 'Pause'),
219
- ),
220
- if (runtime.supportsSystemAudio && runtime.active)
221
- OutlinedButton.icon(
222
- onPressed: runtime.paused
223
- ? widget.controller.resumeDesktopRecording
224
- : widget.controller.pauseDesktopRecording,
225
- icon: Icon(
226
- runtime.paused ? Icons.play_arrow : Icons.pause,
227
- ),
228
- label: Text(runtime.paused ? 'Resume' : 'Pause'),
229
- ),
230
- if (runtime.active)
231
- OutlinedButton.icon(
232
- onPressed: widget.controller.isStoppingRecording
233
- ? null
234
- : widget.controller.stopRecording,
235
- icon: Icon(Icons.stop_circle_outlined),
236
- label: Text('Stop'),
237
- ),
238
- if (runtime.supportsFloatingToolbar)
239
- OutlinedButton.icon(
240
- onPressed: !runtime.active
241
- ? null
242
- : (runtime.floatingToolbarVisible
243
- ? widget.controller.hideDesktopFloatingToolbar
244
- : widget
245
- .controller
246
- .showDesktopFloatingToolbar),
247
- icon: Icon(
248
- runtime.floatingToolbarVisible
249
- ? Icons.visibility_off_outlined
250
- : Icons.open_in_new_rounded,
251
- ),
252
- label: Text(
253
- runtime.floatingToolbarVisible
254
- ? 'Hide floating bar'
255
- : 'Show floating bar',
256
- ),
257
- ),
258
- OutlinedButton.icon(
259
- onPressed: widget.controller.refreshRecordings,
260
- icon: Icon(Icons.refresh),
261
- label: Text('Refresh'),
262
- ),
263
- ],
264
- ),
265
- if (runtime.supportsSystemAudio) ...<Widget>[
266
- const SizedBox(height: 20),
267
- _GlassSurface(
268
- width: double.infinity,
269
- padding: const EdgeInsets.all(18),
270
- borderRadius: BorderRadius.circular(22),
271
- blurSigma: 10,
272
- fillColor: _bgCard.withValues(alpha: 0.9),
273
- borderColor: _borderLight,
274
- child: Column(
275
- crossAxisAlignment: CrossAxisAlignment.start,
276
- children: <Widget>[
277
- Text(
278
- 'Desktop runtime diagnostics',
279
- style: TextStyle(
280
- fontSize: 15,
281
- fontWeight: FontWeight.w700,
282
- ),
283
- ),
284
- const SizedBox(height: 6),
285
- Text(
286
- 'Permissions and live levels stay visible while the floating bar handles quick controls.',
287
- style: TextStyle(color: _textSecondary, height: 1.45),
288
- ),
289
- const SizedBox(height: 16),
290
- Wrap(
291
- spacing: 10,
292
- runSpacing: 10,
293
- children: <Widget>[
294
- _RecordingPermissionBadge(
295
- label: 'Microphone',
296
- state: runtime.microphonePermission,
297
- ),
298
- _RecordingPermissionBadge(
299
- label: 'System audio',
300
- state: runtime.systemAudioPermission,
301
- ),
302
- _DotStatus(
303
- label: runtime.backgroundRuntimeActive
304
- ? 'Background runtime ready'
305
- : 'Foreground only',
306
- color: runtime.backgroundRuntimeActive
307
- ? _success
308
- : _warning,
309
- ),
310
- _DotStatus(
311
- label: runtime.supportsGlobalHotkeys
312
- ? 'Hotkey-ready'
313
- : 'No global hotkeys',
314
- color: runtime.supportsGlobalHotkeys
315
- ? _success
316
- : _warning,
317
- ),
318
- ],
319
- ),
320
- const SizedBox(height: 18),
321
- Wrap(
322
- spacing: 18,
323
- runSpacing: 18,
324
- children: <Widget>[
325
- _AudioLevelBar(
326
- label: 'Microphone',
327
- valueDb: runtime.microphoneLevelDb,
328
- color: _accent,
329
- ),
330
- _AudioLevelBar(
331
- label: 'System audio',
332
- valueDb: runtime.systemAudioLevelDb,
333
- color: _accentAlt,
334
- ),
335
- ],
336
- ),
337
- const SizedBox(height: 18),
338
- Wrap(
339
- spacing: 12,
340
- runSpacing: 12,
341
- children: <Widget>[
342
- if ((runtime.selectedInputDeviceName ?? '')
343
- .trim()
344
- .isNotEmpty)
345
- _MetaPill(
346
- icon: Icons.mic_external_on_outlined,
347
- label:
348
- 'Input ${runtime.selectedInputDeviceName!}',
349
- ),
350
- _MetaPill(
351
- icon: Icons.tune_outlined,
352
- label:
353
- '${runtime.availableInputDevices.length} input device${runtime.availableInputDevices.length == 1 ? '' : 's'}',
354
- ),
355
- if (runtime.activeSources.isNotEmpty)
356
- _MetaPill(
357
- icon: Icons.multitrack_audio_outlined,
358
- label: runtime.activeSources.join(' + '),
359
- ),
360
- ],
361
- ),
362
- const SizedBox(height: 16),
363
- Wrap(
364
- spacing: 12,
365
- runSpacing: 12,
366
- children: <Widget>[
367
- OutlinedButton.icon(
368
- onPressed: widget
369
- .controller
370
- .openDesktopMicrophoneSettings,
371
- icon: Icon(Icons.settings_voice_outlined),
372
- label: Text('Mic settings'),
373
- ),
374
- OutlinedButton.icon(
375
- onPressed: widget
376
- .controller
377
- .openDesktopSystemAudioSettings,
378
- icon: Icon(Icons.speaker_group_outlined),
379
- label: Text('System audio settings'),
380
- ),
381
- ],
382
- ),
383
- ],
384
- ),
385
- ),
386
- ],
387
- if (runtime.errorMessage != null &&
388
- runtime.errorMessage!.trim().isNotEmpty) ...<Widget>[
389
- const SizedBox(height: 16),
390
- _InlineError(message: runtime.errorMessage!),
391
- ],
392
- ],
393
- ),
394
- ),
395
- ),
396
- const _SectionTitle('Transcripts'),
397
- const SizedBox(height: 12),
398
- if (widget.controller.recordingSessions.isEmpty)
399
- const _EmptyCard(
400
- title: 'No recordings yet',
401
- subtitle: 'Start one and transcripts will appear here.',
402
- )
403
- else
404
- ...widget.controller.recordingSessions.map(
405
- (session) => Padding(
406
- key: ValueKey<String>(session.id),
407
- padding: const EdgeInsets.only(bottom: 12),
408
- child: _RecordingSessionCard(
409
- controller: widget.controller,
410
- session: session,
411
- onRetry:
412
- (session.status == 'failed' ||
413
- (session.status == 'completed' &&
414
- session.transcriptText.trim().isEmpty &&
415
- session.transcriptSegments.isEmpty &&
416
- session.structuredContent.isEmpty))
417
- ? () => widget.controller.retryRecording(session.id)
418
- : null,
419
- onDeleteSegment: (segment) =>
420
- _deleteSegment(context, session, segment),
421
- onDeleteRecording: () => _deleteRecording(context, session),
422
- ),
423
- ),
424
- ),
425
- ],
426
- );
427
- }
428
- }
429
-
430
- class _RecordingSessionCard extends StatelessWidget {
431
- const _RecordingSessionCard({
432
- required this.controller,
433
- required this.session,
434
- this.onRetry,
435
- this.onDeleteSegment,
436
- this.onDeleteRecording,
437
- });
438
-
439
- final NeoAgentController controller;
440
- final RecordingSessionItem session;
441
- final VoidCallback? onRetry;
442
- final Future<void> Function(RecordingTranscriptSegment segment)?
443
- onDeleteSegment;
444
- final Future<void> Function()? onDeleteRecording;
445
-
446
- @override
447
- Widget build(BuildContext context) {
448
- final runtime = controller.recordingRuntime;
449
- final isLiveSession = runtime.active && runtime.sessionId == session.id;
450
- final canDeleteRecording = onDeleteRecording != null && !isLiveSession;
451
- return AnimatedContainer(
452
- duration: const Duration(milliseconds: 240),
453
- curve: Curves.easeOutCubic,
454
- child: Card(
455
- child: Padding(
456
- padding: const EdgeInsets.all(18),
457
- child: Column(
458
- crossAxisAlignment: CrossAxisAlignment.start,
459
- children: <Widget>[
460
- Row(
461
- crossAxisAlignment: CrossAxisAlignment.start,
462
- children: <Widget>[
463
- Expanded(
464
- child: Column(
465
- crossAxisAlignment: CrossAxisAlignment.start,
466
- children: <Widget>[
467
- Text(
468
- session.title,
469
- style: TextStyle(
470
- fontSize: 16,
471
- fontWeight: FontWeight.w700,
472
- ),
473
- ),
474
- const SizedBox(height: 6),
475
- Text(
476
- '${session.startedAtLabel} • ${session.platformLabel} • ${session.durationLabel}',
477
- style: TextStyle(color: _textSecondary),
478
- ),
479
- ],
480
- ),
481
- ),
482
- _StatusPill(
483
- label: session.statusLabel,
484
- color: session.statusColor,
485
- ),
486
- ],
487
- ),
488
- const SizedBox(height: 14),
489
- Wrap(
490
- spacing: 8,
491
- runSpacing: 8,
492
- children: session.sources
493
- .map(
494
- (source) => _GlassSurface(
495
- padding: const EdgeInsets.symmetric(
496
- horizontal: 10,
497
- vertical: 7,
498
- ),
499
- borderRadius: BorderRadius.circular(999),
500
- blurSigma: 8,
501
- fillColor: _bgCard.withValues(alpha: 0.82),
502
- borderColor: _border,
503
- child: Text(
504
- '${source.label} • ${source.durationLabel}',
505
- style: TextStyle(fontSize: 12),
506
- ),
507
- ),
508
- )
509
- .toList(),
510
- ),
511
- if (session.sources.any(
512
- (source) => source.mediaKind == 'audio',
513
- )) ...<Widget>[
514
- const SizedBox(height: 12),
515
- _RecordingSourceAudioControls(
516
- controller: controller,
517
- session: session,
518
- ),
519
- ],
520
- if (session.lastError != null &&
521
- session.lastError!.trim().isNotEmpty)
522
- Padding(
523
- padding: const EdgeInsets.only(top: 12),
524
- child: Text(
525
- session.lastError!,
526
- style: TextStyle(color: _danger),
527
- ),
528
- ),
529
- if (session.structuredContent.isNotEmpty) ...<Widget>[
530
- const SizedBox(height: 16),
531
- _GlassSurface(
532
- padding: const EdgeInsets.all(16),
533
- borderRadius: BorderRadius.circular(18),
534
- blurSigma: 8,
535
- fillColor: _accentMuted.withValues(alpha: 0.16),
536
- borderColor: _accent.withValues(alpha: 0.26),
537
- child: Column(
538
- crossAxisAlignment: CrossAxisAlignment.start,
539
- children: <Widget>[
540
- Row(
541
- children: <Widget>[
542
- Icon(Icons.auto_awesome, size: 16, color: _accent),
543
- const SizedBox(width: 8),
544
- Text(
545
- 'Insights',
546
- style: TextStyle(
547
- color: _accent,
548
- fontWeight: FontWeight.w600,
549
- ),
550
- ),
551
- ],
552
- ),
553
- if (session.structuredContent['summary'] !=
554
- null) ...<Widget>[
555
- const SizedBox(height: 10),
556
- Text(
557
- 'Summary',
558
- style: TextStyle(
559
- fontWeight: FontWeight.w700,
560
- fontSize: 13,
561
- color: _textSecondary,
562
- ),
563
- ),
564
- const SizedBox(height: 4),
565
- Text(
566
- session.structuredContent['summary'].toString(),
567
- style: TextStyle(height: 1.55, fontSize: 15),
568
- ),
569
- ],
570
- if (session.structuredContent['action_items'] != null &&
571
- _getStructuredList(
572
- session,
573
- 'action_items',
574
- ).isNotEmpty) ...<Widget>[
575
- const SizedBox(height: 10),
576
- Text(
577
- 'Action Items',
578
- style: TextStyle(
579
- fontWeight: FontWeight.w700,
580
- fontSize: 13,
581
- color: _textSecondary,
582
- ),
583
- ),
584
- const SizedBox(height: 4),
585
- ..._getStructuredList(session, 'action_items').map(
586
- (item) => Padding(
587
- padding: const EdgeInsets.only(bottom: 4),
588
- child: Row(
589
- crossAxisAlignment: CrossAxisAlignment.start,
590
- children: [
591
- Text(
592
- '• ',
593
- style: TextStyle(
594
- fontWeight: FontWeight.w700,
595
- color: _accent,
596
- ),
597
- ),
598
- Expanded(
599
- child: Text(
600
- item.toString(),
601
- style: TextStyle(height: 1.35),
602
- ),
603
- ),
604
- ],
605
- ),
606
- ),
607
- ),
608
- ],
609
- if (session.structuredContent['events'] != null &&
610
- _getStructuredList(
611
- session,
612
- 'events',
613
- ).isNotEmpty) ...<Widget>[
614
- const SizedBox(height: 10),
615
- Text(
616
- 'Events Mentioned',
617
- style: TextStyle(
618
- fontWeight: FontWeight.w700,
619
- fontSize: 13,
620
- color: _textSecondary,
621
- ),
622
- ),
623
- const SizedBox(height: 4),
624
- ..._getStructuredList(session, 'events').map(
625
- (item) => Padding(
626
- padding: const EdgeInsets.only(bottom: 4),
627
- child: Row(
628
- crossAxisAlignment: CrossAxisAlignment.start,
629
- children: [
630
- Text(
631
- '• ',
632
- style: TextStyle(
633
- fontWeight: FontWeight.w700,
634
- color: _accent,
635
- ),
636
- ),
637
- Expanded(
638
- child: Text(
639
- item.toString(),
640
- style: TextStyle(height: 1.35),
641
- ),
642
- ),
643
- ],
644
- ),
645
- ),
646
- ),
647
- ],
648
- ],
649
- ),
650
- ),
651
- ],
652
- if (session.transcriptSegments.isNotEmpty) ...<Widget>[
653
- const SizedBox(height: 16),
654
- ...session.transcriptSegments.map(
655
- (segment) => Padding(
656
- padding: const EdgeInsets.only(bottom: 10),
657
- child: _GlassSurface(
658
- padding: const EdgeInsets.fromLTRB(14, 12, 10, 12),
659
- borderRadius: BorderRadius.circular(16),
660
- blurSigma: 6,
661
- fillColor: _bgCard.withValues(alpha: 0.9),
662
- borderColor: _border,
663
- child: Row(
664
- crossAxisAlignment: CrossAxisAlignment.start,
665
- children: <Widget>[
666
- SizedBox(
667
- width: 88,
668
- child: Text(
669
- segment.timestampLabel,
670
- style: TextStyle(
671
- color: _textSecondary,
672
- fontWeight: FontWeight.w600,
673
- ),
674
- ),
675
- ),
676
- Expanded(
677
- child: Text(
678
- segment.displayText,
679
- style: const TextStyle(
680
- height: 1.55,
681
- fontSize: 15,
682
- ),
683
- ),
684
- ),
685
- if (onDeleteSegment != null &&
686
- segment.id > 0) ...<Widget>[
687
- const SizedBox(width: 8),
688
- IconButton(
689
- onPressed: () async {
690
- await onDeleteSegment!(segment);
691
- },
692
- icon: Icon(Icons.delete_outline),
693
- tooltip: 'Delete segment',
694
- visualDensity: VisualDensity.compact,
695
- ),
696
- ],
697
- ],
698
- ),
699
- ),
700
- ),
701
- ),
702
- ] else if (session.transcriptText.isNotEmpty) ...<Widget>[
703
- const SizedBox(height: 16),
704
- _GlassSurface(
705
- width: double.infinity,
706
- padding: const EdgeInsets.all(16),
707
- borderRadius: BorderRadius.circular(16),
708
- blurSigma: 8,
709
- fillColor: _bgCard.withValues(alpha: 0.92),
710
- borderColor: _border,
711
- child: SelectableText(
712
- session.transcriptText,
713
- style: const TextStyle(height: 1.6, fontSize: 15),
714
- ),
715
- ),
716
- ] else ...<Widget>[
717
- const SizedBox(height: 16),
718
- Text(
719
- session.status == 'processing'
720
- ? 'Transcribing...'
721
- : session.status == 'failed'
722
- ? 'Transcription failed. Check the error above and retry.'
723
- : session.status == 'completed'
724
- ? 'No transcript text was returned. You can retry transcription.'
725
- : 'Transcript is not available yet.',
726
- style: TextStyle(color: _textSecondary),
727
- ),
728
- ],
729
- if (onRetry != null || canDeleteRecording) ...<Widget>[
730
- const SizedBox(height: 14),
731
- Wrap(
732
- spacing: 10,
733
- runSpacing: 10,
734
- children: <Widget>[
735
- if (onRetry != null)
736
- OutlinedButton.icon(
737
- onPressed: onRetry,
738
- icon: Icon(Icons.replay),
739
- label: Text('Retry transcription'),
740
- ),
741
- if (canDeleteRecording)
742
- OutlinedButton.icon(
743
- onPressed: () async {
744
- await onDeleteRecording!();
745
- },
746
- icon: Icon(Icons.delete_forever_outlined),
747
- label: Text('Delete recording'),
748
- style: OutlinedButton.styleFrom(
749
- foregroundColor: _danger,
750
- ),
751
- ),
752
- ],
753
- ),
754
- ],
755
- ],
756
- ),
757
- ),
758
- ),
759
- );
760
- }
761
-
762
- List<dynamic> _getStructuredList(RecordingSessionItem session, String key) {
763
- final value = session.structuredContent[key];
764
- if (value is List) {
765
- return value;
766
- }
767
- return const [];
768
- }
769
- }
770
-
771
- class _RecordingSourceAudioControls extends StatefulWidget {
772
- const _RecordingSourceAudioControls({
773
- required this.controller,
774
- required this.session,
775
- });
776
-
777
- final NeoAgentController controller;
778
- final RecordingSessionItem session;
779
-
780
- @override
781
- State<_RecordingSourceAudioControls> createState() =>
782
- _RecordingSourceAudioControlsState();
783
- }
784
-
785
- class _RecordingSourceAudioControlsState
786
- extends State<_RecordingSourceAudioControls> {
787
- late final AudioPlayer _player;
788
- StreamSubscription<void>? _playerCompleteSubscription;
789
- String? _activeSourceKey;
790
- bool _isPlaying = false;
791
- int _loadToken = 0;
792
-
793
- @override
794
- void initState() {
795
- super.initState();
796
- _player = AudioPlayer();
797
- _playerCompleteSubscription = _player.onPlayerComplete.listen((_) {
798
- if (!mounted) {
799
- return;
800
- }
801
- setState(() {
802
- _isPlaying = false;
803
- _activeSourceKey = null;
804
- });
805
- });
806
- }
807
-
808
- @override
809
- void dispose() {
810
- _playerCompleteSubscription?.cancel();
811
- unawaited(_player.dispose());
812
- super.dispose();
813
- }
814
-
815
- Future<void> _toggleSource(RecordingSourceItem source) async {
816
- final token = ++_loadToken;
817
- bool isStale() => !mounted || token != _loadToken;
818
- if (_isPlaying && _activeSourceKey == source.sourceKey) {
819
- await _player.stop();
820
- if (isStale()) {
821
- return;
822
- }
823
- setState(() {
824
- _isPlaying = false;
825
- _activeSourceKey = null;
826
- });
827
- return;
828
- }
829
-
830
- try {
831
- await _player.stop();
832
- if (isStale()) {
833
- return;
834
- }
835
- final bytes = await widget.controller.fetchRecordingSourceAudioBytes(
836
- widget.session.id,
837
- source.sourceKey,
838
- );
839
- if (isStale()) {
840
- return;
841
- }
842
- if (bytes.isEmpty) {
843
- throw StateError('Audio source is empty.');
844
- }
845
- final mime = source.mimeType.trim().isNotEmpty
846
- ? source.mimeType.trim()
847
- : null;
848
- await _player.play(BytesSource(bytes, mimeType: mime));
849
- if (isStale()) {
850
- await _player.stop();
851
- return;
852
- }
853
- if (!mounted) {
854
- return;
855
- }
856
- setState(() {
857
- _isPlaying = true;
858
- _activeSourceKey = source.sourceKey;
859
- });
860
- } catch (e) {
861
- if (isStale()) {
862
- return;
863
- }
864
- AppDiagnostics.log(
865
- 'recording.playback',
866
- 'source.play.failed',
867
- data: <String, Object?>{
868
- 'sessionId': widget.session.id,
869
- 'sourceKey': source.sourceKey,
870
- 'mimeType': source.mimeType,
871
- },
872
- error: e,
873
- );
874
- if (!mounted) {
875
- return;
876
- }
877
- setState(() {
878
- _isPlaying = false;
879
- _activeSourceKey = null;
880
- });
881
- }
882
- }
883
-
884
- @override
885
- Widget build(BuildContext context) {
886
- final audioSources = widget.session.sources
887
- .where((source) => source.mediaKind == 'audio')
888
- .toList();
889
- if (audioSources.isEmpty) {
890
- return const SizedBox.shrink();
891
- }
892
-
893
- return Wrap(
894
- spacing: 8,
895
- runSpacing: 8,
896
- children: audioSources.map((source) {
897
- final isActive = _isPlaying && _activeSourceKey == source.sourceKey;
898
- return AnimatedScale(
899
- duration: const Duration(milliseconds: 180),
900
- curve: Curves.easeOutCubic,
901
- scale: isActive ? 1.02 : 1,
902
- child: OutlinedButton.icon(
903
- onPressed: () => _toggleSource(source),
904
- icon: Icon(
905
- isActive ? Icons.stop_circle_outlined : Icons.play_arrow,
906
- ),
907
- label: Text(
908
- isActive ? 'Stop ${source.label}' : 'Play ${source.label}',
909
- ),
910
- style: OutlinedButton.styleFrom(
911
- backgroundColor: isActive
912
- ? _accentMuted.withValues(alpha: 0.2)
913
- : null,
914
- ),
915
- ),
916
- );
917
- }).toList(),
918
- );
919
- }
920
- }