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.
- package/.env.example +9 -0
- package/docs/configuration.md +11 -0
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_account_settings.dart +50 -22
- package/flutter_app/lib/main_admin.dart +24 -10
- package/flutter_app/lib/main_app_shell.dart +10 -9
- package/flutter_app/lib/main_chat.dart +433 -309
- package/flutter_app/lib/main_controller.dart +164 -6
- package/flutter_app/lib/main_integrations.dart +15 -7
- package/flutter_app/lib/main_models.dart +116 -7
- package/flutter_app/lib/main_navigation.dart +27 -18
- package/flutter_app/lib/main_operations.dart +162 -91
- package/flutter_app/lib/main_runtime.dart +22 -0
- package/flutter_app/lib/main_settings.dart +287 -75
- package/flutter_app/lib/main_shared.dart +165 -6
- package/flutter_app/lib/main_unified.dart +388 -0
- package/flutter_app/lib/src/analytics_service.dart +294 -0
- package/flutter_app/lib/src/backend_client.dart +4 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
- package/flutter_app/pubspec.lock +8 -0
- package/flutter_app/pubspec.yaml +1 -0
- package/flutter_app/web/index.html +1 -0
- package/package.json +1 -1
- package/server/config/analytics.js +30 -0
- package/server/db/database.js +52 -0
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/NOTICES +183 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/packages/mixpanel_flutter/assets/mixpanel.js +3 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/index.html +1 -0
- package/server/public/main.dart.js +83685 -82046
- package/server/routes/agents.js +2 -1
- package/server/routes/memory.js +75 -3
- package/server/routes/runtime.js +14 -0
- package/server/routes/widgets.js +4 -4
- package/server/services/ai/engine.js +86 -0
- package/server/services/ai/runEvents.js +100 -0
- package/server/services/memory/manager.js +242 -26
- package/server/services/websocket.js +3 -1
- package/server/services/widgets/focus_widget.js +126 -0
- package/server/services/widgets/service.js +130 -2
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
part of 'main.dart';
|
|
2
|
+
|
|
3
|
+
enum _ToolsPageTab { integrations, mcp, skills }
|
|
4
|
+
|
|
5
|
+
enum _RunsPageTab { runs, logs }
|
|
6
|
+
|
|
7
|
+
enum _SettingsWorkspaceSection { app, account, security }
|
|
8
|
+
|
|
9
|
+
class ToolsPanel extends StatefulWidget {
|
|
10
|
+
const ToolsPanel({super.key, required this.controller});
|
|
11
|
+
|
|
12
|
+
final NeoAgentController controller;
|
|
13
|
+
|
|
14
|
+
@override
|
|
15
|
+
State<ToolsPanel> createState() => _ToolsPanelState();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class _ToolsPanelState extends State<ToolsPanel>
|
|
19
|
+
with SingleTickerProviderStateMixin {
|
|
20
|
+
late final TabController _tabController;
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
void initState() {
|
|
24
|
+
super.initState();
|
|
25
|
+
_tabController = TabController(
|
|
26
|
+
length: _ToolsPageTab.values.length,
|
|
27
|
+
vsync: this,
|
|
28
|
+
initialIndex: _tabForSection(widget.controller.selectedSection).index,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
void didUpdateWidget(covariant ToolsPanel oldWidget) {
|
|
34
|
+
super.didUpdateWidget(oldWidget);
|
|
35
|
+
final selectedSection = widget.controller.selectedSection;
|
|
36
|
+
if (selectedSection != oldWidget.controller.selectedSection &&
|
|
37
|
+
(selectedSection == AppSection.integrations ||
|
|
38
|
+
selectedSection == AppSection.mcp ||
|
|
39
|
+
selectedSection == AppSection.skills)) {
|
|
40
|
+
final targetIndex = _tabForSection(selectedSection).index;
|
|
41
|
+
if (_tabController.index != targetIndex) {
|
|
42
|
+
_tabController.index = targetIndex;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void dispose() {
|
|
49
|
+
_tabController.dispose();
|
|
50
|
+
super.dispose();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_ToolsPageTab _tabForSection(AppSection section) {
|
|
54
|
+
switch (section) {
|
|
55
|
+
case AppSection.mcp:
|
|
56
|
+
return _ToolsPageTab.mcp;
|
|
57
|
+
case AppSection.skills:
|
|
58
|
+
return _ToolsPageTab.skills;
|
|
59
|
+
default:
|
|
60
|
+
return _ToolsPageTab.integrations;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
Widget build(BuildContext context) {
|
|
66
|
+
final controller = widget.controller;
|
|
67
|
+
final visibleIntegrations = controller.officialIntegrations
|
|
68
|
+
.where(
|
|
69
|
+
(item) =>
|
|
70
|
+
item.env.configured ||
|
|
71
|
+
item.env.setupMode == 'user' ||
|
|
72
|
+
item.isConnected,
|
|
73
|
+
)
|
|
74
|
+
.length;
|
|
75
|
+
return Padding(
|
|
76
|
+
padding: _pagePadding(context),
|
|
77
|
+
child: Column(
|
|
78
|
+
children: <Widget>[
|
|
79
|
+
const _PageTitle(
|
|
80
|
+
title: 'Tools',
|
|
81
|
+
subtitle:
|
|
82
|
+
'Manage official integrations, MCP servers, and reusable skills in one place.',
|
|
83
|
+
),
|
|
84
|
+
const SizedBox(height: 12),
|
|
85
|
+
Container(
|
|
86
|
+
decoration: BoxDecoration(
|
|
87
|
+
color: _bgSecondary,
|
|
88
|
+
borderRadius: BorderRadius.circular(14),
|
|
89
|
+
border: Border.all(color: _border),
|
|
90
|
+
),
|
|
91
|
+
child: TabBar(
|
|
92
|
+
controller: _tabController,
|
|
93
|
+
dividerColor: Colors.transparent,
|
|
94
|
+
indicatorSize: TabBarIndicatorSize.tab,
|
|
95
|
+
labelStyle: const TextStyle(fontWeight: FontWeight.w700),
|
|
96
|
+
tabs: <Widget>[
|
|
97
|
+
Tab(text: 'Integrations ($visibleIntegrations)'),
|
|
98
|
+
Tab(text: 'MCP (${controller.mcpServers.length})'),
|
|
99
|
+
Tab(text: 'Skills (${controller.skills.length})'),
|
|
100
|
+
],
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
const SizedBox(height: 12),
|
|
104
|
+
Expanded(
|
|
105
|
+
child: TabBarView(
|
|
106
|
+
controller: _tabController,
|
|
107
|
+
children: <Widget>[
|
|
108
|
+
IntegrationsPanel(controller: controller, embedded: true),
|
|
109
|
+
McpPanel(controller: controller, embedded: true),
|
|
110
|
+
SkillsPanel(controller: controller, embedded: true),
|
|
111
|
+
],
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
],
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class RunsAndLogsPanel extends StatefulWidget {
|
|
121
|
+
const RunsAndLogsPanel({super.key, required this.controller});
|
|
122
|
+
|
|
123
|
+
final NeoAgentController controller;
|
|
124
|
+
|
|
125
|
+
@override
|
|
126
|
+
State<RunsAndLogsPanel> createState() => _RunsAndLogsPanelState();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
class _RunsAndLogsPanelState extends State<RunsAndLogsPanel>
|
|
130
|
+
with SingleTickerProviderStateMixin {
|
|
131
|
+
late final TabController _tabController;
|
|
132
|
+
|
|
133
|
+
@override
|
|
134
|
+
void initState() {
|
|
135
|
+
super.initState();
|
|
136
|
+
_tabController = TabController(
|
|
137
|
+
length: _RunsPageTab.values.length,
|
|
138
|
+
vsync: this,
|
|
139
|
+
initialIndex: _tabForSection(widget.controller.selectedSection).index,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
void didUpdateWidget(covariant RunsAndLogsPanel oldWidget) {
|
|
145
|
+
super.didUpdateWidget(oldWidget);
|
|
146
|
+
final selectedSection = widget.controller.selectedSection;
|
|
147
|
+
if (selectedSection != oldWidget.controller.selectedSection &&
|
|
148
|
+
(selectedSection == AppSection.runs ||
|
|
149
|
+
selectedSection == AppSection.logs)) {
|
|
150
|
+
final targetIndex = _tabForSection(selectedSection).index;
|
|
151
|
+
if (_tabController.index != targetIndex) {
|
|
152
|
+
_tabController.index = targetIndex;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@override
|
|
158
|
+
void dispose() {
|
|
159
|
+
_tabController.dispose();
|
|
160
|
+
super.dispose();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
_RunsPageTab _tabForSection(AppSection section) {
|
|
164
|
+
switch (section) {
|
|
165
|
+
case AppSection.logs:
|
|
166
|
+
return _RunsPageTab.logs;
|
|
167
|
+
default:
|
|
168
|
+
return _RunsPageTab.runs;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@override
|
|
173
|
+
Widget build(BuildContext context) {
|
|
174
|
+
final controller = widget.controller;
|
|
175
|
+
return Padding(
|
|
176
|
+
padding: _pagePadding(context),
|
|
177
|
+
child: Column(
|
|
178
|
+
children: <Widget>[
|
|
179
|
+
const _PageTitle(
|
|
180
|
+
title: 'Runs & Logs',
|
|
181
|
+
subtitle:
|
|
182
|
+
'Inspect execution history, failures, tool traces, and diagnostics from one workspace.',
|
|
183
|
+
),
|
|
184
|
+
const SizedBox(height: 12),
|
|
185
|
+
Container(
|
|
186
|
+
decoration: BoxDecoration(
|
|
187
|
+
color: _bgSecondary,
|
|
188
|
+
borderRadius: BorderRadius.circular(14),
|
|
189
|
+
border: Border.all(color: _border),
|
|
190
|
+
),
|
|
191
|
+
child: TabBar(
|
|
192
|
+
controller: _tabController,
|
|
193
|
+
dividerColor: Colors.transparent,
|
|
194
|
+
indicatorSize: TabBarIndicatorSize.tab,
|
|
195
|
+
labelStyle: const TextStyle(fontWeight: FontWeight.w700),
|
|
196
|
+
tabs: <Widget>[
|
|
197
|
+
Tab(text: 'Runs (${controller.recentRuns.length})'),
|
|
198
|
+
Tab(text: 'Logs (${controller.logs.length})'),
|
|
199
|
+
],
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
const SizedBox(height: 12),
|
|
203
|
+
Expanded(
|
|
204
|
+
child: TabBarView(
|
|
205
|
+
controller: _tabController,
|
|
206
|
+
children: <Widget>[
|
|
207
|
+
RunsPanel(controller: controller, embedded: true),
|
|
208
|
+
LogsPanel(controller: controller, embedded: true),
|
|
209
|
+
],
|
|
210
|
+
),
|
|
211
|
+
),
|
|
212
|
+
],
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class SettingsWorkspacePanel extends StatefulWidget {
|
|
219
|
+
const SettingsWorkspacePanel({super.key, required this.controller});
|
|
220
|
+
|
|
221
|
+
final NeoAgentController controller;
|
|
222
|
+
|
|
223
|
+
@override
|
|
224
|
+
State<SettingsWorkspacePanel> createState() => _SettingsWorkspacePanelState();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
class _SettingsWorkspacePanelState extends State<SettingsWorkspacePanel> {
|
|
228
|
+
late _SettingsWorkspaceSection _selectedSection;
|
|
229
|
+
|
|
230
|
+
@override
|
|
231
|
+
void initState() {
|
|
232
|
+
super.initState();
|
|
233
|
+
_selectedSection =
|
|
234
|
+
widget.controller.selectedSection == AppSection.accountSettings
|
|
235
|
+
? _SettingsWorkspaceSection.account
|
|
236
|
+
: _SettingsWorkspaceSection.app;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@override
|
|
240
|
+
void didUpdateWidget(covariant SettingsWorkspacePanel oldWidget) {
|
|
241
|
+
super.didUpdateWidget(oldWidget);
|
|
242
|
+
if (widget.controller.selectedSection == AppSection.settings &&
|
|
243
|
+
_selectedSection != _SettingsWorkspaceSection.app) {
|
|
244
|
+
_selectedSection = _SettingsWorkspaceSection.app;
|
|
245
|
+
}
|
|
246
|
+
if (widget.controller.selectedSection == AppSection.accountSettings &&
|
|
247
|
+
_selectedSection == _SettingsWorkspaceSection.app) {
|
|
248
|
+
_selectedSection = _SettingsWorkspaceSection.account;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@override
|
|
253
|
+
Widget build(BuildContext context) {
|
|
254
|
+
final compact = MediaQuery.sizeOf(context).width < 960;
|
|
255
|
+
return Padding(
|
|
256
|
+
padding: _pagePadding(context),
|
|
257
|
+
child: Column(
|
|
258
|
+
children: <Widget>[
|
|
259
|
+
const _PageTitle(
|
|
260
|
+
title: 'Settings',
|
|
261
|
+
subtitle:
|
|
262
|
+
'Workspace configuration and account security in one place.',
|
|
263
|
+
),
|
|
264
|
+
const SizedBox(height: 12),
|
|
265
|
+
Expanded(
|
|
266
|
+
child: Card(
|
|
267
|
+
child: Padding(
|
|
268
|
+
padding: const EdgeInsets.all(20),
|
|
269
|
+
child: compact
|
|
270
|
+
? Column(
|
|
271
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
272
|
+
children: <Widget>[
|
|
273
|
+
_SettingsWorkspaceNav(
|
|
274
|
+
selected: _selectedSection,
|
|
275
|
+
compact: true,
|
|
276
|
+
onSelected: _selectSection,
|
|
277
|
+
),
|
|
278
|
+
const SizedBox(height: 16),
|
|
279
|
+
Expanded(child: _buildContent()),
|
|
280
|
+
],
|
|
281
|
+
)
|
|
282
|
+
: Row(
|
|
283
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
284
|
+
children: <Widget>[
|
|
285
|
+
SizedBox(
|
|
286
|
+
width: 220,
|
|
287
|
+
child: _SettingsWorkspaceNav(
|
|
288
|
+
selected: _selectedSection,
|
|
289
|
+
compact: false,
|
|
290
|
+
onSelected: _selectSection,
|
|
291
|
+
),
|
|
292
|
+
),
|
|
293
|
+
const SizedBox(width: 24),
|
|
294
|
+
Expanded(child: _buildContent()),
|
|
295
|
+
],
|
|
296
|
+
),
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
),
|
|
300
|
+
],
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
void _selectSection(_SettingsWorkspaceSection section) {
|
|
306
|
+
setState(() => _selectedSection = section);
|
|
307
|
+
if (section == _SettingsWorkspaceSection.app) {
|
|
308
|
+
widget.controller.setSelectedSection(AppSection.settings);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
widget.controller.setSelectedSection(AppSection.accountSettings);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
Widget _buildContent() {
|
|
315
|
+
switch (_selectedSection) {
|
|
316
|
+
case _SettingsWorkspaceSection.app:
|
|
317
|
+
return SettingsPanel(controller: widget.controller, embedded: true);
|
|
318
|
+
case _SettingsWorkspaceSection.account:
|
|
319
|
+
return AccountSettingsPanel(
|
|
320
|
+
controller: widget.controller,
|
|
321
|
+
embedded: true,
|
|
322
|
+
initialTab: AccountSettingsTab.account,
|
|
323
|
+
);
|
|
324
|
+
case _SettingsWorkspaceSection.security:
|
|
325
|
+
return AccountSettingsPanel(
|
|
326
|
+
controller: widget.controller,
|
|
327
|
+
embedded: true,
|
|
328
|
+
initialTab: AccountSettingsTab.security,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
class _SettingsWorkspaceNav extends StatelessWidget {
|
|
335
|
+
const _SettingsWorkspaceNav({
|
|
336
|
+
required this.selected,
|
|
337
|
+
required this.compact,
|
|
338
|
+
required this.onSelected,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
final _SettingsWorkspaceSection selected;
|
|
342
|
+
final bool compact;
|
|
343
|
+
final ValueChanged<_SettingsWorkspaceSection> onSelected;
|
|
344
|
+
|
|
345
|
+
@override
|
|
346
|
+
Widget build(BuildContext context) {
|
|
347
|
+
final items = <Widget>[
|
|
348
|
+
_navButton(
|
|
349
|
+
section: _SettingsWorkspaceSection.app,
|
|
350
|
+
icon: Icons.tune,
|
|
351
|
+
label: 'App Settings',
|
|
352
|
+
),
|
|
353
|
+
_navButton(
|
|
354
|
+
section: _SettingsWorkspaceSection.account,
|
|
355
|
+
icon: Icons.person_outline,
|
|
356
|
+
label: 'Account',
|
|
357
|
+
),
|
|
358
|
+
_navButton(
|
|
359
|
+
section: _SettingsWorkspaceSection.security,
|
|
360
|
+
icon: Icons.security_outlined,
|
|
361
|
+
label: 'Security',
|
|
362
|
+
),
|
|
363
|
+
];
|
|
364
|
+
return compact
|
|
365
|
+
? Wrap(spacing: 8, runSpacing: 8, children: items)
|
|
366
|
+
: Column(
|
|
367
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
368
|
+
children: items,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
Widget _navButton({
|
|
373
|
+
required _SettingsWorkspaceSection section,
|
|
374
|
+
required IconData icon,
|
|
375
|
+
required String label,
|
|
376
|
+
}) {
|
|
377
|
+
final button = _SidebarButton(
|
|
378
|
+
label: label,
|
|
379
|
+
icon: icon,
|
|
380
|
+
active: selected == section,
|
|
381
|
+
onTap: () => onSelected(section),
|
|
382
|
+
);
|
|
383
|
+
if (compact) {
|
|
384
|
+
return button;
|
|
385
|
+
}
|
|
386
|
+
return Padding(padding: const EdgeInsets.only(bottom: 8), child: button);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import 'package:mixpanel_flutter/mixpanel_flutter.dart';
|
|
2
|
+
|
|
3
|
+
class AppAnalytics {
|
|
4
|
+
Mixpanel? _mixpanel;
|
|
5
|
+
bool _initialized = false;
|
|
6
|
+
bool _enabled = false;
|
|
7
|
+
String? _currentToken;
|
|
8
|
+
bool _consentGranted = false;
|
|
9
|
+
final List<_QueuedEvent> _queue = <_QueuedEvent>[];
|
|
10
|
+
|
|
11
|
+
bool get enabled => _enabled && _consentGranted;
|
|
12
|
+
bool get consentGranted => _consentGranted;
|
|
13
|
+
bool get isConfigured => (_currentToken ?? '').isNotEmpty;
|
|
14
|
+
|
|
15
|
+
Future<void> initialize({
|
|
16
|
+
required String? token,
|
|
17
|
+
required bool consentGranted,
|
|
18
|
+
}) async {
|
|
19
|
+
final normalizedToken = token?.trim() ?? '';
|
|
20
|
+
final shouldEnable = normalizedToken.isNotEmpty;
|
|
21
|
+
if (_initialized &&
|
|
22
|
+
normalizedToken == _currentToken &&
|
|
23
|
+
shouldEnable == _enabled &&
|
|
24
|
+
consentGranted == _consentGranted) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_consentGranted = consentGranted;
|
|
29
|
+
_initialized = false;
|
|
30
|
+
_enabled = false;
|
|
31
|
+
_currentToken = normalizedToken.isEmpty ? null : normalizedToken;
|
|
32
|
+
_mixpanel = null;
|
|
33
|
+
|
|
34
|
+
if (!shouldEnable) {
|
|
35
|
+
_queue.clear();
|
|
36
|
+
_initialized = true;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!consentGranted) {
|
|
41
|
+
_queue.clear();
|
|
42
|
+
_initialized = true;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
_mixpanel = await Mixpanel.init(
|
|
48
|
+
normalizedToken,
|
|
49
|
+
trackAutomaticEvents: false,
|
|
50
|
+
);
|
|
51
|
+
_enabled = true;
|
|
52
|
+
_initialized = true;
|
|
53
|
+
await _flushQueue();
|
|
54
|
+
} catch (_) {
|
|
55
|
+
_queue.clear();
|
|
56
|
+
_mixpanel = null;
|
|
57
|
+
_enabled = false;
|
|
58
|
+
_initialized = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Future<void> setConsentGranted(bool consentGranted) async {
|
|
63
|
+
if (_consentGranted == consentGranted && _initialized) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
_consentGranted = consentGranted;
|
|
68
|
+
if (!consentGranted) {
|
|
69
|
+
_mixpanel = null;
|
|
70
|
+
_enabled = false;
|
|
71
|
+
_initialized = true;
|
|
72
|
+
_queue.clear();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await initialize(token: _currentToken, consentGranted: true);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Future<void> track(
|
|
80
|
+
String eventName, {
|
|
81
|
+
Map<String, Object?> properties = const <String, Object?>{},
|
|
82
|
+
}) async {
|
|
83
|
+
final event = _QueuedEvent(
|
|
84
|
+
eventName: eventName,
|
|
85
|
+
properties: _cleanProperties(properties),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!_initialized) {
|
|
89
|
+
_queue.add(event);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!enabled || _mixpanel == null) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await _sendEvent(event);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Future<void> trackAppOpened({
|
|
101
|
+
required String appMode,
|
|
102
|
+
required String platform,
|
|
103
|
+
required String backendMode,
|
|
104
|
+
required String selectedSection,
|
|
105
|
+
required String deploymentProfile,
|
|
106
|
+
required bool authenticated,
|
|
107
|
+
}) {
|
|
108
|
+
return track(
|
|
109
|
+
'app_opened',
|
|
110
|
+
properties: <String, Object?>{
|
|
111
|
+
'app_mode': appMode,
|
|
112
|
+
'platform': platform,
|
|
113
|
+
'backend_mode': backendMode,
|
|
114
|
+
'selected_section': selectedSection,
|
|
115
|
+
'deployment_profile': deploymentProfile,
|
|
116
|
+
'authenticated': authenticated,
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
Future<void> trackBackendUrlSaved({
|
|
122
|
+
required String backendMode,
|
|
123
|
+
}) {
|
|
124
|
+
return track(
|
|
125
|
+
'backend_url_saved',
|
|
126
|
+
properties: <String, Object?>{
|
|
127
|
+
'backend_mode': backendMode,
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Future<void> trackSectionChanged({
|
|
133
|
+
required String section,
|
|
134
|
+
required String previousSection,
|
|
135
|
+
}) {
|
|
136
|
+
return track(
|
|
137
|
+
'section_changed',
|
|
138
|
+
properties: <String, Object?>{
|
|
139
|
+
'section': section,
|
|
140
|
+
'previous_section': previousSection,
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Future<void> trackChatMessageSent({
|
|
146
|
+
required int length,
|
|
147
|
+
required bool steeringLiveRun,
|
|
148
|
+
}) {
|
|
149
|
+
return track(
|
|
150
|
+
'chat_message_sent',
|
|
151
|
+
properties: <String, Object?>{
|
|
152
|
+
'length': length,
|
|
153
|
+
'steering_live_run': steeringLiveRun,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Future<void> trackRecordingStarted({
|
|
159
|
+
required String kind,
|
|
160
|
+
}) {
|
|
161
|
+
return track(
|
|
162
|
+
'recording_started',
|
|
163
|
+
properties: <String, Object?>{
|
|
164
|
+
'kind': kind,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
Future<void> trackRecordingStopped({
|
|
170
|
+
required String kind,
|
|
171
|
+
required String stopReason,
|
|
172
|
+
}) {
|
|
173
|
+
return track(
|
|
174
|
+
'recording_stopped',
|
|
175
|
+
properties: <String, Object?>{
|
|
176
|
+
'kind': kind,
|
|
177
|
+
'stop_reason': stopReason,
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Future<void> trackAppUpdateCheck({
|
|
183
|
+
required bool silent,
|
|
184
|
+
}) {
|
|
185
|
+
return track(
|
|
186
|
+
'app_update_check',
|
|
187
|
+
properties: <String, Object?>{
|
|
188
|
+
'silent': silent,
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Future<void> trackTaskRunRequested({
|
|
194
|
+
required int taskId,
|
|
195
|
+
}) {
|
|
196
|
+
return track(
|
|
197
|
+
'task_run_requested',
|
|
198
|
+
properties: <String, Object?>{
|
|
199
|
+
'task_id': taskId,
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
Future<void> trackWidgetRefreshRequested({
|
|
205
|
+
required bool all,
|
|
206
|
+
}) {
|
|
207
|
+
return track(
|
|
208
|
+
'widget_refresh_requested',
|
|
209
|
+
properties: <String, Object?>{
|
|
210
|
+
'all': all,
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Future<void> trackAppUpdateTriggered() {
|
|
216
|
+
return track('app_update_triggered');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Future<void> trackSignedIn({
|
|
220
|
+
required String authMethod,
|
|
221
|
+
required bool isRegistration,
|
|
222
|
+
}) {
|
|
223
|
+
return track(
|
|
224
|
+
'signed_in',
|
|
225
|
+
properties: <String, Object?>{
|
|
226
|
+
'auth_method': authMethod,
|
|
227
|
+
'is_registration': isRegistration,
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
Future<void> trackSignedOut() {
|
|
233
|
+
return track('signed_out');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
Future<void> trackOnboardingDismissed() {
|
|
237
|
+
return track('onboarding_dismissed');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Future<void> dispose() async {
|
|
241
|
+
try {
|
|
242
|
+
await _flushQueue();
|
|
243
|
+
} catch (_) {}
|
|
244
|
+
_queue.clear();
|
|
245
|
+
_mixpanel = null;
|
|
246
|
+
_initialized = false;
|
|
247
|
+
_enabled = false;
|
|
248
|
+
_currentToken = null;
|
|
249
|
+
_consentGranted = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Future<void> _flushQueue() async {
|
|
253
|
+
if (!enabled || _mixpanel == null || _queue.isEmpty) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
final pending = List<_QueuedEvent>.from(_queue);
|
|
258
|
+
_queue.clear();
|
|
259
|
+
for (final event in pending) {
|
|
260
|
+
await _sendEvent(event);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Future<void> _sendEvent(_QueuedEvent event) async {
|
|
265
|
+
final mixpanel = _mixpanel;
|
|
266
|
+
if (mixpanel == null) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
await mixpanel.track(event.eventName, properties: event.properties);
|
|
272
|
+
} catch (_) {}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
Map<String, Object?> _cleanProperties(Map<String, Object?> properties) {
|
|
276
|
+
final cleaned = <String, Object?>{};
|
|
277
|
+
for (final entry in properties.entries) {
|
|
278
|
+
final value = entry.value;
|
|
279
|
+
if (value == null) continue;
|
|
280
|
+
cleaned[entry.key] = value;
|
|
281
|
+
}
|
|
282
|
+
return cleaned;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
class _QueuedEvent {
|
|
287
|
+
const _QueuedEvent({
|
|
288
|
+
required this.eventName,
|
|
289
|
+
required this.properties,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
final String eventName;
|
|
293
|
+
final Map<String, Object?> properties;
|
|
294
|
+
}
|
|
@@ -463,6 +463,10 @@ class BackendClient {
|
|
|
463
463
|
return getMap(baseUrl, '/api/version');
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
Future<Map<String, dynamic>> fetchRuntimeConfig(String baseUrl) async {
|
|
467
|
+
return getMap(baseUrl, '/api/runtime/config', allowUnauthorized: true);
|
|
468
|
+
}
|
|
469
|
+
|
|
466
470
|
Future<Map<String, dynamic>> fetchBrowserStatus(String baseUrl) async {
|
|
467
471
|
return getMap(baseUrl, '/api/browser/status');
|
|
468
472
|
}
|
|
@@ -11,6 +11,7 @@ import desktop_audio_capture
|
|
|
11
11
|
import flutter_secure_storage_macos
|
|
12
12
|
import geolocator_apple
|
|
13
13
|
import hotkey_manager_macos
|
|
14
|
+
import mixpanel_flutter
|
|
14
15
|
import mobile_scanner
|
|
15
16
|
import package_info_plus
|
|
16
17
|
import path_provider_foundation
|
|
@@ -30,6 +31,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|
|
30
31
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
|
31
32
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
|
32
33
|
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
|
34
|
+
MixpanelFlutterPlugin.register(with: registry.registrar(forPlugin: "MixpanelFlutterPlugin"))
|
|
33
35
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
|
34
36
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
|
35
37
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|