ltcai 3.5.0 → 4.0.0
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 +73 -35
- package/docs/CARRYOVER_AUDIT_v3.6.0.md +61 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/HANDOVER_v3.6.0.md +46 -0
- package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +49 -0
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +509 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/architecture.md +13 -12
- package/docs/kg-schema.md +102 -53
- package/docs/privacy.md +18 -2
- package/docs/security-model.md +17 -0
- package/kg_schema.py +139 -10
- package/knowledge_graph.py +874 -26
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +7 -1
- package/latticeai/api/auth.py +27 -4
- package/latticeai/api/browser.py +217 -0
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/knowledge_graph.py +146 -0
- package/latticeai/api/local_files.py +1 -1
- package/latticeai/api/mcp.py +23 -11
- package/latticeai/api/memory.py +1 -1
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/network.py +81 -0
- package/latticeai/api/portability.py +93 -0
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/search.py +26 -2
- package/latticeai/api/security_dashboard.py +2 -3
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +2 -4
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/workflow_designer.py +46 -0
- package/latticeai/api/workspace.py +71 -49
- package/latticeai/app_factory.py +1710 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -0
- package/latticeai/core/agent.py +31 -7
- package/latticeai/core/audit.py +0 -7
- package/latticeai/core/config.py +1 -1
- package/latticeai/core/context_builder.py +1 -2
- package/latticeai/core/enterprise.py +1 -1
- package/latticeai/core/graph_curator.py +2 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/mcp_registry.py +791 -0
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +0 -1
- package/latticeai/core/multi_agent.py +238 -4
- package/latticeai/core/security.py +1 -1
- package/latticeai/core/sessions.py +37 -7
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +58 -10
- package/latticeai/models/__init__.py +7 -0
- package/latticeai/models/router.py +779 -0
- package/latticeai/server_app.py +29 -1504
- package/latticeai/services/agent_runtime.py +1 -0
- package/latticeai/services/app_context.py +75 -14
- package/latticeai/services/ingestion.py +318 -0
- package/latticeai/services/kg_portability.py +207 -0
- package/latticeai/services/memory_service.py +39 -11
- package/latticeai/services/model_runtime.py +2 -5
- package/latticeai/services/platform_runtime.py +100 -23
- package/latticeai/services/search_service.py +17 -8
- package/latticeai/services/tool_dispatch.py +12 -2
- package/latticeai/services/triggers.py +241 -0
- package/latticeai/services/upload_service.py +37 -12
- package/latticeai/services/workspace_service.py +31 -0
- package/llm_router.py +29 -772
- package/ltcai_cli.py +1 -2
- package/mcp_registry.py +25 -788
- package/p_reinforce.py +124 -14
- package/package.json +11 -8
- package/scripts/build_vsix.mjs +72 -0
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +82 -18
- package/scripts/validate_release_artifacts.py +0 -1
- package/scripts/wheel_smoke.py +142 -0
- package/server.py +11 -7
- package/setup_wizard.py +1142 -0
- package/static/account.html +2 -4
- package/static/admin.html +3 -5
- package/static/chat.html +3 -6
- package/static/graph.html +2 -4
- package/static/sw.js +81 -52
- package/static/v3/asset-manifest.json +20 -19
- package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
- package/static/v3/css/lattice.base.css +1 -1
- package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
- package/static/v3/css/lattice.components.css +1 -1
- package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
- package/static/v3/css/lattice.shell.css +1 -1
- package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
- package/static/v3/css/lattice.tokens.css +3 -0
- package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
- package/static/v3/css/lattice.views.css +2 -2
- package/static/v3/index.html +3 -4
- package/static/v3/js/{app.d086489d.js → app.356e6452.js} +1 -1
- package/static/v3/js/core/{api.12b568ad.js → api.7a308b89.js} +39 -1
- package/static/v3/js/core/api.js +38 -0
- package/static/v3/js/core/{routes.d214b399.js → routes.7222343d.js} +22 -22
- package/static/v3/js/core/routes.js +22 -22
- package/static/v3/js/core/{shell.d05266f5.js → shell.a1657f20.js} +4 -4
- package/static/v3/js/core/shell.js +1 -1
- package/static/v3/js/core/{store.34ebd5e6.js → store.204a08b2.js} +1 -1
- package/static/v3/js/core/store.js +1 -1
- package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
- package/static/v3/js/views/graph-canvas.js +509 -0
- package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
- package/static/v3/js/views/hybrid-search.js +1 -2
- package/static/v3/js/views/knowledge-graph.5e40cbeb.js +509 -0
- package/static/v3/js/views/knowledge-graph.js +326 -54
- package/static/vendor/chart.umd.min.js +20 -0
- package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
- package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
- package/static/vendor/fonts/inter.css +44 -0
- package/static/vendor/icons/tabler-icons.min.css +4 -0
- package/static/vendor/icons/tabler-icons.woff2 +0 -0
- package/static/vendor/marked.min.js +69 -0
- package/static/workspace.html +2 -2
- package/telegram_bot.py +1 -2
- package/tools/commands.py +4 -2
- package/tools/computer.py +1 -1
- package/tools/documents.py +1 -3
- package/tools/filesystem.py +0 -4
- package/tools/knowledge.py +1 -3
- package/tools/network.py +1 -3
- package/codex_telegram_bot.py +0 -195
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
- package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.1/local-agent.png +0 -0
- package/docs/images/admin-dashboard.png +0 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/knowledge-graph.png +0 -0
- package/docs/images/lattice-ai-demo.gif +0 -0
- package/docs/images/lattice-ai-hero.png +0 -0
- package/docs/images/logo.svg +0 -33
- package/docs/images/mobile-responsive.png +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/pipeline.png +0 -0
- package/docs/images/screenshot-admin.png +0 -0
- package/docs/images/screenshot-chat.png +0 -0
- package/docs/images/screenshot-graph.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/workspace-dark.png +0 -0
- package/docs/images/workspace-light.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/requirements.txt +0 -16
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +0 -237
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/images/pipeline.png
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/images/skills.png
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/requirements.txt
DELETED
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
/* ============================================================================
|
|
2
|
-
* View: Knowledge Graph — entity/relation explorer.
|
|
3
|
-
* Renders the graph as an SVG mesh against /api/graph with a live inspector.
|
|
4
|
-
* Missing graph data renders an empty unavailable state.
|
|
5
|
-
* ========================================================================== */
|
|
6
|
-
|
|
7
|
-
import { escapeHtml } from "../core/dom.a2773eb0.js";
|
|
8
|
-
|
|
9
|
-
const TYPE_COLOR = {
|
|
10
|
-
Topic: "var(--lt3-pillar-graph)",
|
|
11
|
-
Concept: "var(--lt3-pillar-vector)",
|
|
12
|
-
Method: "var(--lt3-pillar-hybrid)",
|
|
13
|
-
Model: "var(--accent-3)",
|
|
14
|
-
File: "var(--faint)",
|
|
15
|
-
Decision: "var(--accent-3)",
|
|
16
|
-
Task: "var(--accent-2)",
|
|
17
|
-
Person: "var(--accent-pink)",
|
|
18
|
-
default: "var(--accent)",
|
|
19
|
-
};
|
|
20
|
-
const colorFor = (t) => TYPE_COLOR[t] || TYPE_COLOR.default;
|
|
21
|
-
|
|
22
|
-
export async function render(ctx) {
|
|
23
|
-
const { h, icon, api, store, c } = ctx;
|
|
24
|
-
|
|
25
|
-
const state = { selected: null, query: "", data: { nodes: [], edges: [] }, source: "pending" };
|
|
26
|
-
|
|
27
|
-
const canvasHost = h("div", c.loading({ lines: 0, block: true }));
|
|
28
|
-
const inspectorHost = h("div", c.loading({ lines: 4 }));
|
|
29
|
-
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
30
|
-
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
31
|
-
|
|
32
|
-
const root = h("div.lt3-stack-6",
|
|
33
|
-
c.viewHeader({
|
|
34
|
-
eyebrow: "Retrieval · structure",
|
|
35
|
-
title: "Knowledge Graph",
|
|
36
|
-
sub: "Entities and the relations the workspace extracted between them. Click a node to trace its neighborhood.",
|
|
37
|
-
actions: [
|
|
38
|
-
srcSlot,
|
|
39
|
-
h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => load() } }, icon("refresh"), "Rebuild view"),
|
|
40
|
-
h("button.lt3-btn.lt3-btn--primary", { on: { click: () => ctx.navigate("hybrid-search") } }, icon("arrows-join"), "Search graph"),
|
|
41
|
-
],
|
|
42
|
-
}),
|
|
43
|
-
statHost,
|
|
44
|
-
h("div.lt3-split",
|
|
45
|
-
h("div.lt3-stack-3",
|
|
46
|
-
c.card(canvasHost, { attrs: { style: "padding:0;overflow:hidden" } }),
|
|
47
|
-
buildLegend(ctx),
|
|
48
|
-
),
|
|
49
|
-
h("aside.lt3-panel",
|
|
50
|
-
h("div.lt3-panel__head", h("div", h("div.lt3-eyebrow", "Inspector"), h("h3.lt3-panel__title", "Entities"))),
|
|
51
|
-
h("div.lt3-search", { style: { "margin-bottom": "var(--lt3-space-4)" } },
|
|
52
|
-
icon("search"),
|
|
53
|
-
h("input", { type: "text", placeholder: "Filter entities…", "aria-label": "Filter entities",
|
|
54
|
-
on: { input: (e) => { state.query = e.target.value.toLowerCase(); renderInspector(); } } }),
|
|
55
|
-
),
|
|
56
|
-
inspectorHost,
|
|
57
|
-
),
|
|
58
|
-
),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
async function load() {
|
|
62
|
-
canvasHost.replaceChildren(c.loading({ lines: 0, block: true }));
|
|
63
|
-
const [g, stats] = await Promise.all([api.graph(), api.graphStats()]);
|
|
64
|
-
state.data = normalize(g.data);
|
|
65
|
-
state.source = g.source;
|
|
66
|
-
srcSlot.replaceChildren(c.sourceBadge(g.source));
|
|
67
|
-
renderStats(stats.data, g.data);
|
|
68
|
-
renderCanvas();
|
|
69
|
-
renderInspector();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function renderStats(stats, graphData) {
|
|
73
|
-
const nodes = state.data.nodes.length;
|
|
74
|
-
const edges = state.data.edges.length;
|
|
75
|
-
const types = stats && stats.nodes ? Object.keys(stats.nodes).length : new Set(state.data.nodes.map((n) => n.type)).size;
|
|
76
|
-
const density = nodes > 1 ? (edges / (nodes * (nodes - 1) / 2)) : 0;
|
|
77
|
-
statHost.replaceChildren(
|
|
78
|
-
c.stat({ label: "Entities", value: c.fmtNum(nodes), icon: "circles" }),
|
|
79
|
-
c.stat({ label: "Relations", value: c.fmtNum(edges), icon: "vector-triangle" }),
|
|
80
|
-
c.stat({ label: "Entity types", value: types, icon: "category" }),
|
|
81
|
-
c.stat({ label: "Density", value: density.toFixed(2), icon: "chart-dots" }),
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function renderCanvas() {
|
|
86
|
-
const { nodes, edges } = state.data;
|
|
87
|
-
if (!nodes.length) { canvasHost.replaceChildren(c.emptyState({ icon: "chart-dots-3", title: "No entities yet", body: "Index a source to populate the graph." })); return; }
|
|
88
|
-
const laidOut = layout(nodes);
|
|
89
|
-
const pos = Object.fromEntries(laidOut.map((n) => [n.id, n]));
|
|
90
|
-
const W = 1000, H = 600;
|
|
91
|
-
const edgeSvg = edges.map((e) => {
|
|
92
|
-
const a = pos[e.from], b = pos[e.to];
|
|
93
|
-
if (!a || !b) return "";
|
|
94
|
-
return `<line class="lt3-gedge" x1="${a.px}" y1="${a.py}" x2="${b.px}" y2="${b.py}" stroke-width="${1 + (e.weight || 1) * 0.6}"></line>`;
|
|
95
|
-
}).join("");
|
|
96
|
-
const nodeSvg = laidOut.map((n) => {
|
|
97
|
-
const r = 10 + (n.weight || 0.5) * 16;
|
|
98
|
-
const sel = state.selected === n.id;
|
|
99
|
-
return `<g class="lt3-gnode" data-id="${escapeHtml(n.id)}" opacity="${state.selected && !sel && !isNeighbor(n.id) ? 0.35 : 1}">
|
|
100
|
-
<circle cx="${n.px}" cy="${n.py}" r="${sel ? r + 3 : r}" fill="${colorFor(n.type)}" stroke-width="${sel ? 3 : 2}"></circle>
|
|
101
|
-
<text x="${n.px}" y="${n.py + r + 13}" text-anchor="middle">${escapeHtml(truncate(n.label, 18))}</text>
|
|
102
|
-
</g>`;
|
|
103
|
-
}).join("");
|
|
104
|
-
canvasHost.replaceChildren(
|
|
105
|
-
h("div.lt3-graph-canvas", {
|
|
106
|
-
html: `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Knowledge graph">${edgeSvg}${nodeSvg}</svg>`,
|
|
107
|
-
on: { click: onCanvasClick },
|
|
108
|
-
}),
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function onCanvasClick(e) {
|
|
113
|
-
const g = e.target.closest(".lt3-gnode");
|
|
114
|
-
if (!g) return;
|
|
115
|
-
state.selected = g.dataset.id === state.selected ? null : g.dataset.id;
|
|
116
|
-
renderCanvas();
|
|
117
|
-
renderInspector();
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function isNeighbor(id) {
|
|
121
|
-
if (!state.selected) return false;
|
|
122
|
-
return state.data.edges.some((e) =>
|
|
123
|
-
(e.from === state.selected && e.to === id) || (e.to === state.selected && e.from === id));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function renderInspector() {
|
|
127
|
-
if (state.selected) { inspectorHost.replaceChildren(detailView()); return; }
|
|
128
|
-
const q = state.query;
|
|
129
|
-
const list = state.data.nodes
|
|
130
|
-
.filter((n) => !q || (n.label || "").toLowerCase().includes(q) || (n.type || "").toLowerCase().includes(q))
|
|
131
|
-
.sort((a, b) => (b.weight || 0) - (a.weight || 0));
|
|
132
|
-
inspectorHost.replaceChildren(
|
|
133
|
-
list.length
|
|
134
|
-
? h("div.lt3-stack-2", list.slice(0, 60).map((n) => entityRow(n)))
|
|
135
|
-
: c.emptyState({ icon: "search-off", title: "No matches", body: "Try a different entity name." }),
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function entityRow(n) {
|
|
140
|
-
return h("button.lt3-entity", { on: { click: () => { state.selected = n.id; renderCanvas(); renderInspector(); } } },
|
|
141
|
-
h("div.lt3-entity__type", { style: { background: `color-mix(in srgb, ${colorFor(n.type)} 18%, transparent)`, color: colorFor(n.type) } }, icon(iconForType(n.type))),
|
|
142
|
-
h("div.lt3-entity__body",
|
|
143
|
-
h("div.lt3-entity__name", n.label),
|
|
144
|
-
h("div.lt3-entity__meta", `${n.type || "Entity"} · weight ${(n.weight || 0).toFixed(2)}`),
|
|
145
|
-
),
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function detailView() {
|
|
150
|
-
const n = state.data.nodes.find((x) => x.id === state.selected);
|
|
151
|
-
if (!n) { state.selected = null; return c.emptyState({ title: "Not found" }); }
|
|
152
|
-
const rels = state.data.edges
|
|
153
|
-
.filter((e) => e.from === n.id || e.to === n.id)
|
|
154
|
-
.map((e) => {
|
|
155
|
-
const otherId = e.from === n.id ? e.to : e.from;
|
|
156
|
-
const other = state.data.nodes.find((x) => x.id === otherId);
|
|
157
|
-
return { type: e.type, dir: e.from === n.id ? "→" : "←", other };
|
|
158
|
-
})
|
|
159
|
-
.filter((r) => r.other);
|
|
160
|
-
return h("div.lt3-stack-4",
|
|
161
|
-
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => { state.selected = null; renderCanvas(); renderInspector(); } } }, icon("arrow-left"), "All entities"),
|
|
162
|
-
h("div.lt3-card.lt3-card--flat",
|
|
163
|
-
h("div.lt3-row-2", { style: { "margin-bottom": "var(--lt3-space-2)" } },
|
|
164
|
-
h("span.lt3-pill", { style: { color: colorFor(n.type) } }, n.type || "Entity"),
|
|
165
|
-
),
|
|
166
|
-
h("div", { style: { "font-size": "var(--lt3-text-lg)", "font-weight": 700 } }, n.label),
|
|
167
|
-
n.summary && h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-2)" } }, n.summary),
|
|
168
|
-
),
|
|
169
|
-
h("div",
|
|
170
|
-
h("div.lt3-eyebrow", { style: { "margin-bottom": "var(--lt3-space-2)" } }, `Relations (${rels.length})`),
|
|
171
|
-
rels.length
|
|
172
|
-
? h("div.lt3-stack-2", rels.map((r) => h("button.lt3-entity", { on: { click: () => { state.selected = r.other.id; renderCanvas(); renderInspector(); } } },
|
|
173
|
-
h("div.lt3-entity__type", { style: { background: "var(--surface-3)" } }, h("span.lt3-mono", { style: { "font-size": "var(--lt3-text-sm)" } }, r.dir)),
|
|
174
|
-
h("div.lt3-entity__body",
|
|
175
|
-
h("div.lt3-entity__name", r.other.label),
|
|
176
|
-
h("div.lt3-entity__meta", r.type),
|
|
177
|
-
),
|
|
178
|
-
)))
|
|
179
|
-
: c.emptyState({ icon: "unlink", title: "No relations", body: "This entity is currently isolated." }),
|
|
180
|
-
),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
load();
|
|
185
|
-
return root;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
189
|
-
function normalize(data) {
|
|
190
|
-
const nodes = (data.nodes || []).map((n) => ({
|
|
191
|
-
id: n.id,
|
|
192
|
-
label: n.label || n.title || n.id,
|
|
193
|
-
type: n.type || "Entity",
|
|
194
|
-
weight: n.weight ?? n.importance_norm ?? (n.metadata && n.metadata.graph_metrics && n.metadata.graph_metrics.importance_norm) ?? 0.5,
|
|
195
|
-
summary: n.summary || "",
|
|
196
|
-
x: n.x, y: n.y,
|
|
197
|
-
}));
|
|
198
|
-
const ids = new Set(nodes.map((n) => n.id));
|
|
199
|
-
const edges = (data.edges || []).filter((e) => ids.has(e.from) && ids.has(e.to))
|
|
200
|
-
.map((e) => ({ from: e.from, to: e.to, type: e.type || "related", weight: e.weight || 1 }));
|
|
201
|
-
return { nodes, edges };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function layout(nodes) {
|
|
205
|
-
const W = 1000, H = 600, cx = W / 2, cy = H / 2;
|
|
206
|
-
const golden = Math.PI * (3 - Math.sqrt(5));
|
|
207
|
-
const hasCoords = nodes.length && nodes.every((n) => typeof n.x === "number" && typeof n.y === "number");
|
|
208
|
-
if (hasCoords) {
|
|
209
|
-
return nodes.map((n) => ({ ...n, px: Math.round(60 + n.x * (W - 120)), py: Math.round(50 + n.y * (H - 100)) }));
|
|
210
|
-
}
|
|
211
|
-
// Sunflower (Vogel) spread — even spacing, highest-weight entity centered.
|
|
212
|
-
const order = nodes.map((n, i) => ({ n, i })).sort((a, b) => (b.n.weight || 0) - (a.n.weight || 0));
|
|
213
|
-
const maxR = Math.min(W, H) * 0.42;
|
|
214
|
-
const placed = {};
|
|
215
|
-
order.forEach((o, rank) => {
|
|
216
|
-
const radius = rank === 0 ? 0 : maxR * Math.sqrt(rank / Math.max(1, nodes.length - 1));
|
|
217
|
-
const angle = rank * golden;
|
|
218
|
-
placed[o.i] = {
|
|
219
|
-
px: Math.round(cx + Math.cos(angle) * radius),
|
|
220
|
-
py: Math.round(cy + Math.sin(angle) * radius * 0.66),
|
|
221
|
-
};
|
|
222
|
-
});
|
|
223
|
-
return nodes.map((n, i) => ({ ...n, ...placed[i] }));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function truncate(s, n) { s = String(s || ""); return s.length > n ? s.slice(0, n - 1) + "…" : s; }
|
|
227
|
-
|
|
228
|
-
function iconForType(t) {
|
|
229
|
-
return ({ Topic: "bulb", Concept: "atom", Method: "function", Model: "cpu", File: "file", Decision: "gavel", Task: "checkbox", Person: "user" })[t] || "point";
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function buildLegend({ h }) {
|
|
233
|
-
const types = ["Topic", "Concept", "Method", "Model", "File"];
|
|
234
|
-
return h("div.lt3-graph-legend",
|
|
235
|
-
types.map((t) => h("span", h("i", { style: { background: colorFor(t) } }), t)),
|
|
236
|
-
);
|
|
237
|
-
}
|