neoagent 2.3.1-beta.4 → 2.3.1-beta.41

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 (290) hide show
  1. package/.env.example +45 -0
  2. package/docs/capabilities.md +2 -2
  3. package/docs/configuration.md +12 -5
  4. package/docs/hardware.md +1 -1
  5. package/docs/integrations.md +2 -3
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +109 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  30. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  31. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  32. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  33. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  35. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  36. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  37. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  38. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  39. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  43. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  44. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  45. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  46. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  48. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  49. package/flutter_app/android/build.gradle.kts +24 -0
  50. package/flutter_app/android/ci-release.keystore +0 -0
  51. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  52. package/flutter_app/android/gradle.properties +3 -0
  53. package/flutter_app/android/key.properties +4 -0
  54. package/flutter_app/android/settings.gradle.kts +26 -0
  55. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  57. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  58. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  59. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  60. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  62. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  63. package/flutter_app/lib/features/location/location_service.dart +119 -0
  64. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  65. package/flutter_app/lib/main.dart +99 -0
  66. package/flutter_app/lib/main_account_settings.dart +1250 -0
  67. package/flutter_app/lib/main_admin.dart +886 -0
  68. package/flutter_app/lib/main_app_shell.dart +1682 -0
  69. package/flutter_app/lib/main_chat.dart +3352 -0
  70. package/flutter_app/lib/main_controller.dart +6781 -0
  71. package/flutter_app/lib/main_devices.dart +2301 -0
  72. package/flutter_app/lib/main_integrations.dart +1129 -0
  73. package/flutter_app/lib/main_launcher.dart +959 -0
  74. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  75. package/flutter_app/lib/main_models.dart +3546 -0
  76. package/flutter_app/lib/main_navigation.dart +193 -0
  77. package/flutter_app/lib/main_operations.dart +4851 -0
  78. package/flutter_app/lib/main_recordings.dart +870 -0
  79. package/flutter_app/lib/main_runtime.dart +806 -0
  80. package/flutter_app/lib/main_settings.dart +2024 -0
  81. package/flutter_app/lib/main_shared.dart +2861 -0
  82. package/flutter_app/lib/main_theme.dart +204 -0
  83. package/flutter_app/lib/main_voice_assistant.dart +957 -0
  84. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  85. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  86. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  87. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  88. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  89. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  90. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  91. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  92. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  93. package/flutter_app/lib/src/backend_client.dart +1833 -0
  94. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  95. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  96. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  97. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  98. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  99. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  100. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  101. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  102. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  103. package/flutter_app/lib/src/health_bridge.dart +136 -0
  104. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  105. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  106. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  107. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  108. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  109. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  110. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  111. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  112. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  113. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  114. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  115. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  116. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  117. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  118. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  119. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  120. package/flutter_app/lib/src/theme/palette.dart +81 -0
  121. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  122. package/flutter_app/linux/CMakeLists.txt +128 -0
  123. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  124. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  125. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  126. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  127. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  128. package/flutter_app/linux/runner/main.cc +6 -0
  129. package/flutter_app/linux/runner/my_application.cc +144 -0
  130. package/flutter_app/linux/runner/my_application.h +18 -0
  131. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  132. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  133. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  134. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  135. package/flutter_app/macos/Podfile +42 -0
  136. package/flutter_app/macos/Podfile.lock +87 -0
  137. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  138. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  139. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  140. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  141. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  142. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  143. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  144. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  145. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  146. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  147. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  148. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  149. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  150. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  151. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  152. package/flutter_app/macos/Runner/Info.plist +36 -0
  153. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  154. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  155. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  156. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  157. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  158. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  159. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  160. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  161. package/flutter_app/patch_strings.py +12 -0
  162. package/flutter_app/pubspec.lock +1088 -0
  163. package/flutter_app/pubspec.yaml +53 -0
  164. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  165. package/flutter_app/test/recording_payloads_test.dart +53 -0
  166. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  167. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  168. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  169. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  170. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  171. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  172. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  173. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  174. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  175. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  176. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  177. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  178. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  179. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  180. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  181. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  182. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  183. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  184. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  185. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  186. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  187. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  188. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  189. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  190. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  191. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  192. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  193. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  194. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  195. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  196. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  197. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  198. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  199. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  200. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  201. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  202. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  203. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  204. package/flutter_app/web/favicon.png +0 -0
  205. package/flutter_app/web/favicon.svg +12 -0
  206. package/flutter_app/web/icons/Icon-192.png +0 -0
  207. package/flutter_app/web/icons/Icon-512.png +0 -0
  208. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  209. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  210. package/flutter_app/web/index.html +39 -0
  211. package/flutter_app/web/manifest.json +35 -0
  212. package/flutter_app/windows/CMakeLists.txt +108 -0
  213. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  214. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  215. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  216. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  217. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  218. package/flutter_app/windows/runner/Runner.rc +121 -0
  219. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  220. package/flutter_app/windows/runner/flutter_window.h +37 -0
  221. package/flutter_app/windows/runner/main.cpp +53 -0
  222. package/flutter_app/windows/runner/resource.h +16 -0
  223. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  224. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  225. package/flutter_app/windows/runner/utils.cpp +65 -0
  226. package/flutter_app/windows/runner/utils.h +19 -0
  227. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  228. package/flutter_app/windows/runner/win32_window.h +102 -0
  229. package/lib/install_helpers.js +31 -0
  230. package/lib/manager.js +227 -6
  231. package/package.json +3 -1
  232. package/server/db/database.js +110 -0
  233. package/server/http/middleware.js +55 -2
  234. package/server/http/routes.js +1 -0
  235. package/server/index.js +3 -0
  236. package/server/public/.last_build_id +1 -1
  237. package/server/public/assets/NOTICES +1 -1
  238. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  239. package/server/public/canvaskit/wimp.wasm +0 -0
  240. package/server/public/flutter_bootstrap.js +2 -2
  241. package/server/public/main.dart.js +74324 -73132
  242. package/server/routes/integrations.js +108 -1
  243. package/server/routes/memory.js +11 -2
  244. package/server/{http/routes → routes}/screenHistory.js +2 -2
  245. package/server/routes/settings.js +75 -2
  246. package/server/{http/routes → routes}/triggers.js +2 -2
  247. package/server/routes/wearable.js +67 -0
  248. package/server/services/ai/models.js +30 -0
  249. package/server/services/ai/providers/githubCopilot.js +97 -0
  250. package/server/services/ai/providers/openai.js +2 -1
  251. package/server/services/ai/providers/openaiCodex.js +31 -0
  252. package/server/services/ai/settings.js +20 -0
  253. package/server/services/ai/toolSelector.js +14 -1
  254. package/server/services/ai/tools.js +77 -4
  255. package/server/services/desktop/screenRecorder.js +65 -9
  256. package/server/services/integrations/env.js +5 -0
  257. package/server/services/integrations/figma/provider.js +1 -0
  258. package/server/services/integrations/github/common.js +106 -0
  259. package/server/services/integrations/github/provider.js +499 -0
  260. package/server/services/integrations/github/repos.js +1124 -0
  261. package/server/services/integrations/google/provider.js +1 -0
  262. package/server/services/integrations/home_assistant/provider.js +325 -26
  263. package/server/services/integrations/manager.js +88 -12
  264. package/server/services/integrations/microsoft/provider.js +1 -0
  265. package/server/services/integrations/oauth_provider.js +25 -8
  266. package/server/services/integrations/provider_config_store.js +85 -0
  267. package/server/services/integrations/registry.js +4 -0
  268. package/server/services/integrations/spotify/provider.js +1 -0
  269. package/server/services/integrations/trello/provider.js +842 -0
  270. package/server/services/manager.js +46 -1
  271. package/server/services/mcp/client.js +120 -23
  272. package/server/services/memory/manager.js +39 -2
  273. package/server/services/messaging/access_policy.js +10 -0
  274. package/server/services/messaging/manager.js +49 -0
  275. package/server/services/messaging/meshtastic.js +260 -0
  276. package/server/services/messaging/meshtastic_env.js +100 -0
  277. package/server/services/messaging/meshtastic_protocol.js +476 -0
  278. package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
  279. package/server/services/tasks/runtime.js +1 -1
  280. package/server/services/voice/openaiClient.js +4 -1
  281. package/server/services/voice/openaiSpeech.js +6 -1
  282. package/server/services/voice/providers.js +52 -12
  283. package/server/services/voice/runtimeManager.js +136 -19
  284. package/server/services/voice/turnRunner.js +29 -9
  285. package/server/services/wearable/firmware_manifest.js +370 -0
  286. package/server/services/wearable/gateway.js +350 -0
  287. package/server/services/wearable/protocol.js +45 -0
  288. package/server/services/wearable/service.js +244 -0
  289. package/server/utils/local_secrets.js +56 -0
  290. package/server/utils/logger.js +37 -9
@@ -0,0 +1,870 @@
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 _SectionTitle('Recordings'),
63
+ const SizedBox(height: 12),
64
+ Card(
65
+ child: Padding(
66
+ padding: const EdgeInsets.all(22),
67
+ child: Column(
68
+ crossAxisAlignment: CrossAxisAlignment.start,
69
+ children: <Widget>[
70
+ Wrap(
71
+ spacing: 10,
72
+ runSpacing: 10,
73
+ crossAxisAlignment: WrapCrossAlignment.center,
74
+ children: <Widget>[
75
+ _DotStatus(
76
+ label: statusLabel,
77
+ color: statusColor,
78
+ ),
79
+ if (runtime.platformLabel != null &&
80
+ runtime.platformLabel!.isNotEmpty)
81
+ Text(
82
+ runtime.platformLabel!,
83
+ style: TextStyle(color: _textSecondary),
84
+ ),
85
+ ],
86
+ ),
87
+ const SizedBox(height: 16),
88
+ if (isStarting) ...<Widget>[
89
+ const SizedBox(height: 14),
90
+ Container(
91
+ width: double.infinity,
92
+ padding: const EdgeInsets.symmetric(
93
+ horizontal: 14,
94
+ vertical: 12,
95
+ ),
96
+ decoration: BoxDecoration(
97
+ color: _bgSecondary.withValues(alpha: 0.8),
98
+ borderRadius: BorderRadius.circular(16),
99
+ border: Border.all(color: _borderLight),
100
+ ),
101
+ child: Row(
102
+ children: <Widget>[
103
+ const SizedBox.square(
104
+ dimension: 16,
105
+ child: CircularProgressIndicator(strokeWidth: 2),
106
+ ),
107
+ const SizedBox(width: 12),
108
+ Expanded(
109
+ child: Text(
110
+ 'Starting recording. This can take a few seconds while the session and permissions are prepared.',
111
+ style: TextStyle(
112
+ color: _textSecondary,
113
+ height: 1.4,
114
+ ),
115
+ ),
116
+ ),
117
+ ],
118
+ ),
119
+ ),
120
+ ] else if (isStopping) ...<Widget>[
121
+ const SizedBox(height: 14),
122
+ Container(
123
+ width: double.infinity,
124
+ padding: const EdgeInsets.symmetric(
125
+ horizontal: 14,
126
+ vertical: 12,
127
+ ),
128
+ decoration: BoxDecoration(
129
+ color: _bgSecondary.withValues(alpha: 0.8),
130
+ borderRadius: BorderRadius.circular(16),
131
+ border: Border.all(color: _borderLight),
132
+ ),
133
+ child: Text(
134
+ 'Finalizing recording...',
135
+ style: TextStyle(color: _textSecondary, height: 1.4),
136
+ ),
137
+ ),
138
+ ],
139
+ const SizedBox(height: 18),
140
+ Wrap(
141
+ spacing: 12,
142
+ runSpacing: 12,
143
+ children: <Widget>[
144
+ if (runtime.supportsScreenAndMic)
145
+ FilledButton.icon(
146
+ onPressed:
147
+ widget.controller.isStartingRecording ||
148
+ runtime.active
149
+ ? null
150
+ : widget.controller.startWebRecording,
151
+ icon: Icon(Icons.desktop_windows_outlined),
152
+ label: Text('Screen + mic'),
153
+ ),
154
+ if (runtime.supportsScreenAndMic)
155
+ OutlinedButton.icon(
156
+ onPressed:
157
+ widget.controller.isStartingRecording ||
158
+ runtime.active
159
+ ? null
160
+ : widget.controller.startWebMicrophoneRecording,
161
+ icon: Icon(Icons.graphic_eq_outlined),
162
+ label: Text('Mic only'),
163
+ ),
164
+ if (runtime.supportsBackgroundMic)
165
+ FilledButton.icon(
166
+ onPressed:
167
+ widget.controller.isStartingRecording ||
168
+ runtime.active
169
+ ? null
170
+ : widget.controller.startBackgroundRecording,
171
+ icon: Icon(Icons.mic_none_outlined),
172
+ label: Text('Background mic'),
173
+ ),
174
+ if (runtime.supportsSystemAudio)
175
+ FilledButton.icon(
176
+ onPressed: widget.controller.canStartDesktopRecording
177
+ ? widget.controller.startDesktopRecording
178
+ : null,
179
+ style: FilledButton.styleFrom(
180
+ backgroundColor: _accentAlt,
181
+ foregroundColor: Colors.white,
182
+ ),
183
+ icon: Icon(Icons.surround_sound_outlined),
184
+ label: Text('Desktop studio'),
185
+ ),
186
+ if (runtime.supportsBackgroundMic && runtime.active)
187
+ OutlinedButton.icon(
188
+ onPressed: runtime.paused
189
+ ? widget.controller.resumeBackgroundRecording
190
+ : widget.controller.pauseBackgroundRecording,
191
+ icon: Icon(
192
+ runtime.paused ? Icons.play_arrow : Icons.pause,
193
+ ),
194
+ label: Text(runtime.paused ? 'Resume' : 'Pause'),
195
+ ),
196
+ if (runtime.supportsSystemAudio && runtime.active)
197
+ OutlinedButton.icon(
198
+ onPressed: runtime.paused
199
+ ? widget.controller.resumeDesktopRecording
200
+ : widget.controller.pauseDesktopRecording,
201
+ icon: Icon(
202
+ runtime.paused ? Icons.play_arrow : Icons.pause,
203
+ ),
204
+ label: Text(runtime.paused ? 'Resume' : 'Pause'),
205
+ ),
206
+ if (runtime.active)
207
+ OutlinedButton.icon(
208
+ onPressed: widget.controller.isStoppingRecording
209
+ ? null
210
+ : widget.controller.stopRecording,
211
+ icon: Icon(Icons.stop_circle_outlined),
212
+ label: Text('Stop'),
213
+ ),
214
+ if (runtime.supportsFloatingToolbar)
215
+ OutlinedButton.icon(
216
+ onPressed: !runtime.active
217
+ ? null
218
+ : (runtime.floatingToolbarVisible
219
+ ? widget.controller.hideDesktopFloatingToolbar
220
+ : widget
221
+ .controller
222
+ .showDesktopFloatingToolbar),
223
+ icon: Icon(
224
+ runtime.floatingToolbarVisible
225
+ ? Icons.visibility_off_outlined
226
+ : Icons.open_in_new_rounded,
227
+ ),
228
+ label: Text(
229
+ runtime.floatingToolbarVisible
230
+ ? 'Hide floating bar'
231
+ : 'Show floating bar',
232
+ ),
233
+ ),
234
+ OutlinedButton.icon(
235
+ onPressed: widget.controller.refreshRecordings,
236
+ icon: Icon(Icons.refresh),
237
+ label: Text('Refresh'),
238
+ ),
239
+ ],
240
+ ),
241
+ if (runtime.supportsSystemAudio) ...<Widget>[
242
+ const SizedBox(height: 20),
243
+ Container(
244
+ width: double.infinity,
245
+ padding: const EdgeInsets.all(18),
246
+ decoration: BoxDecoration(
247
+ color: _bgSecondary.withValues(alpha: 0.72),
248
+ borderRadius: BorderRadius.circular(22),
249
+ border: Border.all(color: _borderLight),
250
+ ),
251
+ child: Column(
252
+ crossAxisAlignment: CrossAxisAlignment.start,
253
+ children: <Widget>[
254
+ Text(
255
+ 'Desktop runtime diagnostics',
256
+ style: TextStyle(
257
+ fontSize: 15,
258
+ fontWeight: FontWeight.w700,
259
+ ),
260
+ ),
261
+ const SizedBox(height: 6),
262
+ Text(
263
+ 'Permissions and live levels stay visible while the floating bar handles quick controls.',
264
+ style: TextStyle(color: _textSecondary, height: 1.45),
265
+ ),
266
+ const SizedBox(height: 16),
267
+ Wrap(
268
+ spacing: 10,
269
+ runSpacing: 10,
270
+ children: <Widget>[
271
+ _RecordingPermissionBadge(
272
+ label: 'Microphone',
273
+ state: runtime.microphonePermission,
274
+ ),
275
+ _RecordingPermissionBadge(
276
+ label: 'System audio',
277
+ state: runtime.systemAudioPermission,
278
+ ),
279
+ _DotStatus(
280
+ label: runtime.backgroundRuntimeActive
281
+ ? 'Background runtime ready'
282
+ : 'Foreground only',
283
+ color: runtime.backgroundRuntimeActive
284
+ ? _success
285
+ : _warning,
286
+ ),
287
+ _DotStatus(
288
+ label: runtime.supportsGlobalHotkeys
289
+ ? 'Hotkey-ready'
290
+ : 'No global hotkeys',
291
+ color: runtime.supportsGlobalHotkeys
292
+ ? _success
293
+ : _warning,
294
+ ),
295
+ ],
296
+ ),
297
+ const SizedBox(height: 18),
298
+ Wrap(
299
+ spacing: 18,
300
+ runSpacing: 18,
301
+ children: <Widget>[
302
+ _AudioLevelBar(
303
+ label: 'Microphone',
304
+ valueDb: runtime.microphoneLevelDb,
305
+ color: _accent,
306
+ ),
307
+ _AudioLevelBar(
308
+ label: 'System audio',
309
+ valueDb: runtime.systemAudioLevelDb,
310
+ color: _accentAlt,
311
+ ),
312
+ ],
313
+ ),
314
+ const SizedBox(height: 18),
315
+ Wrap(
316
+ spacing: 12,
317
+ runSpacing: 12,
318
+ children: <Widget>[
319
+ if ((runtime.selectedInputDeviceName ?? '')
320
+ .trim()
321
+ .isNotEmpty)
322
+ _MetaPill(
323
+ icon: Icons.mic_external_on_outlined,
324
+ label:
325
+ 'Input ${runtime.selectedInputDeviceName!}',
326
+ ),
327
+ _MetaPill(
328
+ icon: Icons.tune_outlined,
329
+ label:
330
+ '${runtime.availableInputDevices.length} input device${runtime.availableInputDevices.length == 1 ? '' : 's'}',
331
+ ),
332
+ if (runtime.activeSources.isNotEmpty)
333
+ _MetaPill(
334
+ icon: Icons.multitrack_audio_outlined,
335
+ label: runtime.activeSources.join(' + '),
336
+ ),
337
+ ],
338
+ ),
339
+ const SizedBox(height: 16),
340
+ Wrap(
341
+ spacing: 12,
342
+ runSpacing: 12,
343
+ children: <Widget>[
344
+ OutlinedButton.icon(
345
+ onPressed: widget
346
+ .controller
347
+ .openDesktopMicrophoneSettings,
348
+ icon: Icon(Icons.settings_voice_outlined),
349
+ label: Text('Mic settings'),
350
+ ),
351
+ OutlinedButton.icon(
352
+ onPressed: widget
353
+ .controller
354
+ .openDesktopSystemAudioSettings,
355
+ icon: Icon(Icons.speaker_group_outlined),
356
+ label: Text('System audio settings'),
357
+ ),
358
+ ],
359
+ ),
360
+ ],
361
+ ),
362
+ ),
363
+ ],
364
+ if (runtime.errorMessage != null &&
365
+ runtime.errorMessage!.trim().isNotEmpty) ...<Widget>[
366
+ const SizedBox(height: 16),
367
+ _InlineError(message: runtime.errorMessage!),
368
+ ],
369
+ ],
370
+ ),
371
+ ),
372
+ ),
373
+ const SizedBox(height: 20),
374
+ const _SectionTitle('Transcripts'),
375
+ const SizedBox(height: 12),
376
+ if (widget.controller.recordingSessions.isEmpty)
377
+ const _EmptyCard(
378
+ title: 'No recordings yet',
379
+ subtitle: 'Start one and transcripts will appear here.',
380
+ )
381
+ else
382
+ ...widget.controller.recordingSessions.map(
383
+ (session) => Padding(
384
+ key: ValueKey<String>(session.id),
385
+ padding: const EdgeInsets.only(bottom: 12),
386
+ child: _RecordingSessionCard(
387
+ controller: widget.controller,
388
+ session: session,
389
+ onRetry:
390
+ (session.status == 'failed' ||
391
+ (session.status == 'completed' &&
392
+ session.transcriptText.trim().isEmpty &&
393
+ session.transcriptSegments.isEmpty &&
394
+ session.structuredContent.isEmpty))
395
+ ? () => widget.controller.retryRecording(session.id)
396
+ : null,
397
+ onDeleteSegment: (segment) =>
398
+ _deleteSegment(context, session, segment),
399
+ onDeleteRecording: () => _deleteRecording(context, session),
400
+ ),
401
+ ),
402
+ ),
403
+ ],
404
+ );
405
+ }
406
+ }
407
+
408
+ class _RecordingSessionCard extends StatelessWidget {
409
+ const _RecordingSessionCard({
410
+ required this.controller,
411
+ required this.session,
412
+ this.onRetry,
413
+ this.onDeleteSegment,
414
+ this.onDeleteRecording,
415
+ });
416
+
417
+ final NeoAgentController controller;
418
+ final RecordingSessionItem session;
419
+ final VoidCallback? onRetry;
420
+ final Future<void> Function(RecordingTranscriptSegment segment)?
421
+ onDeleteSegment;
422
+ final Future<void> Function()? onDeleteRecording;
423
+
424
+ @override
425
+ Widget build(BuildContext context) {
426
+ final runtime = controller.recordingRuntime;
427
+ final isLiveSession = runtime.active && runtime.sessionId == session.id;
428
+ final canDeleteRecording = onDeleteRecording != null && !isLiveSession;
429
+ return Card(
430
+ child: Padding(
431
+ padding: const EdgeInsets.all(18),
432
+ child: Column(
433
+ crossAxisAlignment: CrossAxisAlignment.start,
434
+ children: <Widget>[
435
+ Row(
436
+ crossAxisAlignment: CrossAxisAlignment.start,
437
+ children: <Widget>[
438
+ Expanded(
439
+ child: Column(
440
+ crossAxisAlignment: CrossAxisAlignment.start,
441
+ children: <Widget>[
442
+ Text(
443
+ session.title,
444
+ style: TextStyle(
445
+ fontSize: 16,
446
+ fontWeight: FontWeight.w700,
447
+ ),
448
+ ),
449
+ const SizedBox(height: 6),
450
+ Text(
451
+ '${session.startedAtLabel} • ${session.platformLabel} • ${session.durationLabel}',
452
+ style: TextStyle(color: _textSecondary),
453
+ ),
454
+ ],
455
+ ),
456
+ ),
457
+ _StatusPill(
458
+ label: session.statusLabel,
459
+ color: session.statusColor,
460
+ ),
461
+ ],
462
+ ),
463
+ const SizedBox(height: 12),
464
+ Wrap(
465
+ spacing: 8,
466
+ runSpacing: 8,
467
+ children: session.sources
468
+ .map(
469
+ (source) => Container(
470
+ padding: const EdgeInsets.symmetric(
471
+ horizontal: 10,
472
+ vertical: 7,
473
+ ),
474
+ decoration: BoxDecoration(
475
+ color: _bgSecondary,
476
+ borderRadius: BorderRadius.circular(999),
477
+ border: Border.all(color: _border),
478
+ ),
479
+ child: Text(
480
+ '${source.label} • ${source.durationLabel}',
481
+ style: TextStyle(fontSize: 12),
482
+ ),
483
+ ),
484
+ )
485
+ .toList(),
486
+ ),
487
+ if (session.sources.any(
488
+ (source) => source.mediaKind == 'audio',
489
+ )) ...<Widget>[
490
+ const SizedBox(height: 12),
491
+ _RecordingSourceAudioControls(
492
+ controller: controller,
493
+ session: session,
494
+ ),
495
+ ],
496
+ if (session.lastError != null &&
497
+ session.lastError!.trim().isNotEmpty)
498
+ Padding(
499
+ padding: const EdgeInsets.only(top: 12),
500
+ child: Text(
501
+ session.lastError!,
502
+ style: TextStyle(color: _danger),
503
+ ),
504
+ ),
505
+ if (session.structuredContent.isNotEmpty) ...<Widget>[
506
+ const SizedBox(height: 16),
507
+ Container(
508
+ padding: const EdgeInsets.all(14),
509
+ decoration: BoxDecoration(
510
+ color: _bgSecondary,
511
+ borderRadius: BorderRadius.circular(12),
512
+ border: Border.all(color: _accent.withValues(alpha: 0.3)),
513
+ ),
514
+ child: Column(
515
+ crossAxisAlignment: CrossAxisAlignment.start,
516
+ children: <Widget>[
517
+ Row(
518
+ children: <Widget>[
519
+ Icon(Icons.auto_awesome, size: 16, color: _accent),
520
+ const SizedBox(width: 8),
521
+ Text(
522
+ 'Insights',
523
+ style: TextStyle(
524
+ color: _accent,
525
+ fontWeight: FontWeight.w600,
526
+ ),
527
+ ),
528
+ ],
529
+ ),
530
+ if (session.structuredContent['summary'] !=
531
+ null) ...<Widget>[
532
+ const SizedBox(height: 10),
533
+ Text(
534
+ 'Summary',
535
+ style: TextStyle(
536
+ fontWeight: FontWeight.w700,
537
+ fontSize: 13,
538
+ color: _textSecondary,
539
+ ),
540
+ ),
541
+ const SizedBox(height: 4),
542
+ Text(
543
+ session.structuredContent['summary'].toString(),
544
+ style: TextStyle(height: 1.45),
545
+ ),
546
+ ],
547
+ if (session.structuredContent['action_items'] != null &&
548
+ _getStructuredList(
549
+ session,
550
+ 'action_items',
551
+ ).isNotEmpty) ...<Widget>[
552
+ const SizedBox(height: 10),
553
+ Text(
554
+ 'Action Items',
555
+ style: TextStyle(
556
+ fontWeight: FontWeight.w700,
557
+ fontSize: 13,
558
+ color: _textSecondary,
559
+ ),
560
+ ),
561
+ const SizedBox(height: 4),
562
+ ..._getStructuredList(session, 'action_items').map(
563
+ (item) => Padding(
564
+ padding: const EdgeInsets.only(bottom: 4),
565
+ child: Row(
566
+ crossAxisAlignment: CrossAxisAlignment.start,
567
+ children: [
568
+ Text(
569
+ '• ',
570
+ style: TextStyle(
571
+ fontWeight: FontWeight.w700,
572
+ color: _accent,
573
+ ),
574
+ ),
575
+ Expanded(
576
+ child: Text(
577
+ item.toString(),
578
+ style: TextStyle(height: 1.35),
579
+ ),
580
+ ),
581
+ ],
582
+ ),
583
+ ),
584
+ ),
585
+ ],
586
+ if (session.structuredContent['events'] != null &&
587
+ _getStructuredList(
588
+ session,
589
+ 'events',
590
+ ).isNotEmpty) ...<Widget>[
591
+ const SizedBox(height: 10),
592
+ Text(
593
+ 'Events Mentioned',
594
+ style: TextStyle(
595
+ fontWeight: FontWeight.w700,
596
+ fontSize: 13,
597
+ color: _textSecondary,
598
+ ),
599
+ ),
600
+ const SizedBox(height: 4),
601
+ ..._getStructuredList(session, 'events').map(
602
+ (item) => Padding(
603
+ padding: const EdgeInsets.only(bottom: 4),
604
+ child: Row(
605
+ crossAxisAlignment: CrossAxisAlignment.start,
606
+ children: [
607
+ Text(
608
+ '• ',
609
+ style: TextStyle(
610
+ fontWeight: FontWeight.w700,
611
+ color: _accent,
612
+ ),
613
+ ),
614
+ Expanded(
615
+ child: Text(
616
+ item.toString(),
617
+ style: TextStyle(height: 1.35),
618
+ ),
619
+ ),
620
+ ],
621
+ ),
622
+ ),
623
+ ),
624
+ ],
625
+ ],
626
+ ),
627
+ ),
628
+ ],
629
+ if (session.transcriptSegments.isNotEmpty) ...<Widget>[
630
+ const SizedBox(height: 16),
631
+ ...session.transcriptSegments.map(
632
+ (segment) => Padding(
633
+ padding: const EdgeInsets.only(bottom: 10),
634
+ child: Row(
635
+ crossAxisAlignment: CrossAxisAlignment.start,
636
+ children: <Widget>[
637
+ SizedBox(
638
+ width: 88,
639
+ child: Text(
640
+ segment.timestampLabel,
641
+ style: TextStyle(color: _textSecondary),
642
+ ),
643
+ ),
644
+ Expanded(
645
+ child: Text(
646
+ segment.displayText,
647
+ style: TextStyle(height: 1.45),
648
+ ),
649
+ ),
650
+ if (onDeleteSegment != null &&
651
+ segment.id > 0) ...<Widget>[
652
+ const SizedBox(width: 8),
653
+ IconButton(
654
+ onPressed: () async {
655
+ await onDeleteSegment!(segment);
656
+ },
657
+ icon: Icon(Icons.delete_outline),
658
+ tooltip: 'Delete segment',
659
+ visualDensity: VisualDensity.compact,
660
+ ),
661
+ ],
662
+ ],
663
+ ),
664
+ ),
665
+ ),
666
+ ] else if (session.transcriptText.isNotEmpty) ...<Widget>[
667
+ const SizedBox(height: 16),
668
+ Container(
669
+ width: double.infinity,
670
+ padding: const EdgeInsets.all(14),
671
+ decoration: BoxDecoration(
672
+ color: _bgSecondary,
673
+ borderRadius: BorderRadius.circular(14),
674
+ border: Border.all(color: _border),
675
+ ),
676
+ child: SelectableText(
677
+ session.transcriptText,
678
+ style: TextStyle(height: 1.45),
679
+ ),
680
+ ),
681
+ ] else ...<Widget>[
682
+ const SizedBox(height: 16),
683
+ Text(
684
+ session.status == 'processing'
685
+ ? 'Transcribing...'
686
+ : session.status == 'failed'
687
+ ? 'Transcription failed. Check the error above and retry.'
688
+ : session.status == 'completed'
689
+ ? 'No transcript text was returned. You can retry transcription.'
690
+ : 'Transcript is not available yet.',
691
+ style: TextStyle(color: _textSecondary),
692
+ ),
693
+ ],
694
+ if (onRetry != null || canDeleteRecording) ...<Widget>[
695
+ const SizedBox(height: 14),
696
+ Wrap(
697
+ spacing: 10,
698
+ runSpacing: 10,
699
+ children: <Widget>[
700
+ if (onRetry != null)
701
+ OutlinedButton.icon(
702
+ onPressed: onRetry,
703
+ icon: Icon(Icons.replay),
704
+ label: Text('Retry transcription'),
705
+ ),
706
+ if (canDeleteRecording)
707
+ OutlinedButton.icon(
708
+ onPressed: () async {
709
+ await onDeleteRecording!();
710
+ },
711
+ icon: Icon(Icons.delete_forever_outlined),
712
+ label: Text('Delete recording'),
713
+ style: OutlinedButton.styleFrom(foregroundColor: _danger),
714
+ ),
715
+ ],
716
+ ),
717
+ ],
718
+ ],
719
+ ),
720
+ ),
721
+ );
722
+ }
723
+
724
+ List<dynamic> _getStructuredList(RecordingSessionItem session, String key) {
725
+ final value = session.structuredContent[key];
726
+ if (value is List) {
727
+ return value;
728
+ }
729
+ return const [];
730
+ }
731
+ }
732
+
733
+ class _RecordingSourceAudioControls extends StatefulWidget {
734
+ const _RecordingSourceAudioControls({
735
+ required this.controller,
736
+ required this.session,
737
+ });
738
+
739
+ final NeoAgentController controller;
740
+ final RecordingSessionItem session;
741
+
742
+ @override
743
+ State<_RecordingSourceAudioControls> createState() =>
744
+ _RecordingSourceAudioControlsState();
745
+ }
746
+
747
+ class _RecordingSourceAudioControlsState
748
+ extends State<_RecordingSourceAudioControls> {
749
+ late final AudioPlayer _player;
750
+ StreamSubscription<void>? _playerCompleteSubscription;
751
+ String? _activeSourceKey;
752
+ bool _isPlaying = false;
753
+ int _loadToken = 0;
754
+
755
+ @override
756
+ void initState() {
757
+ super.initState();
758
+ _player = AudioPlayer();
759
+ _playerCompleteSubscription = _player.onPlayerComplete.listen((_) {
760
+ if (!mounted) {
761
+ return;
762
+ }
763
+ setState(() {
764
+ _isPlaying = false;
765
+ _activeSourceKey = null;
766
+ });
767
+ });
768
+ }
769
+
770
+ @override
771
+ void dispose() {
772
+ _playerCompleteSubscription?.cancel();
773
+ unawaited(_player.dispose());
774
+ super.dispose();
775
+ }
776
+
777
+ Future<void> _toggleSource(RecordingSourceItem source) async {
778
+ final token = ++_loadToken;
779
+ bool isStale() => !mounted || token != _loadToken;
780
+ if (_isPlaying && _activeSourceKey == source.sourceKey) {
781
+ await _player.stop();
782
+ if (isStale()) {
783
+ return;
784
+ }
785
+ setState(() {
786
+ _isPlaying = false;
787
+ _activeSourceKey = null;
788
+ });
789
+ return;
790
+ }
791
+
792
+ try {
793
+ await _player.stop();
794
+ if (isStale()) {
795
+ return;
796
+ }
797
+ final bytes = await widget.controller.fetchRecordingSourceAudioBytes(
798
+ widget.session.id,
799
+ source.sourceKey,
800
+ );
801
+ if (isStale()) {
802
+ return;
803
+ }
804
+ if (bytes.isEmpty) {
805
+ throw StateError('Audio source is empty.');
806
+ }
807
+ final mime = source.mimeType.trim().isNotEmpty
808
+ ? source.mimeType.trim()
809
+ : null;
810
+ await _player.play(BytesSource(bytes, mimeType: mime));
811
+ if (isStale()) {
812
+ await _player.stop();
813
+ return;
814
+ }
815
+ if (!mounted) {
816
+ return;
817
+ }
818
+ setState(() {
819
+ _isPlaying = true;
820
+ _activeSourceKey = source.sourceKey;
821
+ });
822
+ } catch (e) {
823
+ if (isStale()) {
824
+ return;
825
+ }
826
+ AppDiagnostics.log(
827
+ 'recording.playback',
828
+ 'source.play.failed',
829
+ data: <String, Object?>{
830
+ 'sessionId': widget.session.id,
831
+ 'sourceKey': source.sourceKey,
832
+ 'mimeType': source.mimeType,
833
+ },
834
+ error: e,
835
+ );
836
+ if (!mounted) {
837
+ return;
838
+ }
839
+ setState(() {
840
+ _isPlaying = false;
841
+ _activeSourceKey = null;
842
+ });
843
+ }
844
+ }
845
+
846
+ @override
847
+ Widget build(BuildContext context) {
848
+ final audioSources = widget.session.sources
849
+ .where((source) => source.mediaKind == 'audio')
850
+ .toList();
851
+ if (audioSources.isEmpty) {
852
+ return const SizedBox.shrink();
853
+ }
854
+
855
+ return Wrap(
856
+ spacing: 8,
857
+ runSpacing: 8,
858
+ children: audioSources.map((source) {
859
+ final isActive = _isPlaying && _activeSourceKey == source.sourceKey;
860
+ return OutlinedButton.icon(
861
+ onPressed: () => _toggleSource(source),
862
+ icon: Icon(isActive ? Icons.stop_circle_outlined : Icons.play_arrow),
863
+ label: Text(
864
+ isActive ? 'Stop ${source.label}' : 'Play ${source.label}',
865
+ ),
866
+ );
867
+ }).toList(),
868
+ );
869
+ }
870
+ }