lemma-mcp 0.11.0 → 0.11.2
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/assets/visualizer.html +80 -0
- package/dist/index.js +25 -4
- package/dist/index.js.map +1 -1
- package/dist/server/agents-md.js +1 -1
- package/dist/server/visualizer.html +80 -0
- package/package.json +1 -1
package/assets/visualizer.html
CHANGED
|
@@ -195,6 +195,22 @@ select:focus{outline:none;border-color:var(--accent)}
|
|
|
195
195
|
#tooltip .tt-title{font-weight:600;color:var(--text);margin-bottom:2px}
|
|
196
196
|
#tooltip .tt-meta{color:var(--text2)}
|
|
197
197
|
|
|
198
|
+
#ctx-menu{
|
|
199
|
+
position:fixed;z-index:50;min-width:180px;
|
|
200
|
+
background:var(--bg2);border:1px solid var(--border);border-radius:10px;
|
|
201
|
+
box-shadow:0 8px 24px rgba(0,0,0,.12);padding:4px;
|
|
202
|
+
display:none;
|
|
203
|
+
}
|
|
204
|
+
#ctx-menu.open{display:block}
|
|
205
|
+
.ctx-item{
|
|
206
|
+
display:flex;align-items:center;gap:8px;padding:7px 12px;border-radius:6px;
|
|
207
|
+
font-size:13px;color:var(--text);cursor:pointer;border:none;background:none;width:100%;text-align:left;
|
|
208
|
+
}
|
|
209
|
+
.ctx-item:hover{background:var(--bg3)}
|
|
210
|
+
.ctx-item.danger{color:var(--danger)}
|
|
211
|
+
.ctx-sep{height:1px;background:var(--border);margin:3px 8px}
|
|
212
|
+
.ctx-icon{width:16px;text-align:center;font-size:13px;opacity:.7}
|
|
213
|
+
|
|
198
214
|
::-webkit-scrollbar{width:6px}
|
|
199
215
|
::-webkit-scrollbar-track{background:var(--bg)}
|
|
200
216
|
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
@@ -434,6 +450,17 @@ select:focus{outline:none;border-color:var(--accent)}
|
|
|
434
450
|
</div>
|
|
435
451
|
</div>
|
|
436
452
|
|
|
453
|
+
<div id="ctx-menu">
|
|
454
|
+
<button class="ctx-item" data-action="detail"><span class="ctx-icon">📋</span>View Detail</button>
|
|
455
|
+
<button class="ctx-item" data-action="edit"><span class="ctx-icon">✏</span>Edit</button>
|
|
456
|
+
<div class="ctx-sep"></div>
|
|
457
|
+
<button class="ctx-item" data-action="link"><span class="ctx-icon">🔗</span>Link from here...</button>
|
|
458
|
+
<button class="ctx-item" data-action="unlink"><span class="ctx-icon">✖</span>Remove all links</button>
|
|
459
|
+
<div class="ctx-sep"></div>
|
|
460
|
+
<button class="ctx-item" data-action="select"><span class="ctx-icon">☑</span>Select</button>
|
|
461
|
+
<button class="ctx-item danger" data-action="delete"><span class="ctx-icon">🗑</span>Delete</button>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
437
464
|
<script>
|
|
438
465
|
const TYPE_COLORS = {fact:'#6ba3d6',pattern:'#a47dc4',lesson:'#5cb88a',warning:'#d97c6b',context:'#d4a94e'};
|
|
439
466
|
const LINK_COLORS = {associated:'#c4bfb6',related:'#b8b0c4',related_to:'#b8b0c4',contradicts:'#d97c6b',supersedes:'#d4a94e',supports:'#5cb88a'};
|
|
@@ -676,6 +703,16 @@ function renderGraph() {
|
|
|
676
703
|
.attr('dy', d => nodeRadius(d.confidence, d.access_count) + 13)
|
|
677
704
|
.text(d => { const t = d.title||''; return t.length > 18 ? t.slice(0,16)+'..' : t; });
|
|
678
705
|
|
|
706
|
+
nodeEls.on('contextmenu', (e, d) => {
|
|
707
|
+
e.preventDefault();
|
|
708
|
+
e.stopPropagation();
|
|
709
|
+
const menu = document.getElementById('ctx-menu');
|
|
710
|
+
menu._targetNode = d;
|
|
711
|
+
menu.style.left = e.clientX + 'px';
|
|
712
|
+
menu.style.top = e.clientY + 'px';
|
|
713
|
+
menu.classList.add('open');
|
|
714
|
+
});
|
|
715
|
+
|
|
679
716
|
nodeEls.on('click', (e, d) => {
|
|
680
717
|
e.stopPropagation();
|
|
681
718
|
if (linkMode) {
|
|
@@ -708,6 +745,7 @@ function renderGraph() {
|
|
|
708
745
|
nodeEls.on('pointerleave', () => tooltip.classList.remove('visible'));
|
|
709
746
|
|
|
710
747
|
svg.on('click', () => {
|
|
748
|
+
document.getElementById('ctx-menu').classList.remove('open');
|
|
711
749
|
if (!linkMode) hideDetail();
|
|
712
750
|
selectedLinkData = null;
|
|
713
751
|
document.getElementById('unlink-bar').style.display = 'none';
|
|
@@ -985,6 +1023,48 @@ if (SERVER_MODE) {
|
|
|
985
1023
|
}
|
|
986
1024
|
})();
|
|
987
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
document.getElementById('ctx-menu').addEventListener('click', async (e) => {
|
|
1028
|
+
const btn = e.target.closest('.ctx-item');
|
|
1029
|
+
if (!btn) return;
|
|
1030
|
+
const menu = document.getElementById('ctx-menu');
|
|
1031
|
+
const d = menu._targetNode;
|
|
1032
|
+
menu.classList.remove('open');
|
|
1033
|
+
if (!d) return;
|
|
1034
|
+
|
|
1035
|
+
const action = btn.dataset.action;
|
|
1036
|
+
if (action === 'detail') showDetail(d);
|
|
1037
|
+
else if (action === 'edit') openEditModal(d);
|
|
1038
|
+
else if (action === 'delete') { if (confirm('Delete "' + d.title + '"?')) await deleteFragment(d.id); }
|
|
1039
|
+
else if (action === 'select') {
|
|
1040
|
+
if (!selectedNodes.has(d.id)) selectedNodes.add(d.id);
|
|
1041
|
+
updateStats(); updateBulkBar(); renderGraph();
|
|
1042
|
+
}
|
|
1043
|
+
else if (action === 'link') {
|
|
1044
|
+
linkSource = d.id;
|
|
1045
|
+
linkMode = true;
|
|
1046
|
+
document.getElementById('graph-container').classList.add('link-mode');
|
|
1047
|
+
document.getElementById('link-bar').style.display = 'flex';
|
|
1048
|
+
document.getElementById('link-step-text').textContent = `Source: "${d.title.slice(0,25)}" — click target node`;
|
|
1049
|
+
document.getElementById('btn-link').classList.add('active');
|
|
1050
|
+
nodeGroup.selectAll('.node-group').classed('link-source', n => n.id === d.id);
|
|
1051
|
+
}
|
|
1052
|
+
else if (action === 'unlink') {
|
|
1053
|
+
const count = relations.filter(r => r.source === d.id || r.target === d.id).length;
|
|
1054
|
+
if (count === 0) return;
|
|
1055
|
+
if (!confirm(`Remove all ${count} link(s) from "${d.title}"?`)) return;
|
|
1056
|
+
const toRemove = relations.filter(r => r.source === d.id || r.target === d.id);
|
|
1057
|
+
for (const r of toRemove) {
|
|
1058
|
+
if (SERVER_MODE) await api('DELETE', 'relations', { source: r.source, target: r.target, type: r.type });
|
|
1059
|
+
}
|
|
1060
|
+
relations = relations.filter(r => r.source !== d.id && r.target !== d.id);
|
|
1061
|
+
updateUI();
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
document.addEventListener('click', (e) => {
|
|
1066
|
+
if (!e.target.closest('#ctx-menu')) document.getElementById('ctx-menu').classList.remove('open');
|
|
1067
|
+
});
|
|
988
1068
|
</script>
|
|
989
1069
|
</body>
|
|
990
1070
|
</html>
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "child_process";
|
|
2
3
|
import { startServer, runLibMode } from "./server/index.js";
|
|
3
4
|
import { startVisualizeServer } from "./server/visualize.js";
|
|
4
5
|
const args = process.argv.slice(2);
|
|
@@ -11,10 +12,30 @@ if (args.includes("-lib")) {
|
|
|
11
12
|
else if (args.includes("-vis")) {
|
|
12
13
|
const portIdx = args.indexOf("-p");
|
|
13
14
|
const port = portIdx !== -1 && args[portIdx + 1] ? parseInt(args[portIdx + 1], 10) : undefined;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
if (!args.includes("--fg")) {
|
|
16
|
+
const serverArgs = [process.argv[1], "-vis", "--fg"];
|
|
17
|
+
if (port) {
|
|
18
|
+
serverArgs.push("-p", String(port));
|
|
19
|
+
}
|
|
20
|
+
const child = spawn(process.execPath, serverArgs, {
|
|
21
|
+
detached: true,
|
|
22
|
+
stdio: "ignore",
|
|
23
|
+
windowsHide: true,
|
|
24
|
+
});
|
|
25
|
+
child.unref();
|
|
26
|
+
console.error(``);
|
|
27
|
+
console.error(` Lemma Visualizer started in background (PID ${child.pid})`);
|
|
28
|
+
console.error(` http://localhost:${port || 3456}`);
|
|
29
|
+
console.error(` Stop: kill ${child.pid}`);
|
|
30
|
+
console.error(``);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
startVisualizeServer(port).catch((error) => {
|
|
35
|
+
console.error("Fatal error:", error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
18
39
|
}
|
|
19
40
|
else {
|
|
20
41
|
startServer().catch((error) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IAC1B,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE;QAClC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;KAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAE7D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IAC1B,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE;QAClC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;KAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAE/F,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACrD,IAAI,IAAI,EAAE,CAAC;YAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;QAAC,CAAC;QAElD,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE;YAChD,QAAQ,EAAE,IAAI;YACd,KAAK,EAAE,QAAQ;YACf,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QACH,KAAK,CAAC,KAAK,EAAE,CAAC;QAEd,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,iDAAiD,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;QAC7E,OAAO,CAAC,KAAK,CAAC,sBAAsB,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;QACpD,OAAO,CAAC,KAAK,CAAC,gBAAgB,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;QAC3C,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;SAAM,CAAC;QACN,oBAAoB,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE;YAChD,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;YACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;KAAM,CAAC;IACN,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE;QACnC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/server/agents-md.js
CHANGED
|
@@ -123,7 +123,7 @@ Relations are bidirectional — the reverse relation is created automatically.
|
|
|
123
123
|
When the user sends one of these shorthand commands, execute the corresponding action immediately:
|
|
124
124
|
|
|
125
125
|
- **-lib** → Call memory_library. This gives a full snapshot of your knowledge base with analysis signals, stale fragments, distill candidates, and suggested actions. After reviewing the snapshot, take maintenance actions as needed (merge, forget, distill, relate).
|
|
126
|
-
- **-vis** → Run npx lemma-mcp -vis via bash
|
|
126
|
+
- **-vis** → Run npx lemma-mcp -vis via bash. It auto-daemonizes and stays alive in background. Then sleep 2 and curl http://localhost:3456/api/health to confirm.
|
|
127
127
|
</user_commands>`;
|
|
128
128
|
export function injectAgentsMd(projectDir) {
|
|
129
129
|
const agentsPath = path.join(projectDir, "AGENTS.md");
|
|
@@ -195,6 +195,22 @@ select:focus{outline:none;border-color:var(--accent)}
|
|
|
195
195
|
#tooltip .tt-title{font-weight:600;color:var(--text);margin-bottom:2px}
|
|
196
196
|
#tooltip .tt-meta{color:var(--text2)}
|
|
197
197
|
|
|
198
|
+
#ctx-menu{
|
|
199
|
+
position:fixed;z-index:50;min-width:180px;
|
|
200
|
+
background:var(--bg2);border:1px solid var(--border);border-radius:10px;
|
|
201
|
+
box-shadow:0 8px 24px rgba(0,0,0,.12);padding:4px;
|
|
202
|
+
display:none;
|
|
203
|
+
}
|
|
204
|
+
#ctx-menu.open{display:block}
|
|
205
|
+
.ctx-item{
|
|
206
|
+
display:flex;align-items:center;gap:8px;padding:7px 12px;border-radius:6px;
|
|
207
|
+
font-size:13px;color:var(--text);cursor:pointer;border:none;background:none;width:100%;text-align:left;
|
|
208
|
+
}
|
|
209
|
+
.ctx-item:hover{background:var(--bg3)}
|
|
210
|
+
.ctx-item.danger{color:var(--danger)}
|
|
211
|
+
.ctx-sep{height:1px;background:var(--border);margin:3px 8px}
|
|
212
|
+
.ctx-icon{width:16px;text-align:center;font-size:13px;opacity:.7}
|
|
213
|
+
|
|
198
214
|
::-webkit-scrollbar{width:6px}
|
|
199
215
|
::-webkit-scrollbar-track{background:var(--bg)}
|
|
200
216
|
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
@@ -434,6 +450,17 @@ select:focus{outline:none;border-color:var(--accent)}
|
|
|
434
450
|
</div>
|
|
435
451
|
</div>
|
|
436
452
|
|
|
453
|
+
<div id="ctx-menu">
|
|
454
|
+
<button class="ctx-item" data-action="detail"><span class="ctx-icon">📋</span>View Detail</button>
|
|
455
|
+
<button class="ctx-item" data-action="edit"><span class="ctx-icon">✏</span>Edit</button>
|
|
456
|
+
<div class="ctx-sep"></div>
|
|
457
|
+
<button class="ctx-item" data-action="link"><span class="ctx-icon">🔗</span>Link from here...</button>
|
|
458
|
+
<button class="ctx-item" data-action="unlink"><span class="ctx-icon">✖</span>Remove all links</button>
|
|
459
|
+
<div class="ctx-sep"></div>
|
|
460
|
+
<button class="ctx-item" data-action="select"><span class="ctx-icon">☑</span>Select</button>
|
|
461
|
+
<button class="ctx-item danger" data-action="delete"><span class="ctx-icon">🗑</span>Delete</button>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
437
464
|
<script>
|
|
438
465
|
const TYPE_COLORS = {fact:'#6ba3d6',pattern:'#a47dc4',lesson:'#5cb88a',warning:'#d97c6b',context:'#d4a94e'};
|
|
439
466
|
const LINK_COLORS = {associated:'#c4bfb6',related:'#b8b0c4',related_to:'#b8b0c4',contradicts:'#d97c6b',supersedes:'#d4a94e',supports:'#5cb88a'};
|
|
@@ -676,6 +703,16 @@ function renderGraph() {
|
|
|
676
703
|
.attr('dy', d => nodeRadius(d.confidence, d.access_count) + 13)
|
|
677
704
|
.text(d => { const t = d.title||''; return t.length > 18 ? t.slice(0,16)+'..' : t; });
|
|
678
705
|
|
|
706
|
+
nodeEls.on('contextmenu', (e, d) => {
|
|
707
|
+
e.preventDefault();
|
|
708
|
+
e.stopPropagation();
|
|
709
|
+
const menu = document.getElementById('ctx-menu');
|
|
710
|
+
menu._targetNode = d;
|
|
711
|
+
menu.style.left = e.clientX + 'px';
|
|
712
|
+
menu.style.top = e.clientY + 'px';
|
|
713
|
+
menu.classList.add('open');
|
|
714
|
+
});
|
|
715
|
+
|
|
679
716
|
nodeEls.on('click', (e, d) => {
|
|
680
717
|
e.stopPropagation();
|
|
681
718
|
if (linkMode) {
|
|
@@ -708,6 +745,7 @@ function renderGraph() {
|
|
|
708
745
|
nodeEls.on('pointerleave', () => tooltip.classList.remove('visible'));
|
|
709
746
|
|
|
710
747
|
svg.on('click', () => {
|
|
748
|
+
document.getElementById('ctx-menu').classList.remove('open');
|
|
711
749
|
if (!linkMode) hideDetail();
|
|
712
750
|
selectedLinkData = null;
|
|
713
751
|
document.getElementById('unlink-bar').style.display = 'none';
|
|
@@ -985,6 +1023,48 @@ if (SERVER_MODE) {
|
|
|
985
1023
|
}
|
|
986
1024
|
})();
|
|
987
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
document.getElementById('ctx-menu').addEventListener('click', async (e) => {
|
|
1028
|
+
const btn = e.target.closest('.ctx-item');
|
|
1029
|
+
if (!btn) return;
|
|
1030
|
+
const menu = document.getElementById('ctx-menu');
|
|
1031
|
+
const d = menu._targetNode;
|
|
1032
|
+
menu.classList.remove('open');
|
|
1033
|
+
if (!d) return;
|
|
1034
|
+
|
|
1035
|
+
const action = btn.dataset.action;
|
|
1036
|
+
if (action === 'detail') showDetail(d);
|
|
1037
|
+
else if (action === 'edit') openEditModal(d);
|
|
1038
|
+
else if (action === 'delete') { if (confirm('Delete "' + d.title + '"?')) await deleteFragment(d.id); }
|
|
1039
|
+
else if (action === 'select') {
|
|
1040
|
+
if (!selectedNodes.has(d.id)) selectedNodes.add(d.id);
|
|
1041
|
+
updateStats(); updateBulkBar(); renderGraph();
|
|
1042
|
+
}
|
|
1043
|
+
else if (action === 'link') {
|
|
1044
|
+
linkSource = d.id;
|
|
1045
|
+
linkMode = true;
|
|
1046
|
+
document.getElementById('graph-container').classList.add('link-mode');
|
|
1047
|
+
document.getElementById('link-bar').style.display = 'flex';
|
|
1048
|
+
document.getElementById('link-step-text').textContent = `Source: "${d.title.slice(0,25)}" — click target node`;
|
|
1049
|
+
document.getElementById('btn-link').classList.add('active');
|
|
1050
|
+
nodeGroup.selectAll('.node-group').classed('link-source', n => n.id === d.id);
|
|
1051
|
+
}
|
|
1052
|
+
else if (action === 'unlink') {
|
|
1053
|
+
const count = relations.filter(r => r.source === d.id || r.target === d.id).length;
|
|
1054
|
+
if (count === 0) return;
|
|
1055
|
+
if (!confirm(`Remove all ${count} link(s) from "${d.title}"?`)) return;
|
|
1056
|
+
const toRemove = relations.filter(r => r.source === d.id || r.target === d.id);
|
|
1057
|
+
for (const r of toRemove) {
|
|
1058
|
+
if (SERVER_MODE) await api('DELETE', 'relations', { source: r.source, target: r.target, type: r.type });
|
|
1059
|
+
}
|
|
1060
|
+
relations = relations.filter(r => r.source !== d.id && r.target !== d.id);
|
|
1061
|
+
updateUI();
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
document.addEventListener('click', (e) => {
|
|
1066
|
+
if (!e.target.closest('#ctx-menu')) document.getElementById('ctx-menu').classList.remove('open');
|
|
1067
|
+
});
|
|
988
1068
|
</script>
|
|
989
1069
|
</body>
|
|
990
1070
|
</html>
|