neoagent 2.4.2-beta.3 → 2.4.2-beta.5

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 (33) hide show
  1. package/.env.example +0 -9
  2. package/docs/configuration.md +0 -10
  3. package/flutter_app/lib/main.dart +0 -1
  4. package/flutter_app/lib/main_account_settings.dart +57 -16
  5. package/flutter_app/lib/main_controller.dart +0 -160
  6. package/flutter_app/lib/main_models.dart +6 -0
  7. package/flutter_app/lib/main_operations.dart +17 -1
  8. package/flutter_app/lib/main_runtime.dart +0 -22
  9. package/flutter_app/lib/main_shared.dart +0 -161
  10. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  11. package/flutter_app/pubspec.lock +0 -8
  12. package/flutter_app/pubspec.yaml +0 -1
  13. package/landing/index.html +1 -1
  14. package/package.json +1 -1
  15. package/server/admin/admin.css +23 -0
  16. package/server/admin/admin.js +89 -32
  17. package/server/admin/analytics.js +254 -48
  18. package/server/admin/index.html +36 -2
  19. package/server/admin/users.js +94 -37
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/AssetManifest.bin +1 -1
  22. package/server/public/assets/AssetManifest.bin.json +1 -1
  23. package/server/public/assets/NOTICES +0 -183
  24. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  25. package/server/public/flutter_bootstrap.js +1 -1
  26. package/server/public/main.dart.js +77607 -78350
  27. package/server/routes/account.js +11 -2
  28. package/server/routes/admin.js +106 -24
  29. package/server/routes/runtime.js +1 -4
  30. package/server/services/ai/engine.js +13 -11
  31. package/server/services/ai/models.js +10 -4
  32. package/flutter_app/lib/src/analytics_service.dart +0 -294
  33. package/server/config/analytics.js +0 -30
@@ -2393,167 +2393,6 @@ class _GlobalWebUpdateBanner extends StatelessWidget {
2393
2393
  }
2394
2394
  }
2395
2395
 
2396
- class _GlobalAnalyticsConsentBanner extends StatelessWidget {
2397
- const _GlobalAnalyticsConsentBanner({required this.controller});
2398
-
2399
- final NeoAgentController controller;
2400
-
2401
- @override
2402
- Widget build(BuildContext context) {
2403
- return LayoutBuilder(
2404
- builder: (context, constraints) {
2405
- final compact = constraints.maxWidth < 700;
2406
- final body = Text(
2407
- 'NeoAgent uses anonymous analytics to understand startup, navigation, and setup flows. No messages, credentials, or personal content are collected.',
2408
- style: TextStyle(color: _textSecondary, height: 1.35),
2409
- );
2410
- return Material(
2411
- color: Colors.transparent,
2412
- child: _GlassSurface(
2413
- borderRadius: BorderRadius.circular(22),
2414
- blurSigma: 26,
2415
- fillColor: _bgCard.withValues(alpha: 0.94),
2416
- borderColor: _accent.withValues(alpha: 0.18),
2417
- overlayGradient: LinearGradient(
2418
- colors: <Color>[
2419
- _accent.withValues(alpha: 0.12),
2420
- _bgCard.withValues(alpha: 0.88),
2421
- _bgSecondary.withValues(alpha: 0.86),
2422
- ],
2423
- stops: const <double>[0, 0.22, 1],
2424
- begin: const Alignment(-1, -1),
2425
- end: const Alignment(1, 1),
2426
- ),
2427
- boxShadow: <BoxShadow>[
2428
- BoxShadow(
2429
- color: Colors.black.withValues(alpha: 0.22),
2430
- blurRadius: 34,
2431
- offset: const Offset(0, 18),
2432
- ),
2433
- BoxShadow(
2434
- color: _accent.withValues(alpha: 0.1),
2435
- blurRadius: 24,
2436
- offset: const Offset(0, 8),
2437
- ),
2438
- ],
2439
- padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
2440
- child: compact
2441
- ? Column(
2442
- crossAxisAlignment: CrossAxisAlignment.start,
2443
- children: <Widget>[
2444
- Row(
2445
- crossAxisAlignment: CrossAxisAlignment.start,
2446
- children: <Widget>[
2447
- Container(
2448
- padding: const EdgeInsets.all(10),
2449
- decoration: BoxDecoration(
2450
- color: _accent.withValues(alpha: 0.16),
2451
- shape: BoxShape.circle,
2452
- border: Border.all(
2453
- color: _accent.withValues(alpha: 0.24),
2454
- ),
2455
- ),
2456
- child: Icon(
2457
- Icons.cookie_outlined,
2458
- color: _accent,
2459
- size: 22,
2460
- ),
2461
- ),
2462
- const SizedBox(width: 12),
2463
- Expanded(
2464
- child: Column(
2465
- crossAxisAlignment: CrossAxisAlignment.start,
2466
- children: <Widget>[
2467
- Text(
2468
- 'Analytics cookies',
2469
- style: TextStyle(
2470
- color: _textPrimary,
2471
- fontSize: 15,
2472
- fontWeight: FontWeight.w700,
2473
- ),
2474
- ),
2475
- const SizedBox(height: 6),
2476
- body,
2477
- ],
2478
- ),
2479
- ),
2480
- ],
2481
- ),
2482
- const SizedBox(height: 14),
2483
- Row(
2484
- children: <Widget>[
2485
- Expanded(
2486
- child: OutlinedButton(
2487
- onPressed: controller.declineAnalyticsConsent,
2488
- child: const Text('Decline'),
2489
- ),
2490
- ),
2491
- const SizedBox(width: 10),
2492
- Expanded(
2493
- child: FilledButton(
2494
- onPressed: controller.acceptAnalyticsConsent,
2495
- child: const Text('Allow analytics'),
2496
- ),
2497
- ),
2498
- ],
2499
- ),
2500
- ],
2501
- )
2502
- : Row(
2503
- crossAxisAlignment: CrossAxisAlignment.center,
2504
- children: <Widget>[
2505
- Container(
2506
- padding: const EdgeInsets.all(10),
2507
- decoration: BoxDecoration(
2508
- color: _accent.withValues(alpha: 0.16),
2509
- shape: BoxShape.circle,
2510
- border: Border.all(
2511
- color: _accent.withValues(alpha: 0.24),
2512
- ),
2513
- ),
2514
- child: Icon(
2515
- Icons.cookie_outlined,
2516
- color: _accent,
2517
- size: 22,
2518
- ),
2519
- ),
2520
- const SizedBox(width: 14),
2521
- Expanded(
2522
- child: Column(
2523
- crossAxisAlignment: CrossAxisAlignment.start,
2524
- children: <Widget>[
2525
- Text(
2526
- 'Analytics cookies',
2527
- style: TextStyle(
2528
- color: _textPrimary,
2529
- fontSize: 15,
2530
- fontWeight: FontWeight.w700,
2531
- ),
2532
- ),
2533
- const SizedBox(height: 6),
2534
- body,
2535
- ],
2536
- ),
2537
- ),
2538
- const SizedBox(width: 16),
2539
- OutlinedButton(
2540
- onPressed: controller.declineAnalyticsConsent,
2541
- child: const Text('Decline'),
2542
- ),
2543
- const SizedBox(width: 10),
2544
- FilledButton(
2545
- onPressed: controller.acceptAnalyticsConsent,
2546
- child: const Text('Allow analytics'),
2547
- ),
2548
- ],
2549
- ),
2550
- ),
2551
- );
2552
- },
2553
- );
2554
- }
2555
- }
2556
-
2557
2396
  class _DesktopCloseDecision {
2558
2397
  const _DesktopCloseDecision({
2559
2398
  required this.keepRunning,
@@ -12,7 +12,6 @@ import file_picker
12
12
  import flutter_secure_storage_macos
13
13
  import geolocator_apple
14
14
  import hotkey_manager_macos
15
- import mixpanel_flutter
16
15
  import mobile_scanner
17
16
  import package_info_plus
18
17
  import path_provider_foundation
@@ -33,7 +32,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
33
32
  FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
34
33
  GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
35
34
  HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
36
- MixpanelFlutterPlugin.register(with: registry.registrar(forPlugin: "MixpanelFlutterPlugin"))
37
35
  MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
38
36
  FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
39
37
  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
@@ -598,14 +598,6 @@ packages:
598
598
  url: "https://pub.dev"
599
599
  source: hosted
600
600
  version: "1.18.0"
601
- mixpanel_flutter:
602
- dependency: "direct main"
603
- description:
604
- name: mixpanel_flutter
605
- sha256: "6962b8ce61eb6202d6545bee648e21a7bc9f7e804fa293912e9673329f499ece"
606
- url: "https://pub.dev"
607
- source: hosted
608
- version: "2.7.0"
609
601
  mobile_scanner:
610
602
  dependency: "direct main"
611
603
  description:
@@ -15,7 +15,6 @@ dependencies:
15
15
  google_fonts: ^8.1.0
16
16
  image: ^4.5.4
17
17
  http: ^1.5.0
18
- mixpanel_flutter: ^2.6.1
19
18
  connectivity_plus: 6.1.5
20
19
  shared_preferences: ^2.5.3
21
20
  flutter_secure_storage: ^9.2.2
@@ -1015,7 +1015,7 @@ p { margin: 0; text-wrap: pretty; }
1015
1015
  <div class="frame">
1016
1016
  <div class="frame-bar">
1017
1017
  <span class="dot" style="background:#e26e61"></span><span class="dot" style="background:#e0b24a"></span><span class="dot" style="background:#4bbf85"></span>
1018
- <span class="url">app.neoagent.ai · control surface</span>
1018
+ <span class="url">control surface</span>
1019
1019
  </div>
1020
1020
  <div class="frame-body shot-pair">
1021
1021
  <img class="lm" src="images/dashboard-light.png" alt="NeoAgent dashboard" loading="eager">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.2-beta.3",
3
+ "version": "2.4.2-beta.5",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -185,6 +185,29 @@ button:disabled,
185
185
  border: 1px solid rgba(222, 138, 120, 0.20);
186
186
  }
187
187
 
188
+ .range-btn {
189
+ padding: 5px 12px;
190
+ font-size: 12px;
191
+ font-weight: 600;
192
+ border: 1px solid var(--border);
193
+ border-radius: 6px;
194
+ background: transparent;
195
+ color: var(--text-muted);
196
+ cursor: pointer;
197
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
198
+ }
199
+
200
+ .range-btn:hover {
201
+ background: rgba(224, 240, 224, 0.07);
202
+ color: var(--text);
203
+ }
204
+
205
+ .range-btn.active {
206
+ background: var(--accent-muted);
207
+ color: var(--accent);
208
+ border-color: var(--accent);
209
+ }
210
+
188
211
  .btn-danger:hover:not(:disabled) {
189
212
  background: rgba(222, 138, 120, 0.20);
190
213
  }
@@ -328,50 +328,88 @@ async function clearProvider(key, btn) {
328
328
 
329
329
  // ── Models ─────────────────────────────────────────────────────────────────
330
330
 
331
+ function fmtModelPrice(inputCostPerM) {
332
+ if (inputCostPerM === null || inputCostPerM === undefined) return '—';
333
+ if (inputCostPerM === 0) return 'Free';
334
+ if (inputCostPerM < 0.01) return `$${inputCostPerM.toFixed(4)}/M`;
335
+ if (inputCostPerM < 1) return `$${inputCostPerM.toFixed(3)}/M`;
336
+ return `$${inputCostPerM.toFixed(2)}/M`;
337
+ }
338
+
339
+ function priceTierClass(tier) {
340
+ if (tier === 'free') return 'badge-ok';
341
+ if (tier === 'cheap') return 'badge-ok';
342
+ if (tier === 'medium') return 'badge-warn';
343
+ if (tier === 'expensive') return 'badge-err';
344
+ return 'badge-idle';
345
+ }
346
+
347
+ function purposeIcon(purpose) {
348
+ const icons = { fast: '⚡', planning: '🧠', coding: '💻', general: '✦' };
349
+ return icons[purpose] || '✦';
350
+ }
351
+
331
352
  async function loadModels() {
332
353
  const el = document.getElementById('models-content');
333
354
  if (!el) return;
334
355
  try {
335
356
  const data = await api('/admin/api/models').then((r) => r.json());
336
357
  const models = data.models || [];
337
- const enabledModels = data.enabledModels || [];
338
-
339
- if (!models.length) { el.innerHTML = '<div class="empty">No models found</div>'; return; }
340
-
341
- // Sort models by provider, then label
358
+ const disabledSet = new Set(data.disabledModels || []);
359
+
360
+ if (!models.length) { el.innerHTML = '<div class="empty">No models found — configure providers first.</div>'; return; }
361
+
362
+ // Sort: provider alpha, then price low→high within provider
342
363
  models.sort((a, b) => {
343
364
  if (a.provider !== b.provider) return a.provider.localeCompare(b.provider);
344
- return a.label.localeCompare(b.label);
365
+ const ac = a.inputCostPerM ?? Infinity;
366
+ const bc = b.inputCostPerM ?? Infinity;
367
+ return ac - bc;
345
368
  });
346
-
347
- let html = `<table class="users-table">
369
+
370
+ const enabledCount = models.length - disabledSet.size;
371
+
372
+ let html = `
373
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap;">
374
+ <button class="btn btn-ghost" style="padding:5px 12px;font-size:12px;" onclick="toggleAllModels(true)">Enable All</button>
375
+ <button class="btn btn-ghost" style="padding:5px 12px;font-size:12px;" onclick="toggleAllModels(false)">Disable All</button>
376
+ <span style="margin-left:4px;font-size:12px;color:var(--text-muted);" id="model-count-label">${enabledCount} of ${models.length} enabled</span>
377
+ </div>
378
+ <table class="users-table">
348
379
  <thead><tr>
349
- <th style="width:40px;">Enabled</th>
350
- <th>Model Name</th>
380
+ <th style="width:40px;">On</th>
381
+ <th>Model</th>
351
382
  <th>Provider</th>
352
- <th>Purpose</th>
353
- <th>Price Tier</th>
383
+ <th style="width:90px;">Purpose</th>
384
+ <th style="width:80px;">Tier</th>
385
+ <th style="width:110px;text-align:right;">Input / 1M tokens</th>
354
386
  </tr></thead>
355
387
  <tbody>`;
356
-
388
+
357
389
  for (const m of models) {
358
- // Checked if it's in the enabledModels list, OR if the list is empty (all enabled)
359
- const isChecked = enabledModels.length === 0 || enabledModels.includes(m.id);
360
- const rowOpacity = isChecked ? '1' : '0.5';
390
+ const isEnabled = !disabledSet.has(m.id);
391
+ const priceStr = fmtModelPrice(m.inputCostPerM);
392
+ const tierCls = priceTierClass(m.priceTier);
393
+ const icon = purposeIcon(m.purpose);
361
394
  html += `
362
- <tr style="opacity: ${rowOpacity}">
395
+ <tr style="opacity:${isEnabled ? '1' : '0.45'}">
363
396
  <td style="text-align:center;">
364
- <input type="checkbox" class="model-cb" value="${esc(m.id)}" ${isChecked ? 'checked' : ''} onchange="this.closest('tr').style.opacity = this.checked ? '1' : '0.5'">
397
+ <input type="checkbox" class="model-cb" value="${esc(m.id)}" ${isEnabled ? 'checked' : ''}
398
+ onchange="onModelToggle(this)">
399
+ </td>
400
+ <td>
401
+ <div style="font-weight:600;color:var(--text);font-size:13px;">${esc(m.label)}</div>
402
+ <div style="font-size:11px;color:var(--text-muted);font-family:var(--font-mono);margin-top:2px;">${esc(m.id)}</div>
365
403
  </td>
366
- <td style="font-weight:600;color:var(--text);">${esc(m.label)}</td>
367
- <td>${esc(m.provider)}</td>
368
- <td><span class="badge badge-idle">${esc(m.purpose)}</span></td>
369
- <td><span class="badge ${m.priceTier === 'free' ? 'badge-ok' : 'badge-idle'}">${esc(m.priceTier)}</span></td>
404
+ <td style="font-size:13px;text-transform:capitalize;">${esc(m.provider)}</td>
405
+ <td><span class="badge badge-idle">${icon} ${esc(m.purpose)}</span></td>
406
+ <td><span class="badge ${tierCls}">${esc(m.priceTier ?? '?')}</span></td>
407
+ <td style="text-align:right;font-family:var(--font-mono);font-size:13px;font-weight:600;color:var(--text);">${priceStr}</td>
370
408
  </tr>
371
409
  `;
372
410
  }
373
-
374
- html += \`</tbody></table>\`;
411
+
412
+ html += `</tbody></table>`;
375
413
  el.innerHTML = html;
376
414
  } catch (err) {
377
415
  if (err.message !== 'unauthorized') {
@@ -380,23 +418,42 @@ async function loadModels() {
380
418
  }
381
419
  }
382
420
 
421
+ function onModelToggle(cb) {
422
+ const row = cb.closest('tr');
423
+ if (row) row.style.opacity = cb.checked ? '1' : '0.45';
424
+ const all = document.querySelectorAll('.model-cb');
425
+ const checked = Array.from(all).filter(c => c.checked).length;
426
+ const label = document.getElementById('model-count-label');
427
+ if (label) label.textContent = `${checked} of ${all.length} enabled`;
428
+ }
429
+
430
+ function toggleAllModels(enable) {
431
+ document.querySelectorAll('.model-cb').forEach(cb => {
432
+ cb.checked = enable;
433
+ const row = cb.closest('tr');
434
+ if (row) row.style.opacity = enable ? '1' : '0.45';
435
+ });
436
+ const all = document.querySelectorAll('.model-cb');
437
+ const label = document.getElementById('model-count-label');
438
+ if (label) label.textContent = `${enable ? all.length : 0} of ${all.length} enabled`;
439
+ }
440
+
383
441
  async function saveEnabledModels(btn) {
384
442
  const cbs = document.querySelectorAll('.model-cb');
385
443
  if (!cbs.length) return;
386
-
387
- // If all are checked, we can save an empty list to mean "all enabled"
388
- const allChecked = Array.from(cbs).every(cb => cb.checked);
389
- const enabledModels = allChecked ? [] : Array.from(cbs).filter(cb => cb.checked).map(cb => cb.value);
390
-
444
+
445
+ // Persist only the disabled (unchecked) models
446
+ const disabledModels = Array.from(cbs).filter(cb => !cb.checked).map(cb => cb.value);
447
+
391
448
  btn.disabled = true;
392
449
  const original = btn.textContent;
393
450
  btn.textContent = 'Saving…';
394
-
451
+
395
452
  try {
396
- const res = await api('/admin/api/models/enabled', {
453
+ const res = await api('/admin/api/models/config', {
397
454
  method: 'PUT',
398
455
  headers: { 'Content-Type': 'application/json' },
399
- body: JSON.stringify({ enabledModels }),
456
+ body: JSON.stringify({ disabledModels }),
400
457
  });
401
458
  if (!res.ok) {
402
459
  const body = await res.json().catch(() => ({}));