paperclip-plugin-supermemory-otomatik 0.2.13 → 0.2.15
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/README.md +3 -3
- package/dist/manifest.js +1 -1
- package/dist/manifest.js.map +1 -1
- package/dist/ui/index.js +183 -43
- package/dist/ui/index.js.map +2 -2
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Paperclip plugin for Supermemory Local. It adds a native **Memory** sidebar page
|
|
|
6
6
|
|
|
7
7
|
- **Plugin settings:** chooses Paperclip agents from the active organization list and binds one individual ACL secret to each selected agent. The user no longer copies agent UUIDs.
|
|
8
8
|
- **Memory page:** adds a native sidebar entry, lets an Owner assign one isolated Supermemory namespace to an agent or project, and manages text, URLs, and supported files.
|
|
9
|
-
- **Memory graph:** adds a **
|
|
9
|
+
- **Memory graph:** adds a **Graph** submenu. An Owner chooses one active namespace assignment, searches for a topic, then explores the related memories returned by Supermemory in an interactive force-directed view inspired by Obsidian. Drag to pan; use the wheel or zoom controls to navigate. Selecting a node lets the Owner search from that fact to expand another neighborhood. This is a navigable search neighborhood, not a claim that every memory in the namespace has been enumerated.
|
|
10
10
|
- **Agent search:** checks the trusted Paperclip agent, company, run and project context. It searches the agent's company/role ACL compartments and only the agent/project namespaces explicitly assigned to that identity.
|
|
11
11
|
- **Candidate saves:** sends candidate facts to the existing role staging compartment for human review. It does not publish them as shared memory.
|
|
12
12
|
|
|
@@ -14,14 +14,14 @@ The plugin uses Paperclip's standard plugin slots (`settingsPage`, `page`, `side
|
|
|
14
14
|
|
|
15
15
|
## Install from Paperclip
|
|
16
16
|
|
|
17
|
-
Package: [`paperclip-plugin-supermemory-otomatik`](https://www.npmjs.com/package/paperclip-plugin-supermemory-otomatik) **0.2.
|
|
17
|
+
Package: [`paperclip-plugin-supermemory-otomatik`](https://www.npmjs.com/package/paperclip-plugin-supermemory-otomatik) **0.2.15**.
|
|
18
18
|
|
|
19
19
|
1. In Paperclip, open **Settings → Plugins → Install Plugin**.
|
|
20
20
|
2. Enter `paperclip-plugin-supermemory-otomatik` and install it for the instance.
|
|
21
21
|
3. Open the plugin configuration for the target organization.
|
|
22
22
|
4. Select each authorized agent from **Agente Paperclip** and choose that agent's individual ACL secret.
|
|
23
23
|
5. Select the Paperclip secret containing the Supermemory Local master API key. This is required for the Memory page, URL/file ingestion, and explicitly assigned namespaces.
|
|
24
|
-
6. Save. **Memory** appears in the organization sidebar with the **
|
|
24
|
+
6. Save. **Memory** appears in the organization sidebar with the **Graph** submenu. The Paperclip in Italiano Chrome extension translates that label to **Grafo**.
|
|
25
25
|
|
|
26
26
|
The settings form uses the active Paperclip organization to load agents and company secrets. It persists secret references only; it never reads or renders the secret values. The configuration save endpoint requires a Paperclip instance administrator. The settings page also has **Aggiorna all’ultima versione**: only an instance admin can request an upgrade, and Paperclip pauses an update for approval if the new release adds capabilities.
|
|
27
27
|
|
package/dist/manifest.js
CHANGED
|
@@ -11,7 +11,7 @@ var MEMORY_GRAPH_ROUTE = "memory-graph";
|
|
|
11
11
|
var manifest = {
|
|
12
12
|
id: PLUGIN_ID,
|
|
13
13
|
apiVersion: 1,
|
|
14
|
-
version: "0.2.
|
|
14
|
+
version: "0.2.15",
|
|
15
15
|
minimumHostVersion: "2026.1023.0",
|
|
16
16
|
displayName: "Supermemory (self-hosted)",
|
|
17
17
|
description: "Memory tools and an Owner-managed memory library for self-hosted Supermemory, with company-, agent-, and project-scoped access.",
|
package/dist/manifest.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/manifest.ts"],
|
|
4
|
-
"sourcesContent": ["import type { PaperclipPluginManifestV1 } from \"@paperclipai/plugin-sdk\";\n\nexport const PLUGIN_ID = \"paperclipai.supermemory\";\nexport const SEARCH_TOOL = \"search-approved-memory\";\nexport const SAVE_TOOL = \"submit-memory-candidate\";\nexport const MEMORY_SETTINGS_PAGE = \"supermemory-settings\";\nexport const MEMORY_PAGE = \"supermemory-memory\";\nexport const MEMORY_GRAPH_PAGE = \"supermemory-memory-graph\";\nexport const MEMORY_SIDEBAR = \"supermemory-sidebar\";\nexport const MEMORY_ROUTE = \"memory\";\nexport const MEMORY_GRAPH_ROUTE = \"memory-graph\";\n\nconst manifest: PaperclipPluginManifestV1 = {\n id: PLUGIN_ID,\n apiVersion: 1,\n version: \"0.2.
|
|
4
|
+
"sourcesContent": ["import type { PaperclipPluginManifestV1 } from \"@paperclipai/plugin-sdk\";\n\nexport const PLUGIN_ID = \"paperclipai.supermemory\";\nexport const SEARCH_TOOL = \"search-approved-memory\";\nexport const SAVE_TOOL = \"submit-memory-candidate\";\nexport const MEMORY_SETTINGS_PAGE = \"supermemory-settings\";\nexport const MEMORY_PAGE = \"supermemory-memory\";\nexport const MEMORY_GRAPH_PAGE = \"supermemory-memory-graph\";\nexport const MEMORY_SIDEBAR = \"supermemory-sidebar\";\nexport const MEMORY_ROUTE = \"memory\";\nexport const MEMORY_GRAPH_ROUTE = \"memory-graph\";\n\nconst manifest: PaperclipPluginManifestV1 = {\n id: PLUGIN_ID,\n apiVersion: 1,\n version: \"0.2.15\",\n minimumHostVersion: \"2026.1023.0\",\n displayName: \"Supermemory (self-hosted)\",\n description: \"Memory tools and an Owner-managed memory library for self-hosted Supermemory, with company-, agent-, and project-scoped access.\",\n author: \"Otomatik\",\n categories: [\"connector\", \"automation\", \"ui\"],\n capabilities: [\n \"agent.tools.register\",\n \"agents.read\",\n \"projects.read\",\n \"access.members.read\",\n \"http.outbound\",\n \"instance.settings.register\",\n \"secrets.read-ref\",\n \"ui.page.register\",\n \"ui.sidebar.register\",\n ],\n entrypoints: {\n worker: \"./dist/worker.js\",\n ui: \"./dist/ui\",\n },\n instanceConfigSchema: {\n type: \"object\",\n properties: {\n gatewayUrl: {\n type: \"string\",\n title: \"Gateway Supermemory su Tailscale HTTPS\",\n description: \"URL HTTPS tailnet del gateway ACL e delle operazioni Owner. Esempio OTOMATIK: https://otomatik.tail77a9ab.ts.net:18443/mcp. Supermemory Local resta privato sul server.\",\n default: \"https://otomatik.tail77a9ab.ts.net:18443/mcp\",\n },\n localApiUrl: {\n type: \"string\",\n title: \"Legacy Supermemory Local API URL\",\n description: \"Legacy value retained for existing configurations; ignored since plugin 0.2.3. The Local API is reached only by the server-side gateway.\",\n },\n adminApiKeyRef: {\n // Paperclip persists a secret-ref format as an object binding, even\n // though the format marker historically describes a string picker.\n type: [\"string\", \"object\"],\n format: \"secret-ref\",\n title: \"Secret con la chiave master Supermemory Local\",\n description: \"Usata come credenziale per le route Owner del gateway Tailscale. Viene risolta solo dopo il controllo Owner o per una ricerca con namespace assegnato esplicitamente; non viene esposta nei prompt.\",\n },\n agentGrants: {\n type: \"array\",\n title: \"Authorized agents\",\n description: \"Only listed Paperclip agents can use the memory tools. Each one uses its own ACL gateway token secret, already bound to the same agent, company, and role.\",\n items: {\n type: \"object\",\n title: \"Agent grant\",\n required: [\"agentId\", \"tokenRef\"],\n properties: {\n agentId: {\n type: \"string\",\n title: \"Paperclip agent\",\n minLength: 1,\n },\n tokenRef: {\n type: [\"string\", \"object\"],\n format: \"secret-ref\",\n title: \"This agent's ACL token secret\",\n description: \"Select the Paperclip company secret containing only this agent's Supermemory ACL token.\",\n },\n },\n },\n default: [],\n },\n },\n },\n tools: [\n {\n name: SEARCH_TOOL,\n displayName: \"Search approved Supermemory\",\n description: \"Search this agent's explicitly authorized company/team compartments and any assigned agent/project namespaces. Project memory is available only when the Paperclip run is in that project and the agent is on its explicit allowlist. Do not search for credentials or personal records.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n query: { type: \"string\", minLength: 1, maxLength: 300 },\n searchMode: { type: \"string\", enum: [\"hybrid\", \"memories\", \"chunks\"], default: \"hybrid\" },\n limit: { type: \"integer\", minimum: 1, maximum: 5, default: 3 },\n },\n required: [\"query\"],\n additionalProperties: false,\n },\n },\n {\n name: SAVE_TOOL,\n displayName: \"Submit a Supermemory candidate\",\n description: \"Submit one durable, non-sensitive fact or decision to this agent's role staging compartment for human review. This does not publish it to shared approved memory. Never submit credentials, personal data, customer cases, legal/health/workplace records, or full chat transcripts.\",\n parametersSchema: {\n type: \"object\",\n properties: {\n content: { type: \"string\", minLength: 20, maxLength: 1200 },\n },\n required: [\"content\"],\n additionalProperties: false,\n },\n },\n ],\n ui: {\n slots: [\n {\n type: \"settingsPage\",\n id: MEMORY_SETTINGS_PAGE,\n displayName: \"Supermemory settings\",\n exportName: \"SupermemorySettingsPage\",\n },\n {\n type: \"page\",\n id: MEMORY_PAGE,\n displayName: \"Memory\",\n exportName: \"MemoryPage\",\n routePath: MEMORY_ROUTE,\n },\n {\n type: \"page\",\n id: MEMORY_GRAPH_PAGE,\n displayName: \"Memory graph\",\n exportName: \"MemoryGraphPage\",\n routePath: MEMORY_GRAPH_ROUTE,\n },\n {\n type: \"sidebar\",\n id: MEMORY_SIDEBAR,\n displayName: \"Memory\",\n exportName: \"MemorySidebarLink\",\n },\n ],\n },\n};\n\nexport default manifest;\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/ui/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/ui/index.tsx
|
|
2
|
-
import { useCallback, useEffect as useEffect2, useLayoutEffect, useRef, useState as useState2 } from "react";
|
|
2
|
+
import { useCallback, useEffect as useEffect2, useLayoutEffect, useMemo, useRef, useState as useState2 } from "react";
|
|
3
3
|
import {
|
|
4
4
|
MarkdownBlock,
|
|
5
5
|
useHostContext,
|
|
@@ -368,7 +368,7 @@ function MemorySidebarLink({ context }) {
|
|
|
368
368
|
{
|
|
369
369
|
...navigation.linkProps("/memory-graph"),
|
|
370
370
|
"aria-current": graphActive ? "page" : void 0,
|
|
371
|
-
className: `ml-
|
|
371
|
+
className: `ml-4 flex items-center gap-2 rounded-lg px-2 py-1.5 text-(length:--text-compact) transition-colors ${graphActive ? "bg-sidebar-accent text-sidebar-accent-foreground" : "text-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"}`,
|
|
372
372
|
children: [
|
|
373
373
|
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: "flex h-4 w-4 shrink-0 items-center justify-center", children: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
374
374
|
/* @__PURE__ */ jsx2("circle", { cx: "5", cy: "12", r: "2" }),
|
|
@@ -376,7 +376,7 @@ function MemorySidebarLink({ context }) {
|
|
|
376
376
|
/* @__PURE__ */ jsx2("circle", { cx: "18", cy: "19", r: "2" }),
|
|
377
377
|
/* @__PURE__ */ jsx2("path", { d: "m7 11 9-5M7 13l9 5" })
|
|
378
378
|
] }) }),
|
|
379
|
-
/* @__PURE__ */ jsx2("span", { className: "flex-1 truncate", children: "
|
|
379
|
+
/* @__PURE__ */ jsx2("span", { "data-plugin-sidebar-label": "true", className: "flex-1 truncate", children: "Graph" })
|
|
380
380
|
]
|
|
381
381
|
}
|
|
382
382
|
)
|
|
@@ -939,6 +939,77 @@ function MemoryPage() {
|
|
|
939
939
|
] })
|
|
940
940
|
] });
|
|
941
941
|
}
|
|
942
|
+
function layoutMemoryGraph(graph) {
|
|
943
|
+
const nodes = graph?.nodes ?? [];
|
|
944
|
+
const points = /* @__PURE__ */ new Map();
|
|
945
|
+
if (!nodes.length) return points;
|
|
946
|
+
const width = 1e3;
|
|
947
|
+
const height = 720;
|
|
948
|
+
const centerX = width / 2;
|
|
949
|
+
const centerY = height / 2;
|
|
950
|
+
const hash = (value) => [...value].reduce((sum, char) => sum * 31 + char.charCodeAt(0) >>> 0, 7);
|
|
951
|
+
nodes.forEach((node, index) => {
|
|
952
|
+
const angle = index * 2.399963229728653 + hash(node.id) % 1e3 / 1e3;
|
|
953
|
+
const radius = node.matched ? 48 + Math.sqrt(index) * 25 : 145 + Math.sqrt(index) * 34;
|
|
954
|
+
points.set(node.id, {
|
|
955
|
+
x: centerX + Math.cos(angle) * radius,
|
|
956
|
+
y: centerY + Math.sin(angle) * radius * 0.72,
|
|
957
|
+
vx: 0,
|
|
958
|
+
vy: 0
|
|
959
|
+
});
|
|
960
|
+
});
|
|
961
|
+
const edges = (graph?.edges ?? []).flatMap((edge) => {
|
|
962
|
+
const source = points.get(edge.source);
|
|
963
|
+
const target = points.get(edge.target);
|
|
964
|
+
return source && target ? [{ source, target }] : [];
|
|
965
|
+
});
|
|
966
|
+
for (let step = 0; step < 180; step += 1) {
|
|
967
|
+
const cooling = 1 - step / 210;
|
|
968
|
+
const forces = nodes.map(() => ({ x: 0, y: 0 }));
|
|
969
|
+
for (let left = 0; left < nodes.length; left += 1) {
|
|
970
|
+
const a = points.get(nodes[left].id);
|
|
971
|
+
for (let right = left + 1; right < nodes.length; right += 1) {
|
|
972
|
+
const b = points.get(nodes[right].id);
|
|
973
|
+
const dx = a.x - b.x;
|
|
974
|
+
const dy = a.y - b.y;
|
|
975
|
+
const distanceSquared = Math.max(36, dx * dx + dy * dy);
|
|
976
|
+
const distance = Math.sqrt(distanceSquared);
|
|
977
|
+
const force = 2500 / distanceSquared * cooling;
|
|
978
|
+
const fx = dx / distance * force;
|
|
979
|
+
const fy = dy / distance * force;
|
|
980
|
+
forces[left].x += fx;
|
|
981
|
+
forces[left].y += fy;
|
|
982
|
+
forces[right].x -= fx;
|
|
983
|
+
forces[right].y -= fy;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
for (const edge of edges) {
|
|
987
|
+
const dx = edge.target.x - edge.source.x;
|
|
988
|
+
const dy = edge.target.y - edge.source.y;
|
|
989
|
+
const distance = Math.max(1, Math.hypot(dx, dy));
|
|
990
|
+
const force = (distance - 132) * 6e-3 * cooling;
|
|
991
|
+
const fx = dx / distance * force;
|
|
992
|
+
const fy = dy / distance * force;
|
|
993
|
+
const sourceIndex = nodes.findIndex((node) => points.get(node.id) === edge.source);
|
|
994
|
+
const targetIndex = nodes.findIndex((node) => points.get(node.id) === edge.target);
|
|
995
|
+
if (sourceIndex >= 0 && targetIndex >= 0) {
|
|
996
|
+
forces[sourceIndex].x += fx;
|
|
997
|
+
forces[sourceIndex].y += fy;
|
|
998
|
+
forces[targetIndex].x -= fx;
|
|
999
|
+
forces[targetIndex].y -= fy;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
nodes.forEach((node, index) => {
|
|
1003
|
+
const point = points.get(node.id);
|
|
1004
|
+
const pull = node.matched ? 18e-4 : 1e-3;
|
|
1005
|
+
point.vx = (point.vx + forces[index].x + (centerX - point.x) * pull) * 0.82;
|
|
1006
|
+
point.vy = (point.vy + forces[index].y + (centerY - point.y) * pull) * 0.82;
|
|
1007
|
+
point.x = Math.max(45, Math.min(width - 45, point.x + point.vx));
|
|
1008
|
+
point.y = Math.max(45, Math.min(height - 45, point.y + point.vy));
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
return points;
|
|
1012
|
+
}
|
|
942
1013
|
function MemoryGraphPage() {
|
|
943
1014
|
const { companyId } = useHostContext();
|
|
944
1015
|
const navigation = useHostNavigation();
|
|
@@ -953,6 +1024,8 @@ function MemoryGraphPage() {
|
|
|
953
1024
|
const [loading, setLoading] = useState2(true);
|
|
954
1025
|
const [searching, setSearching] = useState2(false);
|
|
955
1026
|
const [error, setError] = useState2(null);
|
|
1027
|
+
const [graphViewport, setGraphViewport] = useState2({ x: 0, y: 0, scale: 1 });
|
|
1028
|
+
const graphDrag = useRef(null);
|
|
956
1029
|
useEffect2(() => {
|
|
957
1030
|
let cancelled = false;
|
|
958
1031
|
if (!companyId) {
|
|
@@ -984,8 +1057,9 @@ function MemoryGraphPage() {
|
|
|
984
1057
|
setSearching(true);
|
|
985
1058
|
setError(null);
|
|
986
1059
|
try {
|
|
987
|
-
const result = await searchGraph({ companyId, assignmentId, query: clean, limit:
|
|
1060
|
+
const result = await searchGraph({ companyId, assignmentId, query: clean, limit: 12 });
|
|
988
1061
|
setGraph(result);
|
|
1062
|
+
setGraphViewport({ x: 0, y: 0, scale: 1 });
|
|
989
1063
|
setSelectedNodeId(result.nodes[0]?.id ?? "");
|
|
990
1064
|
setQuery(clean);
|
|
991
1065
|
} catch (cause) {
|
|
@@ -996,18 +1070,8 @@ function MemoryGraphPage() {
|
|
|
996
1070
|
setSearching(false);
|
|
997
1071
|
}
|
|
998
1072
|
}
|
|
999
|
-
const visibleNodes = graph?.nodes
|
|
1000
|
-
const
|
|
1001
|
-
const points = /* @__PURE__ */ new Map();
|
|
1002
|
-
visibleNodes.forEach((node, index) => {
|
|
1003
|
-
if (index === 0) points.set(node.id, { x: 450, y: 270 });
|
|
1004
|
-
else {
|
|
1005
|
-
const ringIndex = index - 1;
|
|
1006
|
-
const angle = Math.PI * 2 * ringIndex / Math.max(1, visibleNodes.length - 1) - Math.PI / 2;
|
|
1007
|
-
const radius = ringIndex < 8 ? 190 : 250;
|
|
1008
|
-
points.set(node.id, { x: 450 + Math.cos(angle) * radius, y: 270 + Math.sin(angle) * radius });
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1073
|
+
const visibleNodes = graph?.nodes ?? [];
|
|
1074
|
+
const points = useMemo(() => layoutMemoryGraph(graph), [graph]);
|
|
1011
1075
|
const visibleIds = new Set(visibleNodes.map((node) => node.id));
|
|
1012
1076
|
const visibleEdges = (graph?.edges ?? []).filter((edge) => visibleIds.has(edge.source) && visibleIds.has(edge.target));
|
|
1013
1077
|
if (loading) return /* @__PURE__ */ jsx2("main", { style: pageStyle, children: /* @__PURE__ */ jsx2("p", { children: "Caricamento namespace\u2026" }) });
|
|
@@ -1081,34 +1145,110 @@ function MemoryGraphPage() {
|
|
|
1081
1145
|
graph.truncated ? /* @__PURE__ */ jsx2("span", { children: "Vista parziale; affina la ricerca per esplorare altri rami." }) : null
|
|
1082
1146
|
] }),
|
|
1083
1147
|
/* @__PURE__ */ jsxs2("div", { style: { display: "grid", gridTemplateColumns: "minmax(0, 1fr) minmax(230px, 310px)", flex: 1, minHeight: 0 }, children: [
|
|
1084
|
-
/* @__PURE__ */ jsx2("div", { style: { minWidth: 0, minHeight: 0, overflow: "
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
return /* @__PURE__ */ jsxs2("g", { role: "button", tabIndex: 0, "aria-label": `Seleziona memoria: ${node.label}`, onClick: () => setSelectedNodeId(node.id), onKeyDown: (event) => {
|
|
1099
|
-
if (event.key === "Enter" || event.key === " ") {
|
|
1148
|
+
/* @__PURE__ */ jsx2("div", { style: { position: "relative", minWidth: 0, minHeight: 0, overflow: "hidden", background: "radial-gradient(ellipse at center, rgba(87, 113, 132, .13), transparent 68%), #111315" }, children: visibleNodes.length ? /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
1149
|
+
/* @__PURE__ */ jsxs2("div", { style: { position: "absolute", zIndex: 1, top: 12, right: 12, display: "flex", gap: 6, padding: 5, border: "1px solid var(--border)", borderRadius: 9, background: "color-mix(in srgb, var(--background, #151719) 88%, transparent)" }, children: [
|
|
1150
|
+
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Zoom indietro", title: "Zoom indietro", style: { ...secondaryButtonStyle, padding: "5px 10px" }, onPointerDown: (event) => event.stopPropagation(), onClick: () => setGraphViewport((view) => ({ ...view, scale: Math.max(0.55, view.scale / 1.2) })), children: "\u2212" }),
|
|
1151
|
+
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Zoom avanti", title: "Zoom avanti", style: { ...secondaryButtonStyle, padding: "5px 10px" }, onPointerDown: (event) => event.stopPropagation(), onClick: () => setGraphViewport((view) => ({ ...view, scale: Math.min(2.8, view.scale * 1.2) })), children: "\uFF0B" }),
|
|
1152
|
+
/* @__PURE__ */ jsx2("button", { type: "button", style: { ...secondaryButtonStyle, padding: "5px 9px", fontSize: 12 }, onPointerDown: (event) => event.stopPropagation(), onClick: () => setGraphViewport({ x: 0, y: 0, scale: 1 }), children: "Centra" })
|
|
1153
|
+
] }),
|
|
1154
|
+
/* @__PURE__ */ jsxs2(
|
|
1155
|
+
"svg",
|
|
1156
|
+
{
|
|
1157
|
+
role: "application",
|
|
1158
|
+
"aria-label": `Grafo interattivo di ${visibleNodes.length} memorie. Trascina per spostare, usa la rotella per ingrandire.`,
|
|
1159
|
+
viewBox: "0 0 1000 720",
|
|
1160
|
+
style: { display: "block", width: "100%", height: "100%", minHeight: 480, touchAction: "none", cursor: graphDrag.current ? "grabbing" : "grab" },
|
|
1161
|
+
onWheel: (event) => {
|
|
1100
1162
|
event.preventDefault();
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1163
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
1164
|
+
const fitScale = Math.min(rect.width / 1e3, rect.height / 720);
|
|
1165
|
+
if (!fitScale) return;
|
|
1166
|
+
const offsetX = (rect.width - 1e3 * fitScale) / 2;
|
|
1167
|
+
const offsetY = (rect.height - 720 * fitScale) / 2;
|
|
1168
|
+
const localX = (event.clientX - rect.left - offsetX) / fitScale;
|
|
1169
|
+
const localY = (event.clientY - rect.top - offsetY) / fitScale;
|
|
1170
|
+
const multiplier = event.deltaY < 0 ? 1.12 : 1 / 1.12;
|
|
1171
|
+
setGraphViewport((view) => {
|
|
1172
|
+
const scale = Math.max(0.55, Math.min(2.8, view.scale * multiplier));
|
|
1173
|
+
const ratio = scale / view.scale;
|
|
1174
|
+
return { scale, x: localX - (localX - view.x) * ratio, y: localY - (localY - view.y) * ratio };
|
|
1175
|
+
});
|
|
1176
|
+
},
|
|
1177
|
+
onPointerDown: (event) => {
|
|
1178
|
+
if (event.button !== 0) return;
|
|
1179
|
+
graphDrag.current = { startX: event.clientX, startY: event.clientY, originX: graphViewport.x, originY: graphViewport.y };
|
|
1180
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1181
|
+
},
|
|
1182
|
+
onPointerMove: (event) => {
|
|
1183
|
+
const drag = graphDrag.current;
|
|
1184
|
+
if (!drag) return;
|
|
1185
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
1186
|
+
const fitScale = Math.min(rect.width / 1e3, rect.height / 720);
|
|
1187
|
+
if (!fitScale) return;
|
|
1188
|
+
setGraphViewport((view) => ({
|
|
1189
|
+
...view,
|
|
1190
|
+
x: drag.originX + (event.clientX - drag.startX) / (fitScale * graphViewport.scale),
|
|
1191
|
+
y: drag.originY + (event.clientY - drag.startY) / (fitScale * graphViewport.scale)
|
|
1192
|
+
}));
|
|
1193
|
+
},
|
|
1194
|
+
onPointerUp: () => {
|
|
1195
|
+
graphDrag.current = null;
|
|
1196
|
+
},
|
|
1197
|
+
onPointerCancel: () => {
|
|
1198
|
+
graphDrag.current = null;
|
|
1199
|
+
},
|
|
1200
|
+
children: [
|
|
1201
|
+
/* @__PURE__ */ jsx2("defs", { children: /* @__PURE__ */ jsx2("pattern", { id: "memory-graph-dots", width: "28", height: "28", patternUnits: "userSpaceOnUse", children: /* @__PURE__ */ jsx2("circle", { cx: "1", cy: "1", r: "1", fill: "rgba(180, 196, 205, .12)" }) }) }),
|
|
1202
|
+
/* @__PURE__ */ jsx2("rect", { width: "1000", height: "720", fill: "url(#memory-graph-dots)" }),
|
|
1203
|
+
/* @__PURE__ */ jsxs2("g", { transform: `translate(${graphViewport.x} ${graphViewport.y}) scale(${graphViewport.scale})`, children: [
|
|
1204
|
+
visibleEdges.map((edge, index) => {
|
|
1205
|
+
const a = points.get(edge.source);
|
|
1206
|
+
const b = points.get(edge.target);
|
|
1207
|
+
if (!a || !b) return null;
|
|
1208
|
+
return /* @__PURE__ */ jsxs2("g", { children: [
|
|
1209
|
+
/* @__PURE__ */ jsx2("line", { x1: a.x, y1: a.y, x2: b.x, y2: b.y, stroke: edge.relation === "sibling" ? "rgba(145, 164, 177, .38)" : "rgba(173, 194, 207, .62)", strokeWidth: edge.relation === "sibling" ? 1 : 1.25, strokeDasharray: edge.relation === "sibling" ? "4 6" : void 0 }),
|
|
1210
|
+
/* @__PURE__ */ jsx2("title", { children: edge.relation === "parent" ? "Memoria padre" : edge.relation === "child" ? "Memoria figlia" : "Memoria correlata" })
|
|
1211
|
+
] }, `${edge.source}-${edge.target}-${edge.relation}-${index}`);
|
|
1212
|
+
}),
|
|
1213
|
+
visibleNodes.map((node) => {
|
|
1214
|
+
const point = points.get(node.id);
|
|
1215
|
+
if (!point) return null;
|
|
1216
|
+
const active = node.id === selectedNode?.id;
|
|
1217
|
+
const label = node.label.length > 38 ? `${node.label.slice(0, 36)}\u2026` : node.label;
|
|
1218
|
+
const fill = active ? "var(--primary, #e7c46a)" : node.matched ? "#b8d77a" : "#8cbad0";
|
|
1219
|
+
return /* @__PURE__ */ jsxs2(
|
|
1220
|
+
"g",
|
|
1221
|
+
{
|
|
1222
|
+
role: "button",
|
|
1223
|
+
tabIndex: 0,
|
|
1224
|
+
"aria-pressed": active,
|
|
1225
|
+
"aria-label": `Seleziona memoria: ${node.label}`,
|
|
1226
|
+
onPointerDown: (event) => event.stopPropagation(),
|
|
1227
|
+
onClick: () => setSelectedNodeId(node.id),
|
|
1228
|
+
onKeyDown: (event) => {
|
|
1229
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1230
|
+
event.preventDefault();
|
|
1231
|
+
setSelectedNodeId(node.id);
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
style: { cursor: "pointer" },
|
|
1235
|
+
children: [
|
|
1236
|
+
/* @__PURE__ */ jsxs2("title", { children: [
|
|
1237
|
+
node.label,
|
|
1238
|
+
node.similarity !== null ? ` \xB7 ${Math.round(node.similarity * 100)}%` : ""
|
|
1239
|
+
] }),
|
|
1240
|
+
active ? /* @__PURE__ */ jsx2("circle", { cx: point.x, cy: point.y, r: "15", fill, opacity: ".16" }) : null,
|
|
1241
|
+
/* @__PURE__ */ jsx2("circle", { cx: point.x, cy: point.y, r: active ? 7 : node.matched ? 6 : 4.5, fill, stroke: active ? "#f6e8be" : "rgba(12, 16, 18, .9)", strokeWidth: active ? 2 : 1.5 }),
|
|
1242
|
+
/* @__PURE__ */ jsx2("text", { x: point.x + 12, y: point.y + 4, fontSize: "11", fill: active ? "#fff4d2" : "#c6d0d5", stroke: "#111315", strokeWidth: "3", strokeLinejoin: "round", paintOrder: "stroke", children: label })
|
|
1243
|
+
]
|
|
1244
|
+
},
|
|
1245
|
+
node.id
|
|
1246
|
+
);
|
|
1247
|
+
})
|
|
1248
|
+
] })
|
|
1249
|
+
]
|
|
1250
|
+
}
|
|
1251
|
+
)
|
|
1112
1252
|
] }) : /* @__PURE__ */ jsx2("div", { style: { ...mutedStyle, padding: 32, textAlign: "center" }, children: "Nessuna memoria trovata nel namespace per questa ricerca." }) }),
|
|
1113
1253
|
/* @__PURE__ */ jsxs2("aside", { style: { borderLeft: "1px solid var(--border)", padding: 14, display: "flex", flexDirection: "column", minWidth: 0, minHeight: 0, gap: 12 }, children: [
|
|
1114
1254
|
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", justifyContent: "space-between", gap: 8, alignItems: "start" }, children: [
|