neoagent 2.4.1-beta.8 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -139,15 +139,6 @@ const _desktopSettingsSection = _SettingsSection('desktop', <String>[
|
|
|
139
139
|
'input',
|
|
140
140
|
], requiresDesktop: true);
|
|
141
141
|
|
|
142
|
-
const _updatesSettingsSection = _SettingsSection('updates', <String>[
|
|
143
|
-
'updates',
|
|
144
|
-
'release',
|
|
145
|
-
'channel',
|
|
146
|
-
'version',
|
|
147
|
-
'self update',
|
|
148
|
-
'upgrade',
|
|
149
|
-
]);
|
|
150
|
-
|
|
151
142
|
const _diagnosticsSettingsSection = _SettingsSection('diagnostics', <String>[
|
|
152
143
|
'diagnostics',
|
|
153
144
|
'logs',
|
|
@@ -163,13 +154,13 @@ const List<_SettingsSection> _settingsSearchSections = <_SettingsSection>[
|
|
|
163
154
|
_modelsSettingsSection,
|
|
164
155
|
_voiceRecordingSettingsSection,
|
|
165
156
|
_desktopSettingsSection,
|
|
166
|
-
_updatesSettingsSection,
|
|
167
157
|
_diagnosticsSettingsSection,
|
|
168
158
|
];
|
|
169
159
|
|
|
170
160
|
class _SettingsPanelState extends State<SettingsPanel> {
|
|
171
161
|
late final TextEditingController _searchController;
|
|
172
162
|
late String _browserBackend;
|
|
163
|
+
String? _browserExtensionTokenId;
|
|
173
164
|
late String _cliBackend;
|
|
174
165
|
String? _cliDesktopDeviceId;
|
|
175
166
|
late bool _smarterSelector;
|
|
@@ -186,7 +177,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
186
177
|
final Map<String, bool> _providerEnabled = <String, bool>{};
|
|
187
178
|
final Map<String, TextEditingController> _providerBaseUrlControllers =
|
|
188
179
|
<String, TextEditingController>{};
|
|
189
|
-
final Set<String> _expandedProviderIds = <String>{};
|
|
190
180
|
|
|
191
181
|
// Inline runtime test state — ephemeral, not stored in controller.
|
|
192
182
|
bool _cliTestRunning = false;
|
|
@@ -233,6 +223,9 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
233
223
|
.map((model) => model.id)
|
|
234
224
|
.toSet();
|
|
235
225
|
_browserBackend = _normalizeBrowserBackend(controller.browserBackend);
|
|
226
|
+
_browserExtensionTokenId =
|
|
227
|
+
controller.browserExtensionTokenId ??
|
|
228
|
+
controller.selectedBrowserExtensionTokenId;
|
|
236
229
|
_cliBackend = _normalizeCliBackend(controller.cliBackend);
|
|
237
230
|
_cliDesktopDeviceId = controller.cliDesktopDeviceId;
|
|
238
231
|
_smarterSelector = controller.smarterSelector;
|
|
@@ -309,26 +302,8 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
309
302
|
final routingModels = availableModels.isEmpty
|
|
310
303
|
? controller.supportedModels
|
|
311
304
|
: availableModels;
|
|
312
|
-
final modelChoices =
|
|
313
|
-
|
|
314
|
-
value: 'auto',
|
|
315
|
-
child: Text(
|
|
316
|
-
'Smart Selector (Auto)',
|
|
317
|
-
maxLines: 1,
|
|
318
|
-
overflow: TextOverflow.ellipsis,
|
|
319
|
-
),
|
|
320
|
-
),
|
|
321
|
-
...routingModels.map(
|
|
322
|
-
(model) => DropdownMenuItem<String>(
|
|
323
|
-
value: model.id,
|
|
324
|
-
child: Text(
|
|
325
|
-
model.label,
|
|
326
|
-
maxLines: 1,
|
|
327
|
-
overflow: TextOverflow.ellipsis,
|
|
328
|
-
),
|
|
329
|
-
),
|
|
330
|
-
),
|
|
331
|
-
];
|
|
305
|
+
final List<_ModelPickerOption> modelChoices =
|
|
306
|
+
_modelPickerOptions(routingModels, allowAuto: true);
|
|
332
307
|
final enabledSmartModels = _enabledModels
|
|
333
308
|
.where((id) => routingModels.any((model) => model.id == id))
|
|
334
309
|
.length;
|
|
@@ -343,7 +318,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
343
318
|
_PageTitle(
|
|
344
319
|
title: 'Settings',
|
|
345
320
|
subtitle:
|
|
346
|
-
'Workspace, models, recording,
|
|
321
|
+
'Workspace, models, recording, and diagnostics controls.',
|
|
347
322
|
trailing: _settingsSaveButton(controller),
|
|
348
323
|
)
|
|
349
324
|
else
|
|
@@ -363,7 +338,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
363
338
|
onChanged: (_) => setState(() {}),
|
|
364
339
|
decoration: InputDecoration(
|
|
365
340
|
labelText: 'Search settings',
|
|
366
|
-
hintText: 'Models, browser, voice,
|
|
341
|
+
hintText: 'Models, browser, voice, diagnostics...',
|
|
367
342
|
prefixIcon: const Icon(Icons.search),
|
|
368
343
|
suffixIcon: searchQuery.isEmpty
|
|
369
344
|
? null
|
|
@@ -396,6 +371,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
396
371
|
_modelsSettingsSection,
|
|
397
372
|
)) ...<Widget>[
|
|
398
373
|
_buildModelsSection(
|
|
374
|
+
context: context,
|
|
399
375
|
controller: controller,
|
|
400
376
|
modelChoices: modelChoices,
|
|
401
377
|
routingModels: routingModels,
|
|
@@ -423,13 +399,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
423
399
|
_buildDesktopSection(controller),
|
|
424
400
|
const SizedBox(height: 16),
|
|
425
401
|
],
|
|
426
|
-
if (_matchesSettingsSection(
|
|
427
|
-
searchQuery,
|
|
428
|
-
_updatesSettingsSection,
|
|
429
|
-
)) ...<Widget>[
|
|
430
|
-
_buildUpdatesSection(controller),
|
|
431
|
-
const SizedBox(height: 16),
|
|
432
|
-
],
|
|
433
402
|
if (_matchesSettingsSection(
|
|
434
403
|
searchQuery,
|
|
435
404
|
_diagnosticsSettingsSection,
|
|
@@ -438,7 +407,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
438
407
|
const _EmptyCard(
|
|
439
408
|
title: 'No matching settings',
|
|
440
409
|
subtitle:
|
|
441
|
-
'Try a broader search like models, browser,
|
|
410
|
+
'Try a broader search like models, browser, or voice.',
|
|
442
411
|
),
|
|
443
412
|
],
|
|
444
413
|
],
|
|
@@ -476,6 +445,9 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
476
445
|
browserBackend: _browserBackend == 'extension'
|
|
477
446
|
? 'extension'
|
|
478
447
|
: 'vm',
|
|
448
|
+
browserExtensionTokenId: _browserBackend == 'extension'
|
|
449
|
+
? _browserExtensionTokenId
|
|
450
|
+
: null,
|
|
479
451
|
cliBackend: _cliBackend == 'desktop' ? 'desktop' : 'vm',
|
|
480
452
|
cliDesktopDeviceId: _cliDesktopDeviceId,
|
|
481
453
|
smarterSelector: _smarterSelector,
|
|
@@ -545,7 +517,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
545
517
|
const _SectionTitle('Overview'),
|
|
546
518
|
const SizedBox(height: 10),
|
|
547
519
|
Text(
|
|
548
|
-
'Configure workspace behavior,
|
|
520
|
+
'Configure workspace behavior, models, and recording defaults.',
|
|
549
521
|
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
550
522
|
),
|
|
551
523
|
const SizedBox(height: 14),
|
|
@@ -642,12 +614,62 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
642
614
|
],
|
|
643
615
|
onChanged: (value) {
|
|
644
616
|
if (value != null) {
|
|
645
|
-
setState(()
|
|
617
|
+
setState(() {
|
|
618
|
+
_browserBackend = value;
|
|
619
|
+
_browserExtensionTokenId ??=
|
|
620
|
+
controller.selectedBrowserExtensionTokenId;
|
|
621
|
+
});
|
|
646
622
|
}
|
|
647
623
|
},
|
|
648
624
|
),
|
|
649
625
|
const SizedBox(height: 10),
|
|
650
626
|
if (_browserBackend == 'extension') ...<Widget>[
|
|
627
|
+
if (controller.browserExtensionTokens.isNotEmpty) ...<Widget>[
|
|
628
|
+
DropdownButtonFormField<String>(
|
|
629
|
+
initialValue: controller.browserExtensionTokens.any(
|
|
630
|
+
(token) => token['tokenId']?.toString() == _browserExtensionTokenId,
|
|
631
|
+
)
|
|
632
|
+
? _browserExtensionTokenId
|
|
633
|
+
: null,
|
|
634
|
+
decoration: const InputDecoration(
|
|
635
|
+
labelText: 'Default extension',
|
|
636
|
+
helperText: 'Choose which paired Chrome extension controls browser actions.',
|
|
637
|
+
),
|
|
638
|
+
items: controller.browserExtensionTokens.map((token) {
|
|
639
|
+
final tokenId = token['tokenId']?.toString() ?? '';
|
|
640
|
+
final label = token['name']?.toString().trim().isNotEmpty == true
|
|
641
|
+
? token['name'].toString()
|
|
642
|
+
: tokenId;
|
|
643
|
+
final online = token['online'] == true || token['connected'] == true;
|
|
644
|
+
return DropdownMenuItem<String>(
|
|
645
|
+
value: tokenId,
|
|
646
|
+
child: Row(
|
|
647
|
+
children: <Widget>[
|
|
648
|
+
Icon(
|
|
649
|
+
online ? Icons.circle : Icons.circle_outlined,
|
|
650
|
+
size: 10,
|
|
651
|
+
color: online ? Colors.green : Colors.grey,
|
|
652
|
+
),
|
|
653
|
+
const SizedBox(width: 8),
|
|
654
|
+
Flexible(
|
|
655
|
+
child: Text(
|
|
656
|
+
label,
|
|
657
|
+
maxLines: 1,
|
|
658
|
+
overflow: TextOverflow.ellipsis,
|
|
659
|
+
),
|
|
660
|
+
),
|
|
661
|
+
],
|
|
662
|
+
),
|
|
663
|
+
);
|
|
664
|
+
}).toList(),
|
|
665
|
+
onChanged: (value) {
|
|
666
|
+
if (value != null) {
|
|
667
|
+
setState(() => _browserExtensionTokenId = value);
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
),
|
|
671
|
+
const SizedBox(height: 10),
|
|
672
|
+
],
|
|
651
673
|
_buildInlineTestRow(
|
|
652
674
|
label: 'Chrome extension',
|
|
653
675
|
running: _extensionTestRunning,
|
|
@@ -659,11 +681,17 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
659
681
|
setState(() { _extensionTestRunning = true; _extensionTestResult = null; });
|
|
660
682
|
try {
|
|
661
683
|
final r = await controller.testBrowserExtension();
|
|
662
|
-
|
|
684
|
+
if (mounted) {
|
|
685
|
+
setState(() => _extensionTestResult = r);
|
|
686
|
+
}
|
|
663
687
|
} catch (e) {
|
|
664
|
-
|
|
688
|
+
if (mounted) {
|
|
689
|
+
setState(() => _extensionTestResult = <String, dynamic>{'passed': false, 'detail': e.toString()});
|
|
690
|
+
}
|
|
665
691
|
} finally {
|
|
666
|
-
|
|
692
|
+
if (mounted) {
|
|
693
|
+
setState(() => _extensionTestRunning = false);
|
|
694
|
+
}
|
|
667
695
|
}
|
|
668
696
|
},
|
|
669
697
|
),
|
|
@@ -764,18 +792,24 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
764
792
|
result: _cliTestResult,
|
|
765
793
|
note: _cliBackend == 'desktop'
|
|
766
794
|
? (controller.desktopCompanionConnected
|
|
767
|
-
? 'Desktop app connected — commands
|
|
795
|
+
? 'Desktop app connected — commands route locally through the companion.'
|
|
768
796
|
: 'Desktop app selected but not connected. Commands fall back to cloud VM until the companion is online.')
|
|
769
797
|
: 'Cloud VM — commands run in an isolated container.',
|
|
770
798
|
onTest: () async {
|
|
771
799
|
setState(() { _cliTestRunning = true; _cliTestResult = null; });
|
|
772
800
|
try {
|
|
773
801
|
final r = await controller.testCliRuntime();
|
|
774
|
-
|
|
802
|
+
if (mounted) {
|
|
803
|
+
setState(() => _cliTestResult = r);
|
|
804
|
+
}
|
|
775
805
|
} catch (e) {
|
|
776
|
-
|
|
806
|
+
if (mounted) {
|
|
807
|
+
setState(() => _cliTestResult = <String, dynamic>{'passed': false, 'detail': e.toString()});
|
|
808
|
+
}
|
|
777
809
|
} finally {
|
|
778
|
-
|
|
810
|
+
if (mounted) {
|
|
811
|
+
setState(() => _cliTestRunning = false);
|
|
812
|
+
}
|
|
779
813
|
}
|
|
780
814
|
},
|
|
781
815
|
),
|
|
@@ -802,8 +836,9 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
802
836
|
}
|
|
803
837
|
|
|
804
838
|
Widget _buildModelsSection({
|
|
839
|
+
required BuildContext context,
|
|
805
840
|
required NeoAgentController controller,
|
|
806
|
-
required List<
|
|
841
|
+
required List<_ModelPickerOption> modelChoices,
|
|
807
842
|
required List<ModelMeta> routingModels,
|
|
808
843
|
required List<ModelMeta> availableModels,
|
|
809
844
|
required int enabledSmartModels,
|
|
@@ -817,88 +852,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
817
852
|
const _SectionTitle('Models'),
|
|
818
853
|
const SizedBox(height: 10),
|
|
819
854
|
Text(
|
|
820
|
-
'
|
|
855
|
+
'Choose defaults for chat, agents, fallback behavior, and smart routing.',
|
|
821
856
|
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
822
857
|
),
|
|
823
858
|
const SizedBox(height: 16),
|
|
824
|
-
Text(
|
|
825
|
-
'Providers',
|
|
826
|
-
style: TextStyle(
|
|
827
|
-
fontWeight: FontWeight.w700,
|
|
828
|
-
color: _textPrimary,
|
|
829
|
-
),
|
|
830
|
-
),
|
|
831
|
-
const SizedBox(height: 14),
|
|
832
|
-
if (controller.aiProviders.isEmpty)
|
|
833
|
-
Text(
|
|
834
|
-
'Provider metadata is unavailable on this server version.',
|
|
835
|
-
style: TextStyle(color: _textSecondary),
|
|
836
|
-
)
|
|
837
|
-
else
|
|
838
|
-
LayoutBuilder(
|
|
839
|
-
builder: (context, constraints) {
|
|
840
|
-
final compact = constraints.maxWidth < 960;
|
|
841
|
-
final cardWidth = compact
|
|
842
|
-
? constraints.maxWidth
|
|
843
|
-
: (constraints.maxWidth - 16) / 2;
|
|
844
|
-
return Wrap(
|
|
845
|
-
spacing: 16,
|
|
846
|
-
runSpacing: 16,
|
|
847
|
-
children: controller.aiProviders
|
|
848
|
-
.where(
|
|
849
|
-
(provider) =>
|
|
850
|
-
provider.available ||
|
|
851
|
-
_providerEnabled[provider.id] == true ||
|
|
852
|
-
controller
|
|
853
|
-
.aiProviderConfigs[provider.id]
|
|
854
|
-
?.enabled ==
|
|
855
|
-
true,
|
|
856
|
-
)
|
|
857
|
-
.map((provider) {
|
|
858
|
-
return SizedBox(
|
|
859
|
-
width: cardWidth,
|
|
860
|
-
child: _AiProviderCard(
|
|
861
|
-
provider: provider,
|
|
862
|
-
enabled:
|
|
863
|
-
_providerEnabled[provider.id] ??
|
|
864
|
-
controller
|
|
865
|
-
.aiProviderConfigs[provider.id]
|
|
866
|
-
?.enabled ??
|
|
867
|
-
true,
|
|
868
|
-
models: controller.supportedModels
|
|
869
|
-
.where(
|
|
870
|
-
(model) => model.provider == provider.id,
|
|
871
|
-
)
|
|
872
|
-
.toList(),
|
|
873
|
-
baseUrlController:
|
|
874
|
-
_providerBaseUrlControllers[provider.id]!,
|
|
875
|
-
expanded: _expandedProviderIds.contains(
|
|
876
|
-
provider.id,
|
|
877
|
-
),
|
|
878
|
-
onEnabledChanged: (value) {
|
|
879
|
-
setState(() {
|
|
880
|
-
_providerEnabled[provider.id] = value;
|
|
881
|
-
});
|
|
882
|
-
},
|
|
883
|
-
onExpandToggle: () {
|
|
884
|
-
setState(() {
|
|
885
|
-
if (_expandedProviderIds.contains(
|
|
886
|
-
provider.id,
|
|
887
|
-
)) {
|
|
888
|
-
_expandedProviderIds.remove(provider.id);
|
|
889
|
-
} else {
|
|
890
|
-
_expandedProviderIds.add(provider.id);
|
|
891
|
-
}
|
|
892
|
-
});
|
|
893
|
-
},
|
|
894
|
-
),
|
|
895
|
-
);
|
|
896
|
-
})
|
|
897
|
-
.toList(),
|
|
898
|
-
);
|
|
899
|
-
},
|
|
900
|
-
),
|
|
901
|
-
const Divider(height: 32),
|
|
902
859
|
Text(
|
|
903
860
|
'Default Routing',
|
|
904
861
|
style: TextStyle(
|
|
@@ -928,7 +885,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
928
885
|
routingModels,
|
|
929
886
|
allowAuto: true,
|
|
930
887
|
),
|
|
931
|
-
|
|
888
|
+
options: modelChoices,
|
|
932
889
|
onChanged: (value) {
|
|
933
890
|
if (value != null) {
|
|
934
891
|
setState(() => _defaultChatModel = value);
|
|
@@ -946,7 +903,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
946
903
|
routingModels,
|
|
947
904
|
allowAuto: true,
|
|
948
905
|
),
|
|
949
|
-
|
|
906
|
+
options: modelChoices,
|
|
950
907
|
onChanged: (value) {
|
|
951
908
|
if (value != null) {
|
|
952
909
|
setState(() => _defaultSubagentModel = value);
|
|
@@ -964,18 +921,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
964
921
|
routingModels,
|
|
965
922
|
allowAuto: false,
|
|
966
923
|
),
|
|
967
|
-
|
|
968
|
-
.map(
|
|
969
|
-
(model) => DropdownMenuItem<String>(
|
|
970
|
-
value: model.id,
|
|
971
|
-
child: Text(
|
|
972
|
-
model.label,
|
|
973
|
-
maxLines: 1,
|
|
974
|
-
overflow: TextOverflow.ellipsis,
|
|
975
|
-
),
|
|
976
|
-
),
|
|
977
|
-
)
|
|
978
|
-
.toList(),
|
|
924
|
+
options: _modelPickerOptions(routingModels),
|
|
979
925
|
onChanged: (value) {
|
|
980
926
|
if (value != null) {
|
|
981
927
|
setState(() => _fallbackModel = value);
|
|
@@ -996,46 +942,38 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
996
942
|
),
|
|
997
943
|
),
|
|
998
944
|
const SizedBox(height: 10),
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
945
|
+
Text(
|
|
946
|
+
'The models the Smart Selector routes between automatically.',
|
|
947
|
+
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
948
|
+
),
|
|
949
|
+
const SizedBox(height: 12),
|
|
950
|
+
_SmartPoolSummary(
|
|
951
|
+
allModels: controller.supportedModels,
|
|
952
|
+
selectedIds: _enabledModels,
|
|
953
|
+
onManage: () async {
|
|
954
|
+
final result = await showGeneralDialog<Set<String>>(
|
|
955
|
+
context: context,
|
|
956
|
+
barrierDismissible: true,
|
|
957
|
+
barrierLabel: 'Dismiss',
|
|
958
|
+
barrierColor: Colors.black.withValues(alpha: 0.55),
|
|
959
|
+
transitionDuration: const Duration(milliseconds: 220),
|
|
960
|
+
transitionBuilder: (ctx, anim, _, child) => FadeTransition(
|
|
961
|
+
opacity: CurvedAnimation(parent: anim, curve: Curves.easeOut),
|
|
962
|
+
child: SlideTransition(
|
|
963
|
+
position: Tween<Offset>(
|
|
964
|
+
begin: const Offset(0, 0.04),
|
|
965
|
+
end: Offset.zero,
|
|
966
|
+
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)),
|
|
967
|
+
child: child,
|
|
968
|
+
),
|
|
1009
969
|
),
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
backgroundColor: _bgSecondary,
|
|
1014
|
-
side: BorderSide(
|
|
1015
|
-
color: model.available
|
|
1016
|
-
? _border
|
|
1017
|
-
: _warning.withValues(alpha: 0.35),
|
|
970
|
+
pageBuilder: (ctx, _, __) => _SmartPoolDialog(
|
|
971
|
+
models: controller.supportedModels,
|
|
972
|
+
selectedIds: _enabledModels,
|
|
1018
973
|
),
|
|
1019
|
-
onSelected: model.available
|
|
1020
|
-
? (value) {
|
|
1021
|
-
setState(() {
|
|
1022
|
-
if (value) {
|
|
1023
|
-
_enabledModels.add(model.id);
|
|
1024
|
-
} else if (_enabledModels.length > 1) {
|
|
1025
|
-
_enabledModels.remove(model.id);
|
|
1026
|
-
}
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
: null,
|
|
1030
974
|
);
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const SizedBox(height: 14),
|
|
1034
|
-
Text(
|
|
1035
|
-
availableModels.isEmpty
|
|
1036
|
-
? 'Enable a ready provider above to unlock model routing.'
|
|
1037
|
-
: '$enabledSmartModels models are currently eligible for smart routing.',
|
|
1038
|
-
style: TextStyle(color: _textSecondary),
|
|
975
|
+
if (result != null) setState(() => _enabledModels = result);
|
|
976
|
+
},
|
|
1039
977
|
),
|
|
1040
978
|
],
|
|
1041
979
|
),
|
|
@@ -1045,7 +983,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1045
983
|
|
|
1046
984
|
Widget _buildVoiceAndRecordingSection({
|
|
1047
985
|
required NeoAgentController controller,
|
|
1048
|
-
required List<
|
|
986
|
+
required List<_ModelPickerOption> modelChoices,
|
|
1049
987
|
required List<ModelMeta> routingModels,
|
|
1050
988
|
}) {
|
|
1051
989
|
final liveVoiceOptions =
|
|
@@ -1091,7 +1029,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1091
1029
|
routingModels,
|
|
1092
1030
|
allowAuto: true,
|
|
1093
1031
|
),
|
|
1094
|
-
|
|
1032
|
+
options: modelChoices,
|
|
1095
1033
|
onChanged: (value) {
|
|
1096
1034
|
if (value != null) {
|
|
1097
1035
|
setState(
|
|
@@ -1107,7 +1045,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1107
1045
|
label: 'Recording Transcription',
|
|
1108
1046
|
icon: Icons.hearing_outlined,
|
|
1109
1047
|
value: _defaultRecordingTranscriptionModel,
|
|
1110
|
-
|
|
1048
|
+
options: _recordingTranscriptionOptions(
|
|
1111
1049
|
_defaultRecordingTranscriptionModel,
|
|
1112
1050
|
),
|
|
1113
1051
|
onChanged: (value) {
|
|
@@ -1152,7 +1090,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1152
1090
|
routingModels,
|
|
1153
1091
|
allowAuto: true,
|
|
1154
1092
|
),
|
|
1155
|
-
|
|
1093
|
+
options: modelChoices,
|
|
1156
1094
|
onChanged: (value) {
|
|
1157
1095
|
if (value != null) {
|
|
1158
1096
|
setState(() => _defaultSpeechModel = value);
|
|
@@ -1194,14 +1132,9 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1194
1132
|
label: 'Live Provider',
|
|
1195
1133
|
icon: Icons.call_outlined,
|
|
1196
1134
|
value: _voiceLiveProvider,
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
value: value,
|
|
1201
|
-
child: Text(value),
|
|
1202
|
-
),
|
|
1203
|
-
)
|
|
1204
|
-
.toList(),
|
|
1135
|
+
options: _simplePickerOptions(
|
|
1136
|
+
const <String>['openai', 'gemini'],
|
|
1137
|
+
),
|
|
1205
1138
|
onChanged: (value) {
|
|
1206
1139
|
if (value == null) return;
|
|
1207
1140
|
setState(() {
|
|
@@ -1230,16 +1163,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1230
1163
|
label: 'Live Model',
|
|
1231
1164
|
icon: Icons.speed_outlined,
|
|
1232
1165
|
value: _voiceLiveModel,
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
(value) => DropdownMenuItem<String>(
|
|
1238
|
-
value: value,
|
|
1239
|
-
child: Text(value),
|
|
1240
|
-
),
|
|
1241
|
-
)
|
|
1242
|
-
.toList(),
|
|
1166
|
+
options: _simplePickerOptions(
|
|
1167
|
+
_voiceLiveModelsByProvider[_voiceLiveProvider] ??
|
|
1168
|
+
const <String>[],
|
|
1169
|
+
),
|
|
1243
1170
|
onChanged: (value) {
|
|
1244
1171
|
if (value != null) {
|
|
1245
1172
|
setState(() => _voiceLiveModel = value);
|
|
@@ -1254,14 +1181,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1254
1181
|
label: 'Live Voice',
|
|
1255
1182
|
icon: Icons.graphic_eq_outlined,
|
|
1256
1183
|
value: _voiceLiveVoice,
|
|
1257
|
-
|
|
1258
|
-
.map(
|
|
1259
|
-
(value) => DropdownMenuItem<String>(
|
|
1260
|
-
value: value,
|
|
1261
|
-
child: Text(value),
|
|
1262
|
-
),
|
|
1263
|
-
)
|
|
1264
|
-
.toList(),
|
|
1184
|
+
options: _simplePickerOptions(liveVoiceOptions),
|
|
1265
1185
|
onChanged: (value) {
|
|
1266
1186
|
if (value != null) {
|
|
1267
1187
|
setState(() => _voiceLiveVoice = value);
|
|
@@ -1453,14 +1373,20 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1453
1373
|
} else if (multi) {
|
|
1454
1374
|
detail = '${r['onlineCount']} devices online — select one in Desktop › Devices';
|
|
1455
1375
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1376
|
+
if (mounted) {
|
|
1377
|
+
setState(() => _desktopTestResult = <String, dynamic>{
|
|
1378
|
+
...r,
|
|
1379
|
+
'detail': detail,
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1460
1382
|
} catch (e) {
|
|
1461
|
-
|
|
1383
|
+
if (mounted) {
|
|
1384
|
+
setState(() => _desktopTestResult = <String, dynamic>{'passed': false, 'detail': e.toString()});
|
|
1385
|
+
}
|
|
1462
1386
|
} finally {
|
|
1463
|
-
|
|
1387
|
+
if (mounted) {
|
|
1388
|
+
setState(() => _desktopTestRunning = false);
|
|
1389
|
+
}
|
|
1464
1390
|
}
|
|
1465
1391
|
},
|
|
1466
1392
|
),
|
|
@@ -1580,426 +1506,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1580
1506
|
);
|
|
1581
1507
|
}
|
|
1582
1508
|
|
|
1583
|
-
Widget _buildUpdatesSection(NeoAgentController controller) {
|
|
1584
|
-
return Card(
|
|
1585
|
-
child: Padding(
|
|
1586
|
-
padding: const EdgeInsets.all(20),
|
|
1587
|
-
child: Column(
|
|
1588
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1589
|
-
children: <Widget>[
|
|
1590
|
-
const _SectionTitle('Updates'),
|
|
1591
|
-
const SizedBox(height: 10),
|
|
1592
|
-
Text(
|
|
1593
|
-
'Client and runtime update controls live here.',
|
|
1594
|
-
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
1595
|
-
),
|
|
1596
|
-
const SizedBox(height: 16),
|
|
1597
|
-
LayoutBuilder(
|
|
1598
|
-
builder: (context, constraints) {
|
|
1599
|
-
final compact = constraints.maxWidth < 720;
|
|
1600
|
-
final checkButton = FilledButton.icon(
|
|
1601
|
-
onPressed:
|
|
1602
|
-
controller.isCheckingAppUpdate ||
|
|
1603
|
-
!controller.appUpdaterConfigured
|
|
1604
|
-
? null
|
|
1605
|
-
: () => controller.checkForAppUpdates(),
|
|
1606
|
-
style: FilledButton.styleFrom(backgroundColor: _accent),
|
|
1607
|
-
icon: controller.isCheckingAppUpdate
|
|
1608
|
-
? _inlineProgressIndicator()
|
|
1609
|
-
: const Icon(Icons.sync),
|
|
1610
|
-
label: Text(
|
|
1611
|
-
controller.isCheckingAppUpdate
|
|
1612
|
-
? 'Checking...'
|
|
1613
|
-
: 'Check now',
|
|
1614
|
-
),
|
|
1615
|
-
);
|
|
1616
|
-
final appHeading = Text(
|
|
1617
|
-
'Client App',
|
|
1618
|
-
style: TextStyle(
|
|
1619
|
-
fontWeight: FontWeight.w700,
|
|
1620
|
-
color: _textPrimary,
|
|
1621
|
-
),
|
|
1622
|
-
);
|
|
1623
|
-
if (compact) {
|
|
1624
|
-
return Column(
|
|
1625
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1626
|
-
children: <Widget>[
|
|
1627
|
-
appHeading,
|
|
1628
|
-
const SizedBox(height: 10),
|
|
1629
|
-
checkButton,
|
|
1630
|
-
],
|
|
1631
|
-
);
|
|
1632
|
-
}
|
|
1633
|
-
return Row(
|
|
1634
|
-
children: <Widget>[
|
|
1635
|
-
Expanded(child: appHeading),
|
|
1636
|
-
checkButton,
|
|
1637
|
-
],
|
|
1638
|
-
);
|
|
1639
|
-
},
|
|
1640
|
-
),
|
|
1641
|
-
const SizedBox(height: 12),
|
|
1642
|
-
if (!controller.appUpdaterConfigured)
|
|
1643
|
-
if (!kIsWeb)
|
|
1644
|
-
Text(
|
|
1645
|
-
'Client app updates are not configured for this build.',
|
|
1646
|
-
style: TextStyle(color: _textSecondary, height: 1.5),
|
|
1647
|
-
)
|
|
1648
|
-
else ...<Widget>[
|
|
1649
|
-
LayoutBuilder(
|
|
1650
|
-
builder: (context, constraints) {
|
|
1651
|
-
final compact = constraints.maxWidth < 780;
|
|
1652
|
-
final channelPicker = DropdownButtonFormField<String>(
|
|
1653
|
-
initialValue: controller.appUpdateChannel,
|
|
1654
|
-
decoration: const InputDecoration(
|
|
1655
|
-
labelText: 'App release channel',
|
|
1656
|
-
),
|
|
1657
|
-
items: const <DropdownMenuItem<String>>[
|
|
1658
|
-
DropdownMenuItem<String>(
|
|
1659
|
-
value: 'stable',
|
|
1660
|
-
child: Text('Stable'),
|
|
1661
|
-
),
|
|
1662
|
-
DropdownMenuItem<String>(
|
|
1663
|
-
value: 'beta',
|
|
1664
|
-
child: Text('Beta'),
|
|
1665
|
-
),
|
|
1666
|
-
],
|
|
1667
|
-
onChanged: (value) {
|
|
1668
|
-
if (value != null) {
|
|
1669
|
-
unawaited(controller.setAppUpdateChannel(value));
|
|
1670
|
-
}
|
|
1671
|
-
},
|
|
1672
|
-
);
|
|
1673
|
-
final autoCheck = SwitchListTile.adaptive(
|
|
1674
|
-
value: controller.appUpdateAutoCheckEnabled,
|
|
1675
|
-
contentPadding: EdgeInsets.zero,
|
|
1676
|
-
title: Text('Check automatically on launch'),
|
|
1677
|
-
subtitle: Text(
|
|
1678
|
-
'This only checks GitHub Releases on startup. Installation still requires your confirmation.',
|
|
1679
|
-
style: TextStyle(color: _textSecondary),
|
|
1680
|
-
),
|
|
1681
|
-
onChanged: controller.setAppUpdateAutoCheckEnabled,
|
|
1682
|
-
);
|
|
1683
|
-
|
|
1684
|
-
if (compact) {
|
|
1685
|
-
return Column(
|
|
1686
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1687
|
-
children: <Widget>[
|
|
1688
|
-
channelPicker,
|
|
1689
|
-
const SizedBox(height: 10),
|
|
1690
|
-
autoCheck,
|
|
1691
|
-
],
|
|
1692
|
-
);
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
return Row(
|
|
1696
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1697
|
-
children: <Widget>[
|
|
1698
|
-
Expanded(child: channelPicker),
|
|
1699
|
-
const SizedBox(width: 16),
|
|
1700
|
-
Expanded(child: autoCheck),
|
|
1701
|
-
],
|
|
1702
|
-
);
|
|
1703
|
-
},
|
|
1704
|
-
),
|
|
1705
|
-
const SizedBox(height: 8),
|
|
1706
|
-
Text(
|
|
1707
|
-
'Installed: ${controller.installedAppVersion ?? 'Unknown'} | Channel: ${controller.appUpdateChannelLabel} | Last checked: ${controller.appUpdateLastCheckedLabel}',
|
|
1708
|
-
style: TextStyle(color: _textSecondary),
|
|
1709
|
-
),
|
|
1710
|
-
const SizedBox(height: 6),
|
|
1711
|
-
Text(
|
|
1712
|
-
'Source: ${app_release_updater.appUpdaterGithubOwner}/${app_release_updater.appUpdaterGithubRepo}${app_release_updater.appUpdaterGithubToken.trim().isNotEmpty ? ' (override active)' : ''}',
|
|
1713
|
-
style: TextStyle(color: _textSecondary),
|
|
1714
|
-
),
|
|
1715
|
-
if (controller.appUpdateErrorMessage
|
|
1716
|
-
case final message?) ...<Widget>[
|
|
1717
|
-
const SizedBox(height: 12),
|
|
1718
|
-
_InlineError(message: message),
|
|
1719
|
-
],
|
|
1720
|
-
if (controller.availableAppUpdate
|
|
1721
|
-
case final release?) ...<Widget>[
|
|
1722
|
-
const SizedBox(height: 16),
|
|
1723
|
-
Container(
|
|
1724
|
-
width: double.infinity,
|
|
1725
|
-
padding: const EdgeInsets.all(16),
|
|
1726
|
-
decoration: BoxDecoration(
|
|
1727
|
-
color: _bgSecondary,
|
|
1728
|
-
borderRadius: BorderRadius.circular(18),
|
|
1729
|
-
border: Border.all(color: _border),
|
|
1730
|
-
),
|
|
1731
|
-
child: Column(
|
|
1732
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1733
|
-
children: <Widget>[
|
|
1734
|
-
Wrap(
|
|
1735
|
-
spacing: 10,
|
|
1736
|
-
runSpacing: 10,
|
|
1737
|
-
children: <Widget>[
|
|
1738
|
-
_StatusPill(
|
|
1739
|
-
label: 'Update ${release.version}',
|
|
1740
|
-
color: release.channel == 'beta'
|
|
1741
|
-
? _warning
|
|
1742
|
-
: _accent,
|
|
1743
|
-
),
|
|
1744
|
-
_StatusPill(
|
|
1745
|
-
label: release.asset.name,
|
|
1746
|
-
color: _textSecondary,
|
|
1747
|
-
),
|
|
1748
|
-
],
|
|
1749
|
-
),
|
|
1750
|
-
const SizedBox(height: 10),
|
|
1751
|
-
Text(
|
|
1752
|
-
'${release.title} · ${release.publishedLabel} · ${release.asset.sizeLabel}',
|
|
1753
|
-
style: TextStyle(color: _textSecondary),
|
|
1754
|
-
),
|
|
1755
|
-
if (release.body.trim().isNotEmpty) ...<Widget>[
|
|
1756
|
-
const SizedBox(height: 14),
|
|
1757
|
-
ConstrainedBox(
|
|
1758
|
-
constraints: const BoxConstraints(maxHeight: 220),
|
|
1759
|
-
child: SingleChildScrollView(
|
|
1760
|
-
child: MarkdownBody(
|
|
1761
|
-
data: release.body,
|
|
1762
|
-
selectable: true,
|
|
1763
|
-
styleSheet: MarkdownStyleSheet(
|
|
1764
|
-
p: TextStyle(
|
|
1765
|
-
color: _textSecondary,
|
|
1766
|
-
height: 1.45,
|
|
1767
|
-
),
|
|
1768
|
-
),
|
|
1769
|
-
),
|
|
1770
|
-
),
|
|
1771
|
-
),
|
|
1772
|
-
],
|
|
1773
|
-
const SizedBox(height: 14),
|
|
1774
|
-
Wrap(
|
|
1775
|
-
spacing: 10,
|
|
1776
|
-
runSpacing: 10,
|
|
1777
|
-
children: <Widget>[
|
|
1778
|
-
FilledButton.icon(
|
|
1779
|
-
onPressed: controller.isOpeningAppUpdate
|
|
1780
|
-
? null
|
|
1781
|
-
: controller.openAppUpdate,
|
|
1782
|
-
style: FilledButton.styleFrom(
|
|
1783
|
-
backgroundColor: _accent,
|
|
1784
|
-
),
|
|
1785
|
-
icon: controller.isOpeningAppUpdate
|
|
1786
|
-
? _inlineProgressIndicator()
|
|
1787
|
-
: const Icon(Icons.system_update_alt),
|
|
1788
|
-
label: Text(
|
|
1789
|
-
controller.isOpeningAppUpdate
|
|
1790
|
-
? 'Opening...'
|
|
1791
|
-
: 'Download update',
|
|
1792
|
-
),
|
|
1793
|
-
),
|
|
1794
|
-
if (release.htmlUrl.trim().isNotEmpty)
|
|
1795
|
-
OutlinedButton.icon(
|
|
1796
|
-
onPressed: () {
|
|
1797
|
-
unawaited(
|
|
1798
|
-
widget.controller._oauthLauncher
|
|
1799
|
-
.openExternal(
|
|
1800
|
-
url: release.htmlUrl,
|
|
1801
|
-
label: 'release_notes',
|
|
1802
|
-
),
|
|
1803
|
-
);
|
|
1804
|
-
},
|
|
1805
|
-
icon: const Icon(Icons.open_in_new),
|
|
1806
|
-
label: Text('View release'),
|
|
1807
|
-
),
|
|
1808
|
-
],
|
|
1809
|
-
),
|
|
1810
|
-
],
|
|
1811
|
-
),
|
|
1812
|
-
),
|
|
1813
|
-
] else ...<Widget>[
|
|
1814
|
-
const SizedBox(height: 12),
|
|
1815
|
-
Text(
|
|
1816
|
-
controller.isCheckingAppUpdate
|
|
1817
|
-
? 'Checking GitHub releases...'
|
|
1818
|
-
: controller.appUpdateLastCheckedAt == null
|
|
1819
|
-
? 'Choose a channel, then check GitHub releases.'
|
|
1820
|
-
: 'No newer app release is available on the selected channel.',
|
|
1821
|
-
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
1822
|
-
),
|
|
1823
|
-
],
|
|
1824
|
-
],
|
|
1825
|
-
const Divider(height: 32),
|
|
1826
|
-
if (controller.updateStatus.allowSelfUpdate) ...<Widget>[
|
|
1827
|
-
LayoutBuilder(
|
|
1828
|
-
builder: (context, constraints) {
|
|
1829
|
-
final compact = constraints.maxWidth < 780;
|
|
1830
|
-
final channelPicker = DropdownButtonFormField<String>(
|
|
1831
|
-
initialValue: controller.updateStatus.releaseChannel,
|
|
1832
|
-
decoration: const InputDecoration(
|
|
1833
|
-
labelText: 'Runtime release channel',
|
|
1834
|
-
),
|
|
1835
|
-
items: const <DropdownMenuItem<String>>[
|
|
1836
|
-
DropdownMenuItem<String>(
|
|
1837
|
-
value: 'stable',
|
|
1838
|
-
child: Text('Stable'),
|
|
1839
|
-
),
|
|
1840
|
-
DropdownMenuItem<String>(
|
|
1841
|
-
value: 'beta',
|
|
1842
|
-
child: Text('Beta'),
|
|
1843
|
-
),
|
|
1844
|
-
],
|
|
1845
|
-
onChanged:
|
|
1846
|
-
controller.isSavingReleaseChannel ||
|
|
1847
|
-
controller.isTriggeringUpdate ||
|
|
1848
|
-
controller.updateStatus.state == 'running'
|
|
1849
|
-
? null
|
|
1850
|
-
: (value) {
|
|
1851
|
-
if (value != null) {
|
|
1852
|
-
unawaited(controller.setReleaseChannel(value));
|
|
1853
|
-
}
|
|
1854
|
-
},
|
|
1855
|
-
);
|
|
1856
|
-
|
|
1857
|
-
final channelHelper = Text(
|
|
1858
|
-
controller.updateStatus.releaseChannel == 'beta'
|
|
1859
|
-
? 'Beta follows preview releases.'
|
|
1860
|
-
: 'Stable follows production releases.',
|
|
1861
|
-
style: TextStyle(color: _textSecondary),
|
|
1862
|
-
);
|
|
1863
|
-
|
|
1864
|
-
if (compact) {
|
|
1865
|
-
return Column(
|
|
1866
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1867
|
-
children: <Widget>[
|
|
1868
|
-
channelPicker,
|
|
1869
|
-
const SizedBox(height: 8),
|
|
1870
|
-
channelHelper,
|
|
1871
|
-
const SizedBox(height: 16),
|
|
1872
|
-
],
|
|
1873
|
-
);
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
return Padding(
|
|
1877
|
-
padding: const EdgeInsets.only(bottom: 16),
|
|
1878
|
-
child: Row(
|
|
1879
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1880
|
-
children: <Widget>[
|
|
1881
|
-
Expanded(child: channelPicker),
|
|
1882
|
-
const SizedBox(width: 12),
|
|
1883
|
-
Expanded(child: channelHelper),
|
|
1884
|
-
],
|
|
1885
|
-
),
|
|
1886
|
-
);
|
|
1887
|
-
},
|
|
1888
|
-
),
|
|
1889
|
-
LayoutBuilder(
|
|
1890
|
-
builder: (context, constraints) {
|
|
1891
|
-
final compact = constraints.maxWidth < 780;
|
|
1892
|
-
final runtimeTitle = Text(
|
|
1893
|
-
'Runtime',
|
|
1894
|
-
style: TextStyle(
|
|
1895
|
-
fontWeight: FontWeight.w700,
|
|
1896
|
-
color: _textPrimary,
|
|
1897
|
-
),
|
|
1898
|
-
);
|
|
1899
|
-
final updateButton = FilledButton.icon(
|
|
1900
|
-
onPressed:
|
|
1901
|
-
controller.isSavingReleaseChannel ||
|
|
1902
|
-
controller.isTriggeringUpdate ||
|
|
1903
|
-
controller.updateStatus.state == 'running'
|
|
1904
|
-
? null
|
|
1905
|
-
: controller.triggerUpdate,
|
|
1906
|
-
style: FilledButton.styleFrom(backgroundColor: _accent),
|
|
1907
|
-
icon: controller.isTriggeringUpdate
|
|
1908
|
-
? const SizedBox.square(
|
|
1909
|
-
dimension: 16,
|
|
1910
|
-
child: CircularProgressIndicator(
|
|
1911
|
-
strokeWidth: 2,
|
|
1912
|
-
color: Colors.white,
|
|
1913
|
-
),
|
|
1914
|
-
)
|
|
1915
|
-
: Icon(Icons.system_update),
|
|
1916
|
-
label: Text('Update'),
|
|
1917
|
-
);
|
|
1918
|
-
if (compact) {
|
|
1919
|
-
return Column(
|
|
1920
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1921
|
-
children: <Widget>[
|
|
1922
|
-
runtimeTitle,
|
|
1923
|
-
const SizedBox(height: 10),
|
|
1924
|
-
updateButton,
|
|
1925
|
-
],
|
|
1926
|
-
);
|
|
1927
|
-
}
|
|
1928
|
-
return Row(
|
|
1929
|
-
children: <Widget>[
|
|
1930
|
-
Expanded(child: runtimeTitle),
|
|
1931
|
-
updateButton,
|
|
1932
|
-
],
|
|
1933
|
-
);
|
|
1934
|
-
},
|
|
1935
|
-
),
|
|
1936
|
-
] else ...<Widget>[
|
|
1937
|
-
Text(
|
|
1938
|
-
'Runtime',
|
|
1939
|
-
style: TextStyle(
|
|
1940
|
-
fontWeight: FontWeight.w700,
|
|
1941
|
-
color: _textPrimary,
|
|
1942
|
-
),
|
|
1943
|
-
),
|
|
1944
|
-
const SizedBox(height: 10),
|
|
1945
|
-
Text(
|
|
1946
|
-
'Updates and release tracks are managed for this deployment.',
|
|
1947
|
-
style: TextStyle(color: _textSecondary),
|
|
1948
|
-
),
|
|
1949
|
-
],
|
|
1950
|
-
const SizedBox(height: 12),
|
|
1951
|
-
LayoutBuilder(
|
|
1952
|
-
builder: (context, constraints) {
|
|
1953
|
-
final compact = constraints.maxWidth < 760;
|
|
1954
|
-
final statusRow = Wrap(
|
|
1955
|
-
spacing: 10,
|
|
1956
|
-
runSpacing: 10,
|
|
1957
|
-
crossAxisAlignment: WrapCrossAlignment.center,
|
|
1958
|
-
children: <Widget>[
|
|
1959
|
-
_StatusPill(
|
|
1960
|
-
label: controller.updateStatus.badgeLabel,
|
|
1961
|
-
color: controller.updateStatus.badgeColor,
|
|
1962
|
-
),
|
|
1963
|
-
_StatusPill(
|
|
1964
|
-
label: controller.updateStatus.releaseChannelLabel,
|
|
1965
|
-
color: controller.updateStatus.releaseChannel == 'beta'
|
|
1966
|
-
? _warning
|
|
1967
|
-
: _accent,
|
|
1968
|
-
),
|
|
1969
|
-
Text(
|
|
1970
|
-
controller.updateStatus.message,
|
|
1971
|
-
style: TextStyle(color: _textSecondary),
|
|
1972
|
-
),
|
|
1973
|
-
Text('${controller.updateStatus.progress}%'),
|
|
1974
|
-
],
|
|
1975
|
-
);
|
|
1976
|
-
if (compact) {
|
|
1977
|
-
return Column(
|
|
1978
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1979
|
-
children: <Widget>[statusRow],
|
|
1980
|
-
);
|
|
1981
|
-
}
|
|
1982
|
-
return statusRow;
|
|
1983
|
-
},
|
|
1984
|
-
),
|
|
1985
|
-
const SizedBox(height: 10),
|
|
1986
|
-
ClipRRect(
|
|
1987
|
-
borderRadius: BorderRadius.circular(999),
|
|
1988
|
-
child: LinearProgressIndicator(
|
|
1989
|
-
minHeight: 8,
|
|
1990
|
-
value: controller.updateStatus.progress / 100,
|
|
1991
|
-
backgroundColor: _bgSecondary,
|
|
1992
|
-
color: _accent,
|
|
1993
|
-
),
|
|
1994
|
-
),
|
|
1995
|
-
const SizedBox(height: 12),
|
|
1996
|
-
Text(controller.updateStatus.versionLine),
|
|
1997
|
-
],
|
|
1998
|
-
),
|
|
1999
|
-
),
|
|
2000
|
-
);
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
1509
|
Widget _buildDiagnosticsSection(NeoAgentController controller) {
|
|
2004
1510
|
return Card(
|
|
2005
1511
|
child: Padding(
|
|
@@ -2104,20 +1610,12 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
2104
1610
|
}
|
|
2105
1611
|
}
|
|
2106
1612
|
|
|
2107
|
-
List<
|
|
2108
|
-
String
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
if (normalizedCurrent.isNotEmpty) {
|
|
2114
|
-
values.add(normalizedCurrent);
|
|
2115
|
-
}
|
|
2116
|
-
return values
|
|
2117
|
-
.map(
|
|
2118
|
-
(value) => DropdownMenuItem<String>(value: value, child: Text(value)),
|
|
2119
|
-
)
|
|
2120
|
-
.toList();
|
|
1613
|
+
List<_ModelPickerOption> _recordingTranscriptionOptions(String current) {
|
|
1614
|
+
const List<String> defaults = <String>['nova-3', 'nova-2-general'];
|
|
1615
|
+
final String normalizedCurrent = current.trim();
|
|
1616
|
+
final Set<String> values = <String>{...defaults};
|
|
1617
|
+
if (normalizedCurrent.isNotEmpty) values.add(normalizedCurrent);
|
|
1618
|
+
return _simplePickerOptions(values.toList());
|
|
2121
1619
|
}
|
|
2122
1620
|
|
|
2123
1621
|
// Shared helper: small "Test" button + inline result row.
|
|
@@ -2185,287 +1683,666 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
2185
1683
|
}
|
|
2186
1684
|
}
|
|
2187
1685
|
|
|
2188
|
-
class
|
|
2189
|
-
const
|
|
2190
|
-
required this.
|
|
2191
|
-
required this.
|
|
2192
|
-
required this.
|
|
2193
|
-
required this.
|
|
2194
|
-
required this.
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
final
|
|
2200
|
-
final
|
|
2201
|
-
final
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
final ValueChanged<bool> onEnabledChanged;
|
|
2205
|
-
final VoidCallback onExpandToggle;
|
|
2206
|
-
|
|
2207
|
-
@override
|
|
1686
|
+
class _RoutingSelectCard extends StatelessWidget {
|
|
1687
|
+
const _RoutingSelectCard({
|
|
1688
|
+
required this.label,
|
|
1689
|
+
required this.icon,
|
|
1690
|
+
required this.value,
|
|
1691
|
+
required this.options,
|
|
1692
|
+
required this.onChanged,
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
final String label;
|
|
1696
|
+
final IconData icon;
|
|
1697
|
+
final String value;
|
|
1698
|
+
final List<_ModelPickerOption> options;
|
|
1699
|
+
final ValueChanged<String?> onChanged;
|
|
1700
|
+
|
|
1701
|
+
@override
|
|
2208
1702
|
Widget build(BuildContext context) {
|
|
2209
|
-
final availableCount = models.where((model) => model.available).length;
|
|
2210
|
-
final hasAdvancedFields = provider.supportsBaseUrl || models.isNotEmpty;
|
|
2211
1703
|
return Container(
|
|
2212
|
-
padding: const EdgeInsets.all(
|
|
1704
|
+
padding: const EdgeInsets.all(14),
|
|
2213
1705
|
decoration: BoxDecoration(
|
|
2214
1706
|
color: _bgSecondary,
|
|
2215
|
-
borderRadius: BorderRadius.circular(
|
|
2216
|
-
border: Border.all(color:
|
|
1707
|
+
borderRadius: BorderRadius.circular(16),
|
|
1708
|
+
border: Border.all(color: _border),
|
|
2217
1709
|
),
|
|
2218
1710
|
child: Column(
|
|
2219
1711
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2220
1712
|
children: <Widget>[
|
|
2221
1713
|
Row(
|
|
2222
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2223
1714
|
children: <Widget>[
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
1715
|
+
Icon(icon, size: 16, color: _accentHover),
|
|
1716
|
+
const SizedBox(width: 8),
|
|
1717
|
+
Text(label, style: TextStyle(fontWeight: FontWeight.w700)),
|
|
1718
|
+
],
|
|
1719
|
+
),
|
|
1720
|
+
const SizedBox(height: 10),
|
|
1721
|
+
_ModelPickerButton(
|
|
1722
|
+
value: value,
|
|
1723
|
+
options: options,
|
|
1724
|
+
onChanged: onChanged,
|
|
1725
|
+
dialogTitle: 'Select $label',
|
|
1726
|
+
),
|
|
1727
|
+
],
|
|
1728
|
+
),
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1734
|
+
// Smart Pool Summary — compact summary card shown in settings
|
|
1735
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1736
|
+
|
|
1737
|
+
class _SmartPoolSummary extends StatelessWidget {
|
|
1738
|
+
const _SmartPoolSummary({
|
|
1739
|
+
required this.allModels,
|
|
1740
|
+
required this.selectedIds,
|
|
1741
|
+
required this.onManage,
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
final List<ModelMeta> allModels;
|
|
1745
|
+
final Set<String> selectedIds;
|
|
1746
|
+
final VoidCallback onManage;
|
|
1747
|
+
|
|
1748
|
+
@override
|
|
1749
|
+
Widget build(BuildContext context) {
|
|
1750
|
+
final selected = allModels
|
|
1751
|
+
.where((m) => selectedIds.contains(m.id) && m.available)
|
|
1752
|
+
.toList();
|
|
1753
|
+
final providers = <String>{for (final m in selected) m.provider};
|
|
1754
|
+
final totalAvailable = allModels.where((m) => m.available).length;
|
|
1755
|
+
|
|
1756
|
+
return Container(
|
|
1757
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
1758
|
+
decoration: BoxDecoration(
|
|
1759
|
+
color: _bgSecondary,
|
|
1760
|
+
borderRadius: BorderRadius.circular(14),
|
|
1761
|
+
border: Border.all(color: _border),
|
|
1762
|
+
),
|
|
1763
|
+
child: Row(
|
|
1764
|
+
children: <Widget>[
|
|
1765
|
+
Container(
|
|
1766
|
+
width: 38,
|
|
1767
|
+
height: 38,
|
|
1768
|
+
decoration: BoxDecoration(
|
|
1769
|
+
color: _accentMuted,
|
|
1770
|
+
borderRadius: BorderRadius.circular(10),
|
|
1771
|
+
),
|
|
1772
|
+
child: Icon(Icons.hub_outlined, size: 18, color: _accentHover),
|
|
1773
|
+
),
|
|
1774
|
+
const SizedBox(width: 12),
|
|
1775
|
+
Expanded(
|
|
1776
|
+
child: Column(
|
|
1777
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1778
|
+
children: <Widget>[
|
|
1779
|
+
Text(
|
|
1780
|
+
'${selected.length} of $totalAvailable models',
|
|
1781
|
+
style: TextStyle(
|
|
1782
|
+
fontWeight: FontWeight.w600,
|
|
1783
|
+
fontSize: 14,
|
|
1784
|
+
color: _textPrimary,
|
|
1785
|
+
),
|
|
2230
1786
|
),
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
const SizedBox(width: 12),
|
|
2234
|
-
Expanded(
|
|
2235
|
-
child: Column(
|
|
2236
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1787
|
+
const SizedBox(height: 5),
|
|
1788
|
+
Row(
|
|
2237
1789
|
children: <Widget>[
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
fontSize:
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
1790
|
+
if (providers.isEmpty)
|
|
1791
|
+
Text(
|
|
1792
|
+
'No models selected',
|
|
1793
|
+
style: TextStyle(fontSize: 12, color: _textMuted),
|
|
1794
|
+
)
|
|
1795
|
+
else
|
|
1796
|
+
...providers.take(12).map(
|
|
1797
|
+
(p) => Container(
|
|
1798
|
+
width: 8,
|
|
1799
|
+
height: 8,
|
|
1800
|
+
margin: const EdgeInsets.only(right: 5),
|
|
1801
|
+
decoration: BoxDecoration(
|
|
1802
|
+
color: _providerPickerColor(p),
|
|
1803
|
+
shape: BoxShape.circle,
|
|
1804
|
+
),
|
|
1805
|
+
),
|
|
1806
|
+
),
|
|
2252
1807
|
],
|
|
2253
1808
|
),
|
|
1809
|
+
],
|
|
1810
|
+
),
|
|
1811
|
+
),
|
|
1812
|
+
const SizedBox(width: 10),
|
|
1813
|
+
OutlinedButton.icon(
|
|
1814
|
+
onPressed: onManage,
|
|
1815
|
+
icon: const Icon(Icons.tune_rounded, size: 14),
|
|
1816
|
+
label: const Text('Manage'),
|
|
1817
|
+
style: OutlinedButton.styleFrom(
|
|
1818
|
+
padding:
|
|
1819
|
+
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
1820
|
+
textStyle: const TextStyle(fontSize: 13),
|
|
1821
|
+
),
|
|
1822
|
+
),
|
|
1823
|
+
],
|
|
1824
|
+
),
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1830
|
+
// Smart Pool Dialog — searchable, grouped multi-select manager
|
|
1831
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1832
|
+
|
|
1833
|
+
class _SmartPoolDialog extends StatefulWidget {
|
|
1834
|
+
const _SmartPoolDialog({
|
|
1835
|
+
required this.models,
|
|
1836
|
+
required this.selectedIds,
|
|
1837
|
+
});
|
|
1838
|
+
|
|
1839
|
+
final List<ModelMeta> models;
|
|
1840
|
+
final Set<String> selectedIds;
|
|
1841
|
+
|
|
1842
|
+
@override
|
|
1843
|
+
State<_SmartPoolDialog> createState() => _SmartPoolDialogState();
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
1847
|
+
late Set<String> _selected;
|
|
1848
|
+
final TextEditingController _searchCtrl = TextEditingController();
|
|
1849
|
+
String _query = '';
|
|
1850
|
+
bool _onlyAvailable = true;
|
|
1851
|
+
|
|
1852
|
+
@override
|
|
1853
|
+
void initState() {
|
|
1854
|
+
super.initState();
|
|
1855
|
+
_selected = Set<String>.from(widget.selectedIds);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
@override
|
|
1859
|
+
void dispose() {
|
|
1860
|
+
_searchCtrl.dispose();
|
|
1861
|
+
super.dispose();
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
List<ModelMeta> get _filtered {
|
|
1865
|
+
var list = _onlyAvailable
|
|
1866
|
+
? widget.models.where((m) => m.available).toList()
|
|
1867
|
+
: List<ModelMeta>.from(widget.models);
|
|
1868
|
+
if (_query.isNotEmpty) {
|
|
1869
|
+
final q = _query.toLowerCase();
|
|
1870
|
+
list = list
|
|
1871
|
+
.where((m) =>
|
|
1872
|
+
m.label.toLowerCase().contains(q) ||
|
|
1873
|
+
m.id.toLowerCase().contains(q) ||
|
|
1874
|
+
m.provider.toLowerCase().contains(q))
|
|
1875
|
+
.toList();
|
|
1876
|
+
}
|
|
1877
|
+
return list;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
void _selectAllVisible(List<ModelMeta> filtered) {
|
|
1881
|
+
setState(() {
|
|
1882
|
+
for (final m in filtered) {
|
|
1883
|
+
if (m.available) _selected.add(m.id);
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
void _clearAllVisible(List<ModelMeta> filtered) {
|
|
1889
|
+
setState(() {
|
|
1890
|
+
final toRemove = filtered.map((m) => m.id).toSet();
|
|
1891
|
+
final remaining = _selected.difference(toRemove);
|
|
1892
|
+
_selected = remaining.isNotEmpty
|
|
1893
|
+
? remaining
|
|
1894
|
+
: <String>{_selected.first};
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
@override
|
|
1899
|
+
Widget build(BuildContext context) {
|
|
1900
|
+
final filtered = _filtered;
|
|
1901
|
+
|
|
1902
|
+
// Build grouped structure
|
|
1903
|
+
final Map<String, List<ModelMeta>> grouped = <String, List<ModelMeta>>{};
|
|
1904
|
+
for (final m in filtered) {
|
|
1905
|
+
grouped.putIfAbsent(m.provider, () => <ModelMeta>[]).add(m);
|
|
1906
|
+
}
|
|
1907
|
+
final providerOrder = grouped.keys.toList();
|
|
1908
|
+
|
|
1909
|
+
final selectedAvailableCount = widget.models
|
|
1910
|
+
.where((m) => _selected.contains(m.id) && m.available)
|
|
1911
|
+
.length;
|
|
1912
|
+
|
|
1913
|
+
// Build flat row list (headers + model rows)
|
|
1914
|
+
final List<Widget> rows = <Widget>[];
|
|
1915
|
+
for (final provider in providerOrder) {
|
|
1916
|
+
final models = grouped[provider]!;
|
|
1917
|
+
final providerColor = _providerPickerColor(provider);
|
|
1918
|
+
final available = models.where((m) => m.available).toList();
|
|
1919
|
+
final allGroupSelected = available.isNotEmpty &&
|
|
1920
|
+
available.every((m) => _selected.contains(m.id));
|
|
1921
|
+
|
|
1922
|
+
rows.add(Padding(
|
|
1923
|
+
padding: const EdgeInsets.fromLTRB(16, 14, 16, 4),
|
|
1924
|
+
child: Row(
|
|
1925
|
+
children: <Widget>[
|
|
1926
|
+
Container(
|
|
1927
|
+
width: 6,
|
|
1928
|
+
height: 6,
|
|
1929
|
+
decoration: BoxDecoration(
|
|
1930
|
+
color: providerColor,
|
|
1931
|
+
shape: BoxShape.circle,
|
|
1932
|
+
),
|
|
1933
|
+
),
|
|
1934
|
+
const SizedBox(width: 8),
|
|
1935
|
+
Expanded(
|
|
1936
|
+
child: Text(
|
|
1937
|
+
_providerPickerLabel(provider).toUpperCase(),
|
|
1938
|
+
style: TextStyle(
|
|
1939
|
+
fontSize: 10.5,
|
|
1940
|
+
fontWeight: FontWeight.w700,
|
|
1941
|
+
color: _textMuted,
|
|
1942
|
+
letterSpacing: 0.8,
|
|
1943
|
+
),
|
|
2254
1944
|
),
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
1945
|
+
),
|
|
1946
|
+
const SizedBox(width: 8),
|
|
1947
|
+
GestureDetector(
|
|
1948
|
+
onTap: available.isEmpty
|
|
1949
|
+
? null
|
|
1950
|
+
: () {
|
|
1951
|
+
setState(() {
|
|
1952
|
+
if (allGroupSelected) {
|
|
1953
|
+
final toRemove =
|
|
1954
|
+
available.map((m) => m.id).toSet();
|
|
1955
|
+
final remaining =
|
|
1956
|
+
_selected.difference(toRemove);
|
|
1957
|
+
_selected = remaining.isNotEmpty
|
|
1958
|
+
? remaining
|
|
1959
|
+
: <String>{_selected.first};
|
|
1960
|
+
} else {
|
|
1961
|
+
for (final m in available) {
|
|
1962
|
+
_selected.add(m.id);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
},
|
|
1967
|
+
child: Text(
|
|
1968
|
+
allGroupSelected ? 'None' : 'All',
|
|
1969
|
+
style: TextStyle(
|
|
1970
|
+
fontSize: 11,
|
|
1971
|
+
fontWeight: FontWeight.w600,
|
|
1972
|
+
color: available.isEmpty ? _textMuted : _accent,
|
|
1973
|
+
),
|
|
1974
|
+
),
|
|
1975
|
+
),
|
|
1976
|
+
],
|
|
1977
|
+
),
|
|
1978
|
+
));
|
|
1979
|
+
|
|
1980
|
+
for (final model in models) {
|
|
1981
|
+
rows.add(_SmartPoolRow(
|
|
1982
|
+
model: model,
|
|
1983
|
+
selected: _selected.contains(model.id),
|
|
1984
|
+
onToggle: (val) => setState(() {
|
|
1985
|
+
if (val) {
|
|
1986
|
+
_selected.add(model.id);
|
|
1987
|
+
} else if (_selected.length > 1) {
|
|
1988
|
+
_selected.remove(model.id);
|
|
1989
|
+
}
|
|
1990
|
+
}),
|
|
1991
|
+
));
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
return Center(
|
|
1996
|
+
child: ConstrainedBox(
|
|
1997
|
+
constraints: BoxConstraints(
|
|
1998
|
+
maxWidth: 560,
|
|
1999
|
+
minWidth: 320,
|
|
2000
|
+
maxHeight: MediaQuery.sizeOf(context).height * 0.85,
|
|
2001
|
+
),
|
|
2002
|
+
child: Padding(
|
|
2003
|
+
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
|
2004
|
+
child: Material(
|
|
2005
|
+
color: _bgCard,
|
|
2006
|
+
borderRadius: BorderRadius.circular(20),
|
|
2007
|
+
elevation: 24,
|
|
2008
|
+
shadowColor: Colors.black.withValues(alpha: 0.5),
|
|
2009
|
+
child: Container(
|
|
2010
|
+
decoration: BoxDecoration(
|
|
2011
|
+
borderRadius: BorderRadius.circular(20),
|
|
2012
|
+
border: Border.all(color: _borderLight),
|
|
2013
|
+
),
|
|
2014
|
+
child: ClipRRect(
|
|
2015
|
+
borderRadius: BorderRadius.circular(20),
|
|
2016
|
+
child: Column(
|
|
2017
|
+
mainAxisSize: MainAxisSize.min,
|
|
2018
|
+
children: <Widget>[
|
|
2019
|
+
// Header
|
|
2020
|
+
Padding(
|
|
2021
|
+
padding: const EdgeInsets.fromLTRB(20, 16, 10, 0),
|
|
2022
|
+
child: Row(
|
|
2023
|
+
children: <Widget>[
|
|
2024
|
+
Expanded(
|
|
2025
|
+
child: Text(
|
|
2026
|
+
'Smart Selector Pool',
|
|
2027
|
+
style: TextStyle(
|
|
2028
|
+
fontSize: 17,
|
|
2029
|
+
fontWeight: FontWeight.w700,
|
|
2030
|
+
color: _textPrimary,
|
|
2031
|
+
),
|
|
2032
|
+
),
|
|
2033
|
+
),
|
|
2034
|
+
IconButton(
|
|
2035
|
+
onPressed: () =>
|
|
2036
|
+
Navigator.of(context).pop(_selected),
|
|
2037
|
+
icon: Icon(
|
|
2038
|
+
Icons.close_rounded,
|
|
2039
|
+
size: 20,
|
|
2040
|
+
color: _textSecondary,
|
|
2041
|
+
),
|
|
2042
|
+
style: IconButton.styleFrom(
|
|
2043
|
+
minimumSize: const Size(36, 36),
|
|
2044
|
+
padding: EdgeInsets.zero,
|
|
2045
|
+
tapTargetSize:
|
|
2046
|
+
MaterialTapTargetSize.shrinkWrap,
|
|
2047
|
+
),
|
|
2048
|
+
),
|
|
2049
|
+
],
|
|
2273
2050
|
),
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2051
|
+
),
|
|
2052
|
+
// Search + available toggle
|
|
2053
|
+
Padding(
|
|
2054
|
+
padding: const EdgeInsets.fromLTRB(14, 10, 14, 8),
|
|
2055
|
+
child: Row(
|
|
2056
|
+
children: <Widget>[
|
|
2057
|
+
Expanded(
|
|
2058
|
+
child: TextField(
|
|
2059
|
+
controller: _searchCtrl,
|
|
2060
|
+
autofocus: true,
|
|
2061
|
+
onChanged: (v) =>
|
|
2062
|
+
setState(() => _query = v.trim()),
|
|
2063
|
+
style: TextStyle(
|
|
2064
|
+
color: _textPrimary,
|
|
2065
|
+
fontSize: 14,
|
|
2066
|
+
),
|
|
2067
|
+
decoration: InputDecoration(
|
|
2068
|
+
hintText: 'Search models or providers…',
|
|
2069
|
+
hintStyle: TextStyle(
|
|
2070
|
+
color: _textMuted,
|
|
2071
|
+
fontSize: 14,
|
|
2072
|
+
),
|
|
2073
|
+
prefixIcon: Icon(
|
|
2074
|
+
Icons.search_rounded,
|
|
2075
|
+
size: 18,
|
|
2076
|
+
color: _textMuted,
|
|
2077
|
+
),
|
|
2078
|
+
suffixIcon: _query.isNotEmpty
|
|
2079
|
+
? GestureDetector(
|
|
2080
|
+
onTap: () => setState(() {
|
|
2081
|
+
_searchCtrl.clear();
|
|
2082
|
+
_query = '';
|
|
2083
|
+
}),
|
|
2084
|
+
child: Padding(
|
|
2085
|
+
padding: const EdgeInsets.all(10),
|
|
2086
|
+
child: Icon(
|
|
2087
|
+
Icons.cancel_rounded,
|
|
2088
|
+
size: 16,
|
|
2089
|
+
color: _textMuted,
|
|
2090
|
+
),
|
|
2091
|
+
),
|
|
2092
|
+
)
|
|
2093
|
+
: null,
|
|
2094
|
+
isDense: true,
|
|
2095
|
+
contentPadding: const EdgeInsets.symmetric(
|
|
2096
|
+
vertical: 10),
|
|
2097
|
+
filled: true,
|
|
2098
|
+
fillColor: _bgSecondary,
|
|
2099
|
+
border: OutlineInputBorder(
|
|
2100
|
+
borderRadius: BorderRadius.circular(12),
|
|
2101
|
+
borderSide:
|
|
2102
|
+
BorderSide(color: _border),
|
|
2103
|
+
),
|
|
2104
|
+
enabledBorder: OutlineInputBorder(
|
|
2105
|
+
borderRadius: BorderRadius.circular(12),
|
|
2106
|
+
borderSide:
|
|
2107
|
+
BorderSide(color: _border),
|
|
2108
|
+
),
|
|
2109
|
+
focusedBorder: OutlineInputBorder(
|
|
2110
|
+
borderRadius: BorderRadius.circular(12),
|
|
2111
|
+
borderSide: BorderSide(
|
|
2112
|
+
color: _accent,
|
|
2113
|
+
width: 1.5,
|
|
2114
|
+
),
|
|
2115
|
+
),
|
|
2116
|
+
),
|
|
2117
|
+
),
|
|
2118
|
+
),
|
|
2119
|
+
const SizedBox(width: 8),
|
|
2120
|
+
GestureDetector(
|
|
2121
|
+
onTap: () => setState(
|
|
2122
|
+
() => _onlyAvailable = !_onlyAvailable),
|
|
2123
|
+
child: AnimatedContainer(
|
|
2124
|
+
duration: const Duration(milliseconds: 150),
|
|
2125
|
+
padding: const EdgeInsets.symmetric(
|
|
2126
|
+
horizontal: 10,
|
|
2127
|
+
vertical: 7,
|
|
2128
|
+
),
|
|
2129
|
+
decoration: BoxDecoration(
|
|
2130
|
+
color: _onlyAvailable
|
|
2131
|
+
? _accentMuted
|
|
2132
|
+
: _bgSecondary,
|
|
2133
|
+
borderRadius: BorderRadius.circular(8),
|
|
2134
|
+
border: Border.all(
|
|
2135
|
+
color: _onlyAvailable
|
|
2136
|
+
? _accent.withValues(alpha: 0.5)
|
|
2137
|
+
: _border,
|
|
2138
|
+
),
|
|
2139
|
+
),
|
|
2140
|
+
child: Text(
|
|
2141
|
+
'Available',
|
|
2142
|
+
style: TextStyle(
|
|
2143
|
+
fontSize: 12,
|
|
2144
|
+
fontWeight: FontWeight.w600,
|
|
2145
|
+
color: _onlyAvailable
|
|
2146
|
+
? _accentHover
|
|
2147
|
+
: _textSecondary,
|
|
2148
|
+
),
|
|
2149
|
+
),
|
|
2150
|
+
),
|
|
2151
|
+
),
|
|
2152
|
+
],
|
|
2278
2153
|
),
|
|
2154
|
+
),
|
|
2155
|
+
// Quick-action toolbar
|
|
2156
|
+
Padding(
|
|
2157
|
+
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
|
2279
2158
|
child: Row(
|
|
2280
|
-
mainAxisSize: MainAxisSize.min,
|
|
2281
2159
|
children: <Widget>[
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2160
|
+
_PoolActionChip(
|
|
2161
|
+
label: 'Select all',
|
|
2162
|
+
onTap: () => _selectAllVisible(filtered),
|
|
2285
2163
|
),
|
|
2286
|
-
const SizedBox(width:
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2164
|
+
const SizedBox(width: 6),
|
|
2165
|
+
_PoolActionChip(
|
|
2166
|
+
label: 'Clear all',
|
|
2167
|
+
onTap: () => _clearAllVisible(filtered),
|
|
2168
|
+
),
|
|
2169
|
+
const Spacer(),
|
|
2170
|
+
Text(
|
|
2171
|
+
'$selectedAvailableCount selected',
|
|
2172
|
+
style: TextStyle(
|
|
2173
|
+
fontSize: 12,
|
|
2174
|
+
color: _textMuted,
|
|
2175
|
+
),
|
|
2293
2176
|
),
|
|
2294
2177
|
],
|
|
2295
2178
|
),
|
|
2296
2179
|
),
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2180
|
+
Divider(height: 1, thickness: 1, color: _border),
|
|
2181
|
+
// Model list
|
|
2182
|
+
Flexible(
|
|
2183
|
+
child: rows.isEmpty
|
|
2184
|
+
? Padding(
|
|
2185
|
+
padding: const EdgeInsets.all(36),
|
|
2186
|
+
child: Column(
|
|
2187
|
+
mainAxisSize: MainAxisSize.min,
|
|
2188
|
+
children: <Widget>[
|
|
2189
|
+
Icon(
|
|
2190
|
+
Icons.search_off_rounded,
|
|
2191
|
+
size: 36,
|
|
2192
|
+
color: _textMuted,
|
|
2193
|
+
),
|
|
2194
|
+
const SizedBox(height: 12),
|
|
2195
|
+
Text(
|
|
2196
|
+
'No results for "$_query"',
|
|
2197
|
+
style: TextStyle(
|
|
2198
|
+
color: _textSecondary,
|
|
2199
|
+
fontSize: 14,
|
|
2200
|
+
),
|
|
2201
|
+
),
|
|
2202
|
+
],
|
|
2203
|
+
),
|
|
2204
|
+
)
|
|
2205
|
+
: ListView(
|
|
2206
|
+
padding:
|
|
2207
|
+
const EdgeInsets.only(top: 4, bottom: 8),
|
|
2208
|
+
shrinkWrap: true,
|
|
2209
|
+
children: rows,
|
|
2210
|
+
),
|
|
2211
|
+
),
|
|
2212
|
+
],
|
|
2326
2213
|
),
|
|
2327
|
-
|
|
2328
|
-
),
|
|
2329
|
-
const SizedBox(height: 12),
|
|
2330
|
-
Container(
|
|
2331
|
-
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
2332
|
-
decoration: BoxDecoration(
|
|
2333
|
-
color: _bgCard,
|
|
2334
|
-
borderRadius: BorderRadius.circular(14),
|
|
2335
|
-
border: Border.all(color: _border),
|
|
2214
|
+
),
|
|
2336
2215
|
),
|
|
2216
|
+
),
|
|
2217
|
+
),
|
|
2218
|
+
),
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2224
|
+
// Smart Pool Row — individual model row inside the dialog
|
|
2225
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2226
|
+
|
|
2227
|
+
class _SmartPoolRow extends StatelessWidget {
|
|
2228
|
+
const _SmartPoolRow({
|
|
2229
|
+
required this.model,
|
|
2230
|
+
required this.selected,
|
|
2231
|
+
required this.onToggle,
|
|
2232
|
+
});
|
|
2233
|
+
|
|
2234
|
+
final ModelMeta model;
|
|
2235
|
+
final bool selected;
|
|
2236
|
+
final ValueChanged<bool> onToggle;
|
|
2237
|
+
|
|
2238
|
+
@override
|
|
2239
|
+
Widget build(BuildContext context) {
|
|
2240
|
+
final color = _providerPickerColor(model.provider);
|
|
2241
|
+
return Opacity(
|
|
2242
|
+
opacity: model.available ? 1.0 : 0.4,
|
|
2243
|
+
child: Material(
|
|
2244
|
+
color: selected
|
|
2245
|
+
? _accentMuted.withValues(alpha: 0.12)
|
|
2246
|
+
: Colors.transparent,
|
|
2247
|
+
child: InkWell(
|
|
2248
|
+
onTap: model.available ? () => onToggle(!selected) : null,
|
|
2249
|
+
child: Padding(
|
|
2250
|
+
padding:
|
|
2251
|
+
const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
|
2337
2252
|
child: Row(
|
|
2338
2253
|
children: <Widget>[
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2254
|
+
// Thin provider accent bar on the left
|
|
2255
|
+
Container(
|
|
2256
|
+
width: 3,
|
|
2257
|
+
height: 30,
|
|
2258
|
+
decoration: BoxDecoration(
|
|
2259
|
+
color: color.withValues(
|
|
2260
|
+
alpha: selected ? 0.85 : 0.28),
|
|
2261
|
+
borderRadius: BorderRadius.circular(2),
|
|
2343
2262
|
),
|
|
2344
2263
|
),
|
|
2345
|
-
const SizedBox(width:
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
border: Border.all(color: _border),
|
|
2361
|
-
),
|
|
2362
|
-
child: Text(
|
|
2363
|
-
provider.credentialConfigured
|
|
2364
|
-
? 'Credentials for this provider are already available to the runtime.'
|
|
2365
|
-
: 'Credentials for this provider are managed outside this workspace UI. Finish the server or admin setup, then return here to enable routing.',
|
|
2366
|
-
style: TextStyle(color: _textSecondary, height: 1.35),
|
|
2367
|
-
),
|
|
2368
|
-
),
|
|
2369
|
-
if (provider.supportsBaseUrl) ...<Widget>[
|
|
2370
|
-
TextField(
|
|
2371
|
-
controller: baseUrlController,
|
|
2372
|
-
keyboardType: TextInputType.url,
|
|
2373
|
-
autocorrect: false,
|
|
2374
|
-
decoration: InputDecoration(
|
|
2375
|
-
labelText: provider.id == 'ollama'
|
|
2376
|
-
? 'Server URL'
|
|
2377
|
-
: 'Base URL',
|
|
2378
|
-
helperText: provider.defaultBaseUrl.trim().isEmpty
|
|
2379
|
-
? 'Optional override.'
|
|
2380
|
-
: 'Default: ${provider.defaultBaseUrl}',
|
|
2264
|
+
const SizedBox(width: 10),
|
|
2265
|
+
SizedBox(
|
|
2266
|
+
width: 20,
|
|
2267
|
+
height: 20,
|
|
2268
|
+
child: Checkbox(
|
|
2269
|
+
value: selected,
|
|
2270
|
+
onChanged: model.available
|
|
2271
|
+
? (v) => onToggle(v ?? false)
|
|
2272
|
+
: null,
|
|
2273
|
+
activeColor: _accent,
|
|
2274
|
+
side: BorderSide(color: _textMuted, width: 1.5),
|
|
2275
|
+
materialTapTargetSize:
|
|
2276
|
+
MaterialTapTargetSize.shrinkWrap,
|
|
2277
|
+
visualDensity: VisualDensity.compact,
|
|
2278
|
+
),
|
|
2381
2279
|
),
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
padding: const EdgeInsets.symmetric(
|
|
2395
|
-
horizontal: 10,
|
|
2396
|
-
vertical: 8,
|
|
2397
|
-
),
|
|
2398
|
-
decoration: BoxDecoration(
|
|
2399
|
-
color: model.available ? _bgCard : _bgPrimary,
|
|
2400
|
-
borderRadius: BorderRadius.circular(999),
|
|
2401
|
-
border: Border.all(
|
|
2402
|
-
color: model.available ? _border : _borderLight,
|
|
2403
|
-
),
|
|
2280
|
+
const SizedBox(width: 10),
|
|
2281
|
+
Expanded(
|
|
2282
|
+
child: Column(
|
|
2283
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2284
|
+
children: <Widget>[
|
|
2285
|
+
Text(
|
|
2286
|
+
model.label,
|
|
2287
|
+
style: TextStyle(
|
|
2288
|
+
fontSize: 13,
|
|
2289
|
+
fontWeight: FontWeight.w500,
|
|
2290
|
+
color:
|
|
2291
|
+
selected ? _accentHover : _textPrimary,
|
|
2404
2292
|
),
|
|
2405
|
-
|
|
2406
|
-
|
|
2293
|
+
maxLines: 1,
|
|
2294
|
+
overflow: TextOverflow.ellipsis,
|
|
2295
|
+
),
|
|
2296
|
+
if (model.purpose.isNotEmpty)
|
|
2297
|
+
Text(
|
|
2298
|
+
model.purpose,
|
|
2407
2299
|
style: TextStyle(
|
|
2408
|
-
fontSize:
|
|
2409
|
-
color:
|
|
2410
|
-
? _textPrimary
|
|
2411
|
-
: _textSecondary,
|
|
2300
|
+
fontSize: 11,
|
|
2301
|
+
color: _textMuted,
|
|
2412
2302
|
),
|
|
2413
2303
|
),
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2304
|
+
],
|
|
2305
|
+
),
|
|
2306
|
+
),
|
|
2307
|
+
const SizedBox(width: 8),
|
|
2308
|
+
if (model.priceTier != null)
|
|
2309
|
+
_PriceTierChip(tier: model.priceTier!),
|
|
2310
|
+
const SizedBox(width: 2),
|
|
2311
|
+
],
|
|
2312
|
+
),
|
|
2313
|
+
),
|
|
2314
|
+
),
|
|
2421
2315
|
),
|
|
2422
2316
|
);
|
|
2423
2317
|
}
|
|
2424
2318
|
}
|
|
2425
2319
|
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
required this.icon,
|
|
2430
|
-
required this.value,
|
|
2431
|
-
required this.items,
|
|
2432
|
-
required this.onChanged,
|
|
2433
|
-
});
|
|
2434
|
-
|
|
2320
|
+
// Toolbar chip button used inside _SmartPoolDialog
|
|
2321
|
+
class _PoolActionChip extends StatelessWidget {
|
|
2322
|
+
const _PoolActionChip({required this.label, required this.onTap});
|
|
2435
2323
|
final String label;
|
|
2436
|
-
final
|
|
2437
|
-
final String value;
|
|
2438
|
-
final List<DropdownMenuItem<String>> items;
|
|
2439
|
-
final ValueChanged<String?> onChanged;
|
|
2324
|
+
final VoidCallback onTap;
|
|
2440
2325
|
|
|
2441
2326
|
@override
|
|
2442
2327
|
Widget build(BuildContext context) {
|
|
2443
|
-
return
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
),
|
|
2460
|
-
const SizedBox(height: 10),
|
|
2461
|
-
DropdownButtonFormField<String>(
|
|
2462
|
-
initialValue: value,
|
|
2463
|
-
items: items,
|
|
2464
|
-
isExpanded: true,
|
|
2465
|
-
decoration: const InputDecoration(isDense: true),
|
|
2466
|
-
onChanged: onChanged,
|
|
2328
|
+
return GestureDetector(
|
|
2329
|
+
onTap: onTap,
|
|
2330
|
+
child: Container(
|
|
2331
|
+
padding:
|
|
2332
|
+
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
2333
|
+
decoration: BoxDecoration(
|
|
2334
|
+
color: _bgSecondary,
|
|
2335
|
+
borderRadius: BorderRadius.circular(8),
|
|
2336
|
+
border: Border.all(color: _border),
|
|
2337
|
+
),
|
|
2338
|
+
child: Text(
|
|
2339
|
+
label,
|
|
2340
|
+
style: TextStyle(
|
|
2341
|
+
fontSize: 12,
|
|
2342
|
+
fontWeight: FontWeight.w500,
|
|
2343
|
+
color: _textSecondary,
|
|
2467
2344
|
),
|
|
2468
|
-
|
|
2345
|
+
),
|
|
2469
2346
|
),
|
|
2470
2347
|
);
|
|
2471
2348
|
}
|