neoagent 2.4.1-beta.9 β 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 +36 -4
- 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 -3
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +13 -0
- 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 +2 -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 +197 -42
- package/flutter_app/lib/main_devices.dart +436 -4
- 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 +673 -871
- 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 +46 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +42 -3
- 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/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 +88537 -85936
- 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/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 +10 -2
- package/server/routes/stream.js +1 -1
- 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 +78 -13
- package/server/services/browser/extension/registry.js +94 -9
- 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 -53
- 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/streaming/android-stream.js +293 -40
- 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,7 +154,6 @@ const List<_SettingsSection> _settingsSearchSections = <_SettingsSection>[
|
|
|
163
154
|
_modelsSettingsSection,
|
|
164
155
|
_voiceRecordingSettingsSection,
|
|
165
156
|
_desktopSettingsSection,
|
|
166
|
-
_updatesSettingsSection,
|
|
167
157
|
_diagnosticsSettingsSection,
|
|
168
158
|
];
|
|
169
159
|
|
|
@@ -187,7 +177,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
187
177
|
final Map<String, bool> _providerEnabled = <String, bool>{};
|
|
188
178
|
final Map<String, TextEditingController> _providerBaseUrlControllers =
|
|
189
179
|
<String, TextEditingController>{};
|
|
190
|
-
final Set<String> _expandedProviderIds = <String>{};
|
|
191
180
|
|
|
192
181
|
// Inline runtime test state β ephemeral, not stored in controller.
|
|
193
182
|
bool _cliTestRunning = false;
|
|
@@ -313,26 +302,8 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
313
302
|
final routingModels = availableModels.isEmpty
|
|
314
303
|
? controller.supportedModels
|
|
315
304
|
: availableModels;
|
|
316
|
-
final modelChoices =
|
|
317
|
-
|
|
318
|
-
value: 'auto',
|
|
319
|
-
child: Text(
|
|
320
|
-
'Smart Selector (Auto)',
|
|
321
|
-
maxLines: 1,
|
|
322
|
-
overflow: TextOverflow.ellipsis,
|
|
323
|
-
),
|
|
324
|
-
),
|
|
325
|
-
...routingModels.map(
|
|
326
|
-
(model) => DropdownMenuItem<String>(
|
|
327
|
-
value: model.id,
|
|
328
|
-
child: Text(
|
|
329
|
-
model.label,
|
|
330
|
-
maxLines: 1,
|
|
331
|
-
overflow: TextOverflow.ellipsis,
|
|
332
|
-
),
|
|
333
|
-
),
|
|
334
|
-
),
|
|
335
|
-
];
|
|
305
|
+
final List<_ModelPickerOption> modelChoices =
|
|
306
|
+
_modelPickerOptions(routingModels, allowAuto: true);
|
|
336
307
|
final enabledSmartModels = _enabledModels
|
|
337
308
|
.where((id) => routingModels.any((model) => model.id == id))
|
|
338
309
|
.length;
|
|
@@ -347,7 +318,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
347
318
|
_PageTitle(
|
|
348
319
|
title: 'Settings',
|
|
349
320
|
subtitle:
|
|
350
|
-
'Workspace, models, recording,
|
|
321
|
+
'Workspace, models, recording, and diagnostics controls.',
|
|
351
322
|
trailing: _settingsSaveButton(controller),
|
|
352
323
|
)
|
|
353
324
|
else
|
|
@@ -367,7 +338,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
367
338
|
onChanged: (_) => setState(() {}),
|
|
368
339
|
decoration: InputDecoration(
|
|
369
340
|
labelText: 'Search settings',
|
|
370
|
-
hintText: 'Models, browser, voice,
|
|
341
|
+
hintText: 'Models, browser, voice, diagnostics...',
|
|
371
342
|
prefixIcon: const Icon(Icons.search),
|
|
372
343
|
suffixIcon: searchQuery.isEmpty
|
|
373
344
|
? null
|
|
@@ -400,6 +371,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
400
371
|
_modelsSettingsSection,
|
|
401
372
|
)) ...<Widget>[
|
|
402
373
|
_buildModelsSection(
|
|
374
|
+
context: context,
|
|
403
375
|
controller: controller,
|
|
404
376
|
modelChoices: modelChoices,
|
|
405
377
|
routingModels: routingModels,
|
|
@@ -427,13 +399,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
427
399
|
_buildDesktopSection(controller),
|
|
428
400
|
const SizedBox(height: 16),
|
|
429
401
|
],
|
|
430
|
-
if (_matchesSettingsSection(
|
|
431
|
-
searchQuery,
|
|
432
|
-
_updatesSettingsSection,
|
|
433
|
-
)) ...<Widget>[
|
|
434
|
-
_buildUpdatesSection(controller),
|
|
435
|
-
const SizedBox(height: 16),
|
|
436
|
-
],
|
|
437
402
|
if (_matchesSettingsSection(
|
|
438
403
|
searchQuery,
|
|
439
404
|
_diagnosticsSettingsSection,
|
|
@@ -442,7 +407,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
442
407
|
const _EmptyCard(
|
|
443
408
|
title: 'No matching settings',
|
|
444
409
|
subtitle:
|
|
445
|
-
'Try a broader search like models, browser,
|
|
410
|
+
'Try a broader search like models, browser, or voice.',
|
|
446
411
|
),
|
|
447
412
|
],
|
|
448
413
|
],
|
|
@@ -552,7 +517,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
552
517
|
const _SectionTitle('Overview'),
|
|
553
518
|
const SizedBox(height: 10),
|
|
554
519
|
Text(
|
|
555
|
-
'Configure workspace behavior,
|
|
520
|
+
'Configure workspace behavior, models, and recording defaults.',
|
|
556
521
|
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
557
522
|
),
|
|
558
523
|
const SizedBox(height: 14),
|
|
@@ -871,8 +836,9 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
871
836
|
}
|
|
872
837
|
|
|
873
838
|
Widget _buildModelsSection({
|
|
839
|
+
required BuildContext context,
|
|
874
840
|
required NeoAgentController controller,
|
|
875
|
-
required List<
|
|
841
|
+
required List<_ModelPickerOption> modelChoices,
|
|
876
842
|
required List<ModelMeta> routingModels,
|
|
877
843
|
required List<ModelMeta> availableModels,
|
|
878
844
|
required int enabledSmartModels,
|
|
@@ -886,88 +852,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
886
852
|
const _SectionTitle('Models'),
|
|
887
853
|
const SizedBox(height: 10),
|
|
888
854
|
Text(
|
|
889
|
-
'
|
|
855
|
+
'Choose defaults for chat, agents, fallback behavior, and smart routing.',
|
|
890
856
|
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
891
857
|
),
|
|
892
858
|
const SizedBox(height: 16),
|
|
893
|
-
Text(
|
|
894
|
-
'Providers',
|
|
895
|
-
style: TextStyle(
|
|
896
|
-
fontWeight: FontWeight.w700,
|
|
897
|
-
color: _textPrimary,
|
|
898
|
-
),
|
|
899
|
-
),
|
|
900
|
-
const SizedBox(height: 14),
|
|
901
|
-
if (controller.aiProviders.isEmpty)
|
|
902
|
-
Text(
|
|
903
|
-
'Provider metadata is unavailable on this server version.',
|
|
904
|
-
style: TextStyle(color: _textSecondary),
|
|
905
|
-
)
|
|
906
|
-
else
|
|
907
|
-
LayoutBuilder(
|
|
908
|
-
builder: (context, constraints) {
|
|
909
|
-
final compact = constraints.maxWidth < 960;
|
|
910
|
-
final cardWidth = compact
|
|
911
|
-
? constraints.maxWidth
|
|
912
|
-
: (constraints.maxWidth - 16) / 2;
|
|
913
|
-
return Wrap(
|
|
914
|
-
spacing: 16,
|
|
915
|
-
runSpacing: 16,
|
|
916
|
-
children: controller.aiProviders
|
|
917
|
-
.where(
|
|
918
|
-
(provider) =>
|
|
919
|
-
provider.available ||
|
|
920
|
-
_providerEnabled[provider.id] == true ||
|
|
921
|
-
controller
|
|
922
|
-
.aiProviderConfigs[provider.id]
|
|
923
|
-
?.enabled ==
|
|
924
|
-
true,
|
|
925
|
-
)
|
|
926
|
-
.map((provider) {
|
|
927
|
-
return SizedBox(
|
|
928
|
-
width: cardWidth,
|
|
929
|
-
child: _AiProviderCard(
|
|
930
|
-
provider: provider,
|
|
931
|
-
enabled:
|
|
932
|
-
_providerEnabled[provider.id] ??
|
|
933
|
-
controller
|
|
934
|
-
.aiProviderConfigs[provider.id]
|
|
935
|
-
?.enabled ??
|
|
936
|
-
true,
|
|
937
|
-
models: controller.supportedModels
|
|
938
|
-
.where(
|
|
939
|
-
(model) => model.provider == provider.id,
|
|
940
|
-
)
|
|
941
|
-
.toList(),
|
|
942
|
-
baseUrlController:
|
|
943
|
-
_providerBaseUrlControllers[provider.id]!,
|
|
944
|
-
expanded: _expandedProviderIds.contains(
|
|
945
|
-
provider.id,
|
|
946
|
-
),
|
|
947
|
-
onEnabledChanged: (value) {
|
|
948
|
-
setState(() {
|
|
949
|
-
_providerEnabled[provider.id] = value;
|
|
950
|
-
});
|
|
951
|
-
},
|
|
952
|
-
onExpandToggle: () {
|
|
953
|
-
setState(() {
|
|
954
|
-
if (_expandedProviderIds.contains(
|
|
955
|
-
provider.id,
|
|
956
|
-
)) {
|
|
957
|
-
_expandedProviderIds.remove(provider.id);
|
|
958
|
-
} else {
|
|
959
|
-
_expandedProviderIds.add(provider.id);
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
},
|
|
963
|
-
),
|
|
964
|
-
);
|
|
965
|
-
})
|
|
966
|
-
.toList(),
|
|
967
|
-
);
|
|
968
|
-
},
|
|
969
|
-
),
|
|
970
|
-
const Divider(height: 32),
|
|
971
859
|
Text(
|
|
972
860
|
'Default Routing',
|
|
973
861
|
style: TextStyle(
|
|
@@ -997,7 +885,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
997
885
|
routingModels,
|
|
998
886
|
allowAuto: true,
|
|
999
887
|
),
|
|
1000
|
-
|
|
888
|
+
options: modelChoices,
|
|
1001
889
|
onChanged: (value) {
|
|
1002
890
|
if (value != null) {
|
|
1003
891
|
setState(() => _defaultChatModel = value);
|
|
@@ -1015,7 +903,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1015
903
|
routingModels,
|
|
1016
904
|
allowAuto: true,
|
|
1017
905
|
),
|
|
1018
|
-
|
|
906
|
+
options: modelChoices,
|
|
1019
907
|
onChanged: (value) {
|
|
1020
908
|
if (value != null) {
|
|
1021
909
|
setState(() => _defaultSubagentModel = value);
|
|
@@ -1033,18 +921,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1033
921
|
routingModels,
|
|
1034
922
|
allowAuto: false,
|
|
1035
923
|
),
|
|
1036
|
-
|
|
1037
|
-
.map(
|
|
1038
|
-
(model) => DropdownMenuItem<String>(
|
|
1039
|
-
value: model.id,
|
|
1040
|
-
child: Text(
|
|
1041
|
-
model.label,
|
|
1042
|
-
maxLines: 1,
|
|
1043
|
-
overflow: TextOverflow.ellipsis,
|
|
1044
|
-
),
|
|
1045
|
-
),
|
|
1046
|
-
)
|
|
1047
|
-
.toList(),
|
|
924
|
+
options: _modelPickerOptions(routingModels),
|
|
1048
925
|
onChanged: (value) {
|
|
1049
926
|
if (value != null) {
|
|
1050
927
|
setState(() => _fallbackModel = value);
|
|
@@ -1065,46 +942,38 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1065
942
|
),
|
|
1066
943
|
),
|
|
1067
944
|
const SizedBox(height: 10),
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
+
),
|
|
1078
969
|
),
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
backgroundColor: _bgSecondary,
|
|
1083
|
-
side: BorderSide(
|
|
1084
|
-
color: model.available
|
|
1085
|
-
? _border
|
|
1086
|
-
: _warning.withValues(alpha: 0.35),
|
|
970
|
+
pageBuilder: (ctx, _, __) => _SmartPoolDialog(
|
|
971
|
+
models: controller.supportedModels,
|
|
972
|
+
selectedIds: _enabledModels,
|
|
1087
973
|
),
|
|
1088
|
-
onSelected: model.available
|
|
1089
|
-
? (value) {
|
|
1090
|
-
setState(() {
|
|
1091
|
-
if (value) {
|
|
1092
|
-
_enabledModels.add(model.id);
|
|
1093
|
-
} else if (_enabledModels.length > 1) {
|
|
1094
|
-
_enabledModels.remove(model.id);
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
: null,
|
|
1099
974
|
);
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const SizedBox(height: 14),
|
|
1103
|
-
Text(
|
|
1104
|
-
availableModels.isEmpty
|
|
1105
|
-
? 'Enable a ready provider above to unlock model routing.'
|
|
1106
|
-
: '$enabledSmartModels models are currently eligible for smart routing.',
|
|
1107
|
-
style: TextStyle(color: _textSecondary),
|
|
975
|
+
if (result != null) setState(() => _enabledModels = result);
|
|
976
|
+
},
|
|
1108
977
|
),
|
|
1109
978
|
],
|
|
1110
979
|
),
|
|
@@ -1114,7 +983,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1114
983
|
|
|
1115
984
|
Widget _buildVoiceAndRecordingSection({
|
|
1116
985
|
required NeoAgentController controller,
|
|
1117
|
-
required List<
|
|
986
|
+
required List<_ModelPickerOption> modelChoices,
|
|
1118
987
|
required List<ModelMeta> routingModels,
|
|
1119
988
|
}) {
|
|
1120
989
|
final liveVoiceOptions =
|
|
@@ -1160,7 +1029,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1160
1029
|
routingModels,
|
|
1161
1030
|
allowAuto: true,
|
|
1162
1031
|
),
|
|
1163
|
-
|
|
1032
|
+
options: modelChoices,
|
|
1164
1033
|
onChanged: (value) {
|
|
1165
1034
|
if (value != null) {
|
|
1166
1035
|
setState(
|
|
@@ -1176,7 +1045,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1176
1045
|
label: 'Recording Transcription',
|
|
1177
1046
|
icon: Icons.hearing_outlined,
|
|
1178
1047
|
value: _defaultRecordingTranscriptionModel,
|
|
1179
|
-
|
|
1048
|
+
options: _recordingTranscriptionOptions(
|
|
1180
1049
|
_defaultRecordingTranscriptionModel,
|
|
1181
1050
|
),
|
|
1182
1051
|
onChanged: (value) {
|
|
@@ -1221,7 +1090,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1221
1090
|
routingModels,
|
|
1222
1091
|
allowAuto: true,
|
|
1223
1092
|
),
|
|
1224
|
-
|
|
1093
|
+
options: modelChoices,
|
|
1225
1094
|
onChanged: (value) {
|
|
1226
1095
|
if (value != null) {
|
|
1227
1096
|
setState(() => _defaultSpeechModel = value);
|
|
@@ -1263,14 +1132,9 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1263
1132
|
label: 'Live Provider',
|
|
1264
1133
|
icon: Icons.call_outlined,
|
|
1265
1134
|
value: _voiceLiveProvider,
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
value: value,
|
|
1270
|
-
child: Text(value),
|
|
1271
|
-
),
|
|
1272
|
-
)
|
|
1273
|
-
.toList(),
|
|
1135
|
+
options: _simplePickerOptions(
|
|
1136
|
+
const <String>['openai', 'gemini'],
|
|
1137
|
+
),
|
|
1274
1138
|
onChanged: (value) {
|
|
1275
1139
|
if (value == null) return;
|
|
1276
1140
|
setState(() {
|
|
@@ -1299,16 +1163,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1299
1163
|
label: 'Live Model',
|
|
1300
1164
|
icon: Icons.speed_outlined,
|
|
1301
1165
|
value: _voiceLiveModel,
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
(value) => DropdownMenuItem<String>(
|
|
1307
|
-
value: value,
|
|
1308
|
-
child: Text(value),
|
|
1309
|
-
),
|
|
1310
|
-
)
|
|
1311
|
-
.toList(),
|
|
1166
|
+
options: _simplePickerOptions(
|
|
1167
|
+
_voiceLiveModelsByProvider[_voiceLiveProvider] ??
|
|
1168
|
+
const <String>[],
|
|
1169
|
+
),
|
|
1312
1170
|
onChanged: (value) {
|
|
1313
1171
|
if (value != null) {
|
|
1314
1172
|
setState(() => _voiceLiveModel = value);
|
|
@@ -1323,14 +1181,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1323
1181
|
label: 'Live Voice',
|
|
1324
1182
|
icon: Icons.graphic_eq_outlined,
|
|
1325
1183
|
value: _voiceLiveVoice,
|
|
1326
|
-
|
|
1327
|
-
.map(
|
|
1328
|
-
(value) => DropdownMenuItem<String>(
|
|
1329
|
-
value: value,
|
|
1330
|
-
child: Text(value),
|
|
1331
|
-
),
|
|
1332
|
-
)
|
|
1333
|
-
.toList(),
|
|
1184
|
+
options: _simplePickerOptions(liveVoiceOptions),
|
|
1334
1185
|
onChanged: (value) {
|
|
1335
1186
|
if (value != null) {
|
|
1336
1187
|
setState(() => _voiceLiveVoice = value);
|
|
@@ -1655,426 +1506,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1655
1506
|
);
|
|
1656
1507
|
}
|
|
1657
1508
|
|
|
1658
|
-
Widget _buildUpdatesSection(NeoAgentController controller) {
|
|
1659
|
-
return Card(
|
|
1660
|
-
child: Padding(
|
|
1661
|
-
padding: const EdgeInsets.all(20),
|
|
1662
|
-
child: Column(
|
|
1663
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1664
|
-
children: <Widget>[
|
|
1665
|
-
const _SectionTitle('Updates'),
|
|
1666
|
-
const SizedBox(height: 10),
|
|
1667
|
-
Text(
|
|
1668
|
-
'Client and runtime update controls live here.',
|
|
1669
|
-
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
1670
|
-
),
|
|
1671
|
-
const SizedBox(height: 16),
|
|
1672
|
-
LayoutBuilder(
|
|
1673
|
-
builder: (context, constraints) {
|
|
1674
|
-
final compact = constraints.maxWidth < 720;
|
|
1675
|
-
final checkButton = FilledButton.icon(
|
|
1676
|
-
onPressed:
|
|
1677
|
-
controller.isCheckingAppUpdate ||
|
|
1678
|
-
!controller.appUpdaterConfigured
|
|
1679
|
-
? null
|
|
1680
|
-
: () => controller.checkForAppUpdates(),
|
|
1681
|
-
style: FilledButton.styleFrom(backgroundColor: _accent),
|
|
1682
|
-
icon: controller.isCheckingAppUpdate
|
|
1683
|
-
? _inlineProgressIndicator()
|
|
1684
|
-
: const Icon(Icons.sync),
|
|
1685
|
-
label: Text(
|
|
1686
|
-
controller.isCheckingAppUpdate
|
|
1687
|
-
? 'Checking...'
|
|
1688
|
-
: 'Check now',
|
|
1689
|
-
),
|
|
1690
|
-
);
|
|
1691
|
-
final appHeading = Text(
|
|
1692
|
-
'Client App',
|
|
1693
|
-
style: TextStyle(
|
|
1694
|
-
fontWeight: FontWeight.w700,
|
|
1695
|
-
color: _textPrimary,
|
|
1696
|
-
),
|
|
1697
|
-
);
|
|
1698
|
-
if (compact) {
|
|
1699
|
-
return Column(
|
|
1700
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1701
|
-
children: <Widget>[
|
|
1702
|
-
appHeading,
|
|
1703
|
-
const SizedBox(height: 10),
|
|
1704
|
-
checkButton,
|
|
1705
|
-
],
|
|
1706
|
-
);
|
|
1707
|
-
}
|
|
1708
|
-
return Row(
|
|
1709
|
-
children: <Widget>[
|
|
1710
|
-
Expanded(child: appHeading),
|
|
1711
|
-
checkButton,
|
|
1712
|
-
],
|
|
1713
|
-
);
|
|
1714
|
-
},
|
|
1715
|
-
),
|
|
1716
|
-
const SizedBox(height: 12),
|
|
1717
|
-
if (!controller.appUpdaterConfigured)
|
|
1718
|
-
if (!kIsWeb)
|
|
1719
|
-
Text(
|
|
1720
|
-
'Client app updates are not configured for this build.',
|
|
1721
|
-
style: TextStyle(color: _textSecondary, height: 1.5),
|
|
1722
|
-
)
|
|
1723
|
-
else ...<Widget>[
|
|
1724
|
-
LayoutBuilder(
|
|
1725
|
-
builder: (context, constraints) {
|
|
1726
|
-
final compact = constraints.maxWidth < 780;
|
|
1727
|
-
final channelPicker = DropdownButtonFormField<String>(
|
|
1728
|
-
initialValue: controller.appUpdateChannel,
|
|
1729
|
-
decoration: const InputDecoration(
|
|
1730
|
-
labelText: 'App release channel',
|
|
1731
|
-
),
|
|
1732
|
-
items: const <DropdownMenuItem<String>>[
|
|
1733
|
-
DropdownMenuItem<String>(
|
|
1734
|
-
value: 'stable',
|
|
1735
|
-
child: Text('Stable'),
|
|
1736
|
-
),
|
|
1737
|
-
DropdownMenuItem<String>(
|
|
1738
|
-
value: 'beta',
|
|
1739
|
-
child: Text('Beta'),
|
|
1740
|
-
),
|
|
1741
|
-
],
|
|
1742
|
-
onChanged: (value) {
|
|
1743
|
-
if (value != null) {
|
|
1744
|
-
unawaited(controller.setAppUpdateChannel(value));
|
|
1745
|
-
}
|
|
1746
|
-
},
|
|
1747
|
-
);
|
|
1748
|
-
final autoCheck = SwitchListTile.adaptive(
|
|
1749
|
-
value: controller.appUpdateAutoCheckEnabled,
|
|
1750
|
-
contentPadding: EdgeInsets.zero,
|
|
1751
|
-
title: Text('Check automatically on launch'),
|
|
1752
|
-
subtitle: Text(
|
|
1753
|
-
'This only checks GitHub Releases on startup. Installation still requires your confirmation.',
|
|
1754
|
-
style: TextStyle(color: _textSecondary),
|
|
1755
|
-
),
|
|
1756
|
-
onChanged: controller.setAppUpdateAutoCheckEnabled,
|
|
1757
|
-
);
|
|
1758
|
-
|
|
1759
|
-
if (compact) {
|
|
1760
|
-
return Column(
|
|
1761
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1762
|
-
children: <Widget>[
|
|
1763
|
-
channelPicker,
|
|
1764
|
-
const SizedBox(height: 10),
|
|
1765
|
-
autoCheck,
|
|
1766
|
-
],
|
|
1767
|
-
);
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
return Row(
|
|
1771
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1772
|
-
children: <Widget>[
|
|
1773
|
-
Expanded(child: channelPicker),
|
|
1774
|
-
const SizedBox(width: 16),
|
|
1775
|
-
Expanded(child: autoCheck),
|
|
1776
|
-
],
|
|
1777
|
-
);
|
|
1778
|
-
},
|
|
1779
|
-
),
|
|
1780
|
-
const SizedBox(height: 8),
|
|
1781
|
-
Text(
|
|
1782
|
-
'Installed: ${controller.installedAppVersion ?? 'Unknown'} | Channel: ${controller.appUpdateChannelLabel} | Last checked: ${controller.appUpdateLastCheckedLabel}',
|
|
1783
|
-
style: TextStyle(color: _textSecondary),
|
|
1784
|
-
),
|
|
1785
|
-
const SizedBox(height: 6),
|
|
1786
|
-
Text(
|
|
1787
|
-
'Source: ${app_release_updater.appUpdaterGithubOwner}/${app_release_updater.appUpdaterGithubRepo}${app_release_updater.appUpdaterGithubToken.trim().isNotEmpty ? ' (override active)' : ''}',
|
|
1788
|
-
style: TextStyle(color: _textSecondary),
|
|
1789
|
-
),
|
|
1790
|
-
if (controller.appUpdateErrorMessage
|
|
1791
|
-
case final message?) ...<Widget>[
|
|
1792
|
-
const SizedBox(height: 12),
|
|
1793
|
-
_InlineError(message: message),
|
|
1794
|
-
],
|
|
1795
|
-
if (controller.availableAppUpdate
|
|
1796
|
-
case final release?) ...<Widget>[
|
|
1797
|
-
const SizedBox(height: 16),
|
|
1798
|
-
Container(
|
|
1799
|
-
width: double.infinity,
|
|
1800
|
-
padding: const EdgeInsets.all(16),
|
|
1801
|
-
decoration: BoxDecoration(
|
|
1802
|
-
color: _bgSecondary,
|
|
1803
|
-
borderRadius: BorderRadius.circular(18),
|
|
1804
|
-
border: Border.all(color: _border),
|
|
1805
|
-
),
|
|
1806
|
-
child: Column(
|
|
1807
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1808
|
-
children: <Widget>[
|
|
1809
|
-
Wrap(
|
|
1810
|
-
spacing: 10,
|
|
1811
|
-
runSpacing: 10,
|
|
1812
|
-
children: <Widget>[
|
|
1813
|
-
_StatusPill(
|
|
1814
|
-
label: 'Update ${release.version}',
|
|
1815
|
-
color: release.channel == 'beta'
|
|
1816
|
-
? _warning
|
|
1817
|
-
: _accent,
|
|
1818
|
-
),
|
|
1819
|
-
_StatusPill(
|
|
1820
|
-
label: release.asset.name,
|
|
1821
|
-
color: _textSecondary,
|
|
1822
|
-
),
|
|
1823
|
-
],
|
|
1824
|
-
),
|
|
1825
|
-
const SizedBox(height: 10),
|
|
1826
|
-
Text(
|
|
1827
|
-
'${release.title} Β· ${release.publishedLabel} Β· ${release.asset.sizeLabel}',
|
|
1828
|
-
style: TextStyle(color: _textSecondary),
|
|
1829
|
-
),
|
|
1830
|
-
if (release.body.trim().isNotEmpty) ...<Widget>[
|
|
1831
|
-
const SizedBox(height: 14),
|
|
1832
|
-
ConstrainedBox(
|
|
1833
|
-
constraints: const BoxConstraints(maxHeight: 220),
|
|
1834
|
-
child: SingleChildScrollView(
|
|
1835
|
-
child: MarkdownBody(
|
|
1836
|
-
data: release.body,
|
|
1837
|
-
selectable: true,
|
|
1838
|
-
styleSheet: MarkdownStyleSheet(
|
|
1839
|
-
p: TextStyle(
|
|
1840
|
-
color: _textSecondary,
|
|
1841
|
-
height: 1.45,
|
|
1842
|
-
),
|
|
1843
|
-
),
|
|
1844
|
-
),
|
|
1845
|
-
),
|
|
1846
|
-
),
|
|
1847
|
-
],
|
|
1848
|
-
const SizedBox(height: 14),
|
|
1849
|
-
Wrap(
|
|
1850
|
-
spacing: 10,
|
|
1851
|
-
runSpacing: 10,
|
|
1852
|
-
children: <Widget>[
|
|
1853
|
-
FilledButton.icon(
|
|
1854
|
-
onPressed: controller.isOpeningAppUpdate
|
|
1855
|
-
? null
|
|
1856
|
-
: controller.openAppUpdate,
|
|
1857
|
-
style: FilledButton.styleFrom(
|
|
1858
|
-
backgroundColor: _accent,
|
|
1859
|
-
),
|
|
1860
|
-
icon: controller.isOpeningAppUpdate
|
|
1861
|
-
? _inlineProgressIndicator()
|
|
1862
|
-
: const Icon(Icons.system_update_alt),
|
|
1863
|
-
label: Text(
|
|
1864
|
-
controller.isOpeningAppUpdate
|
|
1865
|
-
? 'Opening...'
|
|
1866
|
-
: 'Download update',
|
|
1867
|
-
),
|
|
1868
|
-
),
|
|
1869
|
-
if (release.htmlUrl.trim().isNotEmpty)
|
|
1870
|
-
OutlinedButton.icon(
|
|
1871
|
-
onPressed: () {
|
|
1872
|
-
unawaited(
|
|
1873
|
-
widget.controller._oauthLauncher
|
|
1874
|
-
.openExternal(
|
|
1875
|
-
url: release.htmlUrl,
|
|
1876
|
-
label: 'release_notes',
|
|
1877
|
-
),
|
|
1878
|
-
);
|
|
1879
|
-
},
|
|
1880
|
-
icon: const Icon(Icons.open_in_new),
|
|
1881
|
-
label: Text('View release'),
|
|
1882
|
-
),
|
|
1883
|
-
],
|
|
1884
|
-
),
|
|
1885
|
-
],
|
|
1886
|
-
),
|
|
1887
|
-
),
|
|
1888
|
-
] else ...<Widget>[
|
|
1889
|
-
const SizedBox(height: 12),
|
|
1890
|
-
Text(
|
|
1891
|
-
controller.isCheckingAppUpdate
|
|
1892
|
-
? 'Checking GitHub releases...'
|
|
1893
|
-
: controller.appUpdateLastCheckedAt == null
|
|
1894
|
-
? 'Choose a channel, then check GitHub releases.'
|
|
1895
|
-
: 'No newer app release is available on the selected channel.',
|
|
1896
|
-
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
1897
|
-
),
|
|
1898
|
-
],
|
|
1899
|
-
],
|
|
1900
|
-
const Divider(height: 32),
|
|
1901
|
-
if (controller.updateStatus.allowSelfUpdate) ...<Widget>[
|
|
1902
|
-
LayoutBuilder(
|
|
1903
|
-
builder: (context, constraints) {
|
|
1904
|
-
final compact = constraints.maxWidth < 780;
|
|
1905
|
-
final channelPicker = DropdownButtonFormField<String>(
|
|
1906
|
-
initialValue: controller.updateStatus.releaseChannel,
|
|
1907
|
-
decoration: const InputDecoration(
|
|
1908
|
-
labelText: 'Runtime release channel',
|
|
1909
|
-
),
|
|
1910
|
-
items: const <DropdownMenuItem<String>>[
|
|
1911
|
-
DropdownMenuItem<String>(
|
|
1912
|
-
value: 'stable',
|
|
1913
|
-
child: Text('Stable'),
|
|
1914
|
-
),
|
|
1915
|
-
DropdownMenuItem<String>(
|
|
1916
|
-
value: 'beta',
|
|
1917
|
-
child: Text('Beta'),
|
|
1918
|
-
),
|
|
1919
|
-
],
|
|
1920
|
-
onChanged:
|
|
1921
|
-
controller.isSavingReleaseChannel ||
|
|
1922
|
-
controller.isTriggeringUpdate ||
|
|
1923
|
-
controller.updateStatus.state == 'running'
|
|
1924
|
-
? null
|
|
1925
|
-
: (value) {
|
|
1926
|
-
if (value != null) {
|
|
1927
|
-
unawaited(controller.setReleaseChannel(value));
|
|
1928
|
-
}
|
|
1929
|
-
},
|
|
1930
|
-
);
|
|
1931
|
-
|
|
1932
|
-
final channelHelper = Text(
|
|
1933
|
-
controller.updateStatus.releaseChannel == 'beta'
|
|
1934
|
-
? 'Beta follows preview releases.'
|
|
1935
|
-
: 'Stable follows production releases.',
|
|
1936
|
-
style: TextStyle(color: _textSecondary),
|
|
1937
|
-
);
|
|
1938
|
-
|
|
1939
|
-
if (compact) {
|
|
1940
|
-
return Column(
|
|
1941
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1942
|
-
children: <Widget>[
|
|
1943
|
-
channelPicker,
|
|
1944
|
-
const SizedBox(height: 8),
|
|
1945
|
-
channelHelper,
|
|
1946
|
-
const SizedBox(height: 16),
|
|
1947
|
-
],
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
return Padding(
|
|
1952
|
-
padding: const EdgeInsets.only(bottom: 16),
|
|
1953
|
-
child: Row(
|
|
1954
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1955
|
-
children: <Widget>[
|
|
1956
|
-
Expanded(child: channelPicker),
|
|
1957
|
-
const SizedBox(width: 12),
|
|
1958
|
-
Expanded(child: channelHelper),
|
|
1959
|
-
],
|
|
1960
|
-
),
|
|
1961
|
-
);
|
|
1962
|
-
},
|
|
1963
|
-
),
|
|
1964
|
-
LayoutBuilder(
|
|
1965
|
-
builder: (context, constraints) {
|
|
1966
|
-
final compact = constraints.maxWidth < 780;
|
|
1967
|
-
final runtimeTitle = Text(
|
|
1968
|
-
'Runtime',
|
|
1969
|
-
style: TextStyle(
|
|
1970
|
-
fontWeight: FontWeight.w700,
|
|
1971
|
-
color: _textPrimary,
|
|
1972
|
-
),
|
|
1973
|
-
);
|
|
1974
|
-
final updateButton = FilledButton.icon(
|
|
1975
|
-
onPressed:
|
|
1976
|
-
controller.isSavingReleaseChannel ||
|
|
1977
|
-
controller.isTriggeringUpdate ||
|
|
1978
|
-
controller.updateStatus.state == 'running'
|
|
1979
|
-
? null
|
|
1980
|
-
: controller.triggerUpdate,
|
|
1981
|
-
style: FilledButton.styleFrom(backgroundColor: _accent),
|
|
1982
|
-
icon: controller.isTriggeringUpdate
|
|
1983
|
-
? const SizedBox.square(
|
|
1984
|
-
dimension: 16,
|
|
1985
|
-
child: CircularProgressIndicator(
|
|
1986
|
-
strokeWidth: 2,
|
|
1987
|
-
color: Colors.white,
|
|
1988
|
-
),
|
|
1989
|
-
)
|
|
1990
|
-
: Icon(Icons.system_update),
|
|
1991
|
-
label: Text('Update'),
|
|
1992
|
-
);
|
|
1993
|
-
if (compact) {
|
|
1994
|
-
return Column(
|
|
1995
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1996
|
-
children: <Widget>[
|
|
1997
|
-
runtimeTitle,
|
|
1998
|
-
const SizedBox(height: 10),
|
|
1999
|
-
updateButton,
|
|
2000
|
-
],
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
|
-
return Row(
|
|
2004
|
-
children: <Widget>[
|
|
2005
|
-
Expanded(child: runtimeTitle),
|
|
2006
|
-
updateButton,
|
|
2007
|
-
],
|
|
2008
|
-
);
|
|
2009
|
-
},
|
|
2010
|
-
),
|
|
2011
|
-
] else ...<Widget>[
|
|
2012
|
-
Text(
|
|
2013
|
-
'Runtime',
|
|
2014
|
-
style: TextStyle(
|
|
2015
|
-
fontWeight: FontWeight.w700,
|
|
2016
|
-
color: _textPrimary,
|
|
2017
|
-
),
|
|
2018
|
-
),
|
|
2019
|
-
const SizedBox(height: 10),
|
|
2020
|
-
Text(
|
|
2021
|
-
'Updates and release tracks are managed for this deployment.',
|
|
2022
|
-
style: TextStyle(color: _textSecondary),
|
|
2023
|
-
),
|
|
2024
|
-
],
|
|
2025
|
-
const SizedBox(height: 12),
|
|
2026
|
-
LayoutBuilder(
|
|
2027
|
-
builder: (context, constraints) {
|
|
2028
|
-
final compact = constraints.maxWidth < 760;
|
|
2029
|
-
final statusRow = Wrap(
|
|
2030
|
-
spacing: 10,
|
|
2031
|
-
runSpacing: 10,
|
|
2032
|
-
crossAxisAlignment: WrapCrossAlignment.center,
|
|
2033
|
-
children: <Widget>[
|
|
2034
|
-
_StatusPill(
|
|
2035
|
-
label: controller.updateStatus.badgeLabel,
|
|
2036
|
-
color: controller.updateStatus.badgeColor,
|
|
2037
|
-
),
|
|
2038
|
-
_StatusPill(
|
|
2039
|
-
label: controller.updateStatus.releaseChannelLabel,
|
|
2040
|
-
color: controller.updateStatus.releaseChannel == 'beta'
|
|
2041
|
-
? _warning
|
|
2042
|
-
: _accent,
|
|
2043
|
-
),
|
|
2044
|
-
Text(
|
|
2045
|
-
controller.updateStatus.message,
|
|
2046
|
-
style: TextStyle(color: _textSecondary),
|
|
2047
|
-
),
|
|
2048
|
-
Text('${controller.updateStatus.progress}%'),
|
|
2049
|
-
],
|
|
2050
|
-
);
|
|
2051
|
-
if (compact) {
|
|
2052
|
-
return Column(
|
|
2053
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2054
|
-
children: <Widget>[statusRow],
|
|
2055
|
-
);
|
|
2056
|
-
}
|
|
2057
|
-
return statusRow;
|
|
2058
|
-
},
|
|
2059
|
-
),
|
|
2060
|
-
const SizedBox(height: 10),
|
|
2061
|
-
ClipRRect(
|
|
2062
|
-
borderRadius: BorderRadius.circular(999),
|
|
2063
|
-
child: LinearProgressIndicator(
|
|
2064
|
-
minHeight: 8,
|
|
2065
|
-
value: controller.updateStatus.progress / 100,
|
|
2066
|
-
backgroundColor: _bgSecondary,
|
|
2067
|
-
color: _accent,
|
|
2068
|
-
),
|
|
2069
|
-
),
|
|
2070
|
-
const SizedBox(height: 12),
|
|
2071
|
-
Text(controller.updateStatus.versionLine),
|
|
2072
|
-
],
|
|
2073
|
-
),
|
|
2074
|
-
),
|
|
2075
|
-
);
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
1509
|
Widget _buildDiagnosticsSection(NeoAgentController controller) {
|
|
2079
1510
|
return Card(
|
|
2080
1511
|
child: Padding(
|
|
@@ -2179,20 +1610,12 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
2179
1610
|
}
|
|
2180
1611
|
}
|
|
2181
1612
|
|
|
2182
|
-
List<
|
|
2183
|
-
String
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
if (normalizedCurrent.isNotEmpty) {
|
|
2189
|
-
values.add(normalizedCurrent);
|
|
2190
|
-
}
|
|
2191
|
-
return values
|
|
2192
|
-
.map(
|
|
2193
|
-
(value) => DropdownMenuItem<String>(value: value, child: Text(value)),
|
|
2194
|
-
)
|
|
2195
|
-
.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());
|
|
2196
1619
|
}
|
|
2197
1620
|
|
|
2198
1621
|
// Shared helper: small "Test" button + inline result row.
|
|
@@ -2260,287 +1683,666 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
2260
1683
|
}
|
|
2261
1684
|
}
|
|
2262
1685
|
|
|
2263
|
-
class
|
|
2264
|
-
const
|
|
2265
|
-
required this.
|
|
2266
|
-
required this.
|
|
2267
|
-
required this.
|
|
2268
|
-
required this.
|
|
2269
|
-
required this.
|
|
2270
|
-
required this.onEnabledChanged,
|
|
2271
|
-
required this.onExpandToggle,
|
|
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,
|
|
2272
1693
|
});
|
|
2273
1694
|
|
|
2274
|
-
final
|
|
2275
|
-
final
|
|
2276
|
-
final
|
|
2277
|
-
final List<
|
|
2278
|
-
final
|
|
2279
|
-
final ValueChanged<bool> onEnabledChanged;
|
|
2280
|
-
final VoidCallback onExpandToggle;
|
|
1695
|
+
final String label;
|
|
1696
|
+
final IconData icon;
|
|
1697
|
+
final String value;
|
|
1698
|
+
final List<_ModelPickerOption> options;
|
|
1699
|
+
final ValueChanged<String?> onChanged;
|
|
2281
1700
|
|
|
2282
1701
|
@override
|
|
2283
1702
|
Widget build(BuildContext context) {
|
|
2284
|
-
final availableCount = models.where((model) => model.available).length;
|
|
2285
|
-
final hasAdvancedFields = provider.supportsBaseUrl || models.isNotEmpty;
|
|
2286
1703
|
return Container(
|
|
2287
|
-
padding: const EdgeInsets.all(
|
|
1704
|
+
padding: const EdgeInsets.all(14),
|
|
2288
1705
|
decoration: BoxDecoration(
|
|
2289
1706
|
color: _bgSecondary,
|
|
2290
|
-
borderRadius: BorderRadius.circular(
|
|
2291
|
-
border: Border.all(color:
|
|
1707
|
+
borderRadius: BorderRadius.circular(16),
|
|
1708
|
+
border: Border.all(color: _border),
|
|
2292
1709
|
),
|
|
2293
1710
|
child: Column(
|
|
2294
1711
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2295
1712
|
children: <Widget>[
|
|
2296
1713
|
Row(
|
|
2297
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2298
1714
|
children: <Widget>[
|
|
2299
|
-
|
|
2300
|
-
width: 44,
|
|
2301
|
-
height: 44,
|
|
2302
|
-
decoration: BoxDecoration(
|
|
2303
|
-
color: _accentMuted,
|
|
2304
|
-
borderRadius: BorderRadius.circular(14),
|
|
2305
|
-
),
|
|
2306
|
-
child: Icon(provider.icon, color: _accentHover),
|
|
2307
|
-
),
|
|
2308
|
-
const SizedBox(width: 12),
|
|
2309
|
-
Expanded(
|
|
2310
|
-
child: Column(
|
|
2311
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2312
|
-
children: <Widget>[
|
|
2313
|
-
Text(
|
|
2314
|
-
provider.label,
|
|
2315
|
-
style: TextStyle(
|
|
2316
|
-
fontSize: 16,
|
|
2317
|
-
fontWeight: FontWeight.w700,
|
|
2318
|
-
),
|
|
2319
|
-
),
|
|
2320
|
-
const SizedBox(height: 4),
|
|
2321
|
-
Text(
|
|
2322
|
-
provider.description,
|
|
2323
|
-
maxLines: 2,
|
|
2324
|
-
overflow: TextOverflow.ellipsis,
|
|
2325
|
-
style: TextStyle(color: _textSecondary, height: 1.4),
|
|
2326
|
-
),
|
|
2327
|
-
],
|
|
2328
|
-
),
|
|
2329
|
-
),
|
|
1715
|
+
Icon(icon, size: 16, color: _accentHover),
|
|
2330
1716
|
const SizedBox(width: 8),
|
|
2331
|
-
|
|
2332
|
-
crossAxisAlignment: CrossAxisAlignment.end,
|
|
2333
|
-
children: <Widget>[
|
|
2334
|
-
_StatusPill(
|
|
2335
|
-
label: enabled ? provider.statusLabel : 'Disabled',
|
|
2336
|
-
color: enabled ? provider.statusColor : _textSecondary,
|
|
2337
|
-
),
|
|
2338
|
-
const SizedBox(height: 8),
|
|
2339
|
-
InkWell(
|
|
2340
|
-
onTap: hasAdvancedFields || models.isNotEmpty
|
|
2341
|
-
? onExpandToggle
|
|
2342
|
-
: null,
|
|
2343
|
-
borderRadius: BorderRadius.circular(999),
|
|
2344
|
-
child: Container(
|
|
2345
|
-
padding: const EdgeInsets.symmetric(
|
|
2346
|
-
horizontal: 10,
|
|
2347
|
-
vertical: 6,
|
|
2348
|
-
),
|
|
2349
|
-
decoration: BoxDecoration(
|
|
2350
|
-
color: _bgCard,
|
|
2351
|
-
borderRadius: BorderRadius.circular(999),
|
|
2352
|
-
border: Border.all(color: _border),
|
|
2353
|
-
),
|
|
2354
|
-
child: Row(
|
|
2355
|
-
mainAxisSize: MainAxisSize.min,
|
|
2356
|
-
children: <Widget>[
|
|
2357
|
-
Text(
|
|
2358
|
-
expanded ? 'Hide' : 'Setup',
|
|
2359
|
-
style: TextStyle(fontSize: 12),
|
|
2360
|
-
),
|
|
2361
|
-
const SizedBox(width: 4),
|
|
2362
|
-
Icon(
|
|
2363
|
-
expanded
|
|
2364
|
-
? Icons.keyboard_arrow_up
|
|
2365
|
-
: Icons.keyboard_arrow_down,
|
|
2366
|
-
size: 16,
|
|
2367
|
-
color: _textSecondary,
|
|
2368
|
-
),
|
|
2369
|
-
],
|
|
2370
|
-
),
|
|
2371
|
-
),
|
|
2372
|
-
),
|
|
2373
|
-
],
|
|
2374
|
-
),
|
|
1717
|
+
Text(label, style: TextStyle(fontWeight: FontWeight.w700)),
|
|
2375
1718
|
],
|
|
2376
1719
|
),
|
|
2377
|
-
const SizedBox(height:
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
label: '$availableCount of ${models.length} models ready',
|
|
2384
|
-
icon: Icons.memory_outlined,
|
|
2385
|
-
),
|
|
2386
|
-
if (provider.supportsApiKey && provider.credentialConfigured)
|
|
2387
|
-
const _MetaPill(
|
|
2388
|
-
label: 'Credentials ready',
|
|
2389
|
-
icon: Icons.lock_outline,
|
|
2390
|
-
),
|
|
2391
|
-
if (provider.supportsApiKey && !provider.credentialConfigured)
|
|
2392
|
-
const _MetaPill(
|
|
2393
|
-
label: 'Credentials needed',
|
|
2394
|
-
icon: Icons.admin_panel_settings_outlined,
|
|
2395
|
-
),
|
|
2396
|
-
if (provider.supportsBaseUrl &&
|
|
2397
|
-
baseUrlController.text.trim().isNotEmpty)
|
|
2398
|
-
_MetaPill(
|
|
2399
|
-
label: _friendlyBaseUrlLabel(baseUrlController.text.trim()),
|
|
2400
|
-
icon: Icons.link_outlined,
|
|
2401
|
-
),
|
|
2402
|
-
],
|
|
1720
|
+
const SizedBox(height: 10),
|
|
1721
|
+
_ModelPickerButton(
|
|
1722
|
+
value: value,
|
|
1723
|
+
options: options,
|
|
1724
|
+
onChanged: onChanged,
|
|
1725
|
+
dialogTitle: 'Select $label',
|
|
2403
1726
|
),
|
|
2404
|
-
|
|
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>[
|
|
2405
1765
|
Container(
|
|
2406
|
-
|
|
1766
|
+
width: 38,
|
|
1767
|
+
height: 38,
|
|
2407
1768
|
decoration: BoxDecoration(
|
|
2408
|
-
color:
|
|
2409
|
-
borderRadius: BorderRadius.circular(
|
|
2410
|
-
border: Border.all(color: _border),
|
|
1769
|
+
color: _accentMuted,
|
|
1770
|
+
borderRadius: BorderRadius.circular(10),
|
|
2411
1771
|
),
|
|
2412
|
-
child:
|
|
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,
|
|
2413
1778
|
children: <Widget>[
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
1779
|
+
Text(
|
|
1780
|
+
'${selected.length} of $totalAvailable models',
|
|
1781
|
+
style: TextStyle(
|
|
1782
|
+
fontWeight: FontWeight.w600,
|
|
1783
|
+
fontSize: 14,
|
|
1784
|
+
color: _textPrimary,
|
|
2418
1785
|
),
|
|
2419
1786
|
),
|
|
2420
|
-
const SizedBox(
|
|
2421
|
-
|
|
1787
|
+
const SizedBox(height: 5),
|
|
1788
|
+
Row(
|
|
1789
|
+
children: <Widget>[
|
|
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
|
+
),
|
|
1807
|
+
],
|
|
1808
|
+
),
|
|
2422
1809
|
],
|
|
2423
1810
|
),
|
|
2424
1811
|
),
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
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,
|
|
2442
1943
|
),
|
|
2443
1944
|
),
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
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,
|
|
2456
1973
|
),
|
|
2457
1974
|
),
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
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
|
+
),
|
|
2478
2033
|
),
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
+
),
|
|
2487
2048
|
),
|
|
2488
|
-
|
|
2049
|
+
],
|
|
2489
2050
|
),
|
|
2490
|
-
)
|
|
2491
|
-
|
|
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
|
+
],
|
|
2153
|
+
),
|
|
2154
|
+
),
|
|
2155
|
+
// Quick-action toolbar
|
|
2156
|
+
Padding(
|
|
2157
|
+
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
|
2158
|
+
child: Row(
|
|
2159
|
+
children: <Widget>[
|
|
2160
|
+
_PoolActionChip(
|
|
2161
|
+
label: 'Select all',
|
|
2162
|
+
onTap: () => _selectAllVisible(filtered),
|
|
2163
|
+
),
|
|
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
|
+
),
|
|
2176
|
+
),
|
|
2177
|
+
],
|
|
2178
|
+
),
|
|
2179
|
+
),
|
|
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
|
+
],
|
|
2213
|
+
),
|
|
2492
2214
|
),
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2215
|
+
),
|
|
2216
|
+
),
|
|
2217
|
+
),
|
|
2496
2218
|
),
|
|
2497
2219
|
);
|
|
2498
2220
|
}
|
|
2499
2221
|
}
|
|
2500
2222
|
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
required this.
|
|
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,
|
|
2508
2232
|
});
|
|
2509
2233
|
|
|
2510
|
-
final
|
|
2511
|
-
final
|
|
2512
|
-
final
|
|
2513
|
-
final List<DropdownMenuItem<String>> items;
|
|
2514
|
-
final ValueChanged<String?> onChanged;
|
|
2234
|
+
final ModelMeta model;
|
|
2235
|
+
final bool selected;
|
|
2236
|
+
final ValueChanged<bool> onToggle;
|
|
2515
2237
|
|
|
2516
2238
|
@override
|
|
2517
2239
|
Widget build(BuildContext context) {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
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),
|
|
2252
|
+
child: Row(
|
|
2253
|
+
children: <Widget>[
|
|
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),
|
|
2262
|
+
),
|
|
2263
|
+
),
|
|
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
|
+
),
|
|
2279
|
+
),
|
|
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,
|
|
2292
|
+
),
|
|
2293
|
+
maxLines: 1,
|
|
2294
|
+
overflow: TextOverflow.ellipsis,
|
|
2295
|
+
),
|
|
2296
|
+
if (model.purpose.isNotEmpty)
|
|
2297
|
+
Text(
|
|
2298
|
+
model.purpose,
|
|
2299
|
+
style: TextStyle(
|
|
2300
|
+
fontSize: 11,
|
|
2301
|
+
color: _textMuted,
|
|
2302
|
+
),
|
|
2303
|
+
),
|
|
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
|
+
),
|
|
2534
2313
|
),
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2314
|
+
),
|
|
2315
|
+
),
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// Toolbar chip button used inside _SmartPoolDialog
|
|
2321
|
+
class _PoolActionChip extends StatelessWidget {
|
|
2322
|
+
const _PoolActionChip({required this.label, required this.onTap});
|
|
2323
|
+
final String label;
|
|
2324
|
+
final VoidCallback onTap;
|
|
2325
|
+
|
|
2326
|
+
@override
|
|
2327
|
+
Widget build(BuildContext context) {
|
|
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,
|
|
2542
2344
|
),
|
|
2543
|
-
|
|
2345
|
+
),
|
|
2544
2346
|
),
|
|
2545
2347
|
);
|
|
2546
2348
|
}
|