neoagent 3.1.1-beta.9 → 3.2.1-beta.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 (151) 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 +875 -383
  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 +2 -2
  56. package/server/public/index.html +174 -0
  57. package/server/public/main.dart.js +92036 -91145
  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/formatting_guides.js +1 -0
  80. package/server/services/messaging/whatsapp.js +18 -0
  81. package/server/services/security/tool_categories.js +0 -5
  82. package/server/services/social_video/service.js +2 -20
  83. package/server/services/tasks/adapters/index.js +1 -1
  84. package/server/services/tasks/integration_runtime.js +3 -67
  85. package/server/services/tasks/runtime.js +1 -6
  86. package/server/services/tasks/task_repository.js +6 -13
  87. package/server/services/timeline/service.js +0 -224
  88. package/server/services/voice/message.js +0 -24
  89. package/server/services/voice/providers.js +42 -1
  90. package/server/services/voice/runtimeManager.js +9 -5
  91. package/server/services/voice/turnRunner.js +0 -22
  92. package/server/services/wearable/service.js +0 -5
  93. package/docs/recordings-and-health.md +0 -41
  94. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
  95. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
  96. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
  97. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
  98. package/flutter_app/lib/main_recordings.dart +0 -920
  99. package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
  100. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
  101. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
  102. package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
  103. package/flutter_app/lib/src/recording_bridge.dart +0 -232
  104. package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
  105. package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
  106. package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
  107. package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
  108. package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
  109. package/flutter_app/lib/src/recording_payloads.dart +0 -95
  110. package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
  111. package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
  112. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
  113. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
  114. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
  115. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
  116. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
  117. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
  118. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
  119. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
  120. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
  121. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
  122. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
  123. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
  124. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
  125. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
  126. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
  127. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
  128. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
  129. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
  130. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
  131. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
  132. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
  133. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
  134. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
  135. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
  136. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
  137. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
  138. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
  139. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
  140. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
  141. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
  142. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
  143. package/server/routes/recordings.js +0 -335
  144. package/server/routes/screenHistory.js +0 -190
  145. package/server/services/ai/recordingInsights.js +0 -105
  146. package/server/services/integrations/neomail/provider.js +0 -993
  147. package/server/services/recordings/deepgram.js +0 -58
  148. package/server/services/recordings/manager.js +0 -1273
  149. package/server/services/tasks/adapters/neomail_email_received.js +0 -46
  150. package/server/services/voice/screenshotContext.js +0 -73
  151. 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
- }