neoagent 2.4.1-beta.36 → 2.4.1-beta.37
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/flutter_app/lib/main_operations.dart +1246 -691
- package/package.json +1 -1
- package/server/db/ftsQuery.js +27 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +68156 -67730
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/triggers.js +4 -4
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +99 -501
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +37 -24
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/grok.js +5 -67
- package/server/services/ai/providers/nvidia.js +6 -29
- package/server/services/ai/providers/ollama.js +48 -28
- package/server/services/ai/providers/openai.js +2 -27
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/memory/manager.js +1 -7
- package/server/services/websocket.js +8 -2
|
@@ -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,11 +1311,12 @@ 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
|
-
content: Text(
|
|
1317
|
+
content: Text(
|
|
1318
|
+
'This will import the response into ${applyTargets.join(', ')}.',
|
|
1319
|
+
),
|
|
1336
1320
|
actions: <Widget>[
|
|
1337
1321
|
TextButton(
|
|
1338
1322
|
onPressed: () => Navigator.of(context).pop(false),
|
|
@@ -1346,25 +1330,18 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1346
1330
|
);
|
|
1347
1331
|
},
|
|
1348
1332
|
);
|
|
1349
|
-
if (confirmImport != true)
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
setState(() {
|
|
1353
|
-
_llmImporting = true;
|
|
1354
|
-
});
|
|
1333
|
+
if (confirmImport != true) return;
|
|
1334
|
+
setState(() => _llmImporting = true);
|
|
1355
1335
|
try {
|
|
1356
1336
|
final result = await controller.importMemoryTransfer(
|
|
1357
1337
|
text,
|
|
1358
1338
|
applyBehaviorNotes: _llmApplyBehaviorNotes,
|
|
1359
1339
|
applyCoreMemory: _llmApplyCoreMemory,
|
|
1360
1340
|
);
|
|
1361
|
-
if (!mounted)
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1341
|
+
if (!mounted) return;
|
|
1364
1342
|
_llmImportController.clear();
|
|
1365
|
-
final warningText =
|
|
1366
|
-
? ''
|
|
1367
|
-
: ' ${result.warnings.join(' ')}';
|
|
1343
|
+
final warningText =
|
|
1344
|
+
result.warnings.isEmpty ? '' : ' ${result.warnings.join(' ')}';
|
|
1368
1345
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
1369
1346
|
SnackBar(
|
|
1370
1347
|
content: Text(
|
|
@@ -1376,30 +1353,30 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1376
1353
|
),
|
|
1377
1354
|
);
|
|
1378
1355
|
} catch (error) {
|
|
1379
|
-
if (!mounted)
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1356
|
+
if (!mounted) return;
|
|
1382
1357
|
ScaffoldMessenger.of(
|
|
1383
1358
|
context,
|
|
1384
1359
|
).showSnackBar(SnackBar(content: Text('Import failed: $error')));
|
|
1385
1360
|
} finally {
|
|
1386
|
-
if (mounted)
|
|
1387
|
-
setState(() {
|
|
1388
|
-
_llmImporting = false;
|
|
1389
|
-
});
|
|
1390
|
-
}
|
|
1361
|
+
if (mounted) setState(() => _llmImporting = false);
|
|
1391
1362
|
}
|
|
1392
1363
|
}
|
|
1393
1364
|
|
|
1394
1365
|
List<MemoryItem> get _visibleMemories {
|
|
1395
1366
|
final controller = widget.controller;
|
|
1396
|
-
|
|
1367
|
+
final base = controller.memoryRecallResults.isNotEmpty
|
|
1397
1368
|
? controller.memoryRecallResults
|
|
1398
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();
|
|
1399
1376
|
}
|
|
1400
1377
|
|
|
1401
1378
|
List<String> get _selectedVisibleMemoryIds {
|
|
1402
|
-
final visibleIds = _visibleMemories.map((
|
|
1379
|
+
final visibleIds = _visibleMemories.map((m) => m.id).toSet();
|
|
1403
1380
|
return _selectedMemoryIds
|
|
1404
1381
|
.where(visibleIds.contains)
|
|
1405
1382
|
.toList(growable: false);
|
|
@@ -1416,20 +1393,14 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1416
1393
|
}
|
|
1417
1394
|
|
|
1418
1395
|
void _clearMemorySelection() {
|
|
1419
|
-
if (_selectedMemoryIds.isEmpty)
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
setState(() {
|
|
1423
|
-
_selectedMemoryIds.clear();
|
|
1424
|
-
});
|
|
1396
|
+
if (_selectedMemoryIds.isEmpty) return;
|
|
1397
|
+
setState(() => _selectedMemoryIds.clear());
|
|
1425
1398
|
}
|
|
1426
1399
|
|
|
1427
1400
|
void _selectAllVisibleMemories(List<MemoryItem> memories) {
|
|
1428
|
-
if (memories.isEmpty)
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1401
|
+
if (memories.isEmpty) return;
|
|
1431
1402
|
setState(() {
|
|
1432
|
-
_selectedMemoryIds.addAll(memories.map((
|
|
1403
|
+
_selectedMemoryIds.addAll(memories.map((m) => m.id));
|
|
1433
1404
|
});
|
|
1434
1405
|
}
|
|
1435
1406
|
|
|
@@ -1446,6 +1417,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1446
1417
|
void _resetMemorySearch(NeoAgentController controller) {
|
|
1447
1418
|
_searchController.clear();
|
|
1448
1419
|
_clearMemorySelection();
|
|
1420
|
+
setState(() => _entityFilter = null);
|
|
1449
1421
|
controller.clearMemorySearch();
|
|
1450
1422
|
}
|
|
1451
1423
|
|
|
@@ -1454,12 +1426,8 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1454
1426
|
String id,
|
|
1455
1427
|
) async {
|
|
1456
1428
|
await controller.deleteMemory(id);
|
|
1457
|
-
if (!mounted)
|
|
1458
|
-
|
|
1459
|
-
}
|
|
1460
|
-
setState(() {
|
|
1461
|
-
_selectedMemoryIds.remove(id);
|
|
1462
|
-
});
|
|
1429
|
+
if (!mounted) return;
|
|
1430
|
+
setState(() => _selectedMemoryIds.remove(id));
|
|
1463
1431
|
}
|
|
1464
1432
|
|
|
1465
1433
|
Future<void> _runBulkMemoryAction({
|
|
@@ -1469,55 +1437,50 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1469
1437
|
required Future<void> Function(List<String> ids) onConfirm,
|
|
1470
1438
|
}) async {
|
|
1471
1439
|
final ids = _selectedVisibleMemoryIds;
|
|
1472
|
-
if (ids.isEmpty || _bulkActionInFlight)
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1440
|
+
if (ids.isEmpty || _bulkActionInFlight) return;
|
|
1475
1441
|
await _confirmDelete(
|
|
1476
1442
|
context,
|
|
1477
1443
|
title: title,
|
|
1478
1444
|
message: message,
|
|
1479
1445
|
confirmLabel: confirmLabel,
|
|
1480
1446
|
onConfirm: () async {
|
|
1481
|
-
setState(()
|
|
1482
|
-
_bulkActionInFlight = true;
|
|
1483
|
-
});
|
|
1447
|
+
setState(() => _bulkActionInFlight = true);
|
|
1484
1448
|
try {
|
|
1485
1449
|
await onConfirm(ids);
|
|
1486
|
-
if (!mounted)
|
|
1487
|
-
|
|
1488
|
-
}
|
|
1489
|
-
setState(() {
|
|
1490
|
-
_selectedMemoryIds.removeAll(ids);
|
|
1491
|
-
});
|
|
1450
|
+
if (!mounted) return;
|
|
1451
|
+
setState(() => _selectedMemoryIds.removeAll(ids));
|
|
1492
1452
|
} finally {
|
|
1493
|
-
if (mounted)
|
|
1494
|
-
setState(() {
|
|
1495
|
-
_bulkActionInFlight = false;
|
|
1496
|
-
});
|
|
1497
|
-
}
|
|
1453
|
+
if (mounted) setState(() => _bulkActionInFlight = false);
|
|
1498
1454
|
}
|
|
1499
1455
|
},
|
|
1500
1456
|
);
|
|
1501
1457
|
}
|
|
1502
1458
|
|
|
1459
|
+
void _onEntityTapped(String entityName) {
|
|
1460
|
+
setState(() {
|
|
1461
|
+
_entityFilter = _entityFilter == entityName ? null : entityName;
|
|
1462
|
+
_tabController.animateTo(0);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1503
1466
|
@override
|
|
1504
1467
|
Widget build(BuildContext context) {
|
|
1505
1468
|
final controller = widget.controller;
|
|
1469
|
+
final stats = controller.memoryOverview.stats;
|
|
1506
1470
|
final memoriesToShow = _visibleMemories;
|
|
1507
|
-
final
|
|
1508
|
-
final selectedCount =
|
|
1509
|
-
final allVisibleSelected =
|
|
1510
|
-
memoriesToShow.
|
|
1511
|
-
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));
|
|
1512
1475
|
final showingSearchResults = controller.memoryRecallResults.isNotEmpty;
|
|
1476
|
+
final compact = MediaQuery.sizeOf(context).width < 760;
|
|
1513
1477
|
|
|
1514
1478
|
return ListView(
|
|
1515
1479
|
padding: _pagePadding(context),
|
|
1516
1480
|
children: <Widget>[
|
|
1517
1481
|
_PageTitle(
|
|
1518
1482
|
title: 'Memory',
|
|
1519
|
-
subtitle:
|
|
1520
|
-
'Structured facts, entities, reflections, long-term recall, and behavior notes.',
|
|
1483
|
+
subtitle: 'Long-term recall, structured facts, and knowledge graph.',
|
|
1521
1484
|
trailing: Wrap(
|
|
1522
1485
|
spacing: 10,
|
|
1523
1486
|
runSpacing: 10,
|
|
@@ -1535,628 +1498,554 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
1535
1498
|
],
|
|
1536
1499
|
),
|
|
1537
1500
|
),
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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,
|
|
1522
|
+
),
|
|
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
|
+
],
|
|
1549
|
+
),
|
|
1550
|
+
),
|
|
1551
|
+
],
|
|
1569
1552
|
),
|
|
1570
1553
|
),
|
|
1571
|
-
|
|
1554
|
+
),
|
|
1572
1555
|
),
|
|
1556
|
+
|
|
1573
1557
|
const SizedBox(height: 16),
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1582
|
-
|
|
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,
|
|
1583
1567
|
children: <Widget>[
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
labelText: 'Search memory',
|
|
1568
|
+
Row(
|
|
1569
|
+
children: <Widget>[
|
|
1570
|
+
Expanded(
|
|
1571
|
+
child: const _SectionTitle('Knowledge Graph'),
|
|
1589
1572
|
),
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
+
],
|
|
1592
1581
|
),
|
|
1593
|
-
const SizedBox(
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
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,
|
|
1588
|
+
),
|
|
1597
1589
|
),
|
|
1598
|
-
const SizedBox(
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
child:
|
|
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
|
+
),
|
|
1602
1600
|
),
|
|
1603
1601
|
],
|
|
1604
1602
|
),
|
|
1605
|
-
|
|
1603
|
+
),
|
|
1606
1604
|
),
|
|
1607
1605
|
),
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1606
|
+
const SizedBox(height: 16),
|
|
1607
|
+
],
|
|
1608
|
+
|
|
1609
|
+
// --- Tabbed content ---
|
|
1610
|
+
_EntranceMotion(
|
|
1611
|
+
child: Card(
|
|
1613
1612
|
child: Column(
|
|
1614
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1615
1613
|
children: <Widget>[
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
_MetaPill(
|
|
1628
|
-
label:
|
|
1629
|
-
'Avg importance ${controller.memoryOverview.stats.averageImportance.toStringAsFixed(1)}',
|
|
1630
|
-
icon: Icons.priority_high_outlined,
|
|
1631
|
-
),
|
|
1632
|
-
_MetaPill(
|
|
1633
|
-
label:
|
|
1634
|
-
'${controller.memoryOverview.stats.ingestionDocuments} ingested docs',
|
|
1635
|
-
icon: Icons.source_outlined,
|
|
1636
|
-
),
|
|
1637
|
-
],
|
|
1638
|
-
),
|
|
1639
|
-
const SizedBox(height: 16),
|
|
1640
|
-
if (controller.memoryOverview.entities.isNotEmpty) ...<Widget>[
|
|
1641
|
-
Text(
|
|
1642
|
-
'Top entities',
|
|
1643
|
-
style: TextStyle(fontWeight: FontWeight.w700),
|
|
1644
|
-
),
|
|
1645
|
-
const SizedBox(height: 8),
|
|
1646
|
-
Wrap(
|
|
1647
|
-
spacing: 8,
|
|
1648
|
-
runSpacing: 8,
|
|
1649
|
-
children: controller.memoryOverview.entities.map((entity) {
|
|
1650
|
-
return _MetaPill(
|
|
1651
|
-
label: entity.name,
|
|
1652
|
-
icon: Icons.hub_outlined,
|
|
1653
|
-
);
|
|
1654
|
-
}).toList(),
|
|
1655
|
-
),
|
|
1656
|
-
const SizedBox(height: 16),
|
|
1657
|
-
],
|
|
1658
|
-
if (controller
|
|
1659
|
-
.memoryOverview
|
|
1660
|
-
.knowledgeViews
|
|
1661
|
-
.isNotEmpty) ...<Widget>[
|
|
1662
|
-
Text(
|
|
1663
|
-
'Reflections',
|
|
1664
|
-
style: TextStyle(fontWeight: FontWeight.w700),
|
|
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,
|
|
1665
1625
|
),
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
return Container(
|
|
1671
|
-
width: double.infinity,
|
|
1672
|
-
margin: const EdgeInsets.only(bottom: 10),
|
|
1673
|
-
padding: const EdgeInsets.all(12),
|
|
1674
|
-
decoration: BoxDecoration(
|
|
1675
|
-
color: _bgSecondary,
|
|
1676
|
-
borderRadius: BorderRadius.circular(8),
|
|
1677
|
-
border: Border.all(color: _border),
|
|
1678
|
-
),
|
|
1679
|
-
child: Column(
|
|
1680
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1626
|
+
tabs: <Widget>[
|
|
1627
|
+
Tab(
|
|
1628
|
+
child: Row(
|
|
1629
|
+
mainAxisSize: MainAxisSize.min,
|
|
1681
1630
|
children: <Widget>[
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
child: Text(
|
|
1686
|
-
view.title,
|
|
1687
|
-
style: TextStyle(fontWeight: FontWeight.w700),
|
|
1688
|
-
),
|
|
1689
|
-
),
|
|
1690
|
-
_MetaPill(
|
|
1691
|
-
label: view.viewType,
|
|
1692
|
-
icon: Icons.auto_stories_outlined,
|
|
1693
|
-
),
|
|
1694
|
-
],
|
|
1695
|
-
),
|
|
1696
|
-
if (view.summary.trim().isNotEmpty) ...<Widget>[
|
|
1697
|
-
const SizedBox(height: 8),
|
|
1698
|
-
Text(
|
|
1699
|
-
view.summary,
|
|
1700
|
-
maxLines: 4,
|
|
1701
|
-
overflow: TextOverflow.ellipsis,
|
|
1702
|
-
style: TextStyle(color: _textSecondary),
|
|
1703
|
-
),
|
|
1704
|
-
],
|
|
1631
|
+
Icon(Icons.psychology_outlined, size: 16),
|
|
1632
|
+
const SizedBox(width: 6),
|
|
1633
|
+
Text('Memories'),
|
|
1705
1634
|
],
|
|
1706
1635
|
),
|
|
1707
|
-
);
|
|
1708
|
-
}),
|
|
1709
|
-
],
|
|
1710
|
-
if (controller.memoryOverview.entities.isEmpty &&
|
|
1711
|
-
controller.memoryOverview.knowledgeViews.isEmpty)
|
|
1712
|
-
Text(
|
|
1713
|
-
'No structured entities or reflections yet.',
|
|
1714
|
-
style: TextStyle(color: _textSecondary),
|
|
1715
|
-
),
|
|
1716
|
-
],
|
|
1717
|
-
),
|
|
1718
|
-
),
|
|
1719
|
-
),
|
|
1720
|
-
const SizedBox(height: 16),
|
|
1721
|
-
Card(
|
|
1722
|
-
child: Theme(
|
|
1723
|
-
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
|
|
1724
|
-
child: ExpansionTile(
|
|
1725
|
-
initiallyExpanded: false,
|
|
1726
|
-
tilePadding: const EdgeInsets.symmetric(
|
|
1727
|
-
horizontal: 18,
|
|
1728
|
-
vertical: 6,
|
|
1729
|
-
),
|
|
1730
|
-
childrenPadding: const EdgeInsets.fromLTRB(18, 0, 18, 18),
|
|
1731
|
-
leading: Icon(Icons.swap_horiz_outlined, color: _textSecondary),
|
|
1732
|
-
title: const _SectionTitle('LLM Memory Transfer'),
|
|
1733
|
-
subtitle: Text(
|
|
1734
|
-
'Export/import memories with another AI in one shot.',
|
|
1735
|
-
style: TextStyle(color: _textSecondary),
|
|
1736
|
-
),
|
|
1737
|
-
children: <Widget>[
|
|
1738
|
-
Text(
|
|
1739
|
-
'Generate a prompt to use in another AI, then paste the response here to import memories.',
|
|
1740
|
-
style: TextStyle(color: _textSecondary),
|
|
1741
|
-
),
|
|
1742
|
-
const SizedBox(height: 12),
|
|
1743
|
-
Wrap(
|
|
1744
|
-
spacing: 10,
|
|
1745
|
-
runSpacing: 10,
|
|
1746
|
-
children: <Widget>[
|
|
1747
|
-
FilledButton.icon(
|
|
1748
|
-
onPressed: _llmPromptLoading
|
|
1749
|
-
? null
|
|
1750
|
-
: () => _loadLlmPrompt(controller),
|
|
1751
|
-
icon: Icon(Icons.auto_awesome_outlined),
|
|
1752
|
-
label: Text(
|
|
1753
|
-
_llmPromptLoading ? 'Generating...' : 'Generate Prompt',
|
|
1754
|
-
),
|
|
1755
|
-
),
|
|
1756
|
-
OutlinedButton.icon(
|
|
1757
|
-
onPressed: _llmPromptController.text.trim().isEmpty
|
|
1758
|
-
? null
|
|
1759
|
-
: _copyLlmPrompt,
|
|
1760
|
-
icon: Icon(Icons.copy_all_outlined),
|
|
1761
|
-
label: Text('Copy Prompt'),
|
|
1762
|
-
),
|
|
1763
|
-
],
|
|
1764
|
-
),
|
|
1765
|
-
const SizedBox(height: 12),
|
|
1766
|
-
TextField(
|
|
1767
|
-
controller: _llmPromptController,
|
|
1768
|
-
minLines: 6,
|
|
1769
|
-
maxLines: 10,
|
|
1770
|
-
readOnly: true,
|
|
1771
|
-
decoration: const InputDecoration(
|
|
1772
|
-
labelText: 'Prompt to paste into another AI',
|
|
1773
|
-
),
|
|
1774
|
-
),
|
|
1775
|
-
const SizedBox(height: 12),
|
|
1776
|
-
SwitchListTile.adaptive(
|
|
1777
|
-
contentPadding: EdgeInsets.zero,
|
|
1778
|
-
value: _llmApplyBehaviorNotes,
|
|
1779
|
-
onChanged: _llmImporting
|
|
1780
|
-
? null
|
|
1781
|
-
: (value) {
|
|
1782
|
-
setState(() {
|
|
1783
|
-
_llmApplyBehaviorNotes = value;
|
|
1784
|
-
});
|
|
1785
|
-
},
|
|
1786
|
-
title: Text('Apply behavior notes'),
|
|
1787
|
-
subtitle: Text(
|
|
1788
|
-
'Overwrite assistant behavior notes from the import.',
|
|
1789
|
-
),
|
|
1790
|
-
),
|
|
1791
|
-
SwitchListTile.adaptive(
|
|
1792
|
-
contentPadding: EdgeInsets.zero,
|
|
1793
|
-
value: _llmApplyCoreMemory,
|
|
1794
|
-
onChanged: _llmImporting
|
|
1795
|
-
? null
|
|
1796
|
-
: (value) {
|
|
1797
|
-
setState(() {
|
|
1798
|
-
_llmApplyCoreMemory = value;
|
|
1799
|
-
});
|
|
1800
|
-
},
|
|
1801
|
-
title: Text('Apply core memory'),
|
|
1802
|
-
subtitle: Text(
|
|
1803
|
-
'Update core memory key/value entries from the import.',
|
|
1804
|
-
),
|
|
1805
|
-
),
|
|
1806
|
-
const SizedBox(height: 16),
|
|
1807
|
-
Text(
|
|
1808
|
-
'Paste the response from the other AI below, then import.',
|
|
1809
|
-
style: TextStyle(color: _textSecondary),
|
|
1810
|
-
),
|
|
1811
|
-
const SizedBox(height: 12),
|
|
1812
|
-
TextField(
|
|
1813
|
-
controller: _llmImportController,
|
|
1814
|
-
minLines: 6,
|
|
1815
|
-
maxLines: 12,
|
|
1816
|
-
decoration: const InputDecoration(
|
|
1817
|
-
labelText: 'LLM memory export response',
|
|
1818
|
-
),
|
|
1819
|
-
),
|
|
1820
|
-
const SizedBox(height: 12),
|
|
1821
|
-
FilledButton.icon(
|
|
1822
|
-
onPressed: _llmImporting
|
|
1823
|
-
? null
|
|
1824
|
-
: () => _importLlmMemories(controller),
|
|
1825
|
-
icon: Icon(Icons.file_download_outlined),
|
|
1826
|
-
label: Text(_llmImporting ? 'Importing...' : 'Import'),
|
|
1827
|
-
),
|
|
1828
|
-
],
|
|
1829
|
-
),
|
|
1830
|
-
),
|
|
1831
|
-
),
|
|
1832
|
-
const SizedBox(height: 16),
|
|
1833
|
-
Card(
|
|
1834
|
-
child: Padding(
|
|
1835
|
-
padding: const EdgeInsets.all(18),
|
|
1836
|
-
child: Column(
|
|
1837
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1838
|
-
children: <Widget>[
|
|
1839
|
-
Row(
|
|
1840
|
-
children: <Widget>[
|
|
1841
|
-
Expanded(child: _SectionTitle('Core Memory')),
|
|
1842
|
-
TextButton.icon(
|
|
1843
|
-
onPressed: () =>
|
|
1844
|
-
_openCoreMemoryEditor(context, controller),
|
|
1845
|
-
icon: Icon(Icons.add),
|
|
1846
|
-
label: Text('Add Entry'),
|
|
1847
1636
|
),
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
else
|
|
1857
|
-
...controller.memoryOverview.coreEntries.entries.map((entry) {
|
|
1858
|
-
return Container(
|
|
1859
|
-
width: double.infinity,
|
|
1860
|
-
margin: const EdgeInsets.only(bottom: 10),
|
|
1861
|
-
padding: const EdgeInsets.all(12),
|
|
1862
|
-
decoration: BoxDecoration(
|
|
1863
|
-
color: _bgSecondary,
|
|
1864
|
-
borderRadius: BorderRadius.circular(12),
|
|
1865
|
-
border: Border.all(color: _border),
|
|
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
|
+
],
|
|
1866
1645
|
),
|
|
1646
|
+
),
|
|
1647
|
+
Tab(
|
|
1867
1648
|
child: Row(
|
|
1868
|
-
|
|
1649
|
+
mainAxisSize: MainAxisSize.min,
|
|
1869
1650
|
children: <Widget>[
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
children: <Widget>[
|
|
1874
|
-
Text(
|
|
1875
|
-
entry.key,
|
|
1876
|
-
style: TextStyle(fontWeight: FontWeight.w700),
|
|
1877
|
-
),
|
|
1878
|
-
const SizedBox(height: 6),
|
|
1879
|
-
Text(entry.value.toString()),
|
|
1880
|
-
],
|
|
1881
|
-
),
|
|
1882
|
-
),
|
|
1883
|
-
IconButton(
|
|
1884
|
-
onPressed: () => _openCoreMemoryEditor(
|
|
1885
|
-
context,
|
|
1886
|
-
controller,
|
|
1887
|
-
keyValue: entry,
|
|
1888
|
-
),
|
|
1889
|
-
icon: Icon(Icons.edit_outlined),
|
|
1890
|
-
),
|
|
1891
|
-
IconButton(
|
|
1892
|
-
onPressed: () => _confirmDelete(
|
|
1893
|
-
context,
|
|
1894
|
-
title: 'Delete core memory entry?',
|
|
1895
|
-
message:
|
|
1896
|
-
'Remove "${entry.key}" from core memory.',
|
|
1897
|
-
onConfirm: () =>
|
|
1898
|
-
controller.deleteCoreMemory(entry.key),
|
|
1899
|
-
),
|
|
1900
|
-
icon: Icon(Icons.delete_outline),
|
|
1901
|
-
),
|
|
1651
|
+
Icon(Icons.swap_horiz_outlined, size: 16),
|
|
1652
|
+
const SizedBox(width: 6),
|
|
1653
|
+
Text('Transfer'),
|
|
1902
1654
|
],
|
|
1903
1655
|
),
|
|
1904
|
-
)
|
|
1905
|
-
|
|
1906
|
-
],
|
|
1907
|
-
),
|
|
1908
|
-
),
|
|
1909
|
-
),
|
|
1910
|
-
const SizedBox(height: 16),
|
|
1911
|
-
Card(
|
|
1912
|
-
child: Padding(
|
|
1913
|
-
padding: const EdgeInsets.all(18),
|
|
1914
|
-
child: Column(
|
|
1915
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1916
|
-
children: <Widget>[
|
|
1917
|
-
const _SectionTitle('Memories'),
|
|
1918
|
-
const SizedBox(height: 6),
|
|
1919
|
-
Text(
|
|
1920
|
-
showingSearchResults
|
|
1921
|
-
? 'Showing search results. Select memories to archive or delete them together.'
|
|
1922
|
-
: 'Select one or more memories to archive or delete them together.',
|
|
1923
|
-
style: TextStyle(color: _textSecondary),
|
|
1656
|
+
),
|
|
1657
|
+
],
|
|
1924
1658
|
),
|
|
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
|
-
|
|
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
|
+
),
|
|
1686
|
+
),
|
|
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
|
+
],
|
|
1958
1715
|
),
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
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,
|
|
1734
|
+
),
|
|
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)',
|
|
1790
|
+
),
|
|
1791
|
+
),
|
|
1792
|
+
],
|
|
1793
|
+
],
|
|
1972
1794
|
),
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
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
|
-
|
|
1795
|
+
],
|
|
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
|
+
],
|
|
1836
|
+
),
|
|
2005
1837
|
),
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
value ?? false,
|
|
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
|
+
),
|
|
2023
1854
|
),
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
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,
|
|
2054
1899
|
),
|
|
2055
|
-
|
|
2056
|
-
|
|
1900
|
+
),
|
|
1901
|
+
const SizedBox(height: 6),
|
|
1902
|
+
Text(
|
|
1903
|
+
entry.value.toString(),
|
|
1904
|
+
),
|
|
1905
|
+
],
|
|
2057
1906
|
),
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
'This memory entry will be removed permanently.',
|
|
2066
|
-
onConfirm: () =>
|
|
2067
|
-
_deleteSingleMemory(
|
|
2068
|
-
controller,
|
|
2069
|
-
memory.id,
|
|
2070
|
-
),
|
|
2071
|
-
),
|
|
2072
|
-
icon: Icon(Icons.delete_outline),
|
|
1907
|
+
),
|
|
1908
|
+
IconButton(
|
|
1909
|
+
onPressed: () =>
|
|
1910
|
+
_openCoreMemoryEditor(
|
|
1911
|
+
context,
|
|
1912
|
+
controller,
|
|
1913
|
+
keyValue: entry,
|
|
2073
1914
|
),
|
|
2074
|
-
|
|
2075
|
-
),
|
|
2076
|
-
const SizedBox(height: 10),
|
|
2077
|
-
Text(memory.content),
|
|
2078
|
-
if (memory
|
|
2079
|
-
.entities
|
|
2080
|
-
.isNotEmpty) ...<Widget>[
|
|
2081
|
-
const SizedBox(height: 8),
|
|
2082
|
-
Wrap(
|
|
2083
|
-
spacing: 8,
|
|
2084
|
-
runSpacing: 8,
|
|
2085
|
-
children: memory.entities.take(6).map(
|
|
2086
|
-
(entity) {
|
|
2087
|
-
return _MetaPill(
|
|
2088
|
-
label: entity.name,
|
|
2089
|
-
icon: Icons.hub_outlined,
|
|
2090
|
-
);
|
|
2091
|
-
},
|
|
2092
|
-
).toList(),
|
|
1915
|
+
icon: Icon(Icons.edit_outlined),
|
|
2093
1916
|
),
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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),
|
|
2101
1930
|
),
|
|
2102
|
-
|
|
2103
|
-
|
|
1931
|
+
],
|
|
1932
|
+
),
|
|
1933
|
+
);
|
|
1934
|
+
}),
|
|
1935
|
+
],
|
|
1936
|
+
),
|
|
1937
|
+
),
|
|
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
|
+
),
|
|
2104
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',
|
|
2105
1989
|
),
|
|
2106
|
-
|
|
2107
|
-
|
|
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
|
+
],
|
|
2108
2043
|
),
|
|
2109
2044
|
),
|
|
2110
|
-
|
|
2045
|
+
],
|
|
2111
2046
|
);
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
),
|
|
2115
|
-
),
|
|
2116
|
-
),
|
|
2117
|
-
const SizedBox(height: 16),
|
|
2118
|
-
Card(
|
|
2119
|
-
child: Padding(
|
|
2120
|
-
padding: const EdgeInsets.all(18),
|
|
2121
|
-
child: Column(
|
|
2122
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2123
|
-
children: <Widget>[
|
|
2124
|
-
const _SectionTitle('Recent Conversations'),
|
|
2125
|
-
const SizedBox(height: 10),
|
|
2126
|
-
if (controller.memoryConversations.isEmpty)
|
|
2127
|
-
Text(
|
|
2128
|
-
'No recent conversations found.',
|
|
2129
|
-
style: TextStyle(color: _textSecondary),
|
|
2130
|
-
)
|
|
2131
|
-
else
|
|
2132
|
-
...controller.memoryConversations.map(
|
|
2133
|
-
(conversation) => Padding(
|
|
2134
|
-
padding: const EdgeInsets.only(bottom: 10),
|
|
2135
|
-
child: Container(
|
|
2136
|
-
width: double.infinity,
|
|
2137
|
-
padding: const EdgeInsets.all(12),
|
|
2138
|
-
decoration: BoxDecoration(
|
|
2139
|
-
color: _bgSecondary,
|
|
2140
|
-
borderRadius: BorderRadius.circular(12),
|
|
2141
|
-
border: Border.all(color: _border),
|
|
2142
|
-
),
|
|
2143
|
-
child: Column(
|
|
2144
|
-
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2145
|
-
children: <Widget>[
|
|
2146
|
-
Text(
|
|
2147
|
-
conversation.title,
|
|
2148
|
-
style: TextStyle(fontWeight: FontWeight.w700),
|
|
2149
|
-
),
|
|
2150
|
-
const SizedBox(height: 8),
|
|
2151
|
-
Text(
|
|
2152
|
-
conversation.preview,
|
|
2153
|
-
style: TextStyle(color: _textSecondary),
|
|
2154
|
-
),
|
|
2155
|
-
],
|
|
2156
|
-
),
|
|
2157
|
-
),
|
|
2158
|
-
),
|
|
2159
|
-
),
|
|
2047
|
+
},
|
|
2048
|
+
),
|
|
2160
2049
|
],
|
|
2161
2050
|
),
|
|
2162
2051
|
),
|
|
@@ -2206,9 +2095,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2206
2095
|
],
|
|
2207
2096
|
decoration: const InputDecoration(labelText: 'Category'),
|
|
2208
2097
|
onChanged: (value) {
|
|
2209
|
-
if (value != null)
|
|
2210
|
-
category = value;
|
|
2211
|
-
}
|
|
2098
|
+
if (value != null) category = value;
|
|
2212
2099
|
},
|
|
2213
2100
|
),
|
|
2214
2101
|
const SizedBox(height: 12),
|
|
@@ -2239,9 +2126,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2239
2126
|
importance:
|
|
2240
2127
|
int.tryParse(importanceController.text.trim()) ?? 5,
|
|
2241
2128
|
);
|
|
2242
|
-
if (context.mounted)
|
|
2243
|
-
Navigator.of(context).pop();
|
|
2244
|
-
}
|
|
2129
|
+
if (context.mounted) Navigator.of(context).pop();
|
|
2245
2130
|
},
|
|
2246
2131
|
child: Text('Save'),
|
|
2247
2132
|
),
|
|
@@ -2285,9 +2170,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2285
2170
|
await controller.updateAssistantBehaviorNotes(
|
|
2286
2171
|
contentController.text,
|
|
2287
2172
|
);
|
|
2288
|
-
if (context.mounted)
|
|
2289
|
-
Navigator.of(context).pop();
|
|
2290
|
-
}
|
|
2173
|
+
if (context.mounted) Navigator.of(context).pop();
|
|
2291
2174
|
},
|
|
2292
2175
|
child: Text('Save'),
|
|
2293
2176
|
),
|
|
@@ -2346,9 +2229,7 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2346
2229
|
keyController.text.trim(),
|
|
2347
2230
|
valueController.text.trim(),
|
|
2348
2231
|
);
|
|
2349
|
-
if (context.mounted)
|
|
2350
|
-
Navigator.of(context).pop();
|
|
2351
|
-
}
|
|
2232
|
+
if (context.mounted) Navigator.of(context).pop();
|
|
2352
2233
|
},
|
|
2353
2234
|
child: Text('Save'),
|
|
2354
2235
|
),
|
|
@@ -2359,6 +2240,680 @@ class _MemoryPanelState extends State<MemoryPanel> {
|
|
|
2359
2240
|
}
|
|
2360
2241
|
}
|
|
2361
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
|
+
}
|
|
2380
|
+
|
|
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,
|
|
2413
|
+
);
|
|
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,
|
|
2490
|
+
children: <Widget>[
|
|
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
|
+
),
|
|
2558
|
+
),
|
|
2559
|
+
],
|
|
2560
|
+
),
|
|
2561
|
+
),
|
|
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
|
+
),
|
|
2745
|
+
);
|
|
2746
|
+
},
|
|
2747
|
+
),
|
|
2748
|
+
),
|
|
2749
|
+
);
|
|
2750
|
+
},
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
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
|
+
|
|
2362
2917
|
class WidgetsPanel extends StatelessWidget {
|
|
2363
2918
|
const WidgetsPanel({super.key, required this.controller});
|
|
2364
2919
|
|