ltcai 3.6.0 → 4.0.1
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 +39 -31
- package/docs/CHANGELOG.md +64 -0
- package/docs/REALTIME_COLLABORATION.md +3 -3
- package/docs/V3_FRONTEND.md +9 -8
- package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
- package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
- package/docs/kg-schema.md +51 -53
- package/docs/spec-vs-impl.md +10 -10
- package/kg_schema.py +2 -520
- package/knowledge_graph.py +37 -4629
- package/knowledge_graph_api.py +11 -127
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +16 -17
- package/latticeai/api/agents.py +20 -7
- package/latticeai/api/auth.py +46 -15
- package/latticeai/api/chat.py +112 -76
- package/latticeai/api/health.py +1 -1
- package/latticeai/api/hooks.py +1 -1
- package/latticeai/api/invitations.py +100 -0
- package/latticeai/api/knowledge_graph.py +139 -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/plugins.py +3 -6
- package/latticeai/api/realtime.py +5 -8
- 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 +11 -16
- package/latticeai/api/tools.py +3 -0
- package/latticeai/api/ui_redirects.py +26 -0
- package/latticeai/api/workflow_designer.py +85 -6
- package/latticeai/api/workspace.py +93 -57
- package/latticeai/app_factory.py +1781 -0
- package/latticeai/brain/__init__.py +18 -0
- package/latticeai/brain/_kg_common.py +1123 -0
- package/latticeai/brain/context.py +213 -0
- package/latticeai/brain/conversations.py +236 -0
- package/latticeai/brain/discovery.py +1455 -0
- package/latticeai/brain/documents.py +218 -0
- package/latticeai/brain/identity.py +175 -0
- package/latticeai/brain/ingest.py +644 -0
- package/latticeai/brain/memory.py +102 -0
- package/latticeai/brain/network.py +205 -0
- package/latticeai/brain/projection.py +561 -0
- package/latticeai/brain/provenance.py +401 -0
- package/latticeai/brain/retrieval.py +1316 -0
- package/latticeai/brain/schema.py +640 -0
- package/latticeai/brain/store.py +216 -0
- package/latticeai/brain/write_master.py +225 -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/invitations.py +131 -0
- 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/policy.py +54 -0
- package/latticeai/core/realtime.py +65 -44
- package/latticeai/core/security.py +1 -1
- package/latticeai/core/sessions.py +66 -10
- package/latticeai/core/users.py +147 -0
- package/latticeai/core/workflow_engine.py +114 -2
- package/latticeai/core/workspace_os.py +477 -29
- package/latticeai/models/__init__.py +7 -0
- package/latticeai/models/router.py +779 -0
- package/latticeai/server_app.py +29 -1536
- package/latticeai/services/agent_runtime.py +243 -4
- package/latticeai/services/app_context.py +75 -14
- package/latticeai/services/ingestion.py +47 -0
- package/latticeai/services/kg_portability.py +33 -3
- 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/run_executor.py +328 -0
- 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 +55 -16
- 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 +10 -20
- package/scripts/bump_version.py +99 -0
- package/scripts/generate_diagrams.py +0 -1
- package/scripts/lint_v3.mjs +105 -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/sw.js +81 -52
- package/static/v3/asset-manifest.json +33 -25
- 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.c541f955.js → app.c5c80c46.js} +1 -1
- package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
- package/static/v3/js/core/api.js +57 -0
- package/static/v3/js/core/i18n.880e1fec.js +575 -0
- package/static/v3/js/core/i18n.js +575 -0
- package/static/v3/js/core/routes.37522821.js +101 -0
- package/static/v3/js/core/routes.js +71 -63
- package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
- package/static/v3/js/core/shell.js +66 -37
- package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
- package/static/v3/js/core/store.js +11 -1
- package/static/v3/js/views/account.eff40715.js +143 -0
- package/static/v3/js/views/account.js +143 -0
- package/static/v3/js/views/activity.0d271ef9.js +67 -0
- package/static/v3/js/views/activity.js +67 -0
- package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
- package/static/v3/js/views/admin-users.js +4 -6
- package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
- package/static/v3/js/views/agents.js +35 -12
- package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
- package/static/v3/js/views/chat.js +23 -0
- 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.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
- package/static/v3/js/views/knowledge-graph.js +60 -44
- package/static/v3/js/views/network.52a4f181.js +97 -0
- package/static/v3/js/views/network.js +97 -0
- package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
- package/static/v3/js/views/planning.js +26 -5
- package/static/v3/js/views/runs.b63b2afa.js +144 -0
- package/static/v3/js/views/runs.js +144 -0
- package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
- package/static/v3/js/views/settings.js +7 -8
- package/static/v3/js/views/snapshots.6f5db095.js +135 -0
- package/static/v3/js/views/snapshots.js +135 -0
- package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
- package/static/v3/js/views/workflows.js +87 -2
- package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
- package/static/v3/js/views/workspace-admin.js +156 -0
- 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/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/account.html +0 -115
- package/static/activity.html +0 -73
- package/static/admin.html +0 -488
- package/static/agents.html +0 -139
- package/static/chat.html +0 -844
- package/static/css/reference/account.css +0 -439
- package/static/css/reference/admin.css +0 -610
- package/static/css/reference/base.css +0 -1661
- package/static/css/reference/chat.css +0 -4623
- package/static/css/reference/graph.css +0 -1016
- package/static/css/responsive.css +0 -861
- package/static/graph.html +0 -124
- package/static/platform.css +0 -104
- package/static/plugins.html +0 -136
- package/static/scripts/account.js +0 -238
- package/static/scripts/admin.js +0 -1614
- package/static/scripts/chat.js +0 -5081
- package/static/scripts/graph.js +0 -1804
- package/static/scripts/platform.js +0 -64
- package/static/scripts/ux.js +0 -167
- package/static/scripts/workspace.js +0 -948
- package/static/v3/js/core/routes.2ce3815a.js +0 -93
- package/static/workflows.html +0 -146
- package/static/workspace.css +0 -1121
- package/static/workspace.html +0 -357
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* ========================================================================== */
|
|
9
9
|
|
|
10
10
|
import { escapeHtml } from "../core/dom.a2773eb0.js";
|
|
11
|
+
import { createGraphCanvas } from "./graph-canvas.17c15d65.js";
|
|
12
|
+
import { t } from "../core/i18n.880e1fec.js";
|
|
11
13
|
|
|
12
14
|
const TYPE_COLOR = {
|
|
13
15
|
Topic: "var(--lt3-pillar-graph)",
|
|
@@ -144,47 +146,42 @@ function buildExplore(ctx) {
|
|
|
144
146
|
);
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
// Live force-directed canvas (zoom / pan / drag / physics) — replaces the
|
|
150
|
+
// static SVG spiral. The renderer only draws the data it is given.
|
|
151
|
+
let graphCanvas = null;
|
|
152
|
+
|
|
153
|
+
function ensureGraphCanvas() {
|
|
154
|
+
if (graphCanvas) return graphCanvas;
|
|
155
|
+
graphCanvas = createGraphCanvas({
|
|
156
|
+
colorFor,
|
|
157
|
+
onSelect: (id) => {
|
|
158
|
+
state.selected = id;
|
|
159
|
+
renderInspector();
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
return graphCanvas;
|
|
163
|
+
}
|
|
164
|
+
|
|
147
165
|
function renderCanvas() {
|
|
148
166
|
const { nodes, edges } = state.data;
|
|
149
|
-
if (!nodes.length) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const sel = state.selected === n.id;
|
|
161
|
-
return `<g class="lt3-gnode" data-id="${escapeHtml(n.id)}" opacity="${state.selected && !sel && !isNeighbor(n.id) ? 0.35 : 1}">
|
|
162
|
-
<circle cx="${n.px}" cy="${n.py}" r="${sel ? r + 3 : r}" fill="${colorFor(n.type)}" stroke-width="${sel ? 3 : 2}"></circle>
|
|
163
|
-
<text x="${n.px}" y="${n.py + r + 13}" text-anchor="middle">${escapeHtml(truncate(n.label, 18))}</text>
|
|
164
|
-
</g>`;
|
|
165
|
-
}).join("");
|
|
166
|
-
canvasHost.replaceChildren(
|
|
167
|
-
h("div.lt3-graph-canvas", {
|
|
168
|
-
html: `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Knowledge graph">${edgeSvg}${nodeSvg}</svg>`,
|
|
169
|
-
on: { click: onCanvasClick },
|
|
170
|
-
}),
|
|
171
|
-
);
|
|
167
|
+
if (!nodes.length) {
|
|
168
|
+
if (graphCanvas) { graphCanvas.destroy(); graphCanvas = null; }
|
|
169
|
+
canvasHost.replaceChildren(c.emptyState({ icon: "chart-dots-3", title: "No entities yet", body: "Index a source to populate the graph." }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const gc = ensureGraphCanvas();
|
|
173
|
+
gc.setData({ nodes, edges });
|
|
174
|
+
gc.setSelected(state.selected);
|
|
175
|
+
if (!gc.el.isConnected || gc.el.parentElement !== canvasHost.querySelector(".lt3-graph-canvas")) {
|
|
176
|
+
canvasHost.replaceChildren(h("div.lt3-graph-canvas", gc.el));
|
|
177
|
+
}
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
if (!g) return;
|
|
177
|
-
state.selected = g.dataset.id === state.selected ? null : g.dataset.id;
|
|
178
|
-
renderCanvas();
|
|
180
|
+
function syncSelection() {
|
|
181
|
+
if (graphCanvas) graphCanvas.setSelected(state.selected);
|
|
179
182
|
renderInspector();
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
function isNeighbor(id) {
|
|
183
|
-
if (!state.selected) return false;
|
|
184
|
-
return state.data.edges.some((e) =>
|
|
185
|
-
(e.from === state.selected && e.to === id) || (e.to === state.selected && e.from === id));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
185
|
function renderInspector() {
|
|
189
186
|
if (state.selected) { inspectorHost.replaceChildren(detailView()); return; }
|
|
190
187
|
const q = state.query;
|
|
@@ -199,7 +196,7 @@ function buildExplore(ctx) {
|
|
|
199
196
|
}
|
|
200
197
|
|
|
201
198
|
function entityRow(n) {
|
|
202
|
-
return h("button.lt3-entity", { on: { click: () => { state.selected = n.id;
|
|
199
|
+
return h("button.lt3-entity", { on: { click: () => { state.selected = n.id; syncSelection(); } } },
|
|
203
200
|
h("div.lt3-entity__type", { style: { background: `color-mix(in srgb, ${colorFor(n.type)} 18%, transparent)`, color: colorFor(n.type) } }, icon(iconForType(n.type))),
|
|
204
201
|
h("div.lt3-entity__body",
|
|
205
202
|
h("div.lt3-entity__name", n.label),
|
|
@@ -220,7 +217,7 @@ function buildExplore(ctx) {
|
|
|
220
217
|
})
|
|
221
218
|
.filter((r) => r.other);
|
|
222
219
|
return h("div.lt3-stack-4",
|
|
223
|
-
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => { state.selected = null;
|
|
220
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => { state.selected = null; syncSelection(); } } }, icon("arrow-left"), "All entities"),
|
|
224
221
|
h("div.lt3-card.lt3-card--flat",
|
|
225
222
|
h("div.lt3-row-2", { style: { "margin-bottom": "var(--lt3-space-2)" } },
|
|
226
223
|
h("span.lt3-pill", { style: { color: colorFor(n.type) } }, n.type || "Entity"),
|
|
@@ -231,7 +228,7 @@ function buildExplore(ctx) {
|
|
|
231
228
|
h("div",
|
|
232
229
|
h("div.lt3-eyebrow", { style: { "margin-bottom": "var(--lt3-space-2)" } }, `Relations (${rels.length})`),
|
|
233
230
|
rels.length
|
|
234
|
-
? h("div.lt3-stack-2", rels.map((r) => h("button.lt3-entity", { on: { click: () => { state.selected = r.other.id;
|
|
231
|
+
? h("div.lt3-stack-2", rels.map((r) => h("button.lt3-entity", { on: { click: () => { state.selected = r.other.id; syncSelection(); } } },
|
|
235
232
|
h("div.lt3-entity__type", { style: { background: "var(--surface-3)" } }, h("span.lt3-mono", { style: { "font-size": "var(--lt3-text-sm)" } }, r.dir)),
|
|
236
233
|
h("div.lt3-entity__body",
|
|
237
234
|
h("div.lt3-entity__name", r.other.label),
|
|
@@ -251,21 +248,25 @@ function buildExplore(ctx) {
|
|
|
251
248
|
async function renderStatus(ctx, host) {
|
|
252
249
|
const { h, icon, api, c } = ctx;
|
|
253
250
|
host.replaceChildren(c.loading({ lines: 3 }));
|
|
254
|
-
const [port, gs, idx] = await Promise.all([api.kgPortability(), api.graphStats(), api.indexStatus()]);
|
|
251
|
+
const [port, gs, idx, coverage] = await Promise.all([api.kgPortability(), api.graphStats(), api.indexStatus(), api.kgProvenanceCoverage()]);
|
|
255
252
|
const p = port.data || {};
|
|
256
253
|
const prov = p.provenance || {};
|
|
254
|
+
const cov = coverage.data || {};
|
|
257
255
|
const nodes = sumCounts((gs.data && gs.data.nodes) || {});
|
|
258
256
|
const edges = sumCounts((gs.data && gs.data.edges) || {});
|
|
259
257
|
const pipelines = (idx.data && idx.data.pipelines) || {};
|
|
258
|
+
const ratio = Number(cov.coverage_ratio ?? cov.ratio ?? 0);
|
|
259
|
+
const covered = Number(cov.nodes_with_provenance ?? cov.covered_nodes ?? 0);
|
|
260
260
|
|
|
261
261
|
host.replaceChildren(
|
|
262
|
-
h("div.lt3-row-2", c.sourceBadge(port.source), h("span.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } },
|
|
262
|
+
h("div.lt3-row-2", c.sourceBadge(port.source === "live" || coverage.source === "live" ? "live" : port.source), h("span.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } },
|
|
263
263
|
p.graph_schema_version != null ? `Schema v${p.graph_schema_version} · embed dim ${p.embed_dim ?? "—"}` : "Knowledge Graph status")),
|
|
264
264
|
h("div.lt3-statrow",
|
|
265
265
|
c.stat({ label: "Entities", value: c.fmtNum(nodes), icon: "circles" }),
|
|
266
266
|
c.stat({ label: "Relations", value: c.fmtNum(edges), icon: "vector-triangle" }),
|
|
267
267
|
c.stat({ label: "Ingested items", value: c.fmtNum(prov.total || 0), icon: "package-import" }),
|
|
268
268
|
c.stat({ label: "Embedded (RAG-ready)", value: c.fmtNum(prov.embedded || 0), icon: "vector" }),
|
|
269
|
+
c.stat({ label: t("kg.provenanceCoverage"), value: `${Math.round(ratio * 100)}%`, icon: "shield-check", delta: `${covered}/${cov.total_nodes ?? nodes}` }),
|
|
269
270
|
),
|
|
270
271
|
c.card(
|
|
271
272
|
h("div.lt3-stack-3",
|
|
@@ -275,6 +276,14 @@ async function renderStatus(ctx, host) {
|
|
|
275
276
|
pipelineRow(ctx, "Hybrid retrieval", pipelines.hybrid),
|
|
276
277
|
),
|
|
277
278
|
),
|
|
279
|
+
c.card(h("div.lt3-stack-3",
|
|
280
|
+
h("div.lt3-eyebrow", t("kg.provenanceCoverage")),
|
|
281
|
+
h("dl.lt3-keyval",
|
|
282
|
+
h("dt", t("kg.coveredNodes")), h("dd", `${covered}/${cov.total_nodes ?? nodes}`),
|
|
283
|
+
h("dt", t("kg.sourceTypes")), h("dd", compactCounts(cov.provenance_by_source_type || cov.by_source_type || {})),
|
|
284
|
+
h("dt", t("kg.uncoveredTypes")), h("dd", compactCounts(cov.uncovered_by_type || {})),
|
|
285
|
+
),
|
|
286
|
+
)),
|
|
278
287
|
prov.last_ingested_at
|
|
279
288
|
? h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } }, `Last ingestion: ${fmtWhen(prov.last_ingested_at)} · ${prov.duplicates || 0} duplicate(s) linked, not re-stored.`)
|
|
280
289
|
: c.emptyState({ icon: "package-import", title: "Nothing ingested yet", body: "Add files or capture a page to populate the graph." }),
|
|
@@ -331,18 +340,18 @@ function buildCapture(ctx) {
|
|
|
331
340
|
|
|
332
341
|
async function run() {
|
|
333
342
|
const url = (input.value || "").trim();
|
|
334
|
-
if (!url) { result.replaceChildren(c.banner(
|
|
343
|
+
if (!url) { result.replaceChildren(c.banner("Enter a URL first.", "warn", "alert-triangle")); return; }
|
|
335
344
|
result.replaceChildren(c.loading({ lines: 1 }));
|
|
336
345
|
const res = await api.browserReadUrl(url);
|
|
337
346
|
const d = res.data || {};
|
|
338
347
|
if (res.ok && d.status === "ok") {
|
|
339
|
-
result.replaceChildren(c.banner(
|
|
348
|
+
result.replaceChildren(c.banner(`Added to your Knowledge Graph${d.duplicate ? " (already present — linked)" : ""}. ${d.chunk_count || 0} chunk(s) indexed.`, "ok", "circle-check"));
|
|
340
349
|
ctx.toast && ctx.toast("Page added to Knowledge Graph");
|
|
341
350
|
} else if (d.status === "empty") {
|
|
342
|
-
result.replaceChildren(c.banner(
|
|
351
|
+
result.replaceChildren(c.banner("No readable text was found on that page.", "warn", "alert-triangle"));
|
|
343
352
|
} else {
|
|
344
353
|
const detail = d.detail || (res.status === 422 ? "The page is blocked or login-required." : "Could not read that URL.");
|
|
345
|
-
result.replaceChildren(c.banner(
|
|
354
|
+
result.replaceChildren(c.banner(detail, "err", "alert-triangle"));
|
|
346
355
|
}
|
|
347
356
|
}
|
|
348
357
|
|
|
@@ -370,7 +379,9 @@ async function renderPortability(ctx, host) {
|
|
|
370
379
|
const port = await api.kgPortability();
|
|
371
380
|
const status = h("div");
|
|
372
381
|
|
|
373
|
-
function note(tone, text) {
|
|
382
|
+
function note(tone, text) {
|
|
383
|
+
status.replaceChildren(c.banner(text, tone, tone === "err" ? "alert-triangle" : tone === "ok" ? "circle-check" : "info-circle"));
|
|
384
|
+
}
|
|
374
385
|
|
|
375
386
|
async function doExport() {
|
|
376
387
|
note("info", "Exporting…");
|
|
@@ -443,6 +454,11 @@ function sumCounts(obj) {
|
|
|
443
454
|
return Object.values(obj || {}).reduce((a, b) => a + (Number(b) || 0), 0);
|
|
444
455
|
}
|
|
445
456
|
|
|
457
|
+
function compactCounts(obj) {
|
|
458
|
+
const entries = Object.entries(obj || {});
|
|
459
|
+
return entries.length ? entries.map(([k, v]) => `${k}: ${v}`).join(" · ") : "—";
|
|
460
|
+
}
|
|
461
|
+
|
|
446
462
|
function prettySource(k) {
|
|
447
463
|
return ({ web_url: "Web URL", browser_tab: "Browser tab", file: "Files", local_file: "Local files",
|
|
448
464
|
note: "Notes", text: "Text", markdown: "Markdown", code: "Code", upload: "Uploads" })[k] || k;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* ========================================================================== */
|
|
9
9
|
|
|
10
10
|
import { escapeHtml } from "../core/dom.js";
|
|
11
|
+
import { createGraphCanvas } from "./graph-canvas.js";
|
|
12
|
+
import { t } from "../core/i18n.js";
|
|
11
13
|
|
|
12
14
|
const TYPE_COLOR = {
|
|
13
15
|
Topic: "var(--lt3-pillar-graph)",
|
|
@@ -144,47 +146,42 @@ function buildExplore(ctx) {
|
|
|
144
146
|
);
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
// Live force-directed canvas (zoom / pan / drag / physics) — replaces the
|
|
150
|
+
// static SVG spiral. The renderer only draws the data it is given.
|
|
151
|
+
let graphCanvas = null;
|
|
152
|
+
|
|
153
|
+
function ensureGraphCanvas() {
|
|
154
|
+
if (graphCanvas) return graphCanvas;
|
|
155
|
+
graphCanvas = createGraphCanvas({
|
|
156
|
+
colorFor,
|
|
157
|
+
onSelect: (id) => {
|
|
158
|
+
state.selected = id;
|
|
159
|
+
renderInspector();
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
return graphCanvas;
|
|
163
|
+
}
|
|
164
|
+
|
|
147
165
|
function renderCanvas() {
|
|
148
166
|
const { nodes, edges } = state.data;
|
|
149
|
-
if (!nodes.length) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const sel = state.selected === n.id;
|
|
161
|
-
return `<g class="lt3-gnode" data-id="${escapeHtml(n.id)}" opacity="${state.selected && !sel && !isNeighbor(n.id) ? 0.35 : 1}">
|
|
162
|
-
<circle cx="${n.px}" cy="${n.py}" r="${sel ? r + 3 : r}" fill="${colorFor(n.type)}" stroke-width="${sel ? 3 : 2}"></circle>
|
|
163
|
-
<text x="${n.px}" y="${n.py + r + 13}" text-anchor="middle">${escapeHtml(truncate(n.label, 18))}</text>
|
|
164
|
-
</g>`;
|
|
165
|
-
}).join("");
|
|
166
|
-
canvasHost.replaceChildren(
|
|
167
|
-
h("div.lt3-graph-canvas", {
|
|
168
|
-
html: `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Knowledge graph">${edgeSvg}${nodeSvg}</svg>`,
|
|
169
|
-
on: { click: onCanvasClick },
|
|
170
|
-
}),
|
|
171
|
-
);
|
|
167
|
+
if (!nodes.length) {
|
|
168
|
+
if (graphCanvas) { graphCanvas.destroy(); graphCanvas = null; }
|
|
169
|
+
canvasHost.replaceChildren(c.emptyState({ icon: "chart-dots-3", title: "No entities yet", body: "Index a source to populate the graph." }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const gc = ensureGraphCanvas();
|
|
173
|
+
gc.setData({ nodes, edges });
|
|
174
|
+
gc.setSelected(state.selected);
|
|
175
|
+
if (!gc.el.isConnected || gc.el.parentElement !== canvasHost.querySelector(".lt3-graph-canvas")) {
|
|
176
|
+
canvasHost.replaceChildren(h("div.lt3-graph-canvas", gc.el));
|
|
177
|
+
}
|
|
172
178
|
}
|
|
173
179
|
|
|
174
|
-
function
|
|
175
|
-
|
|
176
|
-
if (!g) return;
|
|
177
|
-
state.selected = g.dataset.id === state.selected ? null : g.dataset.id;
|
|
178
|
-
renderCanvas();
|
|
180
|
+
function syncSelection() {
|
|
181
|
+
if (graphCanvas) graphCanvas.setSelected(state.selected);
|
|
179
182
|
renderInspector();
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
function isNeighbor(id) {
|
|
183
|
-
if (!state.selected) return false;
|
|
184
|
-
return state.data.edges.some((e) =>
|
|
185
|
-
(e.from === state.selected && e.to === id) || (e.to === state.selected && e.from === id));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
185
|
function renderInspector() {
|
|
189
186
|
if (state.selected) { inspectorHost.replaceChildren(detailView()); return; }
|
|
190
187
|
const q = state.query;
|
|
@@ -199,7 +196,7 @@ function buildExplore(ctx) {
|
|
|
199
196
|
}
|
|
200
197
|
|
|
201
198
|
function entityRow(n) {
|
|
202
|
-
return h("button.lt3-entity", { on: { click: () => { state.selected = n.id;
|
|
199
|
+
return h("button.lt3-entity", { on: { click: () => { state.selected = n.id; syncSelection(); } } },
|
|
203
200
|
h("div.lt3-entity__type", { style: { background: `color-mix(in srgb, ${colorFor(n.type)} 18%, transparent)`, color: colorFor(n.type) } }, icon(iconForType(n.type))),
|
|
204
201
|
h("div.lt3-entity__body",
|
|
205
202
|
h("div.lt3-entity__name", n.label),
|
|
@@ -220,7 +217,7 @@ function buildExplore(ctx) {
|
|
|
220
217
|
})
|
|
221
218
|
.filter((r) => r.other);
|
|
222
219
|
return h("div.lt3-stack-4",
|
|
223
|
-
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => { state.selected = null;
|
|
220
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => { state.selected = null; syncSelection(); } } }, icon("arrow-left"), "All entities"),
|
|
224
221
|
h("div.lt3-card.lt3-card--flat",
|
|
225
222
|
h("div.lt3-row-2", { style: { "margin-bottom": "var(--lt3-space-2)" } },
|
|
226
223
|
h("span.lt3-pill", { style: { color: colorFor(n.type) } }, n.type || "Entity"),
|
|
@@ -231,7 +228,7 @@ function buildExplore(ctx) {
|
|
|
231
228
|
h("div",
|
|
232
229
|
h("div.lt3-eyebrow", { style: { "margin-bottom": "var(--lt3-space-2)" } }, `Relations (${rels.length})`),
|
|
233
230
|
rels.length
|
|
234
|
-
? h("div.lt3-stack-2", rels.map((r) => h("button.lt3-entity", { on: { click: () => { state.selected = r.other.id;
|
|
231
|
+
? h("div.lt3-stack-2", rels.map((r) => h("button.lt3-entity", { on: { click: () => { state.selected = r.other.id; syncSelection(); } } },
|
|
235
232
|
h("div.lt3-entity__type", { style: { background: "var(--surface-3)" } }, h("span.lt3-mono", { style: { "font-size": "var(--lt3-text-sm)" } }, r.dir)),
|
|
236
233
|
h("div.lt3-entity__body",
|
|
237
234
|
h("div.lt3-entity__name", r.other.label),
|
|
@@ -251,21 +248,25 @@ function buildExplore(ctx) {
|
|
|
251
248
|
async function renderStatus(ctx, host) {
|
|
252
249
|
const { h, icon, api, c } = ctx;
|
|
253
250
|
host.replaceChildren(c.loading({ lines: 3 }));
|
|
254
|
-
const [port, gs, idx] = await Promise.all([api.kgPortability(), api.graphStats(), api.indexStatus()]);
|
|
251
|
+
const [port, gs, idx, coverage] = await Promise.all([api.kgPortability(), api.graphStats(), api.indexStatus(), api.kgProvenanceCoverage()]);
|
|
255
252
|
const p = port.data || {};
|
|
256
253
|
const prov = p.provenance || {};
|
|
254
|
+
const cov = coverage.data || {};
|
|
257
255
|
const nodes = sumCounts((gs.data && gs.data.nodes) || {});
|
|
258
256
|
const edges = sumCounts((gs.data && gs.data.edges) || {});
|
|
259
257
|
const pipelines = (idx.data && idx.data.pipelines) || {};
|
|
258
|
+
const ratio = Number(cov.coverage_ratio ?? cov.ratio ?? 0);
|
|
259
|
+
const covered = Number(cov.nodes_with_provenance ?? cov.covered_nodes ?? 0);
|
|
260
260
|
|
|
261
261
|
host.replaceChildren(
|
|
262
|
-
h("div.lt3-row-2", c.sourceBadge(port.source), h("span.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } },
|
|
262
|
+
h("div.lt3-row-2", c.sourceBadge(port.source === "live" || coverage.source === "live" ? "live" : port.source), h("span.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } },
|
|
263
263
|
p.graph_schema_version != null ? `Schema v${p.graph_schema_version} · embed dim ${p.embed_dim ?? "—"}` : "Knowledge Graph status")),
|
|
264
264
|
h("div.lt3-statrow",
|
|
265
265
|
c.stat({ label: "Entities", value: c.fmtNum(nodes), icon: "circles" }),
|
|
266
266
|
c.stat({ label: "Relations", value: c.fmtNum(edges), icon: "vector-triangle" }),
|
|
267
267
|
c.stat({ label: "Ingested items", value: c.fmtNum(prov.total || 0), icon: "package-import" }),
|
|
268
268
|
c.stat({ label: "Embedded (RAG-ready)", value: c.fmtNum(prov.embedded || 0), icon: "vector" }),
|
|
269
|
+
c.stat({ label: t("kg.provenanceCoverage"), value: `${Math.round(ratio * 100)}%`, icon: "shield-check", delta: `${covered}/${cov.total_nodes ?? nodes}` }),
|
|
269
270
|
),
|
|
270
271
|
c.card(
|
|
271
272
|
h("div.lt3-stack-3",
|
|
@@ -275,6 +276,14 @@ async function renderStatus(ctx, host) {
|
|
|
275
276
|
pipelineRow(ctx, "Hybrid retrieval", pipelines.hybrid),
|
|
276
277
|
),
|
|
277
278
|
),
|
|
279
|
+
c.card(h("div.lt3-stack-3",
|
|
280
|
+
h("div.lt3-eyebrow", t("kg.provenanceCoverage")),
|
|
281
|
+
h("dl.lt3-keyval",
|
|
282
|
+
h("dt", t("kg.coveredNodes")), h("dd", `${covered}/${cov.total_nodes ?? nodes}`),
|
|
283
|
+
h("dt", t("kg.sourceTypes")), h("dd", compactCounts(cov.provenance_by_source_type || cov.by_source_type || {})),
|
|
284
|
+
h("dt", t("kg.uncoveredTypes")), h("dd", compactCounts(cov.uncovered_by_type || {})),
|
|
285
|
+
),
|
|
286
|
+
)),
|
|
278
287
|
prov.last_ingested_at
|
|
279
288
|
? h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)" } }, `Last ingestion: ${fmtWhen(prov.last_ingested_at)} · ${prov.duplicates || 0} duplicate(s) linked, not re-stored.`)
|
|
280
289
|
: c.emptyState({ icon: "package-import", title: "Nothing ingested yet", body: "Add files or capture a page to populate the graph." }),
|
|
@@ -331,18 +340,18 @@ function buildCapture(ctx) {
|
|
|
331
340
|
|
|
332
341
|
async function run() {
|
|
333
342
|
const url = (input.value || "").trim();
|
|
334
|
-
if (!url) { result.replaceChildren(c.banner(
|
|
343
|
+
if (!url) { result.replaceChildren(c.banner("Enter a URL first.", "warn", "alert-triangle")); return; }
|
|
335
344
|
result.replaceChildren(c.loading({ lines: 1 }));
|
|
336
345
|
const res = await api.browserReadUrl(url);
|
|
337
346
|
const d = res.data || {};
|
|
338
347
|
if (res.ok && d.status === "ok") {
|
|
339
|
-
result.replaceChildren(c.banner(
|
|
348
|
+
result.replaceChildren(c.banner(`Added to your Knowledge Graph${d.duplicate ? " (already present — linked)" : ""}. ${d.chunk_count || 0} chunk(s) indexed.`, "ok", "circle-check"));
|
|
340
349
|
ctx.toast && ctx.toast("Page added to Knowledge Graph");
|
|
341
350
|
} else if (d.status === "empty") {
|
|
342
|
-
result.replaceChildren(c.banner(
|
|
351
|
+
result.replaceChildren(c.banner("No readable text was found on that page.", "warn", "alert-triangle"));
|
|
343
352
|
} else {
|
|
344
353
|
const detail = d.detail || (res.status === 422 ? "The page is blocked or login-required." : "Could not read that URL.");
|
|
345
|
-
result.replaceChildren(c.banner(
|
|
354
|
+
result.replaceChildren(c.banner(detail, "err", "alert-triangle"));
|
|
346
355
|
}
|
|
347
356
|
}
|
|
348
357
|
|
|
@@ -370,7 +379,9 @@ async function renderPortability(ctx, host) {
|
|
|
370
379
|
const port = await api.kgPortability();
|
|
371
380
|
const status = h("div");
|
|
372
381
|
|
|
373
|
-
function note(tone, text) {
|
|
382
|
+
function note(tone, text) {
|
|
383
|
+
status.replaceChildren(c.banner(text, tone, tone === "err" ? "alert-triangle" : tone === "ok" ? "circle-check" : "info-circle"));
|
|
384
|
+
}
|
|
374
385
|
|
|
375
386
|
async function doExport() {
|
|
376
387
|
note("info", "Exporting…");
|
|
@@ -443,6 +454,11 @@ function sumCounts(obj) {
|
|
|
443
454
|
return Object.values(obj || {}).reduce((a, b) => a + (Number(b) || 0), 0);
|
|
444
455
|
}
|
|
445
456
|
|
|
457
|
+
function compactCounts(obj) {
|
|
458
|
+
const entries = Object.entries(obj || {});
|
|
459
|
+
return entries.length ? entries.map(([k, v]) => `${k}: ${v}`).join(" · ") : "—";
|
|
460
|
+
}
|
|
461
|
+
|
|
446
462
|
function prettySource(k) {
|
|
447
463
|
return ({ web_url: "Web URL", browser_tab: "Browser tab", file: "Files", local_file: "Local files",
|
|
448
464
|
note: "Notes", text: "Text", markdown: "Markdown", code: "Code", upload: "Uploads" })[k] || k;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { t } from "../core/i18n.880e1fec.js";
|
|
2
|
+
|
|
3
|
+
export async function render(ctx) {
|
|
4
|
+
const { h, icon, api, c, toast, store } = ctx;
|
|
5
|
+
const host = h("div.lt3-stack-6", c.loading({ lines: 5, block: true }));
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
const [identity, peers] = await Promise.all([api.networkIdentity(), api.networkPeers()]);
|
|
9
|
+
host.replaceChildren(
|
|
10
|
+
c.viewHeader({
|
|
11
|
+
eyebrow: t("network.eyebrow"),
|
|
12
|
+
title: t("network.title"),
|
|
13
|
+
sub: t("network.sub"),
|
|
14
|
+
actions: [c.sourceBadge(identity.source === "live" || peers.source === "live" ? "live" : "unavailable")],
|
|
15
|
+
}),
|
|
16
|
+
identityPanel(identity),
|
|
17
|
+
pairPanel(),
|
|
18
|
+
peersPanel(peers),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identityPanel(res) {
|
|
23
|
+
const d = res.data || {};
|
|
24
|
+
return c.panel({
|
|
25
|
+
title: t("network.identity"),
|
|
26
|
+
actions: [c.sourceBadge(res.source)],
|
|
27
|
+
children: h("dl.lt3-keyval",
|
|
28
|
+
h("dt", "device_id"), h("dd", h("span.lt3-mono", d.device_id || d.id || "—")),
|
|
29
|
+
h("dt", "fingerprint"), h("dd", h("span.lt3-mono", d.fingerprint || d.public_key_fingerprint || "—")),
|
|
30
|
+
h("dt", t("network.publicKey")), h("dd", h("pre.lt3-code", truncate(d.public_key || "—", 420))),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pairPanel() {
|
|
36
|
+
const name = h("input.lt3-input", { type: "text", placeholder: t("network.peerName"), "aria-label": t("network.peerName") });
|
|
37
|
+
const base = h("input.lt3-input", { type: "url", placeholder: t("network.baseUrl"), "aria-label": t("network.baseUrl") });
|
|
38
|
+
const key = h("textarea.lt3-textarea", { rows: 3, placeholder: t("network.publicKey"), "aria-label": t("network.publicKey") });
|
|
39
|
+
return c.panel({
|
|
40
|
+
title: t("network.pair"),
|
|
41
|
+
children: h("div.lt3-stack-4",
|
|
42
|
+
h("div.lt3-grid-2", field(ctx, t("network.peerName"), name), field(ctx, t("network.baseUrl"), base)),
|
|
43
|
+
field(ctx, t("network.publicKey"), key),
|
|
44
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: async () => {
|
|
45
|
+
const res = await api.pairPeer({ name: name.value.trim(), base_url: base.value.trim(), public_key: key.value.trim() });
|
|
46
|
+
toast(resultText(res, t("network.paired")), res.ok ? "ok" : "err");
|
|
47
|
+
if (res.ok) load();
|
|
48
|
+
} } }, icon("link"), t("network.pair")),
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function peersPanel(res) {
|
|
54
|
+
const rows = Array.isArray(res.data?.peers) ? res.data.peers : [];
|
|
55
|
+
return c.panel({
|
|
56
|
+
title: t("network.peers"),
|
|
57
|
+
actions: [c.sourceBadge(res.source)],
|
|
58
|
+
children: rows.length ? c.table([
|
|
59
|
+
{ key: "name", label: t("common.name"), render: (p) => h("div", h("b", p.name || p.peer_id || p.id), h("div.lt3-faint", { style: { "font-family": "var(--lt3-font-mono)", "font-size": "var(--lt3-text-2xs)" } }, p.peer_id || p.id || "")) },
|
|
60
|
+
{ key: "base", label: t("network.baseUrl"), render: (p) => p.base_url || "—" },
|
|
61
|
+
{ key: "fp", label: "fingerprint", render: (p) => h("span.lt3-mono", p.fingerprint || p.public_key_fingerprint || "—") },
|
|
62
|
+
{ key: "act", label: "", width: "1%", render: (p) => h("div.lt3-row-2",
|
|
63
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => pushPeer(p.peer_id || p.id) } }, icon("send"), t("network.push")),
|
|
64
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => unpairPeer(p.peer_id || p.id) } }, icon("unlink"), t("network.unpair")),
|
|
65
|
+
) },
|
|
66
|
+
], rows) : c.emptyState({ icon: "network-off", title: t("network.peers"), body: t("common.none") }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function pushPeer(peerId) {
|
|
71
|
+
const res = await api.pushPeer(peerId, store.get().workspaceId);
|
|
72
|
+
toast(resultText(res, t("network.pushed")), res.ok ? "ok" : "err");
|
|
73
|
+
}
|
|
74
|
+
async function unpairPeer(peerId) {
|
|
75
|
+
const res = await api.unpairPeer(peerId);
|
|
76
|
+
toast(resultText(res, t("network.unpaired")), res.ok ? "ok" : "err");
|
|
77
|
+
if (res.ok) load();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await load();
|
|
81
|
+
return host;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function field({ h }, label, control) {
|
|
85
|
+
return h("div.lt3-field", h("label.lt3-label", label), control);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function truncate(value, n) {
|
|
89
|
+
const s = String(value || "");
|
|
90
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resultText(res, okText) {
|
|
94
|
+
if (res && res.ok) return okText;
|
|
95
|
+
const data = (res && res.data) || {};
|
|
96
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
97
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { t } from "../core/i18n.js";
|
|
2
|
+
|
|
3
|
+
export async function render(ctx) {
|
|
4
|
+
const { h, icon, api, c, toast, store } = ctx;
|
|
5
|
+
const host = h("div.lt3-stack-6", c.loading({ lines: 5, block: true }));
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
const [identity, peers] = await Promise.all([api.networkIdentity(), api.networkPeers()]);
|
|
9
|
+
host.replaceChildren(
|
|
10
|
+
c.viewHeader({
|
|
11
|
+
eyebrow: t("network.eyebrow"),
|
|
12
|
+
title: t("network.title"),
|
|
13
|
+
sub: t("network.sub"),
|
|
14
|
+
actions: [c.sourceBadge(identity.source === "live" || peers.source === "live" ? "live" : "unavailable")],
|
|
15
|
+
}),
|
|
16
|
+
identityPanel(identity),
|
|
17
|
+
pairPanel(),
|
|
18
|
+
peersPanel(peers),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identityPanel(res) {
|
|
23
|
+
const d = res.data || {};
|
|
24
|
+
return c.panel({
|
|
25
|
+
title: t("network.identity"),
|
|
26
|
+
actions: [c.sourceBadge(res.source)],
|
|
27
|
+
children: h("dl.lt3-keyval",
|
|
28
|
+
h("dt", "device_id"), h("dd", h("span.lt3-mono", d.device_id || d.id || "—")),
|
|
29
|
+
h("dt", "fingerprint"), h("dd", h("span.lt3-mono", d.fingerprint || d.public_key_fingerprint || "—")),
|
|
30
|
+
h("dt", t("network.publicKey")), h("dd", h("pre.lt3-code", truncate(d.public_key || "—", 420))),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pairPanel() {
|
|
36
|
+
const name = h("input.lt3-input", { type: "text", placeholder: t("network.peerName"), "aria-label": t("network.peerName") });
|
|
37
|
+
const base = h("input.lt3-input", { type: "url", placeholder: t("network.baseUrl"), "aria-label": t("network.baseUrl") });
|
|
38
|
+
const key = h("textarea.lt3-textarea", { rows: 3, placeholder: t("network.publicKey"), "aria-label": t("network.publicKey") });
|
|
39
|
+
return c.panel({
|
|
40
|
+
title: t("network.pair"),
|
|
41
|
+
children: h("div.lt3-stack-4",
|
|
42
|
+
h("div.lt3-grid-2", field(ctx, t("network.peerName"), name), field(ctx, t("network.baseUrl"), base)),
|
|
43
|
+
field(ctx, t("network.publicKey"), key),
|
|
44
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: async () => {
|
|
45
|
+
const res = await api.pairPeer({ name: name.value.trim(), base_url: base.value.trim(), public_key: key.value.trim() });
|
|
46
|
+
toast(resultText(res, t("network.paired")), res.ok ? "ok" : "err");
|
|
47
|
+
if (res.ok) load();
|
|
48
|
+
} } }, icon("link"), t("network.pair")),
|
|
49
|
+
),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function peersPanel(res) {
|
|
54
|
+
const rows = Array.isArray(res.data?.peers) ? res.data.peers : [];
|
|
55
|
+
return c.panel({
|
|
56
|
+
title: t("network.peers"),
|
|
57
|
+
actions: [c.sourceBadge(res.source)],
|
|
58
|
+
children: rows.length ? c.table([
|
|
59
|
+
{ key: "name", label: t("common.name"), render: (p) => h("div", h("b", p.name || p.peer_id || p.id), h("div.lt3-faint", { style: { "font-family": "var(--lt3-font-mono)", "font-size": "var(--lt3-text-2xs)" } }, p.peer_id || p.id || "")) },
|
|
60
|
+
{ key: "base", label: t("network.baseUrl"), render: (p) => p.base_url || "—" },
|
|
61
|
+
{ key: "fp", label: "fingerprint", render: (p) => h("span.lt3-mono", p.fingerprint || p.public_key_fingerprint || "—") },
|
|
62
|
+
{ key: "act", label: "", width: "1%", render: (p) => h("div.lt3-row-2",
|
|
63
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => pushPeer(p.peer_id || p.id) } }, icon("send"), t("network.push")),
|
|
64
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => unpairPeer(p.peer_id || p.id) } }, icon("unlink"), t("network.unpair")),
|
|
65
|
+
) },
|
|
66
|
+
], rows) : c.emptyState({ icon: "network-off", title: t("network.peers"), body: t("common.none") }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function pushPeer(peerId) {
|
|
71
|
+
const res = await api.pushPeer(peerId, store.get().workspaceId);
|
|
72
|
+
toast(resultText(res, t("network.pushed")), res.ok ? "ok" : "err");
|
|
73
|
+
}
|
|
74
|
+
async function unpairPeer(peerId) {
|
|
75
|
+
const res = await api.unpairPeer(peerId);
|
|
76
|
+
toast(resultText(res, t("network.unpaired")), res.ok ? "ok" : "err");
|
|
77
|
+
if (res.ok) load();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await load();
|
|
81
|
+
return host;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function field({ h }, label, control) {
|
|
85
|
+
return h("div.lt3-field", h("label.lt3-label", label), control);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function truncate(value, n) {
|
|
89
|
+
const s = String(value || "");
|
|
90
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resultText(res, okText) {
|
|
94
|
+
if (res && res.ok) return okText;
|
|
95
|
+
const data = (res && res.data) || {};
|
|
96
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
97
|
+
}
|