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.
Files changed (181) hide show
  1. package/README.md +73 -35
  2. package/docs/CARRYOVER_AUDIT_v3.6.0.md +61 -0
  3. package/docs/CHANGELOG.md +32 -0
  4. package/docs/HANDOVER_v3.6.0.md +46 -0
  5. package/docs/RUNTIME_HOOK_COVERAGE_v3.6.0.md +49 -0
  6. package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
  7. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +509 -0
  8. package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
  9. package/docs/architecture.md +13 -12
  10. package/docs/kg-schema.md +102 -53
  11. package/docs/privacy.md +18 -2
  12. package/docs/security-model.md +17 -0
  13. package/kg_schema.py +139 -10
  14. package/knowledge_graph.py +874 -26
  15. package/knowledge_graph_api.py +11 -127
  16. package/latticeai/__init__.py +1 -1
  17. package/latticeai/api/admin.py +1 -1
  18. package/latticeai/api/agents.py +7 -1
  19. package/latticeai/api/auth.py +27 -4
  20. package/latticeai/api/browser.py +217 -0
  21. package/latticeai/api/chat.py +112 -76
  22. package/latticeai/api/health.py +1 -1
  23. package/latticeai/api/hooks.py +1 -1
  24. package/latticeai/api/knowledge_graph.py +146 -0
  25. package/latticeai/api/local_files.py +1 -1
  26. package/latticeai/api/mcp.py +23 -11
  27. package/latticeai/api/memory.py +1 -1
  28. package/latticeai/api/models.py +1 -1
  29. package/latticeai/api/network.py +81 -0
  30. package/latticeai/api/portability.py +93 -0
  31. package/latticeai/api/realtime.py +1 -1
  32. package/latticeai/api/search.py +26 -2
  33. package/latticeai/api/security_dashboard.py +2 -3
  34. package/latticeai/api/setup.py +2 -2
  35. package/latticeai/api/static_routes.py +2 -4
  36. package/latticeai/api/tools.py +3 -0
  37. package/latticeai/api/workflow_designer.py +46 -0
  38. package/latticeai/api/workspace.py +71 -49
  39. package/latticeai/app_factory.py +1710 -0
  40. package/latticeai/brain/__init__.py +18 -0
  41. package/latticeai/brain/context.py +213 -0
  42. package/latticeai/brain/conversations.py +236 -0
  43. package/latticeai/brain/identity.py +175 -0
  44. package/latticeai/brain/memory.py +102 -0
  45. package/latticeai/brain/network.py +205 -0
  46. package/latticeai/core/agent.py +31 -7
  47. package/latticeai/core/audit.py +0 -7
  48. package/latticeai/core/config.py +1 -1
  49. package/latticeai/core/context_builder.py +1 -2
  50. package/latticeai/core/enterprise.py +1 -1
  51. package/latticeai/core/graph_curator.py +2 -2
  52. package/latticeai/core/marketplace.py +1 -1
  53. package/latticeai/core/mcp_registry.py +791 -0
  54. package/latticeai/core/model_compat.py +1 -1
  55. package/latticeai/core/model_resolution.py +0 -1
  56. package/latticeai/core/multi_agent.py +238 -4
  57. package/latticeai/core/security.py +1 -1
  58. package/latticeai/core/sessions.py +37 -7
  59. package/latticeai/core/workflow_engine.py +114 -2
  60. package/latticeai/core/workspace_os.py +58 -10
  61. package/latticeai/models/__init__.py +7 -0
  62. package/latticeai/models/router.py +779 -0
  63. package/latticeai/server_app.py +29 -1504
  64. package/latticeai/services/agent_runtime.py +1 -0
  65. package/latticeai/services/app_context.py +75 -14
  66. package/latticeai/services/ingestion.py +318 -0
  67. package/latticeai/services/kg_portability.py +207 -0
  68. package/latticeai/services/memory_service.py +39 -11
  69. package/latticeai/services/model_runtime.py +2 -5
  70. package/latticeai/services/platform_runtime.py +100 -23
  71. package/latticeai/services/search_service.py +17 -8
  72. package/latticeai/services/tool_dispatch.py +12 -2
  73. package/latticeai/services/triggers.py +241 -0
  74. package/latticeai/services/upload_service.py +37 -12
  75. package/latticeai/services/workspace_service.py +31 -0
  76. package/llm_router.py +29 -772
  77. package/ltcai_cli.py +1 -2
  78. package/mcp_registry.py +25 -788
  79. package/p_reinforce.py +124 -14
  80. package/package.json +11 -8
  81. package/scripts/build_vsix.mjs +72 -0
  82. package/scripts/bump_version.py +99 -0
  83. package/scripts/generate_diagrams.py +0 -1
  84. package/scripts/lint_v3.mjs +82 -18
  85. package/scripts/validate_release_artifacts.py +0 -1
  86. package/scripts/wheel_smoke.py +142 -0
  87. package/server.py +11 -7
  88. package/setup_wizard.py +1142 -0
  89. package/static/account.html +2 -4
  90. package/static/admin.html +3 -5
  91. package/static/chat.html +3 -6
  92. package/static/graph.html +2 -4
  93. package/static/sw.js +81 -52
  94. package/static/v3/asset-manifest.json +20 -19
  95. package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
  96. package/static/v3/css/lattice.base.css +1 -1
  97. package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
  98. package/static/v3/css/lattice.components.css +1 -1
  99. package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
  100. package/static/v3/css/lattice.shell.css +1 -1
  101. package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
  102. package/static/v3/css/lattice.tokens.css +3 -0
  103. package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
  104. package/static/v3/css/lattice.views.css +2 -2
  105. package/static/v3/index.html +3 -4
  106. package/static/v3/js/{app.d086489d.js → app.356e6452.js} +1 -1
  107. package/static/v3/js/core/{api.12b568ad.js → api.7a308b89.js} +39 -1
  108. package/static/v3/js/core/api.js +38 -0
  109. package/static/v3/js/core/{routes.d214b399.js → routes.7222343d.js} +22 -22
  110. package/static/v3/js/core/routes.js +22 -22
  111. package/static/v3/js/core/{shell.d05266f5.js → shell.a1657f20.js} +4 -4
  112. package/static/v3/js/core/shell.js +1 -1
  113. package/static/v3/js/core/{store.34ebd5e6.js → store.204a08b2.js} +1 -1
  114. package/static/v3/js/core/store.js +1 -1
  115. package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
  116. package/static/v3/js/views/graph-canvas.js +509 -0
  117. package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
  118. package/static/v3/js/views/hybrid-search.js +1 -2
  119. package/static/v3/js/views/knowledge-graph.5e40cbeb.js +509 -0
  120. package/static/v3/js/views/knowledge-graph.js +326 -54
  121. package/static/vendor/chart.umd.min.js +20 -0
  122. package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
  123. package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
  124. package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
  125. package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
  126. package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
  127. package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
  128. package/static/vendor/fonts/inter.css +44 -0
  129. package/static/vendor/icons/tabler-icons.min.css +4 -0
  130. package/static/vendor/icons/tabler-icons.woff2 +0 -0
  131. package/static/vendor/marked.min.js +69 -0
  132. package/static/workspace.html +2 -2
  133. package/telegram_bot.py +1 -2
  134. package/tools/commands.py +4 -2
  135. package/tools/computer.py +1 -1
  136. package/tools/documents.py +1 -3
  137. package/tools/filesystem.py +0 -4
  138. package/tools/knowledge.py +1 -3
  139. package/tools/network.py +1 -3
  140. package/codex_telegram_bot.py +0 -195
  141. package/docs/assets/v3.4.0/agent-run.png +0 -0
  142. package/docs/assets/v3.4.0/agents.png +0 -0
  143. package/docs/assets/v3.4.0/before/chat-before.png +0 -0
  144. package/docs/assets/v3.4.0/before/files-before.png +0 -0
  145. package/docs/assets/v3.4.0/chat.png +0 -0
  146. package/docs/assets/v3.4.0/connect-folder.png +0 -0
  147. package/docs/assets/v3.4.0/files.png +0 -0
  148. package/docs/assets/v3.4.0/home.png +0 -0
  149. package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
  150. package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
  151. package/docs/assets/v3.4.0/local-agent.png +0 -0
  152. package/docs/assets/v3.4.0/memory.png +0 -0
  153. package/docs/assets/v3.4.0/settings.png +0 -0
  154. package/docs/assets/v3.4.0/vision-input.png +0 -0
  155. package/docs/assets/v3.4.0/workflows.png +0 -0
  156. package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
  157. package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
  158. package/docs/assets/v3.4.1/local-agent.png +0 -0
  159. package/docs/images/admin-dashboard.png +0 -0
  160. package/docs/images/architecture.png +0 -0
  161. package/docs/images/enterprise.png +0 -0
  162. package/docs/images/graph.png +0 -0
  163. package/docs/images/hero.gif +0 -0
  164. package/docs/images/knowledge-graph.png +0 -0
  165. package/docs/images/lattice-ai-demo.gif +0 -0
  166. package/docs/images/lattice-ai-hero.png +0 -0
  167. package/docs/images/logo.svg +0 -33
  168. package/docs/images/mobile-responsive.png +0 -0
  169. package/docs/images/model-recommendation.png +0 -0
  170. package/docs/images/onboarding.png +0 -0
  171. package/docs/images/organization.png +0 -0
  172. package/docs/images/pipeline.png +0 -0
  173. package/docs/images/screenshot-admin.png +0 -0
  174. package/docs/images/screenshot-chat.png +0 -0
  175. package/docs/images/screenshot-graph.png +0 -0
  176. package/docs/images/skills.png +0 -0
  177. package/docs/images/workspace-dark.png +0 -0
  178. package/docs/images/workspace-light.png +0 -0
  179. package/docs/images/workspace.png +0 -0
  180. package/requirements.txt +0 -16
  181. package/static/v3/js/views/knowledge-graph.a14ea7e7.js +0 -237
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/requirements.txt DELETED
@@ -1,16 +0,0 @@
1
- fastapi
2
- uvicorn
3
- pydantic
4
- httpx
5
- pillow
6
- openai
7
- python-docx
8
- openpyxl
9
- python-pptx
10
- python-multipart
11
- keyring
12
- authlib
13
- cryptography
14
- pdfplumber
15
- pypdfium2
16
- watchdog
@@ -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
- }