neoagent 2.3.1-beta.62 → 2.3.1-beta.64

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.
Files changed (45) hide show
  1. package/.env.example +9 -0
  2. package/docs/configuration.md +11 -0
  3. package/flutter_app/lib/main.dart +2 -0
  4. package/flutter_app/lib/main_account_settings.dart +50 -22
  5. package/flutter_app/lib/main_admin.dart +24 -10
  6. package/flutter_app/lib/main_app_shell.dart +10 -9
  7. package/flutter_app/lib/main_chat.dart +433 -309
  8. package/flutter_app/lib/main_controller.dart +164 -6
  9. package/flutter_app/lib/main_integrations.dart +15 -7
  10. package/flutter_app/lib/main_models.dart +116 -7
  11. package/flutter_app/lib/main_navigation.dart +27 -18
  12. package/flutter_app/lib/main_operations.dart +162 -91
  13. package/flutter_app/lib/main_runtime.dart +22 -0
  14. package/flutter_app/lib/main_settings.dart +287 -75
  15. package/flutter_app/lib/main_shared.dart +165 -6
  16. package/flutter_app/lib/main_unified.dart +388 -0
  17. package/flutter_app/lib/src/analytics_service.dart +294 -0
  18. package/flutter_app/lib/src/backend_client.dart +4 -0
  19. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  20. package/flutter_app/pubspec.lock +8 -0
  21. package/flutter_app/pubspec.yaml +1 -0
  22. package/flutter_app/web/index.html +1 -0
  23. package/package.json +1 -1
  24. package/server/config/analytics.js +30 -0
  25. package/server/db/database.js +52 -0
  26. package/server/http/routes.js +1 -0
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/AssetManifest.bin +1 -1
  29. package/server/public/assets/AssetManifest.bin.json +1 -1
  30. package/server/public/assets/NOTICES +183 -0
  31. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  32. package/server/public/assets/packages/mixpanel_flutter/assets/mixpanel.js +3 -0
  33. package/server/public/flutter_bootstrap.js +1 -1
  34. package/server/public/index.html +1 -0
  35. package/server/public/main.dart.js +83685 -82046
  36. package/server/routes/agents.js +2 -1
  37. package/server/routes/memory.js +75 -3
  38. package/server/routes/runtime.js +14 -0
  39. package/server/routes/widgets.js +4 -4
  40. package/server/services/ai/engine.js +86 -0
  41. package/server/services/ai/runEvents.js +100 -0
  42. package/server/services/memory/manager.js +242 -26
  43. package/server/services/websocket.js +3 -1
  44. package/server/services/widgets/focus_widget.js +126 -0
  45. package/server/services/widgets/service.js +130 -2
@@ -1,9 +1,14 @@
1
1
  part of 'main.dart';
2
2
 
3
3
  class SettingsPanel extends StatefulWidget {
4
- const SettingsPanel({super.key, required this.controller});
4
+ const SettingsPanel({
5
+ super.key,
6
+ required this.controller,
7
+ this.embedded = false,
8
+ });
5
9
 
6
10
  final NeoAgentController controller;
11
+ final bool embedded;
7
12
 
8
13
  @override
9
14
  State<SettingsPanel> createState() => _SettingsPanelState();
@@ -69,7 +74,99 @@ const Map<String, List<String>> _voiceLiveVoicesByProvider =
69
74
  ],
70
75
  };
71
76
 
77
+ class _SettingsSection {
78
+ const _SettingsSection(
79
+ this.title,
80
+ this.keywords, {
81
+ this.requiresDesktop = false,
82
+ });
83
+
84
+ final String title;
85
+ final List<String> keywords;
86
+ final bool requiresDesktop;
87
+ }
88
+
89
+ const _overviewSettingsSection = _SettingsSection('overview', <String>[
90
+ 'overview',
91
+ 'summary',
92
+ 'onboarding',
93
+ 'platform',
94
+ 'providers',
95
+ ]);
96
+
97
+ const _workspaceSettingsSection = _SettingsSection('workspace', <String>[
98
+ 'workspace',
99
+ 'browser',
100
+ 'extension',
101
+ 'headless',
102
+ 'routing',
103
+ ]);
104
+
105
+ const _modelsSettingsSection = _SettingsSection('models', <String>[
106
+ 'models',
107
+ 'providers',
108
+ 'routing',
109
+ 'fallback',
110
+ 'chat',
111
+ 'sub-agent',
112
+ 'subagent',
113
+ 'smart selector',
114
+ ]);
115
+
116
+ const _voiceRecordingSettingsSection = _SettingsSection(
117
+ 'voice recording',
118
+ <String>[
119
+ 'voice',
120
+ 'recording',
121
+ 'transcription',
122
+ 'summary',
123
+ 'speech',
124
+ 'tts',
125
+ 'stt',
126
+ 'live',
127
+ ],
128
+ );
129
+
130
+ const _desktopSettingsSection = _SettingsSection('desktop', <String>[
131
+ 'desktop',
132
+ 'permissions',
133
+ 'capture',
134
+ 'companion',
135
+ 'screen recording',
136
+ 'accessibility',
137
+ 'input',
138
+ ], requiresDesktop: true);
139
+
140
+ const _updatesSettingsSection = _SettingsSection('updates', <String>[
141
+ 'updates',
142
+ 'release',
143
+ 'channel',
144
+ 'version',
145
+ 'self update',
146
+ 'upgrade',
147
+ ]);
148
+
149
+ const _diagnosticsSettingsSection = _SettingsSection('diagnostics', <String>[
150
+ 'diagnostics',
151
+ 'logs',
152
+ 'token',
153
+ 'usage',
154
+ 'debug',
155
+ 'health',
156
+ ]);
157
+
158
+ const List<_SettingsSection> _settingsSearchSections = <_SettingsSection>[
159
+ _overviewSettingsSection,
160
+ _workspaceSettingsSection,
161
+ _modelsSettingsSection,
162
+ _voiceRecordingSettingsSection,
163
+ _desktopSettingsSection,
164
+ _updatesSettingsSection,
165
+ _diagnosticsSettingsSection,
166
+ ];
167
+
72
168
  class _SettingsPanelState extends State<SettingsPanel> {
169
+ late final TextEditingController _searchController;
73
170
  late bool _headlessBrowser;
74
171
  late String _browserBackend;
75
172
  late bool _smarterSelector;
@@ -91,11 +188,13 @@ class _SettingsPanelState extends State<SettingsPanel> {
91
188
  @override
92
189
  void initState() {
93
190
  super.initState();
191
+ _searchController = TextEditingController();
94
192
  _hydrate();
95
193
  }
96
194
 
97
195
  @override
98
196
  void dispose() {
197
+ _searchController.dispose();
99
198
  for (final controller in _providerBaseUrlControllers.values) {
100
199
  controller.dispose();
101
200
  }
@@ -186,6 +285,7 @@ class _SettingsPanelState extends State<SettingsPanel> {
186
285
  @override
187
286
  Widget build(BuildContext context) {
188
287
  final controller = widget.controller;
288
+ final searchQuery = _searchController.text.trim().toLowerCase();
189
289
  final availableModels = controller.supportedModels
190
290
  .where((model) => model.available)
191
291
  .toList();
@@ -195,103 +295,210 @@ class _SettingsPanelState extends State<SettingsPanel> {
195
295
  final modelChoices = <DropdownMenuItem<String>>[
196
296
  const DropdownMenuItem<String>(
197
297
  value: 'auto',
198
- child: Text('Smart Selector (Auto)'),
298
+ child: Text(
299
+ 'Smart Selector (Auto)',
300
+ maxLines: 1,
301
+ overflow: TextOverflow.ellipsis,
302
+ ),
199
303
  ),
200
304
  ...routingModels.map(
201
- (model) =>
202
- DropdownMenuItem<String>(value: model.id, child: Text(model.label)),
305
+ (model) => DropdownMenuItem<String>(
306
+ value: model.id,
307
+ child: Text(
308
+ model.label,
309
+ maxLines: 1,
310
+ overflow: TextOverflow.ellipsis,
311
+ ),
312
+ ),
203
313
  ),
204
314
  ];
205
315
  final enabledSmartModels = _enabledModels
206
316
  .where((id) => routingModels.any((model) => model.id == id))
207
317
  .length;
318
+ final visibleSearchSections = _settingsSearchSections
319
+ .where((section) => !section.requiresDesktop || _supportsDesktopShell)
320
+ .toSet();
208
321
 
209
322
  return ListView(
210
- padding: _pagePadding(context),
323
+ padding: widget.embedded ? EdgeInsets.zero : _pagePadding(context),
211
324
  children: <Widget>[
212
- _PageTitle(
213
- title: 'Settings',
214
- subtitle:
215
- 'Workspace, models, recording, update, and diagnostics controls.',
216
- trailing: FilledButton.icon(
217
- onPressed: controller.isSavingSettings
218
- ? null
219
- : () => controller.saveSettings(
220
- headlessBrowser: _headlessBrowser,
221
- browserBackend: _browserBackend == 'extension'
222
- ? 'extension'
223
- : controller.cloudBrowserBackend,
224
- smarterSelector: _smarterSelector,
225
- enabledModels: _enabledModels.toList(),
226
- defaultChatModel: _defaultChatModel,
227
- defaultSubagentModel: _defaultSubagentModel,
228
- defaultRecordingTranscriptionProvider: 'deepgram',
229
- defaultRecordingTranscriptionModel:
230
- _defaultRecordingTranscriptionModel,
231
- defaultRecordingSummaryProvider: _providerForSelectedModel(
232
- _defaultRecordingSummaryModel,
233
- controller.supportedModels,
234
- ),
235
- defaultRecordingSummaryModel: _defaultRecordingSummaryModel,
236
- fallbackModel: _fallbackModel,
237
- defaultSpeechModel: _defaultSpeechModel,
238
- voiceSttProvider: controller.voiceSttProvider,
239
- voiceSttModel: controller.voiceSttModel,
240
- voiceTtsProvider: controller.voiceTtsProvider,
241
- voiceTtsModel: controller.voiceTtsModel,
242
- voiceTtsVoice: controller.voiceTtsVoice,
243
- voiceRuntimeMode: 'live',
244
- voiceLiveProvider: _voiceLiveProvider,
245
- voiceLiveModel: _voiceLiveModel,
246
- voiceLiveVoice: _voiceLiveVoice,
247
- aiProviderConfigs: _buildProviderPayload(),
248
- ),
249
- style: FilledButton.styleFrom(backgroundColor: _accent),
250
- icon: controller.isSavingSettings
251
- ? const SizedBox.square(
252
- dimension: 16,
253
- child: CircularProgressIndicator(
254
- strokeWidth: 2,
255
- color: Colors.white,
256
- ),
257
- )
258
- : Icon(Icons.save_outlined),
259
- label: Text('Save'),
325
+ if (!widget.embedded)
326
+ _PageTitle(
327
+ title: 'Settings',
328
+ subtitle:
329
+ 'Workspace, models, recording, update, and diagnostics controls.',
330
+ trailing: _settingsSaveButton(controller),
331
+ )
332
+ else
333
+ Align(
334
+ alignment: Alignment.centerRight,
335
+ child: Padding(
336
+ padding: const EdgeInsets.only(bottom: 12),
337
+ child: _settingsSaveButton(controller),
338
+ ),
260
339
  ),
261
- ),
262
340
  if (controller.errorMessage != null) ...<Widget>[
263
341
  _InlineError(message: controller.errorMessage!),
264
342
  const SizedBox(height: 16),
265
343
  ],
266
- _buildSettingsOverview(controller, availableModels.length),
267
- const SizedBox(height: 16),
268
- _buildWorkspaceSection(controller),
269
- const SizedBox(height: 16),
270
- _buildModelsSection(
271
- controller: controller,
272
- modelChoices: modelChoices,
273
- routingModels: routingModels,
274
- availableModels: availableModels,
275
- enabledSmartModels: enabledSmartModels,
276
- ),
277
- const SizedBox(height: 16),
278
- _buildVoiceAndRecordingSection(
279
- controller: controller,
280
- modelChoices: modelChoices,
281
- routingModels: routingModels,
344
+ TextField(
345
+ controller: _searchController,
346
+ onChanged: (_) => setState(() {}),
347
+ decoration: InputDecoration(
348
+ labelText: 'Search settings',
349
+ hintText: 'Models, browser, voice, updates, diagnostics...',
350
+ prefixIcon: const Icon(Icons.search),
351
+ suffixIcon: searchQuery.isEmpty
352
+ ? null
353
+ : IconButton(
354
+ onPressed: () {
355
+ _searchController.clear();
356
+ setState(() {});
357
+ },
358
+ icon: const Icon(Icons.close),
359
+ ),
360
+ ),
282
361
  ),
283
362
  const SizedBox(height: 16),
284
- if (_supportsDesktopShell) ...<Widget>[
363
+ if (_matchesSettingsSection(
364
+ searchQuery,
365
+ _overviewSettingsSection,
366
+ )) ...<Widget>[
367
+ _buildSettingsOverview(controller, availableModels.length),
368
+ const SizedBox(height: 16),
369
+ ],
370
+ if (_matchesSettingsSection(
371
+ searchQuery,
372
+ _workspaceSettingsSection,
373
+ )) ...<Widget>[
374
+ _buildWorkspaceSection(controller),
375
+ const SizedBox(height: 16),
376
+ ],
377
+ if (_matchesSettingsSection(
378
+ searchQuery,
379
+ _modelsSettingsSection,
380
+ )) ...<Widget>[
381
+ _buildModelsSection(
382
+ controller: controller,
383
+ modelChoices: modelChoices,
384
+ routingModels: routingModels,
385
+ availableModels: availableModels,
386
+ enabledSmartModels: enabledSmartModels,
387
+ ),
388
+ const SizedBox(height: 16),
389
+ ],
390
+ if (_matchesSettingsSection(
391
+ searchQuery,
392
+ _voiceRecordingSettingsSection,
393
+ )) ...<Widget>[
394
+ _buildVoiceAndRecordingSection(
395
+ controller: controller,
396
+ modelChoices: modelChoices,
397
+ routingModels: routingModels,
398
+ ),
399
+ const SizedBox(height: 16),
400
+ ],
401
+ if (visibleSearchSections.contains(_desktopSettingsSection) &&
402
+ _matchesSettingsSection(
403
+ searchQuery,
404
+ _desktopSettingsSection,
405
+ )) ...<Widget>[
285
406
  _buildDesktopSection(controller),
286
407
  const SizedBox(height: 16),
287
408
  ],
288
- _buildUpdatesSection(controller),
289
- const SizedBox(height: 16),
290
- _buildDiagnosticsSection(controller),
409
+ if (_matchesSettingsSection(
410
+ searchQuery,
411
+ _updatesSettingsSection,
412
+ )) ...<Widget>[
413
+ _buildUpdatesSection(controller),
414
+ const SizedBox(height: 16),
415
+ ],
416
+ if (_matchesSettingsSection(
417
+ searchQuery,
418
+ _diagnosticsSettingsSection,
419
+ )) ...<Widget>[_buildDiagnosticsSection(controller)],
420
+ if (_noSettingsMatches(searchQuery, visibleSearchSections)) ...<Widget>[
421
+ const _EmptyCard(
422
+ title: 'No matching settings',
423
+ subtitle:
424
+ 'Try a broader search like models, browser, voice, or updates.',
425
+ ),
426
+ ],
291
427
  ],
292
428
  );
293
429
  }
294
430
 
431
+ bool _matchesSettingsSection(String query, _SettingsSection section) {
432
+ if (query.isEmpty) {
433
+ return true;
434
+ }
435
+ final haystack = <String>[
436
+ section.title,
437
+ ...section.keywords,
438
+ ].join(' ').toLowerCase();
439
+ return haystack.contains(query);
440
+ }
441
+
442
+ bool _noSettingsMatches(
443
+ String query,
444
+ Iterable<_SettingsSection> visibleSections,
445
+ ) {
446
+ if (query.isEmpty) {
447
+ return false;
448
+ }
449
+ return !visibleSections.any(
450
+ (section) => _matchesSettingsSection(query, section),
451
+ );
452
+ }
453
+
454
+ Widget _settingsSaveButton(NeoAgentController controller) {
455
+ return FilledButton.icon(
456
+ onPressed: controller.isSavingSettings
457
+ ? null
458
+ : () => controller.saveSettings(
459
+ headlessBrowser: _headlessBrowser,
460
+ browserBackend: _browserBackend == 'extension'
461
+ ? 'extension'
462
+ : controller.cloudBrowserBackend,
463
+ smarterSelector: _smarterSelector,
464
+ enabledModels: _enabledModels.toList(),
465
+ defaultChatModel: _defaultChatModel,
466
+ defaultSubagentModel: _defaultSubagentModel,
467
+ defaultRecordingTranscriptionProvider: 'deepgram',
468
+ defaultRecordingTranscriptionModel:
469
+ _defaultRecordingTranscriptionModel,
470
+ defaultRecordingSummaryProvider: _providerForSelectedModel(
471
+ _defaultRecordingSummaryModel,
472
+ controller.supportedModels,
473
+ ),
474
+ defaultRecordingSummaryModel: _defaultRecordingSummaryModel,
475
+ fallbackModel: _fallbackModel,
476
+ defaultSpeechModel: _defaultSpeechModel,
477
+ voiceSttProvider: controller.voiceSttProvider,
478
+ voiceSttModel: controller.voiceSttModel,
479
+ voiceTtsProvider: controller.voiceTtsProvider,
480
+ voiceTtsModel: controller.voiceTtsModel,
481
+ voiceTtsVoice: controller.voiceTtsVoice,
482
+ voiceRuntimeMode: 'live',
483
+ voiceLiveProvider: _voiceLiveProvider,
484
+ voiceLiveModel: _voiceLiveModel,
485
+ voiceLiveVoice: _voiceLiveVoice,
486
+ aiProviderConfigs: _buildProviderPayload(),
487
+ ),
488
+ style: FilledButton.styleFrom(backgroundColor: _accent),
489
+ icon: controller.isSavingSettings
490
+ ? const SizedBox.square(
491
+ dimension: 16,
492
+ child: CircularProgressIndicator(
493
+ strokeWidth: 2,
494
+ color: Colors.white,
495
+ ),
496
+ )
497
+ : Icon(Icons.save_outlined),
498
+ label: Text('Save'),
499
+ );
500
+ }
501
+
295
502
  Widget _buildSettingsOverview(
296
503
  NeoAgentController controller,
297
504
  int availableModelCount,
@@ -634,7 +841,11 @@ class _SettingsPanelState extends State<SettingsPanel> {
634
841
  .map(
635
842
  (model) => DropdownMenuItem<String>(
636
843
  value: model.id,
637
- child: Text(model.label),
844
+ child: Text(
845
+ model.label,
846
+ maxLines: 1,
847
+ overflow: TextOverflow.ellipsis,
848
+ ),
638
849
  ),
639
850
  )
640
851
  .toList(),
@@ -2030,6 +2241,7 @@ class _RoutingSelectCard extends StatelessWidget {
2030
2241
  DropdownButtonFormField<String>(
2031
2242
  initialValue: value,
2032
2243
  items: items,
2244
+ isExpanded: true,
2033
2245
  decoration: const InputDecoration(isDense: true),
2034
2246
  onChanged: onChanged,
2035
2247
  ),
@@ -232,13 +232,10 @@ List<AppSection> _mainSections(NeoAgentController controller) {
232
232
  AppSection.chat,
233
233
  AppSection.recordings,
234
234
  AppSection.runs,
235
- AppSection.logs,
236
235
  AppSection.devices,
237
236
  AppSection.tasks,
238
237
  AppSection.widgets,
239
- AppSection.skills,
240
238
  AppSection.integrations,
241
- AppSection.mcp,
242
239
  AppSection.memory,
243
240
  if (controller.showHealthSection) AppSection.health,
244
241
  AppSection.settings,
@@ -256,7 +253,7 @@ List<Widget> _buildSidebarItems(
256
253
  final widgets = <Widget>[];
257
254
  final mainSections = _mainSections(controller);
258
255
  final selectedSidebarSection = mainSections.contains(
259
- controller.selectedSection,
256
+ controller.selectedSection.canonicalSection,
260
257
  );
261
258
  for (final group in SidebarGroup.values) {
262
259
  final sections = mainSections
@@ -267,7 +264,8 @@ List<Widget> _buildSidebarItems(
267
264
  }
268
265
 
269
266
  final active =
270
- selectedSidebarSection && controller.selectedSection.group == group;
267
+ selectedSidebarSection &&
268
+ controller.selectedSection.canonicalSection.group == group;
271
269
  final defaultSection = sections.first;
272
270
  final hasChildren = sections.length > 1;
273
271
  final expanded = expandedGroup == group;
@@ -299,7 +297,7 @@ List<Widget> _buildSidebarItems(
299
297
  _SidebarButton(
300
298
  label: section.label,
301
299
  icon: section.icon,
302
- active: controller.selectedSection == section,
300
+ active: controller.selectedSection.canonicalSection == section,
303
301
  indent: 18,
304
302
  iconSize: 16,
305
303
  fontSize: 12,
@@ -1826,6 +1824,167 @@ class _GlobalWebUpdateBanner extends StatelessWidget {
1826
1824
  }
1827
1825
  }
1828
1826
 
1827
+ class _GlobalAnalyticsConsentBanner extends StatelessWidget {
1828
+ const _GlobalAnalyticsConsentBanner({required this.controller});
1829
+
1830
+ final NeoAgentController controller;
1831
+
1832
+ @override
1833
+ Widget build(BuildContext context) {
1834
+ return LayoutBuilder(
1835
+ builder: (context, constraints) {
1836
+ final compact = constraints.maxWidth < 700;
1837
+ final body = Text(
1838
+ 'NeoAgent uses anonymous analytics to understand startup, navigation, and setup flows. No messages, credentials, or personal content are collected.',
1839
+ style: TextStyle(color: _textSecondary, height: 1.35),
1840
+ );
1841
+ return Material(
1842
+ color: Colors.transparent,
1843
+ child: _GlassSurface(
1844
+ borderRadius: BorderRadius.circular(22),
1845
+ blurSigma: 26,
1846
+ fillColor: _bgCard.withValues(alpha: 0.94),
1847
+ borderColor: _accent.withValues(alpha: 0.18),
1848
+ overlayGradient: LinearGradient(
1849
+ colors: <Color>[
1850
+ _accent.withValues(alpha: 0.12),
1851
+ _bgCard.withValues(alpha: 0.88),
1852
+ _bgSecondary.withValues(alpha: 0.86),
1853
+ ],
1854
+ stops: const <double>[0, 0.22, 1],
1855
+ begin: const Alignment(-1, -1),
1856
+ end: const Alignment(1, 1),
1857
+ ),
1858
+ boxShadow: <BoxShadow>[
1859
+ BoxShadow(
1860
+ color: Colors.black.withValues(alpha: 0.22),
1861
+ blurRadius: 34,
1862
+ offset: const Offset(0, 18),
1863
+ ),
1864
+ BoxShadow(
1865
+ color: _accent.withValues(alpha: 0.1),
1866
+ blurRadius: 24,
1867
+ offset: const Offset(0, 8),
1868
+ ),
1869
+ ],
1870
+ padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
1871
+ child: compact
1872
+ ? Column(
1873
+ crossAxisAlignment: CrossAxisAlignment.start,
1874
+ children: <Widget>[
1875
+ Row(
1876
+ crossAxisAlignment: CrossAxisAlignment.start,
1877
+ children: <Widget>[
1878
+ Container(
1879
+ padding: const EdgeInsets.all(10),
1880
+ decoration: BoxDecoration(
1881
+ color: _accent.withValues(alpha: 0.16),
1882
+ shape: BoxShape.circle,
1883
+ border: Border.all(
1884
+ color: _accent.withValues(alpha: 0.24),
1885
+ ),
1886
+ ),
1887
+ child: Icon(
1888
+ Icons.cookie_outlined,
1889
+ color: _accent,
1890
+ size: 22,
1891
+ ),
1892
+ ),
1893
+ const SizedBox(width: 12),
1894
+ Expanded(
1895
+ child: Column(
1896
+ crossAxisAlignment: CrossAxisAlignment.start,
1897
+ children: <Widget>[
1898
+ Text(
1899
+ 'Analytics cookies',
1900
+ style: TextStyle(
1901
+ color: _textPrimary,
1902
+ fontSize: 15,
1903
+ fontWeight: FontWeight.w700,
1904
+ ),
1905
+ ),
1906
+ const SizedBox(height: 6),
1907
+ body,
1908
+ ],
1909
+ ),
1910
+ ),
1911
+ ],
1912
+ ),
1913
+ const SizedBox(height: 14),
1914
+ Row(
1915
+ children: <Widget>[
1916
+ Expanded(
1917
+ child: OutlinedButton(
1918
+ onPressed: controller.declineAnalyticsConsent,
1919
+ child: const Text('Decline'),
1920
+ ),
1921
+ ),
1922
+ const SizedBox(width: 10),
1923
+ Expanded(
1924
+ child: FilledButton(
1925
+ onPressed: controller.acceptAnalyticsConsent,
1926
+ child: const Text('Allow analytics'),
1927
+ ),
1928
+ ),
1929
+ ],
1930
+ ),
1931
+ ],
1932
+ )
1933
+ : Row(
1934
+ crossAxisAlignment: CrossAxisAlignment.center,
1935
+ children: <Widget>[
1936
+ Container(
1937
+ padding: const EdgeInsets.all(10),
1938
+ decoration: BoxDecoration(
1939
+ color: _accent.withValues(alpha: 0.16),
1940
+ shape: BoxShape.circle,
1941
+ border: Border.all(
1942
+ color: _accent.withValues(alpha: 0.24),
1943
+ ),
1944
+ ),
1945
+ child: Icon(
1946
+ Icons.cookie_outlined,
1947
+ color: _accent,
1948
+ size: 22,
1949
+ ),
1950
+ ),
1951
+ const SizedBox(width: 14),
1952
+ Expanded(
1953
+ child: Column(
1954
+ crossAxisAlignment: CrossAxisAlignment.start,
1955
+ children: <Widget>[
1956
+ Text(
1957
+ 'Analytics cookies',
1958
+ style: TextStyle(
1959
+ color: _textPrimary,
1960
+ fontSize: 15,
1961
+ fontWeight: FontWeight.w700,
1962
+ ),
1963
+ ),
1964
+ const SizedBox(height: 6),
1965
+ body,
1966
+ ],
1967
+ ),
1968
+ ),
1969
+ const SizedBox(width: 16),
1970
+ OutlinedButton(
1971
+ onPressed: controller.declineAnalyticsConsent,
1972
+ child: const Text('Decline'),
1973
+ ),
1974
+ const SizedBox(width: 10),
1975
+ FilledButton(
1976
+ onPressed: controller.acceptAnalyticsConsent,
1977
+ child: const Text('Allow analytics'),
1978
+ ),
1979
+ ],
1980
+ ),
1981
+ ),
1982
+ );
1983
+ },
1984
+ );
1985
+ }
1986
+ }
1987
+
1829
1988
  class _DesktopCloseDecision {
1830
1989
  const _DesktopCloseDecision({
1831
1990
  required this.keepRunning,