neoagent 2.4.1-beta.40 → 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/flutter_app/lib/main_settings.dart +649 -37
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +58177 -57815
|
@@ -381,6 +381,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
381
381
|
_modelsSettingsSection,
|
|
382
382
|
)) ...<Widget>[
|
|
383
383
|
_buildModelsSection(
|
|
384
|
+
context: context,
|
|
384
385
|
controller: controller,
|
|
385
386
|
modelChoices: modelChoices,
|
|
386
387
|
routingModels: routingModels,
|
|
@@ -852,6 +853,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
852
853
|
}
|
|
853
854
|
|
|
854
855
|
Widget _buildModelsSection({
|
|
856
|
+
required BuildContext context,
|
|
855
857
|
required NeoAgentController controller,
|
|
856
858
|
required List<_ModelPickerOption> modelChoices,
|
|
857
859
|
required List<ModelMeta> routingModels,
|
|
@@ -957,46 +959,38 @@ class _SettingsPanelState extends State<SettingsPanel> {
|
|
|
957
959
|
),
|
|
958
960
|
),
|
|
959
961
|
const SizedBox(height: 10),
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
+
),
|
|
970
986
|
),
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
backgroundColor: _bgSecondary,
|
|
975
|
-
side: BorderSide(
|
|
976
|
-
color: model.available
|
|
977
|
-
? _border
|
|
978
|
-
: _warning.withValues(alpha: 0.35),
|
|
987
|
+
pageBuilder: (ctx, _, __) => _SmartPoolDialog(
|
|
988
|
+
models: controller.supportedModels,
|
|
989
|
+
selectedIds: _enabledModels,
|
|
979
990
|
),
|
|
980
|
-
onSelected: model.available
|
|
981
|
-
? (value) {
|
|
982
|
-
setState(() {
|
|
983
|
-
if (value) {
|
|
984
|
-
_enabledModels.add(model.id);
|
|
985
|
-
} else if (_enabledModels.length > 1) {
|
|
986
|
-
_enabledModels.remove(model.id);
|
|
987
|
-
}
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
: null,
|
|
991
991
|
);
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
const SizedBox(height: 14),
|
|
995
|
-
Text(
|
|
996
|
-
availableModels.isEmpty
|
|
997
|
-
? 'Enable a ready provider above to unlock model routing.'
|
|
998
|
-
: '$enabledSmartModels models are currently eligible for smart routing.',
|
|
999
|
-
style: TextStyle(color: _textSecondary),
|
|
992
|
+
if (result != null) setState(() => _enabledModels = result);
|
|
993
|
+
},
|
|
1000
994
|
),
|
|
1001
995
|
],
|
|
1002
996
|
),
|
|
@@ -2172,3 +2166,621 @@ class _RoutingSelectCard extends StatelessWidget {
|
|
|
2172
2166
|
);
|
|
2173
2167
|
}
|
|
2174
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
|
+
),
|
|
2223
|
+
),
|
|
2224
|
+
const SizedBox(height: 5),
|
|
2225
|
+
Row(
|
|
2226
|
+
children: <Widget>[
|
|
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
|
+
),
|
|
2244
|
+
],
|
|
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,
|
|
2369
|
+
),
|
|
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
|
+
],
|
|
2487
|
+
),
|
|
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
|
+
],
|
|
2590
|
+
),
|
|
2591
|
+
),
|
|
2592
|
+
// Quick-action toolbar
|
|
2593
|
+
Padding(
|
|
2594
|
+
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
|
|
2595
|
+
child: Row(
|
|
2596
|
+
children: <Widget>[
|
|
2597
|
+
_PoolActionChip(
|
|
2598
|
+
label: 'Select all',
|
|
2599
|
+
onTap: () => _selectAllVisible(filtered),
|
|
2600
|
+
),
|
|
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
|
+
),
|
|
2613
|
+
),
|
|
2614
|
+
],
|
|
2615
|
+
),
|
|
2616
|
+
),
|
|
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
|
+
],
|
|
2650
|
+
),
|
|
2651
|
+
),
|
|
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),
|
|
2689
|
+
child: Row(
|
|
2690
|
+
children: <Widget>[
|
|
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),
|
|
2699
|
+
),
|
|
2700
|
+
),
|
|
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
|
+
),
|
|
2716
|
+
),
|
|
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,
|
|
2729
|
+
),
|
|
2730
|
+
maxLines: 1,
|
|
2731
|
+
overflow: TextOverflow.ellipsis,
|
|
2732
|
+
),
|
|
2733
|
+
if (model.purpose.isNotEmpty)
|
|
2734
|
+
Text(
|
|
2735
|
+
model.purpose,
|
|
2736
|
+
style: TextStyle(
|
|
2737
|
+
fontSize: 11,
|
|
2738
|
+
color: _textMuted,
|
|
2739
|
+
),
|
|
2740
|
+
),
|
|
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
|
+
),
|
|
2752
|
+
),
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// Toolbar chip button used inside _SmartPoolDialog
|
|
2758
|
+
class _PoolActionChip extends StatelessWidget {
|
|
2759
|
+
const _PoolActionChip({required this.label, required this.onTap});
|
|
2760
|
+
final String label;
|
|
2761
|
+
final VoidCallback onTap;
|
|
2762
|
+
|
|
2763
|
+
@override
|
|
2764
|
+
Widget build(BuildContext context) {
|
|
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,
|
|
2781
|
+
),
|
|
2782
|
+
),
|
|
2783
|
+
),
|
|
2784
|
+
);
|
|
2785
|
+
}
|
|
2786
|
+
}
|