ltcai 3.2.0 → 3.3.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 +2 -1
- package/docs/architecture.md +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/package.json +1 -1
- package/scripts/build_v3_assets.mjs +7 -1
- package/static/css/{tokens.5a595671.css → tokens.8b8e31bd.css} +1 -1
- package/static/css/tokens.css +1 -1
- package/static/v3/asset-manifest.json +13 -13
- package/static/v3/css/{lattice.views.3ee19d4e.css → lattice.views.1d326beb.css} +5 -0
- package/static/v3/css/lattice.views.css +5 -0
- package/static/v3/js/{app.a5adc0f3.js → app.cf5bb712.js} +1 -1
- package/static/v3/js/core/{api.603b978f.js → api.113660c5.js} +59 -4
- package/static/v3/js/core/api.js +59 -4
- package/static/v3/js/core/{shell.ea0b9ae5.js → shell.9e707234.js} +1 -1
- package/static/v3/js/views/{chat.718144ce.js → chat.c48fd9e2.js} +1 -1
- package/static/v3/js/views/chat.js +1 -1
- package/static/v3/js/views/{files.4935197e.js → files.8464634a.js} +64 -18
- package/static/v3/js/views/files.js +64 -18
- package/static/v3/js/views/{memory.d2ed7a7c.js → memory.4ebdf474.js} +5 -4
- package/static/v3/js/views/memory.js +5 -4
- package/static/v3/js/views/{settings.4f777210.js → settings.c7b0cc05.js} +16 -2
- package/static/v3/js/views/settings.js +16 -2
package/README.md
CHANGED
|
@@ -290,7 +290,8 @@ Core areas:
|
|
|
290
290
|
|
|
291
291
|
| Version | Theme |
|
|
292
292
|
| --- | --- |
|
|
293
|
-
| **3.
|
|
293
|
+
| **3.3.0** | Product quality & honesty release — evidence-based feature audit (`FEATURE_STATUS.md`), single-source version truth, working manual document upload in Files, fixed document-generation streaming, truthful Home retrieval status, documented design system (`STYLE_SYSTEM.md`) |
|
|
294
|
+
| 3.2.0 | Feature-complete platform — multi-agent collaboration, agent registry, marketplace + templates, workflow agents, autonomous planning, long-term memory + manager, skills/hooks/tool registries, MCP manager, all operable from `/app` |
|
|
294
295
|
| 3.1.0 | Mainline platform completion — native `/app` workflows, Classic retired from normal paths, production embedding profiles, AgentRuntime/registries, hashed v3 assets |
|
|
295
296
|
| 3.0.1 | Release-blocker remediation — provider-backed embeddings (Hash/MLX/Ollama/OpenAI/Custom), unified AgentRuntime boundary, every v3 surface connected or clearly unavailable |
|
|
296
297
|
| 3.0.0 | v3 local-first AI workspace platform — `/app`, Native Chat, Knowledge Graph, Vector Index, Hybrid Search, workspace modes |
|
package/docs/architecture.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Lattice AI Architecture
|
|
2
2
|
|
|
3
|
-
> v3.
|
|
3
|
+
> v3.3.0 — feature-complete for non-enterprise use cases. The agent ecosystem
|
|
4
4
|
> (registry, marketplace + templates, workflow agents, autonomous planning),
|
|
5
5
|
> the long-term memory platform + manager, and the skills/hooks/tool/MCP
|
|
6
6
|
> registries are all operable from `/app`. Enterprise controls remain future
|
package/latticeai/__init__.py
CHANGED
|
@@ -14,7 +14,7 @@ from datetime import datetime
|
|
|
14
14
|
from typing import Any, Callable, Dict, List, Optional
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
MULTI_AGENT_VERSION = "3.
|
|
17
|
+
MULTI_AGENT_VERSION = "3.3.0"
|
|
18
18
|
|
|
19
19
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
20
20
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
@@ -18,7 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
WORKSPACE_OS_VERSION = "3.
|
|
21
|
+
WORKSPACE_OS_VERSION = "3.3.0"
|
|
22
22
|
|
|
23
23
|
# Workspace types separate single-user Personal workspaces from shared
|
|
24
24
|
# Organization workspaces. Both keep the same local-first JSON store; the type
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Lattice AI v3 local-first AI workspace platform with knowledge graph, vector index, hybrid search, agents, and workspace modes.",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
@@ -23,6 +23,12 @@ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
23
23
|
const staticRoot = join(repoRoot, "static");
|
|
24
24
|
const manifestPath = join(staticRoot, "v3", "asset-manifest.json");
|
|
25
25
|
|
|
26
|
+
// Version is sourced from package.json — the single source of truth for the
|
|
27
|
+
// release. Never hard-code a version string in the generated manifest.
|
|
28
|
+
const pkgVersion = JSON.parse(
|
|
29
|
+
readFileSync(join(repoRoot, "package.json"), "utf8"),
|
|
30
|
+
).version;
|
|
31
|
+
|
|
26
32
|
const cssSources = [
|
|
27
33
|
"static/css/tokens.css",
|
|
28
34
|
"static/v3/css/lattice.tokens.css",
|
|
@@ -151,7 +157,7 @@ for (const mod of modules.values()) {
|
|
|
151
157
|
}
|
|
152
158
|
|
|
153
159
|
const manifest = {
|
|
154
|
-
version:
|
|
160
|
+
version: pkgVersion,
|
|
155
161
|
generated_at: "deterministic",
|
|
156
162
|
entrypoints: {
|
|
157
163
|
app: assets[entry],
|
package/static/css/tokens.css
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
2
|
+
"version": "3.3.0",
|
|
3
3
|
"generated_at": "deterministic",
|
|
4
4
|
"entrypoints": {
|
|
5
|
-
"app": "/static/v3/js/app.
|
|
5
|
+
"app": "/static/v3/js/app.cf5bb712.js",
|
|
6
6
|
"styles": [
|
|
7
|
-
"/static/css/tokens.
|
|
7
|
+
"/static/css/tokens.8b8e31bd.css",
|
|
8
8
|
"/static/v3/css/lattice.tokens.c597ff81.css",
|
|
9
9
|
"/static/v3/css/lattice.base.e4cdd05d.css",
|
|
10
10
|
"/static/v3/css/lattice.components.011e988b.css",
|
|
11
11
|
"/static/v3/css/lattice.shell.4920f42d.css",
|
|
12
|
-
"/static/v3/css/lattice.views.
|
|
12
|
+
"/static/v3/css/lattice.views.1d326beb.css"
|
|
13
13
|
]
|
|
14
14
|
},
|
|
15
15
|
"assets": {
|
|
16
|
-
"static/css/tokens.css": "/static/css/tokens.
|
|
16
|
+
"static/css/tokens.css": "/static/css/tokens.8b8e31bd.css",
|
|
17
17
|
"static/v3/css/lattice.tokens.css": "/static/v3/css/lattice.tokens.c597ff81.css",
|
|
18
18
|
"static/v3/css/lattice.base.css": "/static/v3/css/lattice.base.e4cdd05d.css",
|
|
19
19
|
"static/v3/css/lattice.components.css": "/static/v3/css/lattice.components.011e988b.css",
|
|
20
20
|
"static/v3/css/lattice.shell.css": "/static/v3/css/lattice.shell.4920f42d.css",
|
|
21
|
-
"static/v3/css/lattice.views.css": "/static/v3/css/lattice.views.
|
|
22
|
-
"static/v3/js/app.js": "/static/v3/js/app.
|
|
23
|
-
"static/v3/js/core/api.js": "/static/v3/js/core/api.
|
|
21
|
+
"static/v3/css/lattice.views.css": "/static/v3/css/lattice.views.1d326beb.css",
|
|
22
|
+
"static/v3/js/app.js": "/static/v3/js/app.cf5bb712.js",
|
|
23
|
+
"static/v3/js/core/api.js": "/static/v3/js/core/api.113660c5.js",
|
|
24
24
|
"static/v3/js/core/components.js": "/static/v3/js/core/components.4c83e0a9.js",
|
|
25
25
|
"static/v3/js/core/dom.js": "/static/v3/js/core/dom.a2773eb0.js",
|
|
26
26
|
"static/v3/js/core/router.js": "/static/v3/js/core/router.584570f2.js",
|
|
27
27
|
"static/v3/js/core/routes.js": "/static/v3/js/core/routes.07ad6696.js",
|
|
28
|
-
"static/v3/js/core/shell.js": "/static/v3/js/core/shell.
|
|
28
|
+
"static/v3/js/core/shell.js": "/static/v3/js/core/shell.9e707234.js",
|
|
29
29
|
"static/v3/js/core/store.js": "/static/v3/js/core/store.34ebd5e6.js",
|
|
30
30
|
"static/v3/js/views/admin-audit.js": "/static/v3/js/views/admin-audit.660a1fb1.js",
|
|
31
31
|
"static/v3/js/views/admin-permissions.js": "/static/v3/js/views/admin-permissions.a7ae5f09.js",
|
|
@@ -34,20 +34,20 @@
|
|
|
34
34
|
"static/v3/js/views/admin-security.js": "/static/v3/js/views/admin-security.07c66b72.js",
|
|
35
35
|
"static/v3/js/views/admin-users.js": "/static/v3/js/views/admin-users.03bac88c.js",
|
|
36
36
|
"static/v3/js/views/agents.js": "/static/v3/js/views/agents.c373d48c.js",
|
|
37
|
-
"static/v3/js/views/chat.js": "/static/v3/js/views/chat.
|
|
38
|
-
"static/v3/js/views/files.js": "/static/v3/js/views/files.
|
|
37
|
+
"static/v3/js/views/chat.js": "/static/v3/js/views/chat.c48fd9e2.js",
|
|
38
|
+
"static/v3/js/views/files.js": "/static/v3/js/views/files.8464634a.js",
|
|
39
39
|
"static/v3/js/views/home.js": "/static/v3/js/views/home.cdde3b32.js",
|
|
40
40
|
"static/v3/js/views/hooks.js": "/static/v3/js/views/hooks.f3edebca.js",
|
|
41
41
|
"static/v3/js/views/hybrid-search.js": "/static/v3/js/views/hybrid-search.b22b97e0.js",
|
|
42
42
|
"static/v3/js/views/knowledge-graph.js": "/static/v3/js/views/knowledge-graph.a14ea7e7.js",
|
|
43
43
|
"static/v3/js/views/marketplace.js": "/static/v3/js/views/marketplace.ab0583d4.js",
|
|
44
44
|
"static/v3/js/views/mcp.js": "/static/v3/js/views/mcp.99b5c6a7.js",
|
|
45
|
-
"static/v3/js/views/memory.js": "/static/v3/js/views/memory.
|
|
45
|
+
"static/v3/js/views/memory.js": "/static/v3/js/views/memory.4ebdf474.js",
|
|
46
46
|
"static/v3/js/views/models.js": "/static/v3/js/views/models.a1ffa147.js",
|
|
47
47
|
"static/v3/js/views/my-computer.js": "/static/v3/js/views/my-computer.1b2ff621.js",
|
|
48
48
|
"static/v3/js/views/pipeline.js": "/static/v3/js/views/pipeline.c522f1ce.js",
|
|
49
49
|
"static/v3/js/views/planning.js": "/static/v3/js/views/planning.9ac3e313.js",
|
|
50
|
-
"static/v3/js/views/settings.js": "/static/v3/js/views/settings.
|
|
50
|
+
"static/v3/js/views/settings.js": "/static/v3/js/views/settings.c7b0cc05.js",
|
|
51
51
|
"static/v3/js/views/skills.js": "/static/v3/js/views/skills.c6c2f965.js",
|
|
52
52
|
"static/v3/js/views/tools.js": "/static/v3/js/views/tools.e4f11276.js",
|
|
53
53
|
"static/v3/js/views/workflows.js": "/static/v3/js/views/workflows.26c57290.js"
|
|
@@ -245,6 +245,11 @@
|
|
|
245
245
|
border-radius: var(--lt3-radius-lg);
|
|
246
246
|
background: var(--surface-2);
|
|
247
247
|
text-align: center;
|
|
248
|
+
transition: border-color var(--lt3-dur-2) var(--lt3-ease), background var(--lt3-dur-2) var(--lt3-ease);
|
|
249
|
+
}
|
|
250
|
+
.lt3-drop.is-dragover {
|
|
251
|
+
border-color: var(--accent);
|
|
252
|
+
background: var(--accent-soft);
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
/* ── Pipeline ────────────────────────────────────────────────────────────── */
|
|
@@ -245,6 +245,11 @@
|
|
|
245
245
|
border-radius: var(--lt3-radius-lg);
|
|
246
246
|
background: var(--surface-2);
|
|
247
247
|
text-align: center;
|
|
248
|
+
transition: border-color var(--lt3-dur-2) var(--lt3-ease), background var(--lt3-dur-2) var(--lt3-ease);
|
|
249
|
+
}
|
|
250
|
+
.lt3-drop.is-dragover {
|
|
251
|
+
border-color: var(--accent);
|
|
252
|
+
background: var(--accent-soft);
|
|
248
253
|
}
|
|
249
254
|
|
|
250
255
|
/* ── Pipeline ────────────────────────────────────────────────────────────── */
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Boots the shell. Views are lazy-loaded by the router (see core/routes.js).
|
|
4
4
|
* ========================================================================== */
|
|
5
5
|
|
|
6
|
-
import { boot } from "./core/shell.
|
|
6
|
+
import { boot } from "./core/shell.9e707234.js";
|
|
7
7
|
|
|
8
8
|
const root = document.getElementById("app");
|
|
9
9
|
if (root) boot(root);
|
|
@@ -82,9 +82,35 @@ export const api = {
|
|
|
82
82
|
|
|
83
83
|
/* ── Documented future surfaces ─────────────────────────────────────── */
|
|
84
84
|
|
|
85
|
-
/** GET /api/index/status — KG + Vector + Hybrid pipeline state.
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
/** GET /api/index/status — KG + Vector + Hybrid pipeline state.
|
|
86
|
+
* The backend endpoint is vector-centric (status/storage/source_items/…); the
|
|
87
|
+
* home pillars + topbar chip want a `pipelines` view keyed by
|
|
88
|
+
* knowledge_graph / vector_index / hybrid. Synthesize that shape from the real
|
|
89
|
+
* index status (vectors) plus the KG stats endpoint (entities). Nothing is
|
|
90
|
+
* fabricated: if the index endpoint is unavailable we report unavailable (so
|
|
91
|
+
* the UI shows the honest empty state), and a missing graph-stats count yields
|
|
92
|
+
* an "unavailable" graph pillar rather than a fake number. */
|
|
93
|
+
async indexStatus() {
|
|
94
|
+
const res = await raw("/api/index/status");
|
|
95
|
+
if (!(res.ok && res.data && !res.data.raw)) {
|
|
96
|
+
return { ok: false, status: res.status, data: EMPTY_INDEX_STATUS, source: "unavailable", error: res.error };
|
|
97
|
+
}
|
|
98
|
+
const idx = res.data;
|
|
99
|
+
let entities = null;
|
|
100
|
+
const gs = await raw("/knowledge-graph/stats");
|
|
101
|
+
if (gs.ok && gs.data && !gs.data.raw) {
|
|
102
|
+
const g = gs.data;
|
|
103
|
+
const n = g.total_nodes ?? g.nodes_total ?? (g.nodes && (g.nodes.total ?? g.nodes.count));
|
|
104
|
+
if (n !== undefined && n !== null) entities = Number(n) || 0;
|
|
105
|
+
}
|
|
106
|
+
const vectors = Number(idx.indexed_items ?? idx.ready_items) || 0;
|
|
107
|
+
const vstate = idx.status === "ready" ? "ready" : "pending";
|
|
108
|
+
const pipelines = {
|
|
109
|
+
knowledge_graph: { state: entities === null ? "unavailable" : "ready", entities: entities ?? 0 },
|
|
110
|
+
vector_index: { state: vstate, vectors },
|
|
111
|
+
hybrid: { state: vstate, strategy: vstate === "ready" ? "fused" : "pending" },
|
|
112
|
+
};
|
|
113
|
+
return { ok: true, status: res.status, data: { ...idx, pipelines }, source: "live" };
|
|
88
114
|
},
|
|
89
115
|
|
|
90
116
|
/** POST /api/index/rebuild — rebuild the derived vector index (real run). */
|
|
@@ -176,6 +202,31 @@ export const api = {
|
|
|
176
202
|
},
|
|
177
203
|
sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
|
|
178
204
|
|
|
205
|
+
/** POST /upload/document — manual document ingest (multipart/form-data).
|
|
206
|
+
* Real backend path: parse → chunk → embed → knowledge-graph ingest
|
|
207
|
+
* (latticeai/api/tools.py:/upload/document). Returns { ok, status, data,
|
|
208
|
+
* source }; never throws. FormData must NOT carry a JSON Content-Type — the
|
|
209
|
+
* browser sets the multipart boundary itself. */
|
|
210
|
+
async uploadDocument(file) {
|
|
211
|
+
const ws = store.get().workspaceId;
|
|
212
|
+
const form = new FormData();
|
|
213
|
+
form.append("file", file);
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch("/upload/document", {
|
|
216
|
+
method: "POST",
|
|
217
|
+
credentials: "same-origin",
|
|
218
|
+
headers: { "Accept": "application/json", ...(ws ? { "X-Workspace-Id": ws } : {}) },
|
|
219
|
+
body: form,
|
|
220
|
+
});
|
|
221
|
+
let data = null;
|
|
222
|
+
const text = await res.text();
|
|
223
|
+
if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
|
|
224
|
+
return { ok: res.ok, status: res.status, data, source: res.ok ? "live" : "unavailable" };
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return { ok: false, status: 0, data: null, source: "unavailable", error: String(err) };
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
179
230
|
adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
|
|
180
231
|
adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
|
|
181
232
|
adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
|
|
@@ -310,7 +361,11 @@ export const api = {
|
|
|
310
361
|
const rawData = line.slice(5).trim();
|
|
311
362
|
if (rawData === "[DONE]") return { source: "live", text, trace, model };
|
|
312
363
|
let data; try { data = JSON.parse(rawData); } catch { continue; }
|
|
313
|
-
|
|
364
|
+
// Standard chat streams `chunk`; the document-generation path streams
|
|
365
|
+
// `text` (report body + footnotes). Accept both so doc requests render
|
|
366
|
+
// instead of falsely reporting the backend as unreachable.
|
|
367
|
+
const delta = data.chunk || data.text;
|
|
368
|
+
if (delta) { text += delta; onChunk && onChunk(delta, text); }
|
|
314
369
|
if (data.model) model = data.model;
|
|
315
370
|
if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
|
|
316
371
|
}
|
package/static/v3/js/core/api.js
CHANGED
|
@@ -82,9 +82,35 @@ export const api = {
|
|
|
82
82
|
|
|
83
83
|
/* ── Documented future surfaces ─────────────────────────────────────── */
|
|
84
84
|
|
|
85
|
-
/** GET /api/index/status — KG + Vector + Hybrid pipeline state.
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
/** GET /api/index/status — KG + Vector + Hybrid pipeline state.
|
|
86
|
+
* The backend endpoint is vector-centric (status/storage/source_items/…); the
|
|
87
|
+
* home pillars + topbar chip want a `pipelines` view keyed by
|
|
88
|
+
* knowledge_graph / vector_index / hybrid. Synthesize that shape from the real
|
|
89
|
+
* index status (vectors) plus the KG stats endpoint (entities). Nothing is
|
|
90
|
+
* fabricated: if the index endpoint is unavailable we report unavailable (so
|
|
91
|
+
* the UI shows the honest empty state), and a missing graph-stats count yields
|
|
92
|
+
* an "unavailable" graph pillar rather than a fake number. */
|
|
93
|
+
async indexStatus() {
|
|
94
|
+
const res = await raw("/api/index/status");
|
|
95
|
+
if (!(res.ok && res.data && !res.data.raw)) {
|
|
96
|
+
return { ok: false, status: res.status, data: EMPTY_INDEX_STATUS, source: "unavailable", error: res.error };
|
|
97
|
+
}
|
|
98
|
+
const idx = res.data;
|
|
99
|
+
let entities = null;
|
|
100
|
+
const gs = await raw("/knowledge-graph/stats");
|
|
101
|
+
if (gs.ok && gs.data && !gs.data.raw) {
|
|
102
|
+
const g = gs.data;
|
|
103
|
+
const n = g.total_nodes ?? g.nodes_total ?? (g.nodes && (g.nodes.total ?? g.nodes.count));
|
|
104
|
+
if (n !== undefined && n !== null) entities = Number(n) || 0;
|
|
105
|
+
}
|
|
106
|
+
const vectors = Number(idx.indexed_items ?? idx.ready_items) || 0;
|
|
107
|
+
const vstate = idx.status === "ready" ? "ready" : "pending";
|
|
108
|
+
const pipelines = {
|
|
109
|
+
knowledge_graph: { state: entities === null ? "unavailable" : "ready", entities: entities ?? 0 },
|
|
110
|
+
vector_index: { state: vstate, vectors },
|
|
111
|
+
hybrid: { state: vstate, strategy: vstate === "ready" ? "fused" : "pending" },
|
|
112
|
+
};
|
|
113
|
+
return { ok: true, status: res.status, data: { ...idx, pipelines }, source: "live" };
|
|
88
114
|
},
|
|
89
115
|
|
|
90
116
|
/** POST /api/index/rebuild — rebuild the derived vector index (real run). */
|
|
@@ -176,6 +202,31 @@ export const api = {
|
|
|
176
202
|
},
|
|
177
203
|
sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
|
|
178
204
|
|
|
205
|
+
/** POST /upload/document — manual document ingest (multipart/form-data).
|
|
206
|
+
* Real backend path: parse → chunk → embed → knowledge-graph ingest
|
|
207
|
+
* (latticeai/api/tools.py:/upload/document). Returns { ok, status, data,
|
|
208
|
+
* source }; never throws. FormData must NOT carry a JSON Content-Type — the
|
|
209
|
+
* browser sets the multipart boundary itself. */
|
|
210
|
+
async uploadDocument(file) {
|
|
211
|
+
const ws = store.get().workspaceId;
|
|
212
|
+
const form = new FormData();
|
|
213
|
+
form.append("file", file);
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch("/upload/document", {
|
|
216
|
+
method: "POST",
|
|
217
|
+
credentials: "same-origin",
|
|
218
|
+
headers: { "Accept": "application/json", ...(ws ? { "X-Workspace-Id": ws } : {}) },
|
|
219
|
+
body: form,
|
|
220
|
+
});
|
|
221
|
+
let data = null;
|
|
222
|
+
const text = await res.text();
|
|
223
|
+
if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
|
|
224
|
+
return { ok: res.ok, status: res.status, data, source: res.ok ? "live" : "unavailable" };
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return { ok: false, status: 0, data: null, source: "unavailable", error: String(err) };
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
179
230
|
adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
|
|
180
231
|
adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
|
|
181
232
|
adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
|
|
@@ -310,7 +361,11 @@ export const api = {
|
|
|
310
361
|
const rawData = line.slice(5).trim();
|
|
311
362
|
if (rawData === "[DONE]") return { source: "live", text, trace, model };
|
|
312
363
|
let data; try { data = JSON.parse(rawData); } catch { continue; }
|
|
313
|
-
|
|
364
|
+
// Standard chat streams `chunk`; the document-generation path streams
|
|
365
|
+
// `text` (report body + footnotes). Accept both so doc requests render
|
|
366
|
+
// instead of falsely reporting the backend as unreachable.
|
|
367
|
+
const delta = data.chunk || data.text;
|
|
368
|
+
if (delta) { text += delta; onChunk && onChunk(delta, text); }
|
|
314
369
|
if (data.model) model = data.model;
|
|
315
370
|
if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
|
|
316
371
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { h, icon, $, $$ } from "./dom.a2773eb0.js";
|
|
9
9
|
import { store } from "./store.34ebd5e6.js";
|
|
10
|
-
import { api } from "./api.
|
|
10
|
+
import { api } from "./api.113660c5.js";
|
|
11
11
|
import * as c from "./components.4c83e0a9.js";
|
|
12
12
|
import { createRouter } from "./router.584570f2.js";
|
|
13
13
|
import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.07ad6696.js";
|
|
@@ -52,7 +52,7 @@ export async function render(ctx) {
|
|
|
52
52
|
|
|
53
53
|
const groundChip = (key, label, icn) => h("button.lt3-chip", {
|
|
54
54
|
type: "button", dataset: { active: String(state.grounding[key]) }, "aria-pressed": String(state.grounding[key]),
|
|
55
|
-
title: `
|
|
55
|
+
title: `Show the ${label} signal in the retrieval-context panel`,
|
|
56
56
|
on: { click: (e) => { state.grounding[key] = !state.grounding[key]; const b = e.currentTarget; b.dataset.active = String(state.grounding[key]); b.setAttribute("aria-pressed", String(state.grounding[key])); } },
|
|
57
57
|
}, icon(icn), label);
|
|
58
58
|
|
|
@@ -52,7 +52,7 @@ export async function render(ctx) {
|
|
|
52
52
|
|
|
53
53
|
const groundChip = (key, label, icn) => h("button.lt3-chip", {
|
|
54
54
|
type: "button", dataset: { active: String(state.grounding[key]) }, "aria-pressed": String(state.grounding[key]),
|
|
55
|
-
title: `
|
|
55
|
+
title: `Show the ${label} signal in the retrieval-context panel`,
|
|
56
56
|
on: { click: (e) => { state.grounding[key] = !state.grounding[key]; const b = e.currentTarget; b.dataset.active = String(state.grounding[key]); b.setAttribute("aria-pressed", String(state.grounding[key])); } },
|
|
57
57
|
}, icon(icn), label);
|
|
58
58
|
|
|
@@ -61,11 +61,15 @@ function normalize(data) {
|
|
|
61
61
|
}));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Document types the backend accepts (latticeai/services/upload_service.py). */
|
|
65
|
+
const UPLOAD_ACCEPT = ".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv";
|
|
66
|
+
|
|
64
67
|
export async function render(ctx) {
|
|
65
68
|
const { h, icon, api, c, navigate, toast } = ctx;
|
|
66
69
|
|
|
67
|
-
//
|
|
68
|
-
// not enabled in this build. Say so plainly
|
|
70
|
+
// Connecting/watching a *folder* needs the desktop local-agent connector,
|
|
71
|
+
// which is not enabled in this build. Say so plainly. Manual document upload
|
|
72
|
+
// below works without it.
|
|
69
73
|
const unavailableToast = () =>
|
|
70
74
|
toast("Connecting a folder requires the Lattice desktop local agent — not available in this build.", "warn");
|
|
71
75
|
|
|
@@ -73,6 +77,56 @@ export async function render(ctx) {
|
|
|
73
77
|
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
74
78
|
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
75
79
|
|
|
80
|
+
// ── Manual upload (works in this build; no desktop agent required) ─────────
|
|
81
|
+
let busy = false;
|
|
82
|
+
const fileInput = h("input", {
|
|
83
|
+
type: "file", multiple: true, accept: UPLOAD_ACCEPT,
|
|
84
|
+
style: { display: "none" }, "aria-hidden": "true",
|
|
85
|
+
on: { change: (e) => uploadFiles(e.target.files) },
|
|
86
|
+
});
|
|
87
|
+
const pickFiles = () => { if (!busy) fileInput.click(); };
|
|
88
|
+
const slots = { statHost, srcSlot, tableHost, pickFiles };
|
|
89
|
+
|
|
90
|
+
async function uploadFiles(fileList) {
|
|
91
|
+
const files = Array.from(fileList || []);
|
|
92
|
+
if (!files.length || busy) return;
|
|
93
|
+
busy = true;
|
|
94
|
+
let ok = 0;
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
toast(`Uploading “${file.name}”…`, "info");
|
|
97
|
+
const res = await api.uploadDocument(file);
|
|
98
|
+
if (res.ok && res.data && !res.data.detail && !res.data.error) {
|
|
99
|
+
ok++;
|
|
100
|
+
} else {
|
|
101
|
+
const detail = (res.data && (res.data.detail || res.data.error)) || "the backend is unavailable";
|
|
102
|
+
toast(`Could not ingest “${file.name}” — ${detail}.`, "warn");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
fileInput.value = "";
|
|
106
|
+
busy = false;
|
|
107
|
+
if (ok) {
|
|
108
|
+
toast(`Indexed ${ok} document${ok === 1 ? "" : "s"} into the knowledge graph — now searchable in Chat and Hybrid Search.`, "ok");
|
|
109
|
+
}
|
|
110
|
+
hydrate(ctx, slots);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const dropZone = h("div.lt3-drop", {
|
|
114
|
+
on: {
|
|
115
|
+
dragover: (e) => { e.preventDefault(); dropZone.classList.add("is-dragover"); },
|
|
116
|
+
dragleave: () => dropZone.classList.remove("is-dragover"),
|
|
117
|
+
drop: (e) => { e.preventDefault(); dropZone.classList.remove("is-dragover"); uploadFiles(e.dataTransfer && e.dataTransfer.files); },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
fileInput,
|
|
121
|
+
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
122
|
+
h("div",
|
|
123
|
+
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag documents here, or upload manually"),
|
|
124
|
+
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
125
|
+
"Lattice parses each file, chunks it, embeds it, and links it into the knowledge graph. PDF · DOCX · XLSX · PPTX · TXT · MD · CSV, up to 10 MB each."),
|
|
126
|
+
),
|
|
127
|
+
h("button.lt3-btn.lt3-btn--primary", { type: "button", on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
128
|
+
);
|
|
129
|
+
|
|
76
130
|
const root = h("div.lt3-stack-6",
|
|
77
131
|
c.viewHeader({
|
|
78
132
|
eyebrow: "Data",
|
|
@@ -80,19 +134,12 @@ export async function render(ctx) {
|
|
|
80
134
|
sub: "Connected sources and the documents Lattice has indexed for retrieval. Everything stays on this machine.",
|
|
81
135
|
actions: [
|
|
82
136
|
h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => navigate("knowledge-graph") } }, icon("chart-dots-3"), "View graph"),
|
|
137
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
83
138
|
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Connect folder"),
|
|
84
139
|
],
|
|
85
140
|
}),
|
|
86
141
|
statHost,
|
|
87
|
-
|
|
88
|
-
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
89
|
-
h("div",
|
|
90
|
-
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag files or connect a folder"),
|
|
91
|
-
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
92
|
-
"Lattice watches the source, chunks it, embeds it, and links it into the knowledge graph."),
|
|
93
|
-
),
|
|
94
|
-
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Choose folder"),
|
|
95
|
-
),
|
|
142
|
+
dropZone,
|
|
96
143
|
c.panel({
|
|
97
144
|
head: h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", width: "100%" } },
|
|
98
145
|
h("div",
|
|
@@ -105,13 +152,13 @@ export async function render(ctx) {
|
|
|
105
152
|
}),
|
|
106
153
|
);
|
|
107
154
|
|
|
108
|
-
hydrate(ctx,
|
|
155
|
+
hydrate(ctx, slots);
|
|
109
156
|
return root;
|
|
110
157
|
}
|
|
111
158
|
|
|
112
159
|
async function hydrate(ctx, slots) {
|
|
113
160
|
const { h, icon, api, c, toast } = ctx;
|
|
114
|
-
const { statHost, srcSlot, tableHost } = slots;
|
|
161
|
+
const { statHost, srcSlot, tableHost, pickFiles } = slots;
|
|
115
162
|
|
|
116
163
|
const probe = await api.get("/workspace/indexing", { sources: [], totals: {} });
|
|
117
164
|
const liveFiles = probe.ok && probe.data ? normalize(probe.data) : null;
|
|
@@ -137,11 +184,10 @@ async function hydrate(ctx, slots) {
|
|
|
137
184
|
tableHost.replaceChildren(c.emptyState({
|
|
138
185
|
icon: "folder-off",
|
|
139
186
|
title: "No documents indexed yet",
|
|
140
|
-
body: "
|
|
141
|
-
action: h("button.lt3-btn.lt3-btn--
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
icon("folder-plus"), "Connect folder"),
|
|
187
|
+
body: "Upload a document and Lattice will parse, embed, and link it into the knowledge graph for hybrid retrieval.",
|
|
188
|
+
action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm",
|
|
189
|
+
{ on: { click: () => (pickFiles ? pickFiles() : null) } },
|
|
190
|
+
icon("upload"), "Upload files"),
|
|
145
191
|
}));
|
|
146
192
|
return;
|
|
147
193
|
}
|
|
@@ -61,11 +61,15 @@ function normalize(data) {
|
|
|
61
61
|
}));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Document types the backend accepts (latticeai/services/upload_service.py). */
|
|
65
|
+
const UPLOAD_ACCEPT = ".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv";
|
|
66
|
+
|
|
64
67
|
export async function render(ctx) {
|
|
65
68
|
const { h, icon, api, c, navigate, toast } = ctx;
|
|
66
69
|
|
|
67
|
-
//
|
|
68
|
-
// not enabled in this build. Say so plainly
|
|
70
|
+
// Connecting/watching a *folder* needs the desktop local-agent connector,
|
|
71
|
+
// which is not enabled in this build. Say so plainly. Manual document upload
|
|
72
|
+
// below works without it.
|
|
69
73
|
const unavailableToast = () =>
|
|
70
74
|
toast("Connecting a folder requires the Lattice desktop local agent — not available in this build.", "warn");
|
|
71
75
|
|
|
@@ -73,6 +77,56 @@ export async function render(ctx) {
|
|
|
73
77
|
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
74
78
|
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
75
79
|
|
|
80
|
+
// ── Manual upload (works in this build; no desktop agent required) ─────────
|
|
81
|
+
let busy = false;
|
|
82
|
+
const fileInput = h("input", {
|
|
83
|
+
type: "file", multiple: true, accept: UPLOAD_ACCEPT,
|
|
84
|
+
style: { display: "none" }, "aria-hidden": "true",
|
|
85
|
+
on: { change: (e) => uploadFiles(e.target.files) },
|
|
86
|
+
});
|
|
87
|
+
const pickFiles = () => { if (!busy) fileInput.click(); };
|
|
88
|
+
const slots = { statHost, srcSlot, tableHost, pickFiles };
|
|
89
|
+
|
|
90
|
+
async function uploadFiles(fileList) {
|
|
91
|
+
const files = Array.from(fileList || []);
|
|
92
|
+
if (!files.length || busy) return;
|
|
93
|
+
busy = true;
|
|
94
|
+
let ok = 0;
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
toast(`Uploading “${file.name}”…`, "info");
|
|
97
|
+
const res = await api.uploadDocument(file);
|
|
98
|
+
if (res.ok && res.data && !res.data.detail && !res.data.error) {
|
|
99
|
+
ok++;
|
|
100
|
+
} else {
|
|
101
|
+
const detail = (res.data && (res.data.detail || res.data.error)) || "the backend is unavailable";
|
|
102
|
+
toast(`Could not ingest “${file.name}” — ${detail}.`, "warn");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
fileInput.value = "";
|
|
106
|
+
busy = false;
|
|
107
|
+
if (ok) {
|
|
108
|
+
toast(`Indexed ${ok} document${ok === 1 ? "" : "s"} into the knowledge graph — now searchable in Chat and Hybrid Search.`, "ok");
|
|
109
|
+
}
|
|
110
|
+
hydrate(ctx, slots);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const dropZone = h("div.lt3-drop", {
|
|
114
|
+
on: {
|
|
115
|
+
dragover: (e) => { e.preventDefault(); dropZone.classList.add("is-dragover"); },
|
|
116
|
+
dragleave: () => dropZone.classList.remove("is-dragover"),
|
|
117
|
+
drop: (e) => { e.preventDefault(); dropZone.classList.remove("is-dragover"); uploadFiles(e.dataTransfer && e.dataTransfer.files); },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
fileInput,
|
|
121
|
+
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
122
|
+
h("div",
|
|
123
|
+
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag documents here, or upload manually"),
|
|
124
|
+
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
125
|
+
"Lattice parses each file, chunks it, embeds it, and links it into the knowledge graph. PDF · DOCX · XLSX · PPTX · TXT · MD · CSV, up to 10 MB each."),
|
|
126
|
+
),
|
|
127
|
+
h("button.lt3-btn.lt3-btn--primary", { type: "button", on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
128
|
+
);
|
|
129
|
+
|
|
76
130
|
const root = h("div.lt3-stack-6",
|
|
77
131
|
c.viewHeader({
|
|
78
132
|
eyebrow: "Data",
|
|
@@ -80,19 +134,12 @@ export async function render(ctx) {
|
|
|
80
134
|
sub: "Connected sources and the documents Lattice has indexed for retrieval. Everything stays on this machine.",
|
|
81
135
|
actions: [
|
|
82
136
|
h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => navigate("knowledge-graph") } }, icon("chart-dots-3"), "View graph"),
|
|
137
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
83
138
|
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Connect folder"),
|
|
84
139
|
],
|
|
85
140
|
}),
|
|
86
141
|
statHost,
|
|
87
|
-
|
|
88
|
-
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
89
|
-
h("div",
|
|
90
|
-
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag files or connect a folder"),
|
|
91
|
-
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
92
|
-
"Lattice watches the source, chunks it, embeds it, and links it into the knowledge graph."),
|
|
93
|
-
),
|
|
94
|
-
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Choose folder"),
|
|
95
|
-
),
|
|
142
|
+
dropZone,
|
|
96
143
|
c.panel({
|
|
97
144
|
head: h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", width: "100%" } },
|
|
98
145
|
h("div",
|
|
@@ -105,13 +152,13 @@ export async function render(ctx) {
|
|
|
105
152
|
}),
|
|
106
153
|
);
|
|
107
154
|
|
|
108
|
-
hydrate(ctx,
|
|
155
|
+
hydrate(ctx, slots);
|
|
109
156
|
return root;
|
|
110
157
|
}
|
|
111
158
|
|
|
112
159
|
async function hydrate(ctx, slots) {
|
|
113
160
|
const { h, icon, api, c, toast } = ctx;
|
|
114
|
-
const { statHost, srcSlot, tableHost } = slots;
|
|
161
|
+
const { statHost, srcSlot, tableHost, pickFiles } = slots;
|
|
115
162
|
|
|
116
163
|
const probe = await api.get("/workspace/indexing", { sources: [], totals: {} });
|
|
117
164
|
const liveFiles = probe.ok && probe.data ? normalize(probe.data) : null;
|
|
@@ -137,11 +184,10 @@ async function hydrate(ctx, slots) {
|
|
|
137
184
|
tableHost.replaceChildren(c.emptyState({
|
|
138
185
|
icon: "folder-off",
|
|
139
186
|
title: "No documents indexed yet",
|
|
140
|
-
body: "
|
|
141
|
-
action: h("button.lt3-btn.lt3-btn--
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
icon("folder-plus"), "Connect folder"),
|
|
187
|
+
body: "Upload a document and Lattice will parse, embed, and link it into the knowledge graph for hybrid retrieval.",
|
|
188
|
+
action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm",
|
|
189
|
+
{ on: { click: () => (pickFiles ? pickFiles() : null) } },
|
|
190
|
+
icon("upload"), "Upload files"),
|
|
145
191
|
}));
|
|
146
192
|
return;
|
|
147
193
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
* View: Memory — the long-term memory platform + Memory Manager.
|
|
3
3
|
* Reads /api/memory/manager (usage / sources / health / size / type) and offers
|
|
4
|
-
* recall, inspect,
|
|
5
|
-
*
|
|
4
|
+
* recall, inspect, compact, and rebuild. Every number comes from a real store;
|
|
5
|
+
* tiers with no backing report unavailable. (Destructive prune/clear are
|
|
6
|
+
* available over the API but intentionally not surfaced as one-click UI here.)
|
|
6
7
|
* ========================================================================== */
|
|
7
8
|
|
|
8
9
|
const TIER_ICON = {
|
|
@@ -19,7 +20,7 @@ export async function render(ctx) {
|
|
|
19
20
|
const recallHost = h("div");
|
|
20
21
|
const inspectHost = h("div", h("p.lt3-faint", { style: { margin: 0 } }, "Pick a tier to inspect its contents."));
|
|
21
22
|
|
|
22
|
-
const recallInput = h("input.lt3-input", { type: "text", placeholder: "Recall
|
|
23
|
+
const recallInput = h("input.lt3-input", { type: "text", placeholder: "Recall from workspace + graph memory…", "aria-label": "Recall memory" });
|
|
23
24
|
|
|
24
25
|
const root = h("div.lt3-stack-6",
|
|
25
26
|
c.viewHeader({
|
|
@@ -30,7 +31,7 @@ export async function render(ctx) {
|
|
|
30
31
|
}),
|
|
31
32
|
statHost,
|
|
32
33
|
c.panel({
|
|
33
|
-
title: "Recall", sub: "
|
|
34
|
+
title: "Recall", sub: "Searches your workspace and knowledge-graph memory.",
|
|
34
35
|
children: h("div.lt3-stack-3",
|
|
35
36
|
h("div.lt3-row-2",
|
|
36
37
|
recallInput,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
* View: Memory — the long-term memory platform + Memory Manager.
|
|
3
3
|
* Reads /api/memory/manager (usage / sources / health / size / type) and offers
|
|
4
|
-
* recall, inspect,
|
|
5
|
-
*
|
|
4
|
+
* recall, inspect, compact, and rebuild. Every number comes from a real store;
|
|
5
|
+
* tiers with no backing report unavailable. (Destructive prune/clear are
|
|
6
|
+
* available over the API but intentionally not surfaced as one-click UI here.)
|
|
6
7
|
* ========================================================================== */
|
|
7
8
|
|
|
8
9
|
const TIER_ICON = {
|
|
@@ -19,7 +20,7 @@ export async function render(ctx) {
|
|
|
19
20
|
const recallHost = h("div");
|
|
20
21
|
const inspectHost = h("div", h("p.lt3-faint", { style: { margin: 0 } }, "Pick a tier to inspect its contents."));
|
|
21
22
|
|
|
22
|
-
const recallInput = h("input.lt3-input", { type: "text", placeholder: "Recall
|
|
23
|
+
const recallInput = h("input.lt3-input", { type: "text", placeholder: "Recall from workspace + graph memory…", "aria-label": "Recall memory" });
|
|
23
24
|
|
|
24
25
|
const root = h("div.lt3-stack-6",
|
|
25
26
|
c.viewHeader({
|
|
@@ -30,7 +31,7 @@ export async function render(ctx) {
|
|
|
30
31
|
}),
|
|
31
32
|
statHost,
|
|
32
33
|
c.panel({
|
|
33
|
-
title: "Recall", sub: "
|
|
34
|
+
title: "Recall", sub: "Searches your workspace and knowledge-graph memory.",
|
|
34
35
|
children: h("div.lt3-stack-3",
|
|
35
36
|
h("div.lt3-row-2",
|
|
36
37
|
recallInput,
|
|
@@ -228,7 +228,21 @@ async function probeEndpoints({ h, icon, api, c }, host) {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/* ── About ──────────────────────────────────────────────────────────────── */
|
|
231
|
-
|
|
231
|
+
/* Version is read live from /health (which derives it from the backend's single
|
|
232
|
+
* source of truth, WORKSPACE_OS_VERSION) — never hard-coded in the frontend.
|
|
233
|
+
* If the backend is unreachable we say "unavailable" rather than inventing a
|
|
234
|
+
* number. */
|
|
235
|
+
function aboutPanel({ h, icon, c, api }) {
|
|
236
|
+
const versionSlot = h("dd", h("span.lt3-mono.lt3-faint", "checking…"));
|
|
237
|
+
(async () => {
|
|
238
|
+
const res = await api.raw("/health");
|
|
239
|
+
const v = res && res.ok && res.data && res.data.version;
|
|
240
|
+
versionSlot.replaceChildren(
|
|
241
|
+
v
|
|
242
|
+
? h("span.lt3-mono", `v${String(v).replace(/^v/i, "")}`)
|
|
243
|
+
: h("span.lt3-mono.lt3-faint", "unavailable"),
|
|
244
|
+
);
|
|
245
|
+
})();
|
|
232
246
|
return c.panel({
|
|
233
247
|
eyebrow: "About",
|
|
234
248
|
title: "Lattice AI",
|
|
@@ -236,7 +250,7 @@ function aboutPanel({ h, icon, c }) {
|
|
|
236
250
|
children: h("div.lt3-stack-4",
|
|
237
251
|
h("dl.lt3-keyval",
|
|
238
252
|
h("dt", "Application"), h("dd", "Lattice AI"),
|
|
239
|
-
h("dt", "Version"),
|
|
253
|
+
h("dt", "Version"), versionSlot,
|
|
240
254
|
h("dt", "Edition"), h("dd", "Local-first AI workspace"),
|
|
241
255
|
),
|
|
242
256
|
),
|
|
@@ -228,7 +228,21 @@ async function probeEndpoints({ h, icon, api, c }, host) {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/* ── About ──────────────────────────────────────────────────────────────── */
|
|
231
|
-
|
|
231
|
+
/* Version is read live from /health (which derives it from the backend's single
|
|
232
|
+
* source of truth, WORKSPACE_OS_VERSION) — never hard-coded in the frontend.
|
|
233
|
+
* If the backend is unreachable we say "unavailable" rather than inventing a
|
|
234
|
+
* number. */
|
|
235
|
+
function aboutPanel({ h, icon, c, api }) {
|
|
236
|
+
const versionSlot = h("dd", h("span.lt3-mono.lt3-faint", "checking…"));
|
|
237
|
+
(async () => {
|
|
238
|
+
const res = await api.raw("/health");
|
|
239
|
+
const v = res && res.ok && res.data && res.data.version;
|
|
240
|
+
versionSlot.replaceChildren(
|
|
241
|
+
v
|
|
242
|
+
? h("span.lt3-mono", `v${String(v).replace(/^v/i, "")}`)
|
|
243
|
+
: h("span.lt3-mono.lt3-faint", "unavailable"),
|
|
244
|
+
);
|
|
245
|
+
})();
|
|
232
246
|
return c.panel({
|
|
233
247
|
eyebrow: "About",
|
|
234
248
|
title: "Lattice AI",
|
|
@@ -236,7 +250,7 @@ function aboutPanel({ h, icon, c }) {
|
|
|
236
250
|
children: h("div.lt3-stack-4",
|
|
237
251
|
h("dl.lt3-keyval",
|
|
238
252
|
h("dt", "Application"), h("dd", "Lattice AI"),
|
|
239
|
-
h("dt", "Version"),
|
|
253
|
+
h("dt", "Version"), versionSlot,
|
|
240
254
|
h("dt", "Edition"), h("dd", "Local-first AI workspace"),
|
|
241
255
|
),
|
|
242
256
|
),
|