neoagent 2.4.1-beta.39 → 2.4.1-beta.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +11 -0
- package/docs/configuration.md +2 -0
- package/flutter_app/lib/main_model_picker.dart +55 -1
- package/flutter_app/lib/main_models.dart +4 -0
- package/flutter_app/lib/main_settings.dart +643 -348
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +63624 -63424
- package/server/services/ai/models.js +141 -97
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +34 -7
- package/server/services/ai/providers/grok.js +14 -1
- package/server/services/ai/providers/nvidia.js +12 -0
- package/server/services/ai/providers/openai.js +18 -4
- package/server/services/ai/providers/openrouter.js +139 -0
- package/server/services/ai/settings.js +10 -0
- package/server/services/ai/systemPrompt.js +36 -20
|
@@ -187,7 +187,6 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
187
187
|
final Map<String, bool> _providerEnabled = <String, bool>{};
|
|
188
188
|
final Map<String, TextEditingController> _providerBaseUrlControllers =
|
|
189
189
|
<String, TextEditingController>{};
|
|
190
|
-
final Set<String> _expandedProviderIds = <String>{};
|
|
191
190
|
|
|
192
191
|
// Inline runtime test state — ephemeral, not stored in controller.
|
|
193
192
|
bool _cliTestRunning = false;
|
|
@@ -382,6 +381,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
382
381
|
_modelsSettingsSection,
|
|
383
382
|
)) ...<Widget>[
|
|
384
383
|
_buildModelsSection(
|
|
384
|
+
context: context,
|
|
385
385
|
controller: controller,
|
|
386
386
|
modelChoices: modelChoices,
|
|
387
387
|
routingModels: routingModels,
|
|
@@ -853,6 +853,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
853
853
|
}
|
|
854
854
|
|
|
855
855
|
Widget _buildModelsSection({
|
|
856
|
+
required BuildContext context,
|
|
856
857
|
required NeoAgentController controller,
|
|
857
858
|
required List<_ModelPickerOption> modelChoices,
|
|
858
859
|
required List<ModelMeta> routingModels,
|
|
@@ -868,88 +869,10 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
868
869
|
const _SectionTitle('Models'),
|
|
869
870
|
const SizedBox(height: 10),
|
|
870
871
|
Text(
|
|
871
|
-
'
|
|
872
|
+
'Choose defaults for chat, agents, fallback behavior, and smart routing.',
|
|
872
873
|
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
873
874
|
),
|
|
874
875
|
const SizedBox(height: 16),
|
|
875
|
-
Text(
|
|
876
|
-
'Providers',
|
|
877
|
-
style: TextStyle(
|
|
878
|
-
fontWeight: FontWeight.w700,
|
|
879
|
-
color: _textPrimary,
|
|
880
|
-
),
|
|
881
|
-
),
|
|
882
|
-
const SizedBox(height: 14),
|
|
883
|
-
if (controller.aiProviders.isEmpty)
|
|
884
|
-
Text(
|
|
885
|
-
'Provider metadata is unavailable on this server version.',
|
|
886
|
-
style: TextStyle(color: _textSecondary),
|
|
887
|
-
)
|
|
888
|
-
else
|
|
889
|
-
LayoutBuilder(
|
|
890
|
-
builder: (context, constraints) {
|
|
891
|
-
final compact = constraints.maxWidth < 960;
|
|
892
|
-
final cardWidth = compact
|
|
893
|
-
? constraints.maxWidth
|
|
894
|
-
: (constraints.maxWidth - 16) / 2;
|
|
895
|
-
return Wrap(
|
|
896
|
-
spacing: 16,
|
|
897
|
-
runSpacing: 16,
|
|
898
|
-
children: controller.aiProviders
|
|
899
|
-
.where(
|
|
900
|
-
(provider) =>
|
|
901
|
-
provider.available ||
|
|
902
|
-
_providerEnabled[provider.id] == true ||
|
|
903
|
-
controller
|
|
904
|
-
.aiProviderConfigs[provider.id]
|
|
905
|
-
?.enabled ==
|
|
906
|
-
true,
|
|
907
|
-
)
|
|
908
|
-
.map((provider) {
|
|
909
|
-
return SizedBox(
|
|
910
|
-
width: cardWidth,
|
|
911
|
-
child: _AiProviderCard(
|
|
912
|
-
provider: provider,
|
|
913
|
-
enabled:
|
|
914
|
-
_providerEnabled[provider.id] ??
|
|
915
|
-
controller
|
|
916
|
-
.aiProviderConfigs[provider.id]
|
|
917
|
-
?.enabled ??
|
|
918
|
-
true,
|
|
919
|
-
models: controller.supportedModels
|
|
920
|
-
.where(
|
|
921
|
-
(model) => model.provider == provider.id,
|
|
922
|
-
)
|
|
923
|
-
.toList(),
|
|
924
|
-
baseUrlController:
|
|
925
|
-
_providerBaseUrlControllers[provider.id]!,
|
|
926
|
-
expanded: _expandedProviderIds.contains(
|
|
927
|
-
provider.id,
|
|
928
|
-
),
|
|
929
|
-
onEnabledChanged: (value) {
|
|
930
|
-
setState(() {
|
|
931
|
-
_providerEnabled[provider.id] = value;
|
|
932
|
-
});
|
|
933
|
-
},
|
|
934
|
-
onExpandToggle: () {
|
|
935
|
-
setState(() {
|
|
936
|
-
if (_expandedProviderIds.contains(
|
|
937
|
-
provider.id,
|
|
938
|
-
)) {
|
|
939
|
-
_expandedProviderIds.remove(provider.id);
|
|
940
|
-
} else {
|
|
941
|
-
_expandedProviderIds.add(provider.id);
|
|
942
|
-
}
|
|
943
|
-
});
|
|
944
|
-
},
|
|
945
|
-
),
|
|
946
|
-
);
|
|
947
|
-
})
|
|
948
|
-
.toList(),
|
|
949
|
-
);
|
|
950
|
-
},
|
|
951
|
-
),
|
|
952
|
-
const Divider(height: 32),
|
|
953
876
|
Text(
|
|
954
877
|
'Default Routing',
|
|
955
878
|
style: TextStyle(
|
|
@@ -1036,46 +959,38 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
1036
959
|
),
|
|
1037
960
|
),
|
|
1038
961
|
const SizedBox(height: 10),
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
962
|
+
Text(
|
|
963
|
+
'The models the Smart Selector routes between automatically.',
|
|
964
|
+
style: TextStyle(color: _textSecondary, height: 1.45),
|
|
965
|
+
),
|
|
966
|
+
const SizedBox(height: 12),
|
|
967
|
+
_SmartPoolSummary(
|
|
968
|
+
allModels: controller.supportedModels,
|
|
969
|
+
selectedIds: _enabledModels,
|
|
970
|
+
onManage: () async {
|
|
971
|
+
final result = await showGeneralDialog<Set<String>>(
|
|
972
|
+
context: context,
|
|
973
|
+
barrierDismissible: true,
|
|
974
|
+
barrierLabel: 'Dismiss',
|
|
975
|
+
barrierColor: Colors.black.withValues(alpha: 0.55),
|
|
976
|
+
transitionDuration: const Duration(milliseconds: 220),
|
|
977
|
+
transitionBuilder: (ctx, anim, _, child) => FadeTransition(
|
|
978
|
+
opacity: CurvedAnimation(parent: anim, curve: Curves.easeOut),
|
|
979
|
+
child: SlideTransition(
|
|
980
|
+
position: Tween<Offset>(
|
|
981
|
+
begin: const Offset(0, 0.04),
|
|
982
|
+
end: Offset.zero,
|
|
983
|
+
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)),
|
|
984
|
+
child: child,
|
|
985
|
+
),
|
|
1049
986
|
),
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
backgroundColor: _bgSecondary,
|
|
1054
|
-
side: BorderSide(
|
|
1055
|
-
color: model.available
|
|
1056
|
-
? _border
|
|
1057
|
-
: _warning.withValues(alpha: 0.35),
|
|
987
|
+
pageBuilder: (ctx, _, __) => _SmartPoolDialog(
|
|
988
|
+
models: controller.supportedModels,
|
|
989
|
+
selectedIds: _enabledModels,
|
|
1058
990
|
),
|
|
1059
|
-
onSelected: model.available
|
|
1060
|
-
? (value) {
|
|
1061
|
-
setState(() {
|
|
1062
|
-
if (value) {
|
|
1063
|
-
_enabledModels.add(model.id);
|
|
1064
|
-
} else if (_enabledModels.length > 1) {
|
|
1065
|
-
_enabledModels.remove(model.id);
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
}
|
|
1069
|
-
: null,
|
|
1070
991
|
);
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
const SizedBox(height: 14),
|
|
1074
|
-
Text(
|
|
1075
|
-
availableModels.isEmpty
|
|
1076
|
-
? 'Enable a ready provider above to unlock model routing.'
|
|
1077
|
-
: '$enabledSmartModels models are currently eligible for smart routing.',
|
|
1078
|
-
style: TextStyle(color: _textSecondary),
|
|
992
|
+
if (result != null) setState(() => _enabledModels = result);
|
|
993
|
+
},
|
|
1079
994
|
),
|
|
1080
995
|
],
|
|
1081
996
|
),
|
|
@@ -2205,286 +2120,666 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
2205
2120
|
}
|
|
2206
2121
|
}
|
|
2207
2122
|
|
|
2208
|
-
class
|
|
2209
|
-
const
|
|
2210
|
-
required this.
|
|
2211
|
-
required this.
|
|
2212
|
-
required this.
|
|
2213
|
-
required this.
|
|
2214
|
-
required this.
|
|
2215
|
-
required this.onEnabledChanged,
|
|
2216
|
-
required this.onExpandToggle,
|
|
2123
|
+
class _RoutingSelectCard extends StatelessWidget {
|
|
2124
|
+
const _RoutingSelectCard({
|
|
2125
|
+
required this.label,
|
|
2126
|
+
required this.icon,
|
|
2127
|
+
required this.value,
|
|
2128
|
+
required this.options,
|
|
2129
|
+
required this.onChanged,
|
|
2217
2130
|
});
|
|
2218
2131
|
|
|
2219
|
-
final
|
|
2220
|
-
final
|
|
2221
|
-
final
|
|
2222
|
-
final List<
|
|
2223
|
-
final
|
|
2224
|
-
final ValueChanged<bool> onEnabledChanged;
|
|
2225
|
-
final VoidCallback onExpandToggle;
|
|
2132
|
+
final String label;
|
|
2133
|
+
final IconData icon;
|
|
2134
|
+
final String value;
|
|
2135
|
+
final List<_ModelPickerOption> options;
|
|
2136
|
+
final ValueChanged<String?> onChanged;
|
|
2226
2137
|
|
|
2227
2138
|
@override
|
|
2228
2139
|
Widget build(BuildContext context) {
|
|
2229
|
-
final availableCount = models.where((model) => model.available).length;
|
|
2230
|
-
final hasAdvancedFields = provider.supportsBaseUrl || models.isNotEmpty;
|
|
2231
2140
|
return Container(
|
|
2232
|
-
padding: const EdgeInsets.all(
|
|
2141
|
+
padding: const EdgeInsets.all(14),
|
|
2233
2142
|
decoration: BoxDecoration(
|
|
2234
2143
|
color: _bgSecondary,
|
|
2235
|
-
borderRadius: BorderRadius.circular(
|
|
2236
|
-
border: Border.all(color:
|
|
2144
|
+
borderRadius: BorderRadius.circular(16),
|
|
2145
|
+
border: Border.all(color: _border),
|
|
2237
2146
|
),
|
|
2238
2147
|
child: Column(
|
|
2239
2148
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2240
2149
|
children: <Widget>[
|
|
2241
2150
|
Row(
|
|
2242
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2243
2151
|
children: <Widget>[
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2152
|
+
Icon(icon, size: 16, color: _accentHover),
|
|
2153
|
+
const SizedBox(width: 8),
|
|
2154
|
+
Text(label, style: TextStyle(fontWeight: FontWeight.w700)),
|
|
2155
|
+
],
|
|
2156
|
+
),
|
|
2157
|
+
const SizedBox(height: 10),
|
|
2158
|
+
_ModelPickerButton(
|
|
2159
|
+
value: value,
|
|
2160
|
+
options: options,
|
|
2161
|
+
onChanged: onChanged,
|
|
2162
|
+
dialogTitle: 'Select $label',
|
|
2163
|
+
),
|
|
2164
|
+
],
|
|
2165
|
+
),
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2171
|
+
// Smart Pool Summary — compact summary card shown in settings
|
|
2172
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2173
|
+
|
|
2174
|
+
class _SmartPoolSummary extends StatelessWidget {
|
|
2175
|
+
const _SmartPoolSummary({
|
|
2176
|
+
required this.allModels,
|
|
2177
|
+
required this.selectedIds,
|
|
2178
|
+
required this.onManage,
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2181
|
+
final List<ModelMeta> allModels;
|
|
2182
|
+
final Set<String> selectedIds;
|
|
2183
|
+
final VoidCallback onManage;
|
|
2184
|
+
|
|
2185
|
+
@override
|
|
2186
|
+
Widget build(BuildContext context) {
|
|
2187
|
+
final selected = allModels
|
|
2188
|
+
.where((m) => selectedIds.contains(m.id) && m.available)
|
|
2189
|
+
.toList();
|
|
2190
|
+
final providers = <String>{for (final m in selected) m.provider};
|
|
2191
|
+
final totalAvailable = allModels.where((m) => m.available).length;
|
|
2192
|
+
|
|
2193
|
+
return Container(
|
|
2194
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
|
2195
|
+
decoration: BoxDecoration(
|
|
2196
|
+
color: _bgSecondary,
|
|
2197
|
+
borderRadius: BorderRadius.circular(14),
|
|
2198
|
+
border: Border.all(color: _border),
|
|
2199
|
+
),
|
|
2200
|
+
child: Row(
|
|
2201
|
+
children: <Widget>[
|
|
2202
|
+
Container(
|
|
2203
|
+
width: 38,
|
|
2204
|
+
height: 38,
|
|
2205
|
+
decoration: BoxDecoration(
|
|
2206
|
+
color: _accentMuted,
|
|
2207
|
+
borderRadius: BorderRadius.circular(10),
|
|
2208
|
+
),
|
|
2209
|
+
child: Icon(Icons.hub_outlined, size: 18, color: _accentHover),
|
|
2210
|
+
),
|
|
2211
|
+
const SizedBox(width: 12),
|
|
2212
|
+
Expanded(
|
|
2213
|
+
child: Column(
|
|
2214
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2215
|
+
children: <Widget>[
|
|
2216
|
+
Text(
|
|
2217
|
+
'${selected.length} of $totalAvailable models',
|
|
2218
|
+
style: TextStyle(
|
|
2219
|
+
fontWeight: FontWeight.w600,
|
|
2220
|
+
fontSize: 14,
|
|
2221
|
+
color: _textPrimary,
|
|
2222
|
+
),
|
|
2250
2223
|
),
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
const SizedBox(width: 12),
|
|
2254
|
-
Expanded(
|
|
2255
|
-
child: Column(
|
|
2256
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2224
|
+
const SizedBox(height: 5),
|
|
2225
|
+
Row(
|
|
2257
2226
|
children: <Widget>[
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
fontSize:
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2227
|
+
if (providers.isEmpty)
|
|
2228
|
+
Text(
|
|
2229
|
+
'No models selected',
|
|
2230
|
+
style: TextStyle(fontSize: 12, color: _textMuted),
|
|
2231
|
+
)
|
|
2232
|
+
else
|
|
2233
|
+
...providers.take(12).map(
|
|
2234
|
+
(p) => Container(
|
|
2235
|
+
width: 8,
|
|
2236
|
+
height: 8,
|
|
2237
|
+
margin: const EdgeInsets.only(right: 5),
|
|
2238
|
+
decoration: BoxDecoration(
|
|
2239
|
+
color: _providerPickerColor(p),
|
|
2240
|
+
shape: BoxShape.circle,
|
|
2241
|
+
),
|
|
2242
|
+
),
|
|
2243
|
+
),
|
|
2272
2244
|
],
|
|
2273
2245
|
),
|
|
2246
|
+
],
|
|
2247
|
+
),
|
|
2248
|
+
),
|
|
2249
|
+
const SizedBox(width: 10),
|
|
2250
|
+
OutlinedButton.icon(
|
|
2251
|
+
onPressed: onManage,
|
|
2252
|
+
icon: const Icon(Icons.tune_rounded, size: 14),
|
|
2253
|
+
label: const Text('Manage'),
|
|
2254
|
+
style: OutlinedButton.styleFrom(
|
|
2255
|
+
padding:
|
|
2256
|
+
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
2257
|
+
textStyle: const TextStyle(fontSize: 13),
|
|
2258
|
+
),
|
|
2259
|
+
),
|
|
2260
|
+
],
|
|
2261
|
+
),
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2267
|
+
// Smart Pool Dialog — searchable, grouped multi-select manager
|
|
2268
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2269
|
+
|
|
2270
|
+
class _SmartPoolDialog extends StatefulWidget {
|
|
2271
|
+
const _SmartPoolDialog({
|
|
2272
|
+
required this.models,
|
|
2273
|
+
required this.selectedIds,
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
final List<ModelMeta> models;
|
|
2277
|
+
final Set<String> selectedIds;
|
|
2278
|
+
|
|
2279
|
+
@override
|
|
2280
|
+
State<_SmartPoolDialog> createState() => _SmartPoolDialogState();
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
class _SmartPoolDialogState extends State<_SmartPoolDialog> {
|
|
2284
|
+
late Set<String> _selected;
|
|
2285
|
+
final TextEditingController _searchCtrl = TextEditingController();
|
|
2286
|
+
String _query = '';
|
|
2287
|
+
bool _onlyAvailable = true;
|
|
2288
|
+
|
|
2289
|
+
@override
|
|
2290
|
+
void initState() {
|
|
2291
|
+
super.initState();
|
|
2292
|
+
_selected = Set<String>.from(widget.selectedIds);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
@override
|
|
2296
|
+
void dispose() {
|
|
2297
|
+
_searchCtrl.dispose();
|
|
2298
|
+
super.dispose();
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
List<ModelMeta> get _filtered {
|
|
2302
|
+
var list = _onlyAvailable
|
|
2303
|
+
? widget.models.where((m) => m.available).toList()
|
|
2304
|
+
: List<ModelMeta>.from(widget.models);
|
|
2305
|
+
if (_query.isNotEmpty) {
|
|
2306
|
+
final q = _query.toLowerCase();
|
|
2307
|
+
list = list
|
|
2308
|
+
.where((m) =>
|
|
2309
|
+
m.label.toLowerCase().contains(q) ||
|
|
2310
|
+
m.id.toLowerCase().contains(q) ||
|
|
2311
|
+
m.provider.toLowerCase().contains(q))
|
|
2312
|
+
.toList();
|
|
2313
|
+
}
|
|
2314
|
+
return list;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
void _selectAllVisible(List<ModelMeta> filtered) {
|
|
2318
|
+
setState(() {
|
|
2319
|
+
for (final m in filtered) {
|
|
2320
|
+
if (m.available) _selected.add(m.id);
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
void _clearAllVisible(List<ModelMeta> filtered) {
|
|
2326
|
+
setState(() {
|
|
2327
|
+
final toRemove = filtered.map((m) => m.id).toSet();
|
|
2328
|
+
final remaining = _selected.difference(toRemove);
|
|
2329
|
+
_selected = remaining.isNotEmpty
|
|
2330
|
+
? remaining
|
|
2331
|
+
: <String>{_selected.first};
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
@override
|
|
2336
|
+
Widget build(BuildContext context) {
|
|
2337
|
+
final filtered = _filtered;
|
|
2338
|
+
|
|
2339
|
+
// Build grouped structure
|
|
2340
|
+
final Map<String, List<ModelMeta>> grouped = <String, List<ModelMeta>>{};
|
|
2341
|
+
for (final m in filtered) {
|
|
2342
|
+
grouped.putIfAbsent(m.provider, () => <ModelMeta>[]).add(m);
|
|
2343
|
+
}
|
|
2344
|
+
final providerOrder = grouped.keys.toList();
|
|
2345
|
+
|
|
2346
|
+
final selectedAvailableCount = widget.models
|
|
2347
|
+
.where((m) => _selected.contains(m.id) && m.available)
|
|
2348
|
+
.length;
|
|
2349
|
+
|
|
2350
|
+
// Build flat row list (headers + model rows)
|
|
2351
|
+
final List<Widget> rows = <Widget>[];
|
|
2352
|
+
for (final provider in providerOrder) {
|
|
2353
|
+
final models = grouped[provider]!;
|
|
2354
|
+
final providerColor = _providerPickerColor(provider);
|
|
2355
|
+
final available = models.where((m) => m.available).toList();
|
|
2356
|
+
final allGroupSelected = available.isNotEmpty &&
|
|
2357
|
+
available.every((m) => _selected.contains(m.id));
|
|
2358
|
+
|
|
2359
|
+
rows.add(Padding(
|
|
2360
|
+
padding: const EdgeInsets.fromLTRB(16, 14, 16, 4),
|
|
2361
|
+
child: Row(
|
|
2362
|
+
children: <Widget>[
|
|
2363
|
+
Container(
|
|
2364
|
+
width: 6,
|
|
2365
|
+
height: 6,
|
|
2366
|
+
decoration: BoxDecoration(
|
|
2367
|
+
color: providerColor,
|
|
2368
|
+
shape: BoxShape.circle,
|
|
2274
2369
|
),
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2370
|
+
),
|
|
2371
|
+
const SizedBox(width: 8),
|
|
2372
|
+
Expanded(
|
|
2373
|
+
child: Text(
|
|
2374
|
+
_providerPickerLabel(provider).toUpperCase(),
|
|
2375
|
+
style: TextStyle(
|
|
2376
|
+
fontSize: 10.5,
|
|
2377
|
+
fontWeight: FontWeight.w700,
|
|
2378
|
+
color: _textMuted,
|
|
2379
|
+
letterSpacing: 0.8,
|
|
2380
|
+
),
|
|
2381
|
+
),
|
|
2382
|
+
),
|
|
2383
|
+
const SizedBox(width: 8),
|
|
2384
|
+
GestureDetector(
|
|
2385
|
+
onTap: available.isEmpty
|
|
2386
|
+
? null
|
|
2387
|
+
: () {
|
|
2388
|
+
setState(() {
|
|
2389
|
+
if (allGroupSelected) {
|
|
2390
|
+
final toRemove =
|
|
2391
|
+
available.map((m) => m.id).toSet();
|
|
2392
|
+
final remaining =
|
|
2393
|
+
_selected.difference(toRemove);
|
|
2394
|
+
_selected = remaining.isNotEmpty
|
|
2395
|
+
? remaining
|
|
2396
|
+
: <String>{_selected.first};
|
|
2397
|
+
} else {
|
|
2398
|
+
for (final m in available) {
|
|
2399
|
+
_selected.add(m.id);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2403
|
+
},
|
|
2404
|
+
child: Text(
|
|
2405
|
+
allGroupSelected ? 'None' : 'All',
|
|
2406
|
+
style: TextStyle(
|
|
2407
|
+
fontSize: 11,
|
|
2408
|
+
fontWeight: FontWeight.w600,
|
|
2409
|
+
color: available.isEmpty ? _textMuted : _accent,
|
|
2410
|
+
),
|
|
2411
|
+
),
|
|
2412
|
+
),
|
|
2413
|
+
],
|
|
2414
|
+
),
|
|
2415
|
+
));
|
|
2416
|
+
|
|
2417
|
+
for (final model in models) {
|
|
2418
|
+
rows.add(_SmartPoolRow(
|
|
2419
|
+
model: model,
|
|
2420
|
+
selected: _selected.contains(model.id),
|
|
2421
|
+
onToggle: (val) => setState(() {
|
|
2422
|
+
if (val) {
|
|
2423
|
+
_selected.add(model.id);
|
|
2424
|
+
} else if (_selected.length > 1) {
|
|
2425
|
+
_selected.remove(model.id);
|
|
2426
|
+
}
|
|
2427
|
+
}),
|
|
2428
|
+
));
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
return Center(
|
|
2433
|
+
child: ConstrainedBox(
|
|
2434
|
+
constraints: BoxConstraints(
|
|
2435
|
+
maxWidth: 560,
|
|
2436
|
+
minWidth: 320,
|
|
2437
|
+
maxHeight: MediaQuery.sizeOf(context).height * 0.85,
|
|
2438
|
+
),
|
|
2439
|
+
child: Padding(
|
|
2440
|
+
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
|
|
2441
|
+
child: Material(
|
|
2442
|
+
color: _bgCard,
|
|
2443
|
+
borderRadius: BorderRadius.circular(20),
|
|
2444
|
+
elevation: 24,
|
|
2445
|
+
shadowColor: Colors.black.withValues(alpha: 0.5),
|
|
2446
|
+
child: Container(
|
|
2447
|
+
decoration: BoxDecoration(
|
|
2448
|
+
borderRadius: BorderRadius.circular(20),
|
|
2449
|
+
border: Border.all(color: _borderLight),
|
|
2450
|
+
),
|
|
2451
|
+
child: ClipRRect(
|
|
2452
|
+
borderRadius: BorderRadius.circular(20),
|
|
2453
|
+
child: Column(
|
|
2454
|
+
mainAxisSize: MainAxisSize.min,
|
|
2455
|
+
children: <Widget>[
|
|
2456
|
+
// Header
|
|
2457
|
+
Padding(
|
|
2458
|
+
padding: const EdgeInsets.fromLTRB(20, 16, 10, 0),
|
|
2459
|
+
child: Row(
|
|
2460
|
+
children: <Widget>[
|
|
2461
|
+
Expanded(
|
|
2462
|
+
child: Text(
|
|
2463
|
+
'Smart Selector Pool',
|
|
2464
|
+
style: TextStyle(
|
|
2465
|
+
fontSize: 17,
|
|
2466
|
+
fontWeight: FontWeight.w700,
|
|
2467
|
+
color: _textPrimary,
|
|
2468
|
+
),
|
|
2469
|
+
),
|
|
2470
|
+
),
|
|
2471
|
+
IconButton(
|
|
2472
|
+
onPressed: () =>
|
|
2473
|
+
Navigator.of(context).pop(_selected),
|
|
2474
|
+
icon: Icon(
|
|
2475
|
+
Icons.close_rounded,
|
|
2476
|
+
size: 20,
|
|
2477
|
+
color: _textSecondary,
|
|
2478
|
+
),
|
|
2479
|
+
style: IconButton.styleFrom(
|
|
2480
|
+
minimumSize: const Size(36, 36),
|
|
2481
|
+
padding: EdgeInsets.zero,
|
|
2482
|
+
tapTargetSize:
|
|
2483
|
+
MaterialTapTargetSize.shrinkWrap,
|
|
2484
|
+
),
|
|
2485
|
+
),
|
|
2486
|
+
],
|
|
2293
2487
|
),
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2488
|
+
),
|
|
2489
|
+
// Search + available toggle
|
|
2490
|
+
Padding(
|
|
2491
|
+
padding: const EdgeInsets.fromLTRB(14, 10, 14, 8),
|
|
2492
|
+
child: Row(
|
|
2493
|
+
children: <Widget>[
|
|
2494
|
+
Expanded(
|
|
2495
|
+
child: TextField(
|
|
2496
|
+
controller: _searchCtrl,
|
|
2497
|
+
autofocus: true,
|
|
2498
|
+
onChanged: (v) =>
|
|
2499
|
+
setState(() => _query = v.trim()),
|
|
2500
|
+
style: TextStyle(
|
|
2501
|
+
color: _textPrimary,
|
|
2502
|
+
fontSize: 14,
|
|
2503
|
+
),
|
|
2504
|
+
decoration: InputDecoration(
|
|
2505
|
+
hintText: 'Search models or providers…',
|
|
2506
|
+
hintStyle: TextStyle(
|
|
2507
|
+
color: _textMuted,
|
|
2508
|
+
fontSize: 14,
|
|
2509
|
+
),
|
|
2510
|
+
prefixIcon: Icon(
|
|
2511
|
+
Icons.search_rounded,
|
|
2512
|
+
size: 18,
|
|
2513
|
+
color: _textMuted,
|
|
2514
|
+
),
|
|
2515
|
+
suffixIcon: _query.isNotEmpty
|
|
2516
|
+
? GestureDetector(
|
|
2517
|
+
onTap: () => setState(() {
|
|
2518
|
+
_searchCtrl.clear();
|
|
2519
|
+
_query = '';
|
|
2520
|
+
}),
|
|
2521
|
+
child: Padding(
|
|
2522
|
+
padding: const EdgeInsets.all(10),
|
|
2523
|
+
child: Icon(
|
|
2524
|
+
Icons.cancel_rounded,
|
|
2525
|
+
size: 16,
|
|
2526
|
+
color: _textMuted,
|
|
2527
|
+
),
|
|
2528
|
+
),
|
|
2529
|
+
)
|
|
2530
|
+
: null,
|
|
2531
|
+
isDense: true,
|
|
2532
|
+
contentPadding: const EdgeInsets.symmetric(
|
|
2533
|
+
vertical: 10),
|
|
2534
|
+
filled: true,
|
|
2535
|
+
fillColor: _bgSecondary,
|
|
2536
|
+
border: OutlineInputBorder(
|
|
2537
|
+
borderRadius: BorderRadius.circular(12),
|
|
2538
|
+
borderSide:
|
|
2539
|
+
BorderSide(color: _border),
|
|
2540
|
+
),
|
|
2541
|
+
enabledBorder: OutlineInputBorder(
|
|
2542
|
+
borderRadius: BorderRadius.circular(12),
|
|
2543
|
+
borderSide:
|
|
2544
|
+
BorderSide(color: _border),
|
|
2545
|
+
),
|
|
2546
|
+
focusedBorder: OutlineInputBorder(
|
|
2547
|
+
borderRadius: BorderRadius.circular(12),
|
|
2548
|
+
borderSide: BorderSide(
|
|
2549
|
+
color: _accent,
|
|
2550
|
+
width: 1.5,
|
|
2551
|
+
),
|
|
2552
|
+
),
|
|
2553
|
+
),
|
|
2554
|
+
),
|
|
2555
|
+
),
|
|
2556
|
+
const SizedBox(width: 8),
|
|
2557
|
+
GestureDetector(
|
|
2558
|
+
onTap: () => setState(
|
|
2559
|
+
() => _onlyAvailable = !_onlyAvailable),
|
|
2560
|
+
child: AnimatedContainer(
|
|
2561
|
+
duration: const Duration(milliseconds: 150),
|
|
2562
|
+
padding: const EdgeInsets.symmetric(
|
|
2563
|
+
horizontal: 10,
|
|
2564
|
+
vertical: 7,
|
|
2565
|
+
),
|
|
2566
|
+
decoration: BoxDecoration(
|
|
2567
|
+
color: _onlyAvailable
|
|
2568
|
+
? _accentMuted
|
|
2569
|
+
: _bgSecondary,
|
|
2570
|
+
borderRadius: BorderRadius.circular(8),
|
|
2571
|
+
border: Border.all(
|
|
2572
|
+
color: _onlyAvailable
|
|
2573
|
+
? _accent.withValues(alpha: 0.5)
|
|
2574
|
+
: _border,
|
|
2575
|
+
),
|
|
2576
|
+
),
|
|
2577
|
+
child: Text(
|
|
2578
|
+
'Available',
|
|
2579
|
+
style: TextStyle(
|
|
2580
|
+
fontSize: 12,
|
|
2581
|
+
fontWeight: FontWeight.w600,
|
|
2582
|
+
color: _onlyAvailable
|
|
2583
|
+
? _accentHover
|
|
2584
|
+
: _textSecondary,
|
|
2585
|
+
),
|
|
2586
|
+
),
|
|
2587
|
+
),
|
|
2588
|
+
),
|
|
2589
|
+
],
|
|
2298
2590
|
),
|
|
2591
|
+
),
|
|
2592
|
+
// Quick-action toolbar
|
|
2593
|
+
Padding(
|
|
2594
|
+
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
|
2299
2595
|
child: Row(
|
|
2300
|
-
mainAxisSize: MainAxisSize.min,
|
|
2301
2596
|
children: <Widget>[
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2597
|
+
_PoolActionChip(
|
|
2598
|
+
label: 'Select all',
|
|
2599
|
+
onTap: () => _selectAllVisible(filtered),
|
|
2305
2600
|
),
|
|
2306
|
-
const SizedBox(width:
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2601
|
+
const SizedBox(width: 6),
|
|
2602
|
+
_PoolActionChip(
|
|
2603
|
+
label: 'Clear all',
|
|
2604
|
+
onTap: () => _clearAllVisible(filtered),
|
|
2605
|
+
),
|
|
2606
|
+
const Spacer(),
|
|
2607
|
+
Text(
|
|
2608
|
+
'$selectedAvailableCount selected',
|
|
2609
|
+
style: TextStyle(
|
|
2610
|
+
fontSize: 12,
|
|
2611
|
+
color: _textMuted,
|
|
2612
|
+
),
|
|
2313
2613
|
),
|
|
2314
2614
|
],
|
|
2315
2615
|
),
|
|
2316
2616
|
),
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2617
|
+
Divider(height: 1, thickness: 1, color: _border),
|
|
2618
|
+
// Model list
|
|
2619
|
+
Flexible(
|
|
2620
|
+
child: rows.isEmpty
|
|
2621
|
+
? Padding(
|
|
2622
|
+
padding: const EdgeInsets.all(36),
|
|
2623
|
+
child: Column(
|
|
2624
|
+
mainAxisSize: MainAxisSize.min,
|
|
2625
|
+
children: <Widget>[
|
|
2626
|
+
Icon(
|
|
2627
|
+
Icons.search_off_rounded,
|
|
2628
|
+
size: 36,
|
|
2629
|
+
color: _textMuted,
|
|
2630
|
+
),
|
|
2631
|
+
const SizedBox(height: 12),
|
|
2632
|
+
Text(
|
|
2633
|
+
'No results for "$_query"',
|
|
2634
|
+
style: TextStyle(
|
|
2635
|
+
color: _textSecondary,
|
|
2636
|
+
fontSize: 14,
|
|
2637
|
+
),
|
|
2638
|
+
),
|
|
2639
|
+
],
|
|
2640
|
+
),
|
|
2641
|
+
)
|
|
2642
|
+
: ListView(
|
|
2643
|
+
padding:
|
|
2644
|
+
const EdgeInsets.only(top: 4, bottom: 8),
|
|
2645
|
+
shrinkWrap: true,
|
|
2646
|
+
children: rows,
|
|
2647
|
+
),
|
|
2648
|
+
),
|
|
2649
|
+
],
|
|
2346
2650
|
),
|
|
2347
|
-
|
|
2348
|
-
),
|
|
2349
|
-
const SizedBox(height: 12),
|
|
2350
|
-
Container(
|
|
2351
|
-
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
2352
|
-
decoration: BoxDecoration(
|
|
2353
|
-
color: _bgCard,
|
|
2354
|
-
borderRadius: BorderRadius.circular(14),
|
|
2355
|
-
border: Border.all(color: _border),
|
|
2651
|
+
),
|
|
2356
2652
|
),
|
|
2653
|
+
),
|
|
2654
|
+
),
|
|
2655
|
+
),
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2661
|
+
// Smart Pool Row — individual model row inside the dialog
|
|
2662
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2663
|
+
|
|
2664
|
+
class _SmartPoolRow extends StatelessWidget {
|
|
2665
|
+
const _SmartPoolRow({
|
|
2666
|
+
required this.model,
|
|
2667
|
+
required this.selected,
|
|
2668
|
+
required this.onToggle,
|
|
2669
|
+
});
|
|
2670
|
+
|
|
2671
|
+
final ModelMeta model;
|
|
2672
|
+
final bool selected;
|
|
2673
|
+
final ValueChanged<bool> onToggle;
|
|
2674
|
+
|
|
2675
|
+
@override
|
|
2676
|
+
Widget build(BuildContext context) {
|
|
2677
|
+
final color = _providerPickerColor(model.provider);
|
|
2678
|
+
return Opacity(
|
|
2679
|
+
opacity: model.available ? 1.0 : 0.4,
|
|
2680
|
+
child: Material(
|
|
2681
|
+
color: selected
|
|
2682
|
+
? _accentMuted.withValues(alpha: 0.12)
|
|
2683
|
+
: Colors.transparent,
|
|
2684
|
+
child: InkWell(
|
|
2685
|
+
onTap: model.available ? () => onToggle(!selected) : null,
|
|
2686
|
+
child: Padding(
|
|
2687
|
+
padding:
|
|
2688
|
+
const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
|
2357
2689
|
child: Row(
|
|
2358
2690
|
children: <Widget>[
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2691
|
+
// Thin provider accent bar on the left
|
|
2692
|
+
Container(
|
|
2693
|
+
width: 3,
|
|
2694
|
+
height: 30,
|
|
2695
|
+
decoration: BoxDecoration(
|
|
2696
|
+
color: color.withValues(
|
|
2697
|
+
alpha: selected ? 0.85 : 0.28),
|
|
2698
|
+
borderRadius: BorderRadius.circular(2),
|
|
2363
2699
|
),
|
|
2364
2700
|
),
|
|
2365
|
-
const SizedBox(width:
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
border: Border.all(color: _border),
|
|
2381
|
-
),
|
|
2382
|
-
child: Text(
|
|
2383
|
-
provider.credentialConfigured
|
|
2384
|
-
? 'Credentials for this provider are already available to the runtime.'
|
|
2385
|
-
: 'Credentials for this provider are managed outside this workspace UI. Finish the server or admin setup, then return here to enable routing.',
|
|
2386
|
-
style: TextStyle(color: _textSecondary, height: 1.35),
|
|
2387
|
-
),
|
|
2388
|
-
),
|
|
2389
|
-
if (provider.supportsBaseUrl) ...<Widget>[
|
|
2390
|
-
TextField(
|
|
2391
|
-
controller: baseUrlController,
|
|
2392
|
-
keyboardType: TextInputType.url,
|
|
2393
|
-
autocorrect: false,
|
|
2394
|
-
decoration: InputDecoration(
|
|
2395
|
-
labelText: provider.id == 'ollama'
|
|
2396
|
-
? 'Server URL'
|
|
2397
|
-
: 'Base URL',
|
|
2398
|
-
helperText: provider.defaultBaseUrl.trim().isEmpty
|
|
2399
|
-
? 'Optional override.'
|
|
2400
|
-
: 'Default: ${provider.defaultBaseUrl}',
|
|
2701
|
+
const SizedBox(width: 10),
|
|
2702
|
+
SizedBox(
|
|
2703
|
+
width: 20,
|
|
2704
|
+
height: 20,
|
|
2705
|
+
child: Checkbox(
|
|
2706
|
+
value: selected,
|
|
2707
|
+
onChanged: model.available
|
|
2708
|
+
? (v) => onToggle(v ?? false)
|
|
2709
|
+
: null,
|
|
2710
|
+
activeColor: _accent,
|
|
2711
|
+
side: BorderSide(color: _textMuted, width: 1.5),
|
|
2712
|
+
materialTapTargetSize:
|
|
2713
|
+
MaterialTapTargetSize.shrinkWrap,
|
|
2714
|
+
visualDensity: VisualDensity.compact,
|
|
2715
|
+
),
|
|
2401
2716
|
),
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
padding: const EdgeInsets.symmetric(
|
|
2415
|
-
horizontal: 10,
|
|
2416
|
-
vertical: 8,
|
|
2417
|
-
),
|
|
2418
|
-
decoration: BoxDecoration(
|
|
2419
|
-
color: model.available ? _bgCard : _bgPrimary,
|
|
2420
|
-
borderRadius: BorderRadius.circular(999),
|
|
2421
|
-
border: Border.all(
|
|
2422
|
-
color: model.available ? _border : _borderLight,
|
|
2423
|
-
),
|
|
2717
|
+
const SizedBox(width: 10),
|
|
2718
|
+
Expanded(
|
|
2719
|
+
child: Column(
|
|
2720
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2721
|
+
children: <Widget>[
|
|
2722
|
+
Text(
|
|
2723
|
+
model.label,
|
|
2724
|
+
style: TextStyle(
|
|
2725
|
+
fontSize: 13,
|
|
2726
|
+
fontWeight: FontWeight.w500,
|
|
2727
|
+
color:
|
|
2728
|
+
selected ? _accentHover : _textPrimary,
|
|
2424
2729
|
),
|
|
2425
|
-
|
|
2426
|
-
|
|
2730
|
+
maxLines: 1,
|
|
2731
|
+
overflow: TextOverflow.ellipsis,
|
|
2732
|
+
),
|
|
2733
|
+
if (model.purpose.isNotEmpty)
|
|
2734
|
+
Text(
|
|
2735
|
+
model.purpose,
|
|
2427
2736
|
style: TextStyle(
|
|
2428
|
-
fontSize:
|
|
2429
|
-
color:
|
|
2430
|
-
? _textPrimary
|
|
2431
|
-
: _textSecondary,
|
|
2737
|
+
fontSize: 11,
|
|
2738
|
+
color: _textMuted,
|
|
2432
2739
|
),
|
|
2433
2740
|
),
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2741
|
+
],
|
|
2742
|
+
),
|
|
2743
|
+
),
|
|
2744
|
+
const SizedBox(width: 8),
|
|
2745
|
+
if (model.priceTier != null)
|
|
2746
|
+
_PriceTierChip(tier: model.priceTier!),
|
|
2747
|
+
const SizedBox(width: 2),
|
|
2748
|
+
],
|
|
2749
|
+
),
|
|
2750
|
+
),
|
|
2751
|
+
),
|
|
2441
2752
|
),
|
|
2442
2753
|
);
|
|
2443
2754
|
}
|
|
2444
2755
|
}
|
|
2445
2756
|
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
required this.icon,
|
|
2450
|
-
required this.value,
|
|
2451
|
-
required this.options,
|
|
2452
|
-
required this.onChanged,
|
|
2453
|
-
});
|
|
2454
|
-
|
|
2757
|
+
// Toolbar chip button used inside _SmartPoolDialog
|
|
2758
|
+
class _PoolActionChip extends StatelessWidget {
|
|
2759
|
+
const _PoolActionChip({required this.label, required this.onTap});
|
|
2455
2760
|
final String label;
|
|
2456
|
-
final
|
|
2457
|
-
final String value;
|
|
2458
|
-
final List<_ModelPickerOption> options;
|
|
2459
|
-
final ValueChanged<String?> onChanged;
|
|
2761
|
+
final VoidCallback onTap;
|
|
2460
2762
|
|
|
2461
2763
|
@override
|
|
2462
2764
|
Widget build(BuildContext context) {
|
|
2463
|
-
return
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
),
|
|
2480
|
-
const SizedBox(height: 10),
|
|
2481
|
-
_ModelPickerButton(
|
|
2482
|
-
value: value,
|
|
2483
|
-
options: options,
|
|
2484
|
-
onChanged: onChanged,
|
|
2485
|
-
dialogTitle: 'Select $label',
|
|
2765
|
+
return GestureDetector(
|
|
2766
|
+
onTap: onTap,
|
|
2767
|
+
child: Container(
|
|
2768
|
+
padding:
|
|
2769
|
+
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
2770
|
+
decoration: BoxDecoration(
|
|
2771
|
+
color: _bgSecondary,
|
|
2772
|
+
borderRadius: BorderRadius.circular(8),
|
|
2773
|
+
border: Border.all(color: _border),
|
|
2774
|
+
),
|
|
2775
|
+
child: Text(
|
|
2776
|
+
label,
|
|
2777
|
+
style: TextStyle(
|
|
2778
|
+
fontSize: 12,
|
|
2779
|
+
fontWeight: FontWeight.w500,
|
|
2780
|
+
color: _textSecondary,
|
|
2486
2781
|
),
|
|
2487
|
-
|
|
2782
|
+
),
|
|
2488
2783
|
),
|
|
2489
2784
|
);
|
|
2490
2785
|
}
|