neoagent 2.4.1-beta.8 → 2.4.1
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 +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -470,7 +470,7 @@ class _LogsPanelState extends State<LogsPanel> {
|
|
|
470
470
|
style: TextStyle(
|
|
471
471
|
fontSize: 12,
|
|
472
472
|
height: 1.5,
|
|
473
|
-
fontFamily: GoogleFonts.
|
|
473
|
+
fontFamily: GoogleFonts.geistMono().fontFamily,
|
|
474
474
|
),
|
|
475
475
|
),
|
|
476
476
|
);
|
|
@@ -1240,11 +1240,14 @@ class MemoryPanel extends StatefulWidget {
|
|
|
1240
1240
|
State<MemoryPanel> createState() => _MemoryPanelState();
|
|
1241
1241
|
}
|
|
1242
1242
|
|
|
1243
|
-
class _MemoryPanelState extends State<MemoryPanel>
|
|
1243
|
+
class _MemoryPanelState extends State<MemoryPanel>
|
|
1244
|
+
with SingleTickerProviderStateMixin {
|
|
1244
1245
|
late final TextEditingController _searchController;
|
|
1245
1246
|
late final TextEditingController _llmPromptController;
|
|
1246
1247
|
late final TextEditingController _llmImportController;
|
|
1248
|
+
late final TabController _tabController;
|
|
1247
1249
|
final Set<String> _selectedMemoryIds = <String>{};
|
|
1250
|
+
String? _entityFilter;
|
|
1248
1251
|
bool _bulkActionInFlight = false;
|
|
1249
1252
|
bool _llmPromptLoading = false;
|
|
1250
1253
|
bool _llmImporting = false;
|
|
@@ -1257,6 +1260,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1257
1260
|
_searchController = TextEditingController();
|
|
1258
1261
|
_llmPromptController = TextEditingController();
|
|
1259
1262
|
_llmImportController = TextEditingController();
|
|
1263
|
+
_tabController = TabController(length: 3, vsync: this);
|
|
1260
1264
|
}
|
|
1261
1265
|
|
|
1262
1266
|
@override
|
|
@@ -1264,62 +1268,41 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1264
1268
|
_searchController.dispose();
|
|
1265
1269
|
_llmPromptController.dispose();
|
|
1266
1270
|
_llmImportController.dispose();
|
|
1271
|
+
_tabController.dispose();
|
|
1267
1272
|
super.dispose();
|
|
1268
1273
|
}
|
|
1269
1274
|
|
|
1270
1275
|
Future<void> _loadLlmPrompt(NeoAgentController controller) async {
|
|
1271
|
-
if (_llmPromptLoading)
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1274
|
-
setState(() {
|
|
1275
|
-
_llmPromptLoading = true;
|
|
1276
|
-
});
|
|
1276
|
+
if (_llmPromptLoading) return;
|
|
1277
|
+
setState(() => _llmPromptLoading = true);
|
|
1277
1278
|
try {
|
|
1278
1279
|
final prompt = await controller.fetchMemoryTransferPrompt();
|
|
1279
|
-
if (!mounted)
|
|
1280
|
-
|
|
1281
|
-
}
|
|
1282
|
-
setState(() {
|
|
1283
|
-
_llmPromptController.text = prompt;
|
|
1284
|
-
});
|
|
1280
|
+
if (!mounted) return;
|
|
1281
|
+
setState(() => _llmPromptController.text = prompt);
|
|
1285
1282
|
} catch (error) {
|
|
1286
|
-
if (!mounted)
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1283
|
+
if (!mounted) return;
|
|
1289
1284
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
1290
1285
|
SnackBar(content: Text('Failed to generate prompt: $error')),
|
|
1291
1286
|
);
|
|
1292
1287
|
} finally {
|
|
1293
|
-
if (mounted)
|
|
1294
|
-
setState(() {
|
|
1295
|
-
_llmPromptLoading = false;
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1288
|
+
if (mounted) setState(() => _llmPromptLoading = false);
|
|
1298
1289
|
}
|
|
1299
1290
|
}
|
|
1300
1291
|
|
|
1301
1292
|
Future<void> _copyLlmPrompt() async {
|
|
1302
1293
|
final prompt = _llmPromptController.text.trim();
|
|
1303
|
-
if (prompt.isEmpty)
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1294
|
+
if (prompt.isEmpty) return;
|
|
1306
1295
|
await Clipboard.setData(ClipboardData(text: prompt));
|
|
1307
|
-
if (!mounted)
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1296
|
+
if (!mounted) return;
|
|
1310
1297
|
ScaffoldMessenger.of(
|
|
1311
1298
|
context,
|
|
1312
1299
|
).showSnackBar(const SnackBar(content: Text('Prompt copied.')));
|
|
1313
1300
|
}
|
|
1314
1301
|
|
|
1315
1302
|
Future<void> _importLlmMemories(NeoAgentController controller) async {
|
|
1316
|
-
if (_llmImporting)
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1303
|
+
if (_llmImporting) return;
|
|
1319
1304
|
final text = _llmImportController.text.trim();
|
|
1320
|
-
if (text.isEmpty)
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1305
|
+
if (text.isEmpty) return;
|
|
1323
1306
|
final confirmImport = await showDialog<bool>(
|
|
1324
1307
|
context: context,
|
|
1325
1308
|
builder: (context) {
|
|
@@ -1328,12 +1311,11 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1328
1311
|
if (_llmApplyCoreMemory) 'core memory',
|
|
1329
1312
|
'memories',
|
|
1330
1313
|
];
|
|
1331
|
-
final targetLabel = applyTargets.join(', ');
|
|
1332
1314
|
return AlertDialog(
|
|
1333
1315
|
backgroundColor: _bgCard,
|
|
1334
1316
|
title: Text('Import memory transfer?'),
|
|
1335
1317
|
content: Text(
|
|
1336
|
-
'This will import the response into $
|
|
1318
|
+
'This will import the response into ${applyTargets.join(', ')}.',
|
|
1337
1319
|
),
|
|
1338
1320
|
actions: <Widget>[
|
|
1339
1321
|
TextButton(
|
|
@@ -1348,25 +1330,18 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1348
1330
|
);
|
|
1349
1331
|
},
|
|
1350
1332
|
);
|
|
1351
|
-
if (confirmImport != true)
|
|
1352
|
-
|
|
1353
|
-
}
|
|
1354
|
-
setState(() {
|
|
1355
|
-
_llmImporting = true;
|
|
1356
|
-
});
|
|
1333
|
+
if (confirmImport != true) return;
|
|
1334
|
+
setState(() => _llmImporting = true);
|
|
1357
1335
|
try {
|
|
1358
1336
|
final result = await controller.importMemoryTransfer(
|
|
1359
1337
|
text,
|
|
1360
1338
|
applyBehaviorNotes: _llmApplyBehaviorNotes,
|
|
1361
1339
|
applyCoreMemory: _llmApplyCoreMemory,
|
|
1362
1340
|
);
|
|
1363
|
-
if (!mounted)
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1341
|
+
if (!mounted) return;
|
|
1366
1342
|
_llmImportController.clear();
|
|
1367
|
-
final warningText =
|
|
1368
|
-
? ''
|
|
1369
|
-
: ' ${result.warnings.join(' ')}';
|
|
1343
|
+
final warningText =
|
|
1344
|
+
result.warnings.isEmpty ? '' : ' ${result.warnings.join(' ')}';
|
|
1370
1345
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
1371
1346
|
SnackBar(
|
|
1372
1347
|
content: Text(
|
|
@@ -1378,30 +1353,30 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1378
1353
|
),
|
|
1379
1354
|
);
|
|
1380
1355
|
} catch (error) {
|
|
1381
|
-
if (!mounted)
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
SnackBar(content: Text('Import failed: $error')),
|
|
1386
|
-
);
|
|
1356
|
+
if (!mounted) return;
|
|
1357
|
+
ScaffoldMessenger.of(
|
|
1358
|
+
context,
|
|
1359
|
+
).showSnackBar(SnackBar(content: Text('Import failed: $error')));
|
|
1387
1360
|
} finally {
|
|
1388
|
-
if (mounted)
|
|
1389
|
-
setState(() {
|
|
1390
|
-
_llmImporting = false;
|
|
1391
|
-
});
|
|
1392
|
-
}
|
|
1361
|
+
if (mounted) setState(() => _llmImporting = false);
|
|
1393
1362
|
}
|
|
1394
1363
|
}
|
|
1395
1364
|
|
|
1396
1365
|
List<MemoryItem> get _visibleMemories {
|
|
1397
1366
|
final controller = widget.controller;
|
|
1398
|
-
|
|
1367
|
+
final base = controller.memoryRecallResults.isNotEmpty
|
|
1399
1368
|
? controller.memoryRecallResults
|
|
1400
1369
|
: controller.memories;
|
|
1370
|
+
if (_entityFilter == null) return base;
|
|
1371
|
+
return base
|
|
1372
|
+
.where(
|
|
1373
|
+
(m) => m.entities.any((e) => e.name == _entityFilter),
|
|
1374
|
+
)
|
|
1375
|
+
.toList();
|
|
1401
1376
|
}
|
|
1402
1377
|
|
|
1403
1378
|
List<String> get _selectedVisibleMemoryIds {
|
|
1404
|
-
final visibleIds = _visibleMemories.map((
|
|
1379
|
+
final visibleIds = _visibleMemories.map((m) => m.id).toSet();
|
|
1405
1380
|
return _selectedMemoryIds
|
|
1406
1381
|
.where(visibleIds.contains)
|
|
1407
1382
|
.toList(growable: false);
|
|
@@ -1418,20 +1393,14 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1418
1393
|
}
|
|
1419
1394
|
|
|
1420
1395
|
void _clearMemorySelection() {
|
|
1421
|
-
if (_selectedMemoryIds.isEmpty)
|
|
1422
|
-
|
|
1423
|
-
}
|
|
1424
|
-
setState(() {
|
|
1425
|
-
_selectedMemoryIds.clear();
|
|
1426
|
-
});
|
|
1396
|
+
if (_selectedMemoryIds.isEmpty) return;
|
|
1397
|
+
setState(() => _selectedMemoryIds.clear());
|
|
1427
1398
|
}
|
|
1428
1399
|
|
|
1429
1400
|
void _selectAllVisibleMemories(List<MemoryItem> memories) {
|
|
1430
|
-
if (memories.isEmpty)
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1401
|
+
if (memories.isEmpty) return;
|
|
1433
1402
|
setState(() {
|
|
1434
|
-
_selectedMemoryIds.addAll(memories.map((
|
|
1403
|
+
_selectedMemoryIds.addAll(memories.map((m) => m.id));
|
|
1435
1404
|
});
|
|
1436
1405
|
}
|
|
1437
1406
|
|
|
@@ -1448,6 +1417,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1448
1417
|
void _resetMemorySearch(NeoAgentController controller) {
|
|
1449
1418
|
_searchController.clear();
|
|
1450
1419
|
_clearMemorySelection();
|
|
1420
|
+
setState(() => _entityFilter = null);
|
|
1451
1421
|
controller.clearMemorySearch();
|
|
1452
1422
|
}
|
|
1453
1423
|
|
|
@@ -1456,12 +1426,8 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1456
1426
|
String id,
|
|
1457
1427
|
) async {
|
|
1458
1428
|
await controller.deleteMemory(id);
|
|
1459
|
-
if (!mounted)
|
|
1460
|
-
|
|
1461
|
-
}
|
|
1462
|
-
setState(() {
|
|
1463
|
-
_selectedMemoryIds.remove(id);
|
|
1464
|
-
});
|
|
1429
|
+
if (!mounted) return;
|
|
1430
|
+
setState(() => _selectedMemoryIds.remove(id));
|
|
1465
1431
|
}
|
|
1466
1432
|
|
|
1467
1433
|
Future<void> _runBulkMemoryAction({
|
|
@@ -1471,55 +1437,50 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1471
1437
|
required Future<void> Function(List<String> ids) onConfirm,
|
|
1472
1438
|
}) async {
|
|
1473
1439
|
final ids = _selectedVisibleMemoryIds;
|
|
1474
|
-
if (ids.isEmpty || _bulkActionInFlight)
|
|
1475
|
-
return;
|
|
1476
|
-
}
|
|
1440
|
+
if (ids.isEmpty || _bulkActionInFlight) return;
|
|
1477
1441
|
await _confirmDelete(
|
|
1478
1442
|
context,
|
|
1479
1443
|
title: title,
|
|
1480
1444
|
message: message,
|
|
1481
1445
|
confirmLabel: confirmLabel,
|
|
1482
1446
|
onConfirm: () async {
|
|
1483
|
-
setState(()
|
|
1484
|
-
_bulkActionInFlight = true;
|
|
1485
|
-
});
|
|
1447
|
+
setState(() => _bulkActionInFlight = true);
|
|
1486
1448
|
try {
|
|
1487
1449
|
await onConfirm(ids);
|
|
1488
|
-
if (!mounted)
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
setState(() {
|
|
1492
|
-
_selectedMemoryIds.removeAll(ids);
|
|
1493
|
-
});
|
|
1450
|
+
if (!mounted) return;
|
|
1451
|
+
setState(() => _selectedMemoryIds.removeAll(ids));
|
|
1494
1452
|
} finally {
|
|
1495
|
-
if (mounted)
|
|
1496
|
-
setState(() {
|
|
1497
|
-
_bulkActionInFlight = false;
|
|
1498
|
-
});
|
|
1499
|
-
}
|
|
1453
|
+
if (mounted) setState(() => _bulkActionInFlight = false);
|
|
1500
1454
|
}
|
|
1501
1455
|
},
|
|
1502
1456
|
);
|
|
1503
1457
|
}
|
|
1504
1458
|
|
|
1459
|
+
void _onEntityTapped(String entityName) {
|
|
1460
|
+
setState(() {
|
|
1461
|
+
_entityFilter = _entityFilter == entityName ? null : entityName;
|
|
1462
|
+
_tabController.animateTo(0);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1505
1466
|
@override
|
|
1506
1467
|
Widget build(BuildContext context) {
|
|
1507
1468
|
final controller = widget.controller;
|
|
1469
|
+
final stats = controller.memoryOverview.stats;
|
|
1508
1470
|
final memoriesToShow = _visibleMemories;
|
|
1509
|
-
final
|
|
1510
|
-
final selectedCount =
|
|
1511
|
-
final allVisibleSelected =
|
|
1512
|
-
memoriesToShow.
|
|
1513
|
-
memoriesToShow.every((memory) => selectedMemoryIds.contains(memory.id));
|
|
1471
|
+
final selectedIds = _selectedVisibleMemoryIds.toSet();
|
|
1472
|
+
final selectedCount = selectedIds.length;
|
|
1473
|
+
final allVisibleSelected = memoriesToShow.isNotEmpty &&
|
|
1474
|
+
memoriesToShow.every((m) => selectedIds.contains(m.id));
|
|
1514
1475
|
final showingSearchResults = controller.memoryRecallResults.isNotEmpty;
|
|
1476
|
+
final compact = MediaQuery.sizeOf(context).width < 760;
|
|
1515
1477
|
|
|
1516
1478
|
return ListView(
|
|
1517
1479
|
padding: _pagePadding(context),
|
|
1518
1480
|
children: <Widget>[
|
|
1519
1481
|
_PageTitle(
|
|
1520
1482
|
title: 'Memory',
|
|
1521
|
-
subtitle:
|
|
1522
|
-
'Core memory, thread context, long-term recall, daily logs, and behavior notes.',
|
|
1483
|
+
subtitle: 'Long-term recall, structured facts, and knowledge graph.',
|
|
1523
1484
|
trailing: Wrap(
|
|
1524
1485
|
spacing: 10,
|
|
1525
1486
|
runSpacing: 10,
|
|
@@ -1537,495 +1498,554 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1537
1498
|
],
|
|
1538
1499
|
),
|
|
1539
1500
|
),
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
value: '${controller.memoryOverview.dailyLogCount}',
|
|
1562
|
-
helper: 'Recent captured log files',
|
|
1563
|
-
),
|
|
1564
|
-
),
|
|
1565
|
-
const SizedBox(width: 12),
|
|
1566
|
-
Expanded(
|
|
1567
|
-
child: _OverviewCard(
|
|
1568
|
-
title: 'API Keys',
|
|
1569
|
-
value: '${controller.memoryOverview.apiKeyCount}',
|
|
1570
|
-
helper: 'Masked agent-managed credentials',
|
|
1571
|
-
),
|
|
1572
|
-
),
|
|
1573
|
-
],
|
|
1574
|
-
),
|
|
1575
|
-
const SizedBox(height: 16),
|
|
1576
|
-
Card(
|
|
1577
|
-
child: Padding(
|
|
1578
|
-
padding: const EdgeInsets.all(18),
|
|
1579
|
-
child: Column(
|
|
1580
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1581
|
-
children: <Widget>[
|
|
1582
|
-
const _SectionTitle('Recall Search'),
|
|
1583
|
-
const SizedBox(height: 12),
|
|
1584
|
-
Row(
|
|
1585
|
-
children: <Widget>[
|
|
1586
|
-
Expanded(
|
|
1587
|
-
child: TextField(
|
|
1588
|
-
controller: _searchController,
|
|
1589
|
-
decoration: const InputDecoration(
|
|
1590
|
-
labelText: 'Search memory',
|
|
1501
|
+
|
|
1502
|
+
// --- Stats bar ---
|
|
1503
|
+
_EntranceMotion(
|
|
1504
|
+
child: Card(
|
|
1505
|
+
child: Padding(
|
|
1506
|
+
padding: const EdgeInsets.all(18),
|
|
1507
|
+
child: Row(
|
|
1508
|
+
children: <Widget>[
|
|
1509
|
+
_MemoryConfidenceGauge(
|
|
1510
|
+
confidence: stats.averageConfidence,
|
|
1511
|
+
),
|
|
1512
|
+
const SizedBox(width: 18),
|
|
1513
|
+
Expanded(
|
|
1514
|
+
child: Wrap(
|
|
1515
|
+
spacing: 10,
|
|
1516
|
+
runSpacing: 10,
|
|
1517
|
+
children: <Widget>[
|
|
1518
|
+
_MemoryStatChip(
|
|
1519
|
+
label: '${stats.active}',
|
|
1520
|
+
caption: 'Active',
|
|
1521
|
+
icon: Icons.memory_outlined,
|
|
1591
1522
|
),
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1523
|
+
_MemoryStatChip(
|
|
1524
|
+
label: '${stats.facts}',
|
|
1525
|
+
caption: 'Facts',
|
|
1526
|
+
icon: Icons.fact_check_outlined,
|
|
1527
|
+
),
|
|
1528
|
+
_MemoryStatChip(
|
|
1529
|
+
label: '${stats.entities}',
|
|
1530
|
+
caption: 'Entities',
|
|
1531
|
+
icon: Icons.hub_outlined,
|
|
1532
|
+
),
|
|
1533
|
+
_MemoryStatChip(
|
|
1534
|
+
label: '${stats.knowledgeViews}',
|
|
1535
|
+
caption: 'Reflections',
|
|
1536
|
+
icon: Icons.auto_stories_outlined,
|
|
1537
|
+
),
|
|
1538
|
+
_MemoryStatChip(
|
|
1539
|
+
label: '${stats.ingestionDocuments}',
|
|
1540
|
+
caption: 'Docs',
|
|
1541
|
+
icon: Icons.source_outlined,
|
|
1542
|
+
),
|
|
1543
|
+
_MemoryStatChip(
|
|
1544
|
+
label: stats.averageImportance.toStringAsFixed(1),
|
|
1545
|
+
caption: 'Avg imp.',
|
|
1546
|
+
icon: Icons.priority_high_outlined,
|
|
1547
|
+
),
|
|
1548
|
+
],
|
|
1604
1549
|
),
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1550
|
+
),
|
|
1551
|
+
],
|
|
1552
|
+
),
|
|
1608
1553
|
),
|
|
1609
1554
|
),
|
|
1610
1555
|
),
|
|
1556
|
+
|
|
1611
1557
|
const SizedBox(height: 16),
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
childrenPadding: const EdgeInsets.fromLTRB(18, 0, 18, 18),
|
|
1622
|
-
leading: Icon(Icons.swap_horiz_outlined, color: _textSecondary),
|
|
1623
|
-
title: const _SectionTitle('LLM Memory Transfer'),
|
|
1624
|
-
subtitle: Text(
|
|
1625
|
-
'Export/import memories with another AI in one shot.',
|
|
1626
|
-
style: TextStyle(color: _textSecondary),
|
|
1627
|
-
),
|
|
1628
|
-
children: <Widget>[
|
|
1629
|
-
Text(
|
|
1630
|
-
'Generate a prompt to use in another AI, then paste the response here to import memories.',
|
|
1631
|
-
style: TextStyle(color: _textSecondary),
|
|
1632
|
-
),
|
|
1633
|
-
const SizedBox(height: 12),
|
|
1634
|
-
Wrap(
|
|
1635
|
-
spacing: 10,
|
|
1636
|
-
runSpacing: 10,
|
|
1558
|
+
|
|
1559
|
+
// --- Entity knowledge graph ---
|
|
1560
|
+
if (controller.memoryOverview.entities.isNotEmpty) ...<Widget>[
|
|
1561
|
+
_EntranceMotion(
|
|
1562
|
+
child: Card(
|
|
1563
|
+
child: Padding(
|
|
1564
|
+
padding: const EdgeInsets.all(18),
|
|
1565
|
+
child: Column(
|
|
1566
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1637
1567
|
children: <Widget>[
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
: (
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1568
|
+
Row(
|
|
1569
|
+
children: <Widget>[
|
|
1570
|
+
Expanded(
|
|
1571
|
+
child: const _SectionTitle('Knowledge Graph'),
|
|
1572
|
+
),
|
|
1573
|
+
if (_entityFilter != null)
|
|
1574
|
+
TextButton.icon(
|
|
1575
|
+
onPressed: () =>
|
|
1576
|
+
setState(() => _entityFilter = null),
|
|
1577
|
+
icon: Icon(Icons.close, size: 16),
|
|
1578
|
+
label: Text('Clear filter'),
|
|
1579
|
+
),
|
|
1580
|
+
],
|
|
1581
|
+
),
|
|
1582
|
+
const SizedBox(height: 4),
|
|
1583
|
+
Text(
|
|
1584
|
+
'Tap an entity to filter memories by it.',
|
|
1585
|
+
style: TextStyle(
|
|
1586
|
+
color: _textSecondary,
|
|
1587
|
+
fontSize: 12,
|
|
1645
1588
|
),
|
|
1646
1589
|
),
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1590
|
+
const SizedBox(height: 14),
|
|
1591
|
+
SizedBox(
|
|
1592
|
+
height: compact ? 260 : 320,
|
|
1593
|
+
child: _EntityGraphView(
|
|
1594
|
+
entities: controller.memoryOverview.entities,
|
|
1595
|
+
knowledgeViews:
|
|
1596
|
+
controller.memoryOverview.knowledgeViews,
|
|
1597
|
+
selectedEntity: _entityFilter,
|
|
1598
|
+
onEntityTapped: _onEntityTapped,
|
|
1599
|
+
),
|
|
1653
1600
|
),
|
|
1654
1601
|
],
|
|
1655
1602
|
),
|
|
1656
|
-
|
|
1657
|
-
TextField(
|
|
1658
|
-
controller: _llmPromptController,
|
|
1659
|
-
minLines: 6,
|
|
1660
|
-
maxLines: 10,
|
|
1661
|
-
readOnly: true,
|
|
1662
|
-
decoration: const InputDecoration(
|
|
1663
|
-
labelText: 'Prompt to paste into another AI',
|
|
1664
|
-
),
|
|
1665
|
-
),
|
|
1666
|
-
const SizedBox(height: 12),
|
|
1667
|
-
SwitchListTile.adaptive(
|
|
1668
|
-
contentPadding: EdgeInsets.zero,
|
|
1669
|
-
value: _llmApplyBehaviorNotes,
|
|
1670
|
-
onChanged: _llmImporting
|
|
1671
|
-
? null
|
|
1672
|
-
: (value) {
|
|
1673
|
-
setState(() {
|
|
1674
|
-
_llmApplyBehaviorNotes = value;
|
|
1675
|
-
});
|
|
1676
|
-
},
|
|
1677
|
-
title: Text('Apply behavior notes'),
|
|
1678
|
-
subtitle: Text(
|
|
1679
|
-
'Overwrite assistant behavior notes from the import.',
|
|
1680
|
-
),
|
|
1681
|
-
),
|
|
1682
|
-
SwitchListTile.adaptive(
|
|
1683
|
-
contentPadding: EdgeInsets.zero,
|
|
1684
|
-
value: _llmApplyCoreMemory,
|
|
1685
|
-
onChanged: _llmImporting
|
|
1686
|
-
? null
|
|
1687
|
-
: (value) {
|
|
1688
|
-
setState(() {
|
|
1689
|
-
_llmApplyCoreMemory = value;
|
|
1690
|
-
});
|
|
1691
|
-
},
|
|
1692
|
-
title: Text('Apply core memory'),
|
|
1693
|
-
subtitle: Text(
|
|
1694
|
-
'Update core memory key/value entries from the import.',
|
|
1695
|
-
),
|
|
1696
|
-
),
|
|
1697
|
-
const SizedBox(height: 16),
|
|
1698
|
-
Text(
|
|
1699
|
-
'Paste the response from the other AI below, then import.',
|
|
1700
|
-
style: TextStyle(color: _textSecondary),
|
|
1701
|
-
),
|
|
1702
|
-
const SizedBox(height: 12),
|
|
1703
|
-
TextField(
|
|
1704
|
-
controller: _llmImportController,
|
|
1705
|
-
minLines: 6,
|
|
1706
|
-
maxLines: 12,
|
|
1707
|
-
decoration: const InputDecoration(
|
|
1708
|
-
labelText: 'LLM memory export response',
|
|
1709
|
-
),
|
|
1710
|
-
),
|
|
1711
|
-
const SizedBox(height: 12),
|
|
1712
|
-
FilledButton.icon(
|
|
1713
|
-
onPressed: _llmImporting
|
|
1714
|
-
? null
|
|
1715
|
-
: () => _importLlmMemories(controller),
|
|
1716
|
-
icon: Icon(Icons.file_download_outlined),
|
|
1717
|
-
label: Text(_llmImporting ? 'Importing...' : 'Import'),
|
|
1718
|
-
),
|
|
1719
|
-
],
|
|
1603
|
+
),
|
|
1720
1604
|
),
|
|
1721
1605
|
),
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1606
|
+
const SizedBox(height: 16),
|
|
1607
|
+
],
|
|
1608
|
+
|
|
1609
|
+
// --- Tabbed content ---
|
|
1610
|
+
_EntranceMotion(
|
|
1611
|
+
child: Card(
|
|
1727
1612
|
child: Column(
|
|
1728
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1729
1613
|
children: <Widget>[
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
'No core memory entries yet.',
|
|
1745
|
-
style: TextStyle(color: _textSecondary),
|
|
1746
|
-
)
|
|
1747
|
-
else
|
|
1748
|
-
...controller.memoryOverview.coreEntries.entries.map((entry) {
|
|
1749
|
-
return Container(
|
|
1750
|
-
width: double.infinity,
|
|
1751
|
-
margin: const EdgeInsets.only(bottom: 10),
|
|
1752
|
-
padding: const EdgeInsets.all(12),
|
|
1753
|
-
decoration: BoxDecoration(
|
|
1754
|
-
color: _bgSecondary,
|
|
1755
|
-
borderRadius: BorderRadius.circular(12),
|
|
1756
|
-
border: Border.all(color: _border),
|
|
1757
|
-
),
|
|
1614
|
+
TabBar(
|
|
1615
|
+
controller: _tabController,
|
|
1616
|
+
labelColor: _accentHover,
|
|
1617
|
+
unselectedLabelColor: _textSecondary,
|
|
1618
|
+
indicatorColor: _accent,
|
|
1619
|
+
indicatorSize: TabBarIndicatorSize.label,
|
|
1620
|
+
dividerColor: _border,
|
|
1621
|
+
labelStyle: const TextStyle(
|
|
1622
|
+
fontWeight: FontWeight.w700,
|
|
1623
|
+
fontSize: 13,
|
|
1624
|
+
letterSpacing: 0.3,
|
|
1625
|
+
),
|
|
1626
|
+
tabs: <Widget>[
|
|
1627
|
+
Tab(
|
|
1758
1628
|
child: Row(
|
|
1759
|
-
|
|
1629
|
+
mainAxisSize: MainAxisSize.min,
|
|
1760
1630
|
children: <Widget>[
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
children: <Widget>[
|
|
1765
|
-
Text(
|
|
1766
|
-
entry.key,
|
|
1767
|
-
style: TextStyle(fontWeight: FontWeight.w700),
|
|
1768
|
-
),
|
|
1769
|
-
const SizedBox(height: 6),
|
|
1770
|
-
Text(entry.value.toString()),
|
|
1771
|
-
],
|
|
1772
|
-
),
|
|
1773
|
-
),
|
|
1774
|
-
IconButton(
|
|
1775
|
-
onPressed: () => _openCoreMemoryEditor(
|
|
1776
|
-
context,
|
|
1777
|
-
controller,
|
|
1778
|
-
keyValue: entry,
|
|
1779
|
-
),
|
|
1780
|
-
icon: Icon(Icons.edit_outlined),
|
|
1781
|
-
),
|
|
1782
|
-
IconButton(
|
|
1783
|
-
onPressed: () => _confirmDelete(
|
|
1784
|
-
context,
|
|
1785
|
-
title: 'Delete core memory entry?',
|
|
1786
|
-
message:
|
|
1787
|
-
'Remove "${entry.key}" from core memory.',
|
|
1788
|
-
onConfirm: () =>
|
|
1789
|
-
controller.deleteCoreMemory(entry.key),
|
|
1790
|
-
),
|
|
1791
|
-
icon: Icon(Icons.delete_outline),
|
|
1792
|
-
),
|
|
1631
|
+
Icon(Icons.psychology_outlined, size: 16),
|
|
1632
|
+
const SizedBox(width: 6),
|
|
1633
|
+
Text('Memories'),
|
|
1793
1634
|
],
|
|
1794
1635
|
),
|
|
1795
|
-
)
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
padding: const EdgeInsets.all(18),
|
|
1805
|
-
child: Column(
|
|
1806
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1807
|
-
children: <Widget>[
|
|
1808
|
-
const _SectionTitle('Memories'),
|
|
1809
|
-
const SizedBox(height: 6),
|
|
1810
|
-
Text(
|
|
1811
|
-
showingSearchResults
|
|
1812
|
-
? 'Showing search results. Select memories to archive or delete them together.'
|
|
1813
|
-
: 'Select one or more memories to archive or delete them together.',
|
|
1814
|
-
style: TextStyle(color: _textSecondary),
|
|
1815
|
-
),
|
|
1816
|
-
const SizedBox(height: 10),
|
|
1817
|
-
if (memoriesToShow.isNotEmpty)
|
|
1818
|
-
Wrap(
|
|
1819
|
-
spacing: 10,
|
|
1820
|
-
runSpacing: 10,
|
|
1821
|
-
crossAxisAlignment: WrapCrossAlignment.center,
|
|
1822
|
-
children: <Widget>[
|
|
1823
|
-
OutlinedButton.icon(
|
|
1824
|
-
onPressed: allVisibleSelected || _bulkActionInFlight
|
|
1825
|
-
? null
|
|
1826
|
-
: () => _selectAllVisibleMemories(memoriesToShow),
|
|
1827
|
-
icon: Icon(Icons.done_all_outlined),
|
|
1828
|
-
label: Text(
|
|
1829
|
-
allVisibleSelected ? 'All Selected' : 'Select All',
|
|
1830
|
-
),
|
|
1831
|
-
),
|
|
1832
|
-
OutlinedButton.icon(
|
|
1833
|
-
onPressed: selectedCount == 0 || _bulkActionInFlight
|
|
1834
|
-
? null
|
|
1835
|
-
: _clearMemorySelection,
|
|
1836
|
-
icon: Icon(Icons.deselect_outlined),
|
|
1837
|
-
label: Text('Clear Selection'),
|
|
1636
|
+
),
|
|
1637
|
+
Tab(
|
|
1638
|
+
child: Row(
|
|
1639
|
+
mainAxisSize: MainAxisSize.min,
|
|
1640
|
+
children: <Widget>[
|
|
1641
|
+
Icon(Icons.push_pin_outlined, size: 16),
|
|
1642
|
+
const SizedBox(width: 6),
|
|
1643
|
+
Text('Core'),
|
|
1644
|
+
],
|
|
1838
1645
|
),
|
|
1839
|
-
if (selectedCount > 0)
|
|
1840
|
-
FilledButton.icon(
|
|
1841
|
-
onPressed: _bulkActionInFlight
|
|
1842
|
-
? null
|
|
1843
|
-
: () => _runBulkMemoryAction(
|
|
1844
|
-
title: 'Archive selected memories?',
|
|
1845
|
-
message:
|
|
1846
|
-
'Archive $selectedCount selected ${selectedCount == 1 ? 'memory' : 'memories'}? Archived memories are removed from the main list.',
|
|
1847
|
-
confirmLabel: 'Archive',
|
|
1848
|
-
onConfirm: controller.archiveMemories,
|
|
1849
|
-
),
|
|
1850
|
-
icon: Icon(Icons.archive_outlined),
|
|
1851
|
-
label: Text('Archive ($selectedCount)'),
|
|
1852
|
-
),
|
|
1853
|
-
if (selectedCount > 0)
|
|
1854
|
-
OutlinedButton.icon(
|
|
1855
|
-
onPressed: _bulkActionInFlight
|
|
1856
|
-
? null
|
|
1857
|
-
: () => _runBulkMemoryAction(
|
|
1858
|
-
title: 'Delete selected memories?',
|
|
1859
|
-
message:
|
|
1860
|
-
'Delete $selectedCount selected ${selectedCount == 1 ? 'memory' : 'memories'} permanently?',
|
|
1861
|
-
confirmLabel: 'Delete',
|
|
1862
|
-
onConfirm: controller.deleteMemories,
|
|
1863
|
-
),
|
|
1864
|
-
icon: Icon(Icons.delete_sweep_outlined),
|
|
1865
|
-
label: Text('Delete ($selectedCount)'),
|
|
1866
|
-
),
|
|
1867
|
-
],
|
|
1868
|
-
),
|
|
1869
|
-
if (selectedCount > 0) ...<Widget>[
|
|
1870
|
-
const SizedBox(height: 10),
|
|
1871
|
-
Text(
|
|
1872
|
-
'$selectedCount selected',
|
|
1873
|
-
style: TextStyle(
|
|
1874
|
-
color: _textSecondary,
|
|
1875
|
-
fontWeight: FontWeight.w600,
|
|
1876
1646
|
),
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
else
|
|
1886
|
-
...memoriesToShow.map((memory) {
|
|
1887
|
-
final isSelected = selectedMemoryIds.contains(memory.id);
|
|
1888
|
-
return Container(
|
|
1889
|
-
width: double.infinity,
|
|
1890
|
-
margin: const EdgeInsets.only(bottom: 10),
|
|
1891
|
-
decoration: BoxDecoration(
|
|
1892
|
-
color: isSelected ? _accentMuted : _bgSecondary,
|
|
1893
|
-
borderRadius: BorderRadius.circular(12),
|
|
1894
|
-
border: Border.all(
|
|
1895
|
-
color: isSelected ? _accent : _border,
|
|
1896
|
-
),
|
|
1647
|
+
Tab(
|
|
1648
|
+
child: Row(
|
|
1649
|
+
mainAxisSize: MainAxisSize.min,
|
|
1650
|
+
children: <Widget>[
|
|
1651
|
+
Icon(Icons.swap_horiz_outlined, size: 16),
|
|
1652
|
+
const SizedBox(width: 6),
|
|
1653
|
+
Text('Transfer'),
|
|
1654
|
+
],
|
|
1897
1655
|
),
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1656
|
+
),
|
|
1657
|
+
],
|
|
1658
|
+
),
|
|
1659
|
+
AnimatedBuilder(
|
|
1660
|
+
animation: _tabController,
|
|
1661
|
+
builder: (context, _) {
|
|
1662
|
+
return IndexedStack(
|
|
1663
|
+
index: _tabController.index,
|
|
1664
|
+
children: <Widget>[
|
|
1665
|
+
// --- Memories tab ---
|
|
1666
|
+
Padding(
|
|
1667
|
+
padding: const EdgeInsets.all(18),
|
|
1668
|
+
child: Column(
|
|
1669
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1670
|
+
children: <Widget>[
|
|
1671
|
+
Row(
|
|
1672
|
+
children: <Widget>[
|
|
1673
|
+
Expanded(
|
|
1674
|
+
child: TextField(
|
|
1675
|
+
controller: _searchController,
|
|
1676
|
+
decoration: const InputDecoration(
|
|
1677
|
+
labelText: 'Search memory',
|
|
1678
|
+
prefixIcon: Icon(
|
|
1679
|
+
Icons.search,
|
|
1680
|
+
size: 18,
|
|
1681
|
+
),
|
|
1682
|
+
),
|
|
1683
|
+
onSubmitted: (_) =>
|
|
1684
|
+
_runMemorySearch(controller),
|
|
1685
|
+
),
|
|
1914
1686
|
),
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1687
|
+
const SizedBox(width: 10),
|
|
1688
|
+
FilledButton(
|
|
1689
|
+
onPressed: () =>
|
|
1690
|
+
_runMemorySearch(controller),
|
|
1691
|
+
child: Text('Search'),
|
|
1692
|
+
),
|
|
1693
|
+
if (showingSearchResults ||
|
|
1694
|
+
_entityFilter != null) ...<Widget>[
|
|
1695
|
+
const SizedBox(width: 10),
|
|
1696
|
+
OutlinedButton(
|
|
1697
|
+
onPressed: () =>
|
|
1698
|
+
_resetMemorySearch(controller),
|
|
1699
|
+
child: Text('Reset'),
|
|
1700
|
+
),
|
|
1701
|
+
],
|
|
1702
|
+
],
|
|
1703
|
+
),
|
|
1704
|
+
if (_entityFilter != null) ...<Widget>[
|
|
1705
|
+
const SizedBox(height: 10),
|
|
1706
|
+
Wrap(
|
|
1707
|
+
spacing: 8,
|
|
1708
|
+
children: <Widget>[
|
|
1709
|
+
_MetaPill(
|
|
1710
|
+
label: 'Entity: $_entityFilter',
|
|
1711
|
+
icon: Icons.filter_alt_outlined,
|
|
1712
|
+
color: _accent,
|
|
1713
|
+
),
|
|
1714
|
+
],
|
|
1715
|
+
),
|
|
1716
|
+
],
|
|
1717
|
+
if (memoriesToShow.isNotEmpty) ...<Widget>[
|
|
1718
|
+
const SizedBox(height: 12),
|
|
1719
|
+
Wrap(
|
|
1720
|
+
spacing: 8,
|
|
1721
|
+
runSpacing: 8,
|
|
1722
|
+
crossAxisAlignment:
|
|
1723
|
+
WrapCrossAlignment.center,
|
|
1724
|
+
children: <Widget>[
|
|
1725
|
+
OutlinedButton.icon(
|
|
1726
|
+
onPressed: allVisibleSelected ||
|
|
1727
|
+
_bulkActionInFlight
|
|
1728
|
+
? null
|
|
1729
|
+
: () => _selectAllVisibleMemories(
|
|
1730
|
+
memoriesToShow),
|
|
1731
|
+
icon: Icon(
|
|
1732
|
+
Icons.done_all_outlined,
|
|
1733
|
+
size: 16,
|
|
1961
1734
|
),
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1735
|
+
label: Text(
|
|
1736
|
+
allVisibleSelected
|
|
1737
|
+
? 'All Selected'
|
|
1738
|
+
: 'Select All',
|
|
1739
|
+
),
|
|
1740
|
+
),
|
|
1741
|
+
if (selectedCount > 0) ...<Widget>[
|
|
1742
|
+
OutlinedButton.icon(
|
|
1743
|
+
onPressed: _bulkActionInFlight
|
|
1744
|
+
? null
|
|
1745
|
+
: _clearMemorySelection,
|
|
1746
|
+
icon: Icon(
|
|
1747
|
+
Icons.deselect_outlined,
|
|
1748
|
+
size: 16,
|
|
1749
|
+
),
|
|
1750
|
+
label: Text('Clear'),
|
|
1751
|
+
),
|
|
1752
|
+
FilledButton.icon(
|
|
1753
|
+
onPressed: _bulkActionInFlight
|
|
1754
|
+
? null
|
|
1755
|
+
: () => _runBulkMemoryAction(
|
|
1756
|
+
title:
|
|
1757
|
+
'Archive selected memories?',
|
|
1758
|
+
message:
|
|
1759
|
+
'Archive $selectedCount ${selectedCount == 1 ? 'memory' : 'memories'}?',
|
|
1760
|
+
confirmLabel: 'Archive',
|
|
1761
|
+
onConfirm:
|
|
1762
|
+
controller.archiveMemories,
|
|
1763
|
+
),
|
|
1764
|
+
icon: Icon(
|
|
1765
|
+
Icons.archive_outlined,
|
|
1766
|
+
size: 16,
|
|
1767
|
+
),
|
|
1768
|
+
label: Text(
|
|
1769
|
+
'Archive ($selectedCount)',
|
|
1770
|
+
),
|
|
1771
|
+
),
|
|
1772
|
+
OutlinedButton.icon(
|
|
1773
|
+
onPressed: _bulkActionInFlight
|
|
1774
|
+
? null
|
|
1775
|
+
: () => _runBulkMemoryAction(
|
|
1776
|
+
title:
|
|
1777
|
+
'Delete selected memories?',
|
|
1778
|
+
message:
|
|
1779
|
+
'Delete $selectedCount ${selectedCount == 1 ? 'memory' : 'memories'} permanently?',
|
|
1780
|
+
confirmLabel: 'Delete',
|
|
1781
|
+
onConfirm:
|
|
1782
|
+
controller.deleteMemories,
|
|
1783
|
+
),
|
|
1784
|
+
icon: Icon(
|
|
1785
|
+
Icons.delete_sweep_outlined,
|
|
1786
|
+
size: 16,
|
|
1787
|
+
),
|
|
1788
|
+
label: Text(
|
|
1789
|
+
'Delete ($selectedCount)',
|
|
1970
1790
|
),
|
|
1971
1791
|
),
|
|
1972
1792
|
],
|
|
1973
|
-
|
|
1793
|
+
],
|
|
1974
1794
|
),
|
|
1975
1795
|
],
|
|
1976
|
-
|
|
1796
|
+
const SizedBox(height: 12),
|
|
1797
|
+
if (memoriesToShow.isEmpty)
|
|
1798
|
+
Text(
|
|
1799
|
+
_entityFilter != null
|
|
1800
|
+
? 'No memories linked to "$_entityFilter".'
|
|
1801
|
+
: 'No memory entries found.',
|
|
1802
|
+
style: TextStyle(color: _textSecondary),
|
|
1803
|
+
)
|
|
1804
|
+
else
|
|
1805
|
+
...memoriesToShow.map((memory) {
|
|
1806
|
+
final isSelected =
|
|
1807
|
+
selectedIds.contains(memory.id);
|
|
1808
|
+
return _MemoryRow(
|
|
1809
|
+
memory: memory,
|
|
1810
|
+
isSelected: isSelected,
|
|
1811
|
+
onTap: () => _toggleMemorySelection(
|
|
1812
|
+
memory.id,
|
|
1813
|
+
!isSelected,
|
|
1814
|
+
),
|
|
1815
|
+
onCheck: (value) =>
|
|
1816
|
+
_toggleMemorySelection(
|
|
1817
|
+
memory.id,
|
|
1818
|
+
value ?? false,
|
|
1819
|
+
),
|
|
1820
|
+
onDelete: _bulkActionInFlight
|
|
1821
|
+
? null
|
|
1822
|
+
: () => _confirmDelete(
|
|
1823
|
+
context,
|
|
1824
|
+
title: 'Delete memory?',
|
|
1825
|
+
message:
|
|
1826
|
+
'This memory will be removed permanently.',
|
|
1827
|
+
onConfirm: () =>
|
|
1828
|
+
_deleteSingleMemory(
|
|
1829
|
+
controller,
|
|
1830
|
+
memory.id,
|
|
1831
|
+
),
|
|
1832
|
+
),
|
|
1833
|
+
);
|
|
1834
|
+
}),
|
|
1835
|
+
],
|
|
1977
1836
|
),
|
|
1978
1837
|
),
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
1838
|
+
|
|
1839
|
+
// --- Core tab ---
|
|
1840
|
+
Padding(
|
|
1841
|
+
padding: const EdgeInsets.all(18),
|
|
1842
|
+
child: Column(
|
|
1843
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1844
|
+
children: <Widget>[
|
|
1845
|
+
Row(
|
|
1846
|
+
children: <Widget>[
|
|
1847
|
+
Expanded(
|
|
1848
|
+
child: Text(
|
|
1849
|
+
'Key-value pairs that persist across conversations.',
|
|
1850
|
+
style: TextStyle(
|
|
1851
|
+
color: _textSecondary,
|
|
1852
|
+
),
|
|
1853
|
+
),
|
|
1854
|
+
),
|
|
1855
|
+
TextButton.icon(
|
|
1856
|
+
onPressed: () => _openCoreMemoryEditor(
|
|
1857
|
+
context,
|
|
1858
|
+
controller,
|
|
1859
|
+
),
|
|
1860
|
+
icon: Icon(Icons.add),
|
|
1861
|
+
label: Text('Add Entry'),
|
|
1862
|
+
),
|
|
1863
|
+
],
|
|
1864
|
+
),
|
|
1865
|
+
const SizedBox(height: 10),
|
|
1866
|
+
if (controller
|
|
1867
|
+
.memoryOverview.coreEntries.isEmpty)
|
|
1868
|
+
Text(
|
|
1869
|
+
'No core memory entries yet.',
|
|
1870
|
+
style: TextStyle(color: _textSecondary),
|
|
1871
|
+
)
|
|
1872
|
+
else
|
|
1873
|
+
...controller.memoryOverview.coreEntries
|
|
1874
|
+
.entries
|
|
1875
|
+
.map((entry) {
|
|
1876
|
+
return Container(
|
|
1877
|
+
width: double.infinity,
|
|
1878
|
+
margin: const EdgeInsets.only(bottom: 10),
|
|
1879
|
+
padding: const EdgeInsets.all(12),
|
|
1880
|
+
decoration: BoxDecoration(
|
|
1881
|
+
color: _bgSecondary,
|
|
1882
|
+
borderRadius:
|
|
1883
|
+
BorderRadius.circular(12),
|
|
1884
|
+
border: Border.all(color: _border),
|
|
1885
|
+
),
|
|
1886
|
+
child: Row(
|
|
1887
|
+
crossAxisAlignment:
|
|
1888
|
+
CrossAxisAlignment.start,
|
|
1889
|
+
children: <Widget>[
|
|
1890
|
+
Expanded(
|
|
1891
|
+
child: Column(
|
|
1892
|
+
crossAxisAlignment:
|
|
1893
|
+
CrossAxisAlignment.start,
|
|
1894
|
+
children: <Widget>[
|
|
1895
|
+
Text(
|
|
1896
|
+
entry.key,
|
|
1897
|
+
style: TextStyle(
|
|
1898
|
+
fontWeight: FontWeight.w700,
|
|
1899
|
+
),
|
|
1900
|
+
),
|
|
1901
|
+
const SizedBox(height: 6),
|
|
1902
|
+
Text(
|
|
1903
|
+
entry.value.toString(),
|
|
1904
|
+
),
|
|
1905
|
+
],
|
|
1906
|
+
),
|
|
1907
|
+
),
|
|
1908
|
+
IconButton(
|
|
1909
|
+
onPressed: () =>
|
|
1910
|
+
_openCoreMemoryEditor(
|
|
1911
|
+
context,
|
|
1912
|
+
controller,
|
|
1913
|
+
keyValue: entry,
|
|
1914
|
+
),
|
|
1915
|
+
icon: Icon(Icons.edit_outlined),
|
|
1916
|
+
),
|
|
1917
|
+
IconButton(
|
|
1918
|
+
onPressed: () => _confirmDelete(
|
|
1919
|
+
context,
|
|
1920
|
+
title:
|
|
1921
|
+
'Delete core memory entry?',
|
|
1922
|
+
message:
|
|
1923
|
+
'Remove "${entry.key}" from core memory.',
|
|
1924
|
+
onConfirm: () => controller
|
|
1925
|
+
.deleteCoreMemory(
|
|
1926
|
+
entry.key,
|
|
1927
|
+
),
|
|
1928
|
+
),
|
|
1929
|
+
icon: Icon(Icons.delete_outline),
|
|
1930
|
+
),
|
|
1931
|
+
],
|
|
1932
|
+
),
|
|
1933
|
+
);
|
|
1934
|
+
}),
|
|
1935
|
+
],
|
|
1936
|
+
),
|
|
2011
1937
|
),
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
1938
|
+
|
|
1939
|
+
// --- Transfer tab ---
|
|
1940
|
+
Padding(
|
|
1941
|
+
padding: const EdgeInsets.all(18),
|
|
1942
|
+
child: Column(
|
|
1943
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1944
|
+
children: <Widget>[
|
|
1945
|
+
Text(
|
|
1946
|
+
'Generate a prompt for another AI, paste the response here to import memories.',
|
|
1947
|
+
style: TextStyle(color: _textSecondary),
|
|
1948
|
+
),
|
|
1949
|
+
const SizedBox(height: 12),
|
|
1950
|
+
Wrap(
|
|
1951
|
+
spacing: 10,
|
|
1952
|
+
runSpacing: 10,
|
|
1953
|
+
children: <Widget>[
|
|
1954
|
+
FilledButton.icon(
|
|
1955
|
+
onPressed: _llmPromptLoading
|
|
1956
|
+
? null
|
|
1957
|
+
: () =>
|
|
1958
|
+
_loadLlmPrompt(controller),
|
|
1959
|
+
icon: Icon(
|
|
1960
|
+
Icons.auto_awesome_outlined,
|
|
1961
|
+
),
|
|
1962
|
+
label: Text(
|
|
1963
|
+
_llmPromptLoading
|
|
1964
|
+
? 'Generating...'
|
|
1965
|
+
: 'Generate Prompt',
|
|
1966
|
+
),
|
|
1967
|
+
),
|
|
1968
|
+
OutlinedButton.icon(
|
|
1969
|
+
onPressed:
|
|
1970
|
+
_llmPromptController.text
|
|
1971
|
+
.trim()
|
|
1972
|
+
.isEmpty
|
|
1973
|
+
? null
|
|
1974
|
+
: _copyLlmPrompt,
|
|
1975
|
+
icon: Icon(Icons.copy_all_outlined),
|
|
1976
|
+
label: Text('Copy Prompt'),
|
|
1977
|
+
),
|
|
1978
|
+
],
|
|
1979
|
+
),
|
|
1980
|
+
const SizedBox(height: 12),
|
|
1981
|
+
TextField(
|
|
1982
|
+
controller: _llmPromptController,
|
|
1983
|
+
minLines: 4,
|
|
1984
|
+
maxLines: 8,
|
|
1985
|
+
readOnly: true,
|
|
1986
|
+
decoration: const InputDecoration(
|
|
1987
|
+
labelText:
|
|
1988
|
+
'Prompt to paste into another AI',
|
|
1989
|
+
),
|
|
1990
|
+
),
|
|
1991
|
+
const SizedBox(height: 16),
|
|
1992
|
+
SwitchListTile.adaptive(
|
|
1993
|
+
contentPadding: EdgeInsets.zero,
|
|
1994
|
+
value: _llmApplyBehaviorNotes,
|
|
1995
|
+
onChanged: _llmImporting
|
|
1996
|
+
? null
|
|
1997
|
+
: (value) => setState(
|
|
1998
|
+
() =>
|
|
1999
|
+
_llmApplyBehaviorNotes = value,
|
|
2000
|
+
),
|
|
2001
|
+
title: Text('Apply behavior notes'),
|
|
2002
|
+
subtitle: Text(
|
|
2003
|
+
'Overwrite behavior notes from the import.',
|
|
2004
|
+
),
|
|
2005
|
+
),
|
|
2006
|
+
SwitchListTile.adaptive(
|
|
2007
|
+
contentPadding: EdgeInsets.zero,
|
|
2008
|
+
value: _llmApplyCoreMemory,
|
|
2009
|
+
onChanged: _llmImporting
|
|
2010
|
+
? null
|
|
2011
|
+
: (value) => setState(
|
|
2012
|
+
() =>
|
|
2013
|
+
_llmApplyCoreMemory = value,
|
|
2014
|
+
),
|
|
2015
|
+
title: Text('Apply core memory'),
|
|
2016
|
+
subtitle: Text(
|
|
2017
|
+
'Update core memory entries from the import.',
|
|
2018
|
+
),
|
|
2019
|
+
),
|
|
2020
|
+
const SizedBox(height: 12),
|
|
2021
|
+
TextField(
|
|
2022
|
+
controller: _llmImportController,
|
|
2023
|
+
minLines: 4,
|
|
2024
|
+
maxLines: 10,
|
|
2025
|
+
decoration: const InputDecoration(
|
|
2026
|
+
labelText: 'LLM memory export response',
|
|
2027
|
+
),
|
|
2028
|
+
),
|
|
2029
|
+
const SizedBox(height: 12),
|
|
2030
|
+
FilledButton.icon(
|
|
2031
|
+
onPressed: _llmImporting
|
|
2032
|
+
? null
|
|
2033
|
+
: () =>
|
|
2034
|
+
_importLlmMemories(controller),
|
|
2035
|
+
icon: Icon(Icons.file_download_outlined),
|
|
2036
|
+
label: Text(
|
|
2037
|
+
_llmImporting
|
|
2038
|
+
? 'Importing...'
|
|
2039
|
+
: 'Import',
|
|
2040
|
+
),
|
|
2041
|
+
),
|
|
2042
|
+
],
|
|
2043
|
+
),
|
|
2025
2044
|
),
|
|
2026
|
-
|
|
2027
|
-
)
|
|
2028
|
-
|
|
2045
|
+
],
|
|
2046
|
+
);
|
|
2047
|
+
},
|
|
2048
|
+
),
|
|
2029
2049
|
],
|
|
2030
2050
|
),
|
|
2031
2051
|
),
|
|
@@ -2075,9 +2095,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2075
2095
|
],
|
|
2076
2096
|
decoration: const InputDecoration(labelText: 'Category'),
|
|
2077
2097
|
onChanged: (value) {
|
|
2078
|
-
if (value != null)
|
|
2079
|
-
category = value;
|
|
2080
|
-
}
|
|
2098
|
+
if (value != null) category = value;
|
|
2081
2099
|
},
|
|
2082
2100
|
),
|
|
2083
2101
|
const SizedBox(height: 12),
|
|
@@ -2108,9 +2126,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2108
2126
|
importance:
|
|
2109
2127
|
int.tryParse(importanceController.text.trim()) ?? 5,
|
|
2110
2128
|
);
|
|
2111
|
-
if (context.mounted)
|
|
2112
|
-
Navigator.of(context).pop();
|
|
2113
|
-
}
|
|
2129
|
+
if (context.mounted) Navigator.of(context).pop();
|
|
2114
2130
|
},
|
|
2115
2131
|
child: Text('Save'),
|
|
2116
2132
|
),
|
|
@@ -2154,9 +2170,66 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2154
2170
|
await controller.updateAssistantBehaviorNotes(
|
|
2155
2171
|
contentController.text,
|
|
2156
2172
|
);
|
|
2157
|
-
if (context.mounted)
|
|
2158
|
-
|
|
2159
|
-
|
|
2173
|
+
if (context.mounted) Navigator.of(context).pop();
|
|
2174
|
+
},
|
|
2175
|
+
child: Text('Save'),
|
|
2176
|
+
),
|
|
2177
|
+
],
|
|
2178
|
+
);
|
|
2179
|
+
},
|
|
2180
|
+
);
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
Future<void> _openCoreMemoryEditor(
|
|
2184
|
+
BuildContext context,
|
|
2185
|
+
NeoAgentController controller, {
|
|
2186
|
+
MapEntry<String, dynamic>? keyValue,
|
|
2187
|
+
}) async {
|
|
2188
|
+
final keyController = TextEditingController(text: keyValue?.key ?? '');
|
|
2189
|
+
final valueController = TextEditingController(
|
|
2190
|
+
text: keyValue?.value?.toString() ?? '',
|
|
2191
|
+
);
|
|
2192
|
+
await showDialog<void>(
|
|
2193
|
+
context: context,
|
|
2194
|
+
builder: (context) {
|
|
2195
|
+
return AlertDialog(
|
|
2196
|
+
backgroundColor: _bgCard,
|
|
2197
|
+
title: Text(
|
|
2198
|
+
keyValue == null
|
|
2199
|
+
? 'Add Core Memory Entry'
|
|
2200
|
+
: 'Edit Core Memory Entry',
|
|
2201
|
+
),
|
|
2202
|
+
content: SizedBox(
|
|
2203
|
+
width: 620,
|
|
2204
|
+
child: Column(
|
|
2205
|
+
mainAxisSize: MainAxisSize.min,
|
|
2206
|
+
children: <Widget>[
|
|
2207
|
+
TextField(
|
|
2208
|
+
controller: keyController,
|
|
2209
|
+
decoration: const InputDecoration(labelText: 'Key'),
|
|
2210
|
+
),
|
|
2211
|
+
const SizedBox(height: 12),
|
|
2212
|
+
TextField(
|
|
2213
|
+
controller: valueController,
|
|
2214
|
+
minLines: 3,
|
|
2215
|
+
maxLines: 8,
|
|
2216
|
+
decoration: const InputDecoration(labelText: 'Value'),
|
|
2217
|
+
),
|
|
2218
|
+
],
|
|
2219
|
+
),
|
|
2220
|
+
),
|
|
2221
|
+
actions: <Widget>[
|
|
2222
|
+
TextButton(
|
|
2223
|
+
onPressed: () => Navigator.of(context).pop(),
|
|
2224
|
+
child: Text('Cancel'),
|
|
2225
|
+
),
|
|
2226
|
+
FilledButton(
|
|
2227
|
+
onPressed: () async {
|
|
2228
|
+
await controller.updateCoreMemory(
|
|
2229
|
+
keyController.text.trim(),
|
|
2230
|
+
valueController.text.trim(),
|
|
2231
|
+
);
|
|
2232
|
+
if (context.mounted) Navigator.of(context).pop();
|
|
2160
2233
|
},
|
|
2161
2234
|
child: Text('Save'),
|
|
2162
2235
|
),
|
|
@@ -2165,69 +2238,682 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2165
2238
|
},
|
|
2166
2239
|
);
|
|
2167
2240
|
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
class _MemoryStatChip extends StatelessWidget {
|
|
2244
|
+
const _MemoryStatChip({
|
|
2245
|
+
required this.label,
|
|
2246
|
+
required this.caption,
|
|
2247
|
+
required this.icon,
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
final String label;
|
|
2251
|
+
final String caption;
|
|
2252
|
+
final IconData icon;
|
|
2253
|
+
|
|
2254
|
+
@override
|
|
2255
|
+
Widget build(BuildContext context) {
|
|
2256
|
+
return _GlassSurface(
|
|
2257
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
2258
|
+
borderRadius: BorderRadius.circular(14),
|
|
2259
|
+
blurSigma: 10,
|
|
2260
|
+
fillColor: _bgSecondary.withValues(alpha: 0.7),
|
|
2261
|
+
borderColor: _borderLight,
|
|
2262
|
+
child: Row(
|
|
2263
|
+
mainAxisSize: MainAxisSize.min,
|
|
2264
|
+
children: <Widget>[
|
|
2265
|
+
Icon(icon, size: 15, color: _accentAlt),
|
|
2266
|
+
const SizedBox(width: 8),
|
|
2267
|
+
Column(
|
|
2268
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2269
|
+
mainAxisSize: MainAxisSize.min,
|
|
2270
|
+
children: <Widget>[
|
|
2271
|
+
Text(
|
|
2272
|
+
label,
|
|
2273
|
+
style: TextStyle(
|
|
2274
|
+
fontWeight: FontWeight.w800,
|
|
2275
|
+
fontSize: 15,
|
|
2276
|
+
letterSpacing: -0.3,
|
|
2277
|
+
color: _textPrimary,
|
|
2278
|
+
),
|
|
2279
|
+
),
|
|
2280
|
+
Text(
|
|
2281
|
+
caption,
|
|
2282
|
+
style: TextStyle(
|
|
2283
|
+
fontSize: 10,
|
|
2284
|
+
fontWeight: FontWeight.w600,
|
|
2285
|
+
color: _textSecondary,
|
|
2286
|
+
letterSpacing: 0.2,
|
|
2287
|
+
),
|
|
2288
|
+
),
|
|
2289
|
+
],
|
|
2290
|
+
),
|
|
2291
|
+
],
|
|
2292
|
+
),
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
class _MemoryConfidenceGauge extends StatefulWidget {
|
|
2298
|
+
const _MemoryConfidenceGauge({required this.confidence});
|
|
2299
|
+
|
|
2300
|
+
final double confidence;
|
|
2301
|
+
|
|
2302
|
+
@override
|
|
2303
|
+
State<_MemoryConfidenceGauge> createState() => _MemoryConfidenceGaugeState();
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
class _MemoryConfidenceGaugeState extends State<_MemoryConfidenceGauge>
|
|
2307
|
+
with SingleTickerProviderStateMixin {
|
|
2308
|
+
late final AnimationController _controller;
|
|
2309
|
+
late Animation<double> _progress;
|
|
2310
|
+
|
|
2311
|
+
@override
|
|
2312
|
+
void initState() {
|
|
2313
|
+
super.initState();
|
|
2314
|
+
_controller = AnimationController(
|
|
2315
|
+
vsync: this,
|
|
2316
|
+
duration: const Duration(milliseconds: 1200),
|
|
2317
|
+
);
|
|
2318
|
+
_progress = Tween<double>(begin: 0, end: widget.confidence).animate(
|
|
2319
|
+
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
|
|
2320
|
+
);
|
|
2321
|
+
_controller.forward();
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
@override
|
|
2325
|
+
void didUpdateWidget(_MemoryConfidenceGauge oldWidget) {
|
|
2326
|
+
super.didUpdateWidget(oldWidget);
|
|
2327
|
+
if (oldWidget.confidence != widget.confidence) {
|
|
2328
|
+
_progress = Tween<double>(
|
|
2329
|
+
begin: _progress.value,
|
|
2330
|
+
end: widget.confidence,
|
|
2331
|
+
).animate(
|
|
2332
|
+
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic),
|
|
2333
|
+
);
|
|
2334
|
+
_controller
|
|
2335
|
+
..reset()
|
|
2336
|
+
..forward();
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
@override
|
|
2341
|
+
void dispose() {
|
|
2342
|
+
_controller.dispose();
|
|
2343
|
+
super.dispose();
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
@override
|
|
2347
|
+
Widget build(BuildContext context) {
|
|
2348
|
+
return AnimatedBuilder(
|
|
2349
|
+
animation: _progress,
|
|
2350
|
+
builder: (context, child) {
|
|
2351
|
+
final value = _progress.value;
|
|
2352
|
+
final percent = (value * 100).round();
|
|
2353
|
+
return SizedBox(
|
|
2354
|
+
width: 64,
|
|
2355
|
+
height: 64,
|
|
2356
|
+
child: CustomPaint(
|
|
2357
|
+
painter: _RadialGaugePainter(
|
|
2358
|
+
progress: value,
|
|
2359
|
+
trackColor: _border,
|
|
2360
|
+
fillColor: _accent,
|
|
2361
|
+
glowColor: _accentHover,
|
|
2362
|
+
),
|
|
2363
|
+
child: Center(
|
|
2364
|
+
child: Text(
|
|
2365
|
+
'$percent%',
|
|
2366
|
+
style: TextStyle(
|
|
2367
|
+
fontSize: 13,
|
|
2368
|
+
fontWeight: FontWeight.w800,
|
|
2369
|
+
color: _textPrimary,
|
|
2370
|
+
letterSpacing: -0.5,
|
|
2371
|
+
),
|
|
2372
|
+
),
|
|
2373
|
+
),
|
|
2374
|
+
),
|
|
2375
|
+
);
|
|
2376
|
+
},
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2168
2380
|
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2381
|
+
class _RadialGaugePainter extends CustomPainter {
|
|
2382
|
+
const _RadialGaugePainter({
|
|
2383
|
+
required this.progress,
|
|
2384
|
+
required this.trackColor,
|
|
2385
|
+
required this.fillColor,
|
|
2386
|
+
required this.glowColor,
|
|
2387
|
+
});
|
|
2388
|
+
|
|
2389
|
+
final double progress;
|
|
2390
|
+
final Color trackColor;
|
|
2391
|
+
final Color fillColor;
|
|
2392
|
+
final Color glowColor;
|
|
2393
|
+
|
|
2394
|
+
@override
|
|
2395
|
+
void paint(Canvas canvas, Size size) {
|
|
2396
|
+
final center = Offset(size.width / 2, size.height / 2);
|
|
2397
|
+
final radius = size.width / 2 - 5;
|
|
2398
|
+
const startAngle = -math.pi / 2;
|
|
2399
|
+
const totalAngle = 2 * math.pi;
|
|
2400
|
+
final sweepAngle = totalAngle * progress.clamp(0.0, 1.0);
|
|
2401
|
+
|
|
2402
|
+
final trackPaint = Paint()
|
|
2403
|
+
..color = trackColor
|
|
2404
|
+
..style = PaintingStyle.stroke
|
|
2405
|
+
..strokeWidth = 5
|
|
2406
|
+
..strokeCap = StrokeCap.round;
|
|
2407
|
+
canvas.drawArc(
|
|
2408
|
+
Rect.fromCircle(center: center, radius: radius),
|
|
2409
|
+
startAngle,
|
|
2410
|
+
totalAngle,
|
|
2411
|
+
false,
|
|
2412
|
+
trackPaint,
|
|
2177
2413
|
);
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2414
|
+
|
|
2415
|
+
if (sweepAngle > 0) {
|
|
2416
|
+
final glowPaint = Paint()
|
|
2417
|
+
..color = glowColor.withValues(alpha: 0.25)
|
|
2418
|
+
..style = PaintingStyle.stroke
|
|
2419
|
+
..strokeWidth = 10
|
|
2420
|
+
..strokeCap = StrokeCap.round
|
|
2421
|
+
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6);
|
|
2422
|
+
canvas.drawArc(
|
|
2423
|
+
Rect.fromCircle(center: center, radius: radius),
|
|
2424
|
+
startAngle,
|
|
2425
|
+
sweepAngle,
|
|
2426
|
+
false,
|
|
2427
|
+
glowPaint,
|
|
2428
|
+
);
|
|
2429
|
+
|
|
2430
|
+
final fillPaint = Paint()
|
|
2431
|
+
..shader = SweepGradient(
|
|
2432
|
+
startAngle: startAngle,
|
|
2433
|
+
endAngle: startAngle + sweepAngle,
|
|
2434
|
+
colors: <Color>[fillColor, glowColor],
|
|
2435
|
+
).createShader(Rect.fromCircle(center: center, radius: radius))
|
|
2436
|
+
..style = PaintingStyle.stroke
|
|
2437
|
+
..strokeWidth = 5
|
|
2438
|
+
..strokeCap = StrokeCap.round;
|
|
2439
|
+
canvas.drawArc(
|
|
2440
|
+
Rect.fromCircle(center: center, radius: radius),
|
|
2441
|
+
startAngle,
|
|
2442
|
+
sweepAngle,
|
|
2443
|
+
false,
|
|
2444
|
+
fillPaint,
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
@override
|
|
2450
|
+
bool shouldRepaint(_RadialGaugePainter oldDelegate) =>
|
|
2451
|
+
progress != oldDelegate.progress ||
|
|
2452
|
+
trackColor != oldDelegate.trackColor ||
|
|
2453
|
+
fillColor != oldDelegate.fillColor;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
class _MemoryRow extends StatelessWidget {
|
|
2457
|
+
const _MemoryRow({
|
|
2458
|
+
required this.memory,
|
|
2459
|
+
required this.isSelected,
|
|
2460
|
+
required this.onTap,
|
|
2461
|
+
required this.onCheck,
|
|
2462
|
+
this.onDelete,
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
final MemoryItem memory;
|
|
2466
|
+
final bool isSelected;
|
|
2467
|
+
final VoidCallback onTap;
|
|
2468
|
+
final ValueChanged<bool?> onCheck;
|
|
2469
|
+
final VoidCallback? onDelete;
|
|
2470
|
+
|
|
2471
|
+
@override
|
|
2472
|
+
Widget build(BuildContext context) {
|
|
2473
|
+
return Container(
|
|
2474
|
+
width: double.infinity,
|
|
2475
|
+
margin: const EdgeInsets.only(bottom: 8),
|
|
2476
|
+
decoration: BoxDecoration(
|
|
2477
|
+
color: isSelected ? _accentMuted : _bgSecondary,
|
|
2478
|
+
borderRadius: BorderRadius.circular(12),
|
|
2479
|
+
border: Border.all(color: isSelected ? _accent : _border),
|
|
2480
|
+
),
|
|
2481
|
+
child: Material(
|
|
2482
|
+
color: Colors.transparent,
|
|
2483
|
+
child: InkWell(
|
|
2484
|
+
borderRadius: BorderRadius.circular(12),
|
|
2485
|
+
onTap: onTap,
|
|
2486
|
+
child: Padding(
|
|
2487
|
+
padding: const EdgeInsets.all(12),
|
|
2488
|
+
child: Row(
|
|
2489
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2192
2490
|
children: <Widget>[
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2491
|
+
Checkbox(value: isSelected, onChanged: onCheck),
|
|
2492
|
+
const SizedBox(width: 6),
|
|
2493
|
+
Expanded(
|
|
2494
|
+
child: Column(
|
|
2495
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2496
|
+
children: <Widget>[
|
|
2497
|
+
Row(
|
|
2498
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2499
|
+
children: <Widget>[
|
|
2500
|
+
Expanded(
|
|
2501
|
+
child: Wrap(
|
|
2502
|
+
spacing: 8,
|
|
2503
|
+
runSpacing: 8,
|
|
2504
|
+
children: <Widget>[
|
|
2505
|
+
_MetaPill(
|
|
2506
|
+
label: memory.category,
|
|
2507
|
+
icon: Icons.label_outline,
|
|
2508
|
+
),
|
|
2509
|
+
_MetaPill(
|
|
2510
|
+
label: 'Imp ${memory.importance}',
|
|
2511
|
+
icon: Icons.priority_high_outlined,
|
|
2512
|
+
),
|
|
2513
|
+
_MetaPill(
|
|
2514
|
+
label: '${memory.confidencePercent}%',
|
|
2515
|
+
icon: Icons.verified_outlined,
|
|
2516
|
+
),
|
|
2517
|
+
],
|
|
2518
|
+
),
|
|
2519
|
+
),
|
|
2520
|
+
if (onDelete != null)
|
|
2521
|
+
IconButton(
|
|
2522
|
+
onPressed: onDelete,
|
|
2523
|
+
icon: Icon(
|
|
2524
|
+
Icons.delete_outline,
|
|
2525
|
+
size: 18,
|
|
2526
|
+
),
|
|
2527
|
+
),
|
|
2528
|
+
],
|
|
2529
|
+
),
|
|
2530
|
+
const SizedBox(height: 8),
|
|
2531
|
+
Text(memory.content),
|
|
2532
|
+
if (memory.entities.isNotEmpty) ...<Widget>[
|
|
2533
|
+
const SizedBox(height: 6),
|
|
2534
|
+
Wrap(
|
|
2535
|
+
spacing: 6,
|
|
2536
|
+
runSpacing: 6,
|
|
2537
|
+
children: memory.entities
|
|
2538
|
+
.take(5)
|
|
2539
|
+
.map(
|
|
2540
|
+
(e) => _MetaPill(
|
|
2541
|
+
label: e.name,
|
|
2542
|
+
icon: Icons.hub_outlined,
|
|
2543
|
+
),
|
|
2544
|
+
)
|
|
2545
|
+
.toList(),
|
|
2546
|
+
),
|
|
2547
|
+
],
|
|
2548
|
+
const SizedBox(height: 6),
|
|
2549
|
+
Text(
|
|
2550
|
+
memory.createdAtLabel,
|
|
2551
|
+
style: TextStyle(
|
|
2552
|
+
fontSize: 11,
|
|
2553
|
+
color: _textMuted,
|
|
2554
|
+
),
|
|
2555
|
+
),
|
|
2556
|
+
],
|
|
2557
|
+
),
|
|
2203
2558
|
),
|
|
2204
2559
|
],
|
|
2205
2560
|
),
|
|
2206
2561
|
),
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2562
|
+
),
|
|
2563
|
+
),
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// ---------------------------------------------------------------------------
|
|
2569
|
+
// Interactive Entity Knowledge Graph
|
|
2570
|
+
// ---------------------------------------------------------------------------
|
|
2571
|
+
|
|
2572
|
+
class _EntityGraphView extends StatefulWidget {
|
|
2573
|
+
const _EntityGraphView({
|
|
2574
|
+
required this.entities,
|
|
2575
|
+
required this.knowledgeViews,
|
|
2576
|
+
this.selectedEntity,
|
|
2577
|
+
this.onEntityTapped,
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2580
|
+
final List<MemoryEntity> entities;
|
|
2581
|
+
final List<KnowledgeViewItem> knowledgeViews;
|
|
2582
|
+
final String? selectedEntity;
|
|
2583
|
+
final ValueChanged<String>? onEntityTapped;
|
|
2584
|
+
|
|
2585
|
+
@override
|
|
2586
|
+
State<_EntityGraphView> createState() => _EntityGraphViewState();
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
class _EntityGraphViewState extends State<_EntityGraphView>
|
|
2590
|
+
with SingleTickerProviderStateMixin {
|
|
2591
|
+
late final AnimationController _idleController;
|
|
2592
|
+
final List<_GraphNode> _nodes = <_GraphNode>[];
|
|
2593
|
+
String? _hoveredNode;
|
|
2594
|
+
bool _layoutDone = false;
|
|
2595
|
+
Size? _lastLayoutSize;
|
|
2596
|
+
|
|
2597
|
+
@override
|
|
2598
|
+
void initState() {
|
|
2599
|
+
super.initState();
|
|
2600
|
+
_idleController = AnimationController(
|
|
2601
|
+
vsync: this,
|
|
2602
|
+
duration: const Duration(seconds: 6),
|
|
2603
|
+
)..repeat();
|
|
2604
|
+
_buildNodes();
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
@override
|
|
2608
|
+
void didUpdateWidget(_EntityGraphView oldWidget) {
|
|
2609
|
+
super.didUpdateWidget(oldWidget);
|
|
2610
|
+
if (oldWidget.entities != widget.entities ||
|
|
2611
|
+
oldWidget.knowledgeViews != widget.knowledgeViews) {
|
|
2612
|
+
_buildNodes();
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
@override
|
|
2617
|
+
void dispose() {
|
|
2618
|
+
_idleController.dispose();
|
|
2619
|
+
super.dispose();
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
static const Map<String, Color> _kindColors = <String, Color>{
|
|
2623
|
+
'person': Color(0xFF9B8AE0),
|
|
2624
|
+
'project': Color(0xFF5E9B7C),
|
|
2625
|
+
'file': Color(0xFF7BA5C7),
|
|
2626
|
+
'concept': Color(0xFFB8A06B),
|
|
2627
|
+
'tool': Color(0xFFCF8F6B),
|
|
2628
|
+
'organization': Color(0xFF7DA0B5),
|
|
2629
|
+
};
|
|
2630
|
+
|
|
2631
|
+
void _buildNodes() {
|
|
2632
|
+
_nodes.clear();
|
|
2633
|
+
final entities = widget.entities;
|
|
2634
|
+
final views = widget.knowledgeViews;
|
|
2635
|
+
final maxMention = entities.fold<int>(
|
|
2636
|
+
1,
|
|
2637
|
+
(max, e) => e.mentionCount > max ? e.mentionCount : max,
|
|
2638
|
+
);
|
|
2639
|
+
|
|
2640
|
+
for (int i = 0; i < entities.length; i++) {
|
|
2641
|
+
final entity = entities[i];
|
|
2642
|
+
final sizeFactor = 0.4 + 0.6 * (entity.mentionCount / maxMention);
|
|
2643
|
+
_nodes.add(_GraphNode(
|
|
2644
|
+
id: entity.name,
|
|
2645
|
+
label: entity.name,
|
|
2646
|
+
radius: 18 + 20 * sizeFactor,
|
|
2647
|
+
color: _kindColors[entity.kind] ?? _kindColors['concept']!,
|
|
2648
|
+
kind: entity.kind,
|
|
2649
|
+
isReflection: false,
|
|
2650
|
+
offsetPhase: i * 0.7,
|
|
2651
|
+
));
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
for (int i = 0; i < views.length && i < 6; i++) {
|
|
2655
|
+
_nodes.add(_GraphNode(
|
|
2656
|
+
id: 'kv_${views[i].title}',
|
|
2657
|
+
label: views[i].title,
|
|
2658
|
+
radius: 14,
|
|
2659
|
+
color: const Color(0xFF8B7EC8),
|
|
2660
|
+
kind: views[i].viewType,
|
|
2661
|
+
isReflection: true,
|
|
2662
|
+
offsetPhase: (entities.length + i) * 0.9,
|
|
2663
|
+
));
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
_layoutDone = false;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
void _layoutNodes(Size size) {
|
|
2670
|
+
if (_nodes.isEmpty) return;
|
|
2671
|
+
if (_layoutDone && _lastLayoutSize == size) return;
|
|
2672
|
+
_layoutDone = true;
|
|
2673
|
+
_lastLayoutSize = size;
|
|
2674
|
+
|
|
2675
|
+
final cx = size.width / 2;
|
|
2676
|
+
final cy = size.height / 2;
|
|
2677
|
+
final radiusX = size.width * 0.35;
|
|
2678
|
+
final radiusY = size.height * 0.35;
|
|
2679
|
+
|
|
2680
|
+
for (int i = 0; i < _nodes.length; i++) {
|
|
2681
|
+
final angle = (2 * math.pi * i / _nodes.length) - math.pi / 2;
|
|
2682
|
+
final jitter = (i.isEven ? 0.85 : 1.0) + (i % 3) * 0.05;
|
|
2683
|
+
_nodes[i].x = cx + radiusX * jitter * math.cos(angle);
|
|
2684
|
+
_nodes[i].y = cy + radiusY * jitter * math.sin(angle);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
void _handleTap(Offset localPosition) {
|
|
2689
|
+
for (final node in _nodes.reversed) {
|
|
2690
|
+
final dx = localPosition.dx - node.x;
|
|
2691
|
+
final dy = localPosition.dy - node.y;
|
|
2692
|
+
if (dx * dx + dy * dy <= node.radius * node.radius * 1.8) {
|
|
2693
|
+
if (!node.isReflection) {
|
|
2694
|
+
widget.onEntityTapped?.call(node.label);
|
|
2695
|
+
}
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
void _handleHover(Offset? localPosition) {
|
|
2702
|
+
if (localPosition == null) {
|
|
2703
|
+
if (_hoveredNode != null) setState(() => _hoveredNode = null);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
String? found;
|
|
2707
|
+
for (final node in _nodes.reversed) {
|
|
2708
|
+
final dx = localPosition.dx - node.x;
|
|
2709
|
+
final dy = localPosition.dy - node.y;
|
|
2710
|
+
if (dx * dx + dy * dy <= node.radius * node.radius * 1.8) {
|
|
2711
|
+
found = node.id;
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
if (found != _hoveredNode) setState(() => _hoveredNode = found);
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
@override
|
|
2719
|
+
Widget build(BuildContext context) {
|
|
2720
|
+
return LayoutBuilder(
|
|
2721
|
+
builder: (context, constraints) {
|
|
2722
|
+
final size = Size(constraints.maxWidth, constraints.maxHeight);
|
|
2723
|
+
_layoutNodes(size);
|
|
2724
|
+
return MouseRegion(
|
|
2725
|
+
onHover: (event) => _handleHover(event.localPosition),
|
|
2726
|
+
onExit: (_) => _handleHover(null),
|
|
2727
|
+
child: GestureDetector(
|
|
2728
|
+
onTapDown: (details) => _handleTap(details.localPosition),
|
|
2729
|
+
child: AnimatedBuilder(
|
|
2730
|
+
animation: _idleController,
|
|
2731
|
+
builder: (context, _) {
|
|
2732
|
+
return CustomPaint(
|
|
2733
|
+
size: size,
|
|
2734
|
+
painter: _EntityGraphPainter(
|
|
2735
|
+
nodes: _nodes,
|
|
2736
|
+
selectedEntity: widget.selectedEntity,
|
|
2737
|
+
hoveredNode: _hoveredNode,
|
|
2738
|
+
animationValue: _idleController.value,
|
|
2739
|
+
accentColor: _accent,
|
|
2740
|
+
bgColor: _bgSecondary,
|
|
2741
|
+
textColor: _textPrimary,
|
|
2742
|
+
mutedTextColor: _textSecondary,
|
|
2743
|
+
borderColor: _border,
|
|
2744
|
+
),
|
|
2217
2745
|
);
|
|
2218
|
-
if (context.mounted) {
|
|
2219
|
-
Navigator.of(context).pop();
|
|
2220
|
-
}
|
|
2221
2746
|
},
|
|
2222
|
-
child: Text('Save'),
|
|
2223
2747
|
),
|
|
2224
|
-
|
|
2748
|
+
),
|
|
2225
2749
|
);
|
|
2226
2750
|
},
|
|
2227
2751
|
);
|
|
2228
2752
|
}
|
|
2229
2753
|
}
|
|
2230
2754
|
|
|
2755
|
+
class _GraphNode {
|
|
2756
|
+
_GraphNode({
|
|
2757
|
+
required this.id,
|
|
2758
|
+
required this.label,
|
|
2759
|
+
required this.radius,
|
|
2760
|
+
required this.color,
|
|
2761
|
+
required this.kind,
|
|
2762
|
+
required this.isReflection,
|
|
2763
|
+
required this.offsetPhase,
|
|
2764
|
+
});
|
|
2765
|
+
|
|
2766
|
+
final String id;
|
|
2767
|
+
final String label;
|
|
2768
|
+
final double radius;
|
|
2769
|
+
final Color color;
|
|
2770
|
+
final String kind;
|
|
2771
|
+
final bool isReflection;
|
|
2772
|
+
final double offsetPhase;
|
|
2773
|
+
double x = 0;
|
|
2774
|
+
double y = 0;
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
class _EntityGraphPainter extends CustomPainter {
|
|
2778
|
+
const _EntityGraphPainter({
|
|
2779
|
+
required this.nodes,
|
|
2780
|
+
required this.selectedEntity,
|
|
2781
|
+
required this.hoveredNode,
|
|
2782
|
+
required this.animationValue,
|
|
2783
|
+
required this.accentColor,
|
|
2784
|
+
required this.bgColor,
|
|
2785
|
+
required this.textColor,
|
|
2786
|
+
required this.mutedTextColor,
|
|
2787
|
+
required this.borderColor,
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
final List<_GraphNode> nodes;
|
|
2791
|
+
final String? selectedEntity;
|
|
2792
|
+
final String? hoveredNode;
|
|
2793
|
+
final double animationValue;
|
|
2794
|
+
final Color accentColor;
|
|
2795
|
+
final Color bgColor;
|
|
2796
|
+
final Color textColor;
|
|
2797
|
+
final Color mutedTextColor;
|
|
2798
|
+
final Color borderColor;
|
|
2799
|
+
|
|
2800
|
+
@override
|
|
2801
|
+
void paint(Canvas canvas, Size size) {
|
|
2802
|
+
if (nodes.isEmpty) return;
|
|
2803
|
+
|
|
2804
|
+
final entityNodes =
|
|
2805
|
+
nodes.where((n) => !n.isReflection).toList(growable: false);
|
|
2806
|
+
final reflectionNodes =
|
|
2807
|
+
nodes.where((n) => n.isReflection).toList(growable: false);
|
|
2808
|
+
|
|
2809
|
+
// Draw connections between entity nodes (subtle web)
|
|
2810
|
+
final linePaint = Paint()
|
|
2811
|
+
..color = borderColor.withValues(alpha: 0.18)
|
|
2812
|
+
..strokeWidth = 1;
|
|
2813
|
+
for (int i = 0; i < entityNodes.length; i++) {
|
|
2814
|
+
for (int j = i + 1; j < entityNodes.length; j++) {
|
|
2815
|
+
if ((i + j) % 3 != 0) continue;
|
|
2816
|
+
final a = entityNodes[i];
|
|
2817
|
+
final b = entityNodes[j];
|
|
2818
|
+
final drift = math.sin(animationValue * 2 * math.pi + a.offsetPhase);
|
|
2819
|
+
canvas.drawLine(
|
|
2820
|
+
Offset(a.x, a.y + drift * 2),
|
|
2821
|
+
Offset(b.x, b.y + drift * 2),
|
|
2822
|
+
linePaint,
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
// Draw connections from reflections to closest entity
|
|
2828
|
+
if (entityNodes.isNotEmpty) {
|
|
2829
|
+
final reflectionLinePaint = Paint()
|
|
2830
|
+
..color = borderColor.withValues(alpha: 0.14)
|
|
2831
|
+
..strokeWidth = 1
|
|
2832
|
+
..style = PaintingStyle.stroke;
|
|
2833
|
+
for (final rn in reflectionNodes) {
|
|
2834
|
+
final drift =
|
|
2835
|
+
math.sin(animationValue * 2 * math.pi + rn.offsetPhase) * 3;
|
|
2836
|
+
var closest = entityNodes.first;
|
|
2837
|
+
var minDist = double.infinity;
|
|
2838
|
+
for (final en in entityNodes) {
|
|
2839
|
+
final d = (en.x - rn.x) * (en.x - rn.x) +
|
|
2840
|
+
(en.y - rn.y) * (en.y - rn.y);
|
|
2841
|
+
if (d < minDist) {
|
|
2842
|
+
minDist = d;
|
|
2843
|
+
closest = en;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
canvas.drawLine(
|
|
2847
|
+
Offset(rn.x, rn.y + drift),
|
|
2848
|
+
Offset(closest.x, closest.y + drift),
|
|
2849
|
+
reflectionLinePaint,
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// Draw nodes
|
|
2855
|
+
for (final node in nodes) {
|
|
2856
|
+
final drift =
|
|
2857
|
+
math.sin(animationValue * 2 * math.pi + node.offsetPhase) * 3;
|
|
2858
|
+
final isSelected = node.label == selectedEntity;
|
|
2859
|
+
final isHovered = node.id == hoveredNode;
|
|
2860
|
+
final cx = node.x;
|
|
2861
|
+
final cy = node.y + drift;
|
|
2862
|
+
final r = node.radius * (isHovered ? 1.15 : 1.0);
|
|
2863
|
+
|
|
2864
|
+
// Glow
|
|
2865
|
+
if (isSelected || isHovered) {
|
|
2866
|
+
final glowPaint = Paint()
|
|
2867
|
+
..color = (isSelected ? accentColor : node.color)
|
|
2868
|
+
.withValues(alpha: 0.22)
|
|
2869
|
+
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12);
|
|
2870
|
+
canvas.drawCircle(Offset(cx, cy), r + 6, glowPaint);
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// Fill
|
|
2874
|
+
final fillPaint = Paint()
|
|
2875
|
+
..shader = RadialGradient(
|
|
2876
|
+
colors: <Color>[
|
|
2877
|
+
node.color.withValues(alpha: isSelected ? 0.9 : 0.7),
|
|
2878
|
+
node.color.withValues(alpha: isSelected ? 0.6 : 0.35),
|
|
2879
|
+
],
|
|
2880
|
+
).createShader(Rect.fromCircle(center: Offset(cx, cy), radius: r));
|
|
2881
|
+
canvas.drawCircle(Offset(cx, cy), r, fillPaint);
|
|
2882
|
+
|
|
2883
|
+
// Border
|
|
2884
|
+
final borderPaint = Paint()
|
|
2885
|
+
..color = isSelected
|
|
2886
|
+
? accentColor
|
|
2887
|
+
: (isHovered
|
|
2888
|
+
? node.color.withValues(alpha: 0.8)
|
|
2889
|
+
: node.color.withValues(alpha: 0.35))
|
|
2890
|
+
..style = PaintingStyle.stroke
|
|
2891
|
+
..strokeWidth = isSelected ? 2.5 : 1.5;
|
|
2892
|
+
canvas.drawCircle(Offset(cx, cy), r, borderPaint);
|
|
2893
|
+
|
|
2894
|
+
// Label
|
|
2895
|
+
final labelStyle = TextStyle(
|
|
2896
|
+
color: isSelected ? textColor : mutedTextColor,
|
|
2897
|
+
fontSize: node.isReflection ? 9 : 11,
|
|
2898
|
+
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600,
|
|
2899
|
+
);
|
|
2900
|
+
final tp = TextPainter(
|
|
2901
|
+
text: TextSpan(text: node.label, style: labelStyle),
|
|
2902
|
+
textDirection: TextDirection.ltr,
|
|
2903
|
+
maxLines: 1,
|
|
2904
|
+
ellipsis: '…',
|
|
2905
|
+
)..layout(maxWidth: r * 3);
|
|
2906
|
+
tp.paint(
|
|
2907
|
+
canvas,
|
|
2908
|
+
Offset(cx - tp.width / 2, cy + r + 5),
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
@override
|
|
2914
|
+
bool shouldRepaint(_EntityGraphPainter oldDelegate) => true;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2231
2917
|
class WidgetsPanel extends StatelessWidget {
|
|
2232
2918
|
const WidgetsPanel({super.key, required this.controller});
|
|
2233
2919
|
|
|
@@ -4424,6 +5110,89 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4424
5110
|
|
|
4425
5111
|
NeoAgentController get controller => widget.controller;
|
|
4426
5112
|
|
|
5113
|
+
Color _taskRunStatusColor(String status) {
|
|
5114
|
+
switch (status) {
|
|
5115
|
+
case 'completed':
|
|
5116
|
+
return _success;
|
|
5117
|
+
case 'failed':
|
|
5118
|
+
case 'error':
|
|
5119
|
+
return _danger;
|
|
5120
|
+
case 'running':
|
|
5121
|
+
case 'retrying':
|
|
5122
|
+
return _warning;
|
|
5123
|
+
default:
|
|
5124
|
+
return _textSecondary;
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
|
|
5128
|
+
Future<void> _showLastRun(TaskItem task) async {
|
|
5129
|
+
final runId = task.lastRunId.trim();
|
|
5130
|
+
if (runId.isEmpty) return;
|
|
5131
|
+
|
|
5132
|
+
try {
|
|
5133
|
+
final detail = await controller.fetchRunDetail(runId, force: true);
|
|
5134
|
+
if (!mounted) return;
|
|
5135
|
+
await showDialog<void>(
|
|
5136
|
+
context: context,
|
|
5137
|
+
builder: (dialogContext) => AlertDialog(
|
|
5138
|
+
title: Text(detail.run.title),
|
|
5139
|
+
content: ConstrainedBox(
|
|
5140
|
+
constraints: const BoxConstraints(maxWidth: 620),
|
|
5141
|
+
child: SingleChildScrollView(
|
|
5142
|
+
child: Column(
|
|
5143
|
+
mainAxisSize: MainAxisSize.min,
|
|
5144
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
5145
|
+
children: <Widget>[
|
|
5146
|
+
Wrap(
|
|
5147
|
+
spacing: 8,
|
|
5148
|
+
runSpacing: 8,
|
|
5149
|
+
children: <Widget>[
|
|
5150
|
+
_StatusPill(
|
|
5151
|
+
label: detail.run.statusLabel,
|
|
5152
|
+
color: detail.run.statusColor,
|
|
5153
|
+
),
|
|
5154
|
+
_StatusPill(
|
|
5155
|
+
label: '${detail.steps.length} steps',
|
|
5156
|
+
color: _textSecondary,
|
|
5157
|
+
),
|
|
5158
|
+
],
|
|
5159
|
+
),
|
|
5160
|
+
if (detail.run.error.trim().isNotEmpty) ...<Widget>[
|
|
5161
|
+
const SizedBox(height: 16),
|
|
5162
|
+
Text(detail.run.error, style: TextStyle(color: _danger)),
|
|
5163
|
+
],
|
|
5164
|
+
if (detail.response.trim().isNotEmpty) ...<Widget>[
|
|
5165
|
+
const SizedBox(height: 16),
|
|
5166
|
+
SelectableText(detail.response),
|
|
5167
|
+
],
|
|
5168
|
+
if (detail.response.trim().isEmpty &&
|
|
5169
|
+
detail.run.error.trim().isEmpty) ...<Widget>[
|
|
5170
|
+
const SizedBox(height: 16),
|
|
5171
|
+
Text(
|
|
5172
|
+
'This run did not produce a user-facing response.',
|
|
5173
|
+
style: TextStyle(color: _textSecondary),
|
|
5174
|
+
),
|
|
5175
|
+
],
|
|
5176
|
+
],
|
|
5177
|
+
),
|
|
5178
|
+
),
|
|
5179
|
+
),
|
|
5180
|
+
actions: <Widget>[
|
|
5181
|
+
TextButton(
|
|
5182
|
+
onPressed: () => Navigator.of(dialogContext).pop(),
|
|
5183
|
+
child: const Text('Close'),
|
|
5184
|
+
),
|
|
5185
|
+
],
|
|
5186
|
+
),
|
|
5187
|
+
);
|
|
5188
|
+
} catch (error) {
|
|
5189
|
+
if (!mounted) return;
|
|
5190
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
5191
|
+
SnackBar(content: Text(controller.friendlyErrorMessage(error))),
|
|
5192
|
+
);
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
|
|
4427
5196
|
@override
|
|
4428
5197
|
void didUpdateWidget(covariant TasksPanel oldWidget) {
|
|
4429
5198
|
super.didUpdateWidget(oldWidget);
|
|
@@ -4572,6 +5341,13 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4572
5341
|
label: task.enabled ? 'Active' : 'Paused',
|
|
4573
5342
|
color: task.enabled ? _success : _textSecondary,
|
|
4574
5343
|
),
|
|
5344
|
+
if (task.hasLastRunStatus) ...<Widget>[
|
|
5345
|
+
const SizedBox(width: 8),
|
|
5346
|
+
_StatusPill(
|
|
5347
|
+
label: 'Last: ${task.lastRunStatusLabel}',
|
|
5348
|
+
color: _taskRunStatusColor(task.lastRunStatus),
|
|
5349
|
+
),
|
|
5350
|
+
],
|
|
4575
5351
|
],
|
|
4576
5352
|
),
|
|
4577
5353
|
const SizedBox(height: 10),
|
|
@@ -4579,7 +5355,7 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4579
5355
|
task.scheduleLabel,
|
|
4580
5356
|
style: TextStyle(
|
|
4581
5357
|
color: _textSecondary,
|
|
4582
|
-
fontFamily: GoogleFonts.
|
|
5358
|
+
fontFamily: GoogleFonts.geistMono().fontFamily,
|
|
4583
5359
|
),
|
|
4584
5360
|
),
|
|
4585
5361
|
if (task.hasModelOverride) ...<Widget>[
|
|
@@ -4603,6 +5379,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4603
5379
|
style: TextStyle(color: _textSecondary),
|
|
4604
5380
|
),
|
|
4605
5381
|
],
|
|
5382
|
+
if (task.lastRunFailed &&
|
|
5383
|
+
task.lastRunError.trim().isNotEmpty) ...<Widget>[
|
|
5384
|
+
const SizedBox(height: 8),
|
|
5385
|
+
Text(task.lastRunError, style: TextStyle(color: _danger)),
|
|
5386
|
+
],
|
|
4606
5387
|
const SizedBox(height: 14),
|
|
4607
5388
|
Wrap(
|
|
4608
5389
|
spacing: 10,
|
|
@@ -4622,6 +5403,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4622
5403
|
: () => controller.runTaskNow(task.id),
|
|
4623
5404
|
child: Text(_manualRunButtonLabel('Run Now', remaining)),
|
|
4624
5405
|
),
|
|
5406
|
+
if (task.lastRunId.trim().isNotEmpty)
|
|
5407
|
+
OutlinedButton(
|
|
5408
|
+
onPressed: () => _showLastRun(task),
|
|
5409
|
+
child: const Text('View last run'),
|
|
5410
|
+
),
|
|
4625
5411
|
OutlinedButton(
|
|
4626
5412
|
onPressed: () => _confirmDelete(
|
|
4627
5413
|
context,
|
|
@@ -4674,6 +5460,13 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4674
5460
|
label: task.enabled ? 'Active' : 'Paused',
|
|
4675
5461
|
color: task.enabled ? _success : _textSecondary,
|
|
4676
5462
|
),
|
|
5463
|
+
if (task.hasLastRunStatus) ...<Widget>[
|
|
5464
|
+
const SizedBox(width: 8),
|
|
5465
|
+
_StatusPill(
|
|
5466
|
+
label: 'Last: ${task.lastRunStatusLabel}',
|
|
5467
|
+
color: _taskRunStatusColor(task.lastRunStatus),
|
|
5468
|
+
),
|
|
5469
|
+
],
|
|
4677
5470
|
],
|
|
4678
5471
|
),
|
|
4679
5472
|
const SizedBox(height: 10),
|
|
@@ -4681,7 +5474,7 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4681
5474
|
task.scheduleLabel,
|
|
4682
5475
|
style: TextStyle(
|
|
4683
5476
|
color: _textSecondary,
|
|
4684
|
-
fontFamily: GoogleFonts.
|
|
5477
|
+
fontFamily: GoogleFonts.geistMono().fontFamily,
|
|
4685
5478
|
),
|
|
4686
5479
|
),
|
|
4687
5480
|
const SizedBox(height: 8),
|
|
@@ -4708,6 +5501,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4708
5501
|
style: TextStyle(color: _textSecondary),
|
|
4709
5502
|
),
|
|
4710
5503
|
],
|
|
5504
|
+
if (task.lastRunFailed &&
|
|
5505
|
+
task.lastRunError.trim().isNotEmpty) ...<Widget>[
|
|
5506
|
+
const SizedBox(height: 8),
|
|
5507
|
+
Text(task.lastRunError, style: TextStyle(color: _danger)),
|
|
5508
|
+
],
|
|
4711
5509
|
const SizedBox(height: 14),
|
|
4712
5510
|
Wrap(
|
|
4713
5511
|
spacing: 10,
|
|
@@ -4737,6 +5535,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
4737
5535
|
_manualRunButtonLabel('Refresh Now', remaining),
|
|
4738
5536
|
),
|
|
4739
5537
|
),
|
|
5538
|
+
if (task.lastRunId.trim().isNotEmpty)
|
|
5539
|
+
OutlinedButton(
|
|
5540
|
+
onPressed: () => _showLastRun(task),
|
|
5541
|
+
child: const Text('View last run'),
|
|
5542
|
+
),
|
|
4740
5543
|
OutlinedButton(
|
|
4741
5544
|
onPressed: linkedWidget == null
|
|
4742
5545
|
? null
|