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.
@@ -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
- Wrap(
961
- spacing: 10,
962
- runSpacing: 10,
963
- children: controller.supportedModels.map((model) {
964
- final selected = _enabledModels.contains(model.id);
965
- return FilterChip(
966
- label: Text(
967
- model.available
968
- ? model.label
969
- : '${model.label} (${model.providerStatusLabel})',
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
- selected: selected,
972
- selectedColor: _accentMuted,
973
- checkmarkColor: _accent,
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
- }).toList(),
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
+ }