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 CHANGED
@@ -290,7 +290,8 @@ Core areas:
290
290
 
291
291
  | Version | Theme |
292
292
  | --- | --- |
293
- | **3.2.0** | Feature-complete platformmulti-agent collaboration, agent registry, marketplace + templates, workflow agents, autonomous planning, long-term memory + manager, skills/hooks/tool registries, MCP manager, all operable from `/app` |
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 |
@@ -1,6 +1,6 @@
1
1
  # Lattice AI Architecture
2
2
 
3
- > v3.2.0 — feature-complete for non-enterprise use cases. The agent ecosystem
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
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "3.2.0"
3
+ __version__ = "3.3.0"
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "3.2.0"
14
+ MARKETPLACE_VERSION = "3.3.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -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.2.0"
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.2.0"
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.2.0",
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: "3.2.0",
160
+ version: pkgVersion,
155
161
  generated_at: "deterministic",
156
162
  entrypoints: {
157
163
  app: assets[entry],
@@ -1,5 +1,5 @@
1
1
  /* ============================================================================
2
- * Lattice AI — Design Tokens (Single Source of Truth) v3.1.0
2
+ * Lattice AI — Design Tokens (Single Source of Truth) v3.3.0
3
3
  *
4
4
  * 이 파일이 색·면·테두리·그림자·포커스의 단일 출처다.
5
5
  * :root → 라이트 테마 값
@@ -1,5 +1,5 @@
1
1
  /* ============================================================================
2
- * Lattice AI — Design Tokens (Single Source of Truth) v3.1.0
2
+ * Lattice AI — Design Tokens (Single Source of Truth) v3.3.0
3
3
  *
4
4
  * 이 파일이 색·면·테두리·그림자·포커스의 단일 출처다.
5
5
  * :root → 라이트 테마 값
@@ -1,31 +1,31 @@
1
1
  {
2
- "version": "3.2.0",
2
+ "version": "3.3.0",
3
3
  "generated_at": "deterministic",
4
4
  "entrypoints": {
5
- "app": "/static/v3/js/app.a5adc0f3.js",
5
+ "app": "/static/v3/js/app.cf5bb712.js",
6
6
  "styles": [
7
- "/static/css/tokens.5a595671.css",
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.3ee19d4e.css"
12
+ "/static/v3/css/lattice.views.1d326beb.css"
13
13
  ]
14
14
  },
15
15
  "assets": {
16
- "static/css/tokens.css": "/static/css/tokens.5a595671.css",
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.3ee19d4e.css",
22
- "static/v3/js/app.js": "/static/v3/js/app.a5adc0f3.js",
23
- "static/v3/js/core/api.js": "/static/v3/js/core/api.603b978f.js",
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.ea0b9ae5.js",
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.718144ce.js",
38
- "static/v3/js/views/files.js": "/static/v3/js/views/files.4935197e.js",
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.d2ed7a7c.js",
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.4f777210.js",
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.ea0b9ae5.js";
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
- indexStatus() {
87
- return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
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
- if (data.chunk) { text += data.chunk; onChunk && onChunk(data.chunk, text); }
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
  }
@@ -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
- indexStatus() {
87
- return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
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
- if (data.chunk) { text += data.chunk; onChunk && onChunk(data.chunk, text); }
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.603b978f.js";
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: `Toggle ${label} grounding`,
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: `Toggle ${label} grounding`,
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
- // Folder connection/watch needs the desktop local-agent connector, which is
68
- // not enabled in this build. Say so plainly rather than implying it's coming.
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
- h("div.lt3-drop",
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, { statHost, srcSlot, tableHost });
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: "Connect a folder and Lattice will index it for hybrid retrieval.",
141
- action: h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm",
142
- { title: "Requires the desktop local agent (not in this build)",
143
- on: { click: () => toast("Connecting a folder requires the Lattice desktop local agent — not available in this build.", "warn") } },
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
- // Folder connection/watch needs the desktop local-agent connector, which is
68
- // not enabled in this build. Say so plainly rather than implying it's coming.
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
- h("div.lt3-drop",
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, { statHost, srcSlot, tableHost });
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: "Connect a folder and Lattice will index it for hybrid retrieval.",
141
- action: h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm",
142
- { title: "Requires the desktop local agent (not in this build)",
143
- on: { click: () => toast("Connecting a folder requires the Lattice desktop local agent — not available in this build.", "warn") } },
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, prune, compact, rebuild, and clear. Every number comes from a
5
- * real store; tiers with no backing report unavailable.
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 across every memory tier…", "aria-label": "Recall memory" });
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: "Unified retrieval across the memory tiers.",
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, prune, compact, rebuild, and clear. Every number comes from a
5
- * real store; tiers with no backing report unavailable.
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 across every memory tier…", "aria-label": "Recall memory" });
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: "Unified retrieval across the memory tiers.",
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
- function aboutPanel({ h, icon, c }) {
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"), h("dd", h("span.lt3-mono", "v3.1.0")),
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
- function aboutPanel({ h, icon, c }) {
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"), h("dd", h("span.lt3-mono", "v3.1.0")),
253
+ h("dt", "Version"), versionSlot,
240
254
  h("dt", "Edition"), h("dd", "Local-first AI workspace"),
241
255
  ),
242
256
  ),