ltcai 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +27 -20
  2. package/docs/CHANGELOG.md +37 -0
  3. package/docs/V3_FRONTEND.md +20 -17
  4. package/latticeai/__init__.py +1 -1
  5. package/latticeai/api/auth.py +4 -1
  6. package/latticeai/api/search.py +4 -0
  7. package/latticeai/core/config.py +2 -0
  8. package/latticeai/core/embedding_providers.py +123 -0
  9. package/latticeai/core/workspace_os.py +1 -1
  10. package/latticeai/server_app.py +22 -6
  11. package/package.json +9 -4
  12. package/scripts/build_v3_assets.mjs +164 -0
  13. package/scripts/capture/README.md +28 -0
  14. package/scripts/capture/capture_enterprise.js +8 -0
  15. package/scripts/capture/capture_graph.js +8 -0
  16. package/scripts/capture/capture_onboarding.js +8 -0
  17. package/scripts/capture/capture_page.js +43 -0
  18. package/scripts/capture/capture_release_media.js +125 -0
  19. package/scripts/capture/capture_skills.js +8 -0
  20. package/scripts/capture/capture_workspace.js +8 -0
  21. package/scripts/generate_diagrams.py +513 -0
  22. package/scripts/lint_v3.mjs +33 -0
  23. package/scripts/release-0.3.1.sh +105 -0
  24. package/scripts/take_screenshots.js +69 -0
  25. package/scripts/validate_release_artifacts.py +167 -0
  26. package/static/account.html +9 -9
  27. package/static/activity.html +4 -4
  28. package/static/admin.html +8 -8
  29. package/static/agents.html +4 -4
  30. package/static/chat.html +9 -9
  31. package/static/css/tokens.5a595671.css +260 -0
  32. package/static/css/tokens.css +1 -1
  33. package/static/graph.html +9 -9
  34. package/static/plugins.html +4 -4
  35. package/static/sw.js +3 -1
  36. package/static/v3/asset-manifest.json +47 -0
  37. package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
  38. package/static/v3/css/lattice.components.011e988b.css +447 -0
  39. package/static/v3/css/lattice.components.css +2 -2
  40. package/static/v3/css/lattice.shell.4920f42d.css +407 -0
  41. package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
  42. package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
  43. package/static/v3/index.html +38 -9
  44. package/static/v3/js/app.46fb61d9.js +26 -0
  45. package/static/v3/js/core/api.22a41d42.js +344 -0
  46. package/static/v3/js/core/api.js +68 -51
  47. package/static/v3/js/core/components.4c83e0a9.js +222 -0
  48. package/static/v3/js/core/components.js +9 -2
  49. package/static/v3/js/core/dom.a2773eb0.js +148 -0
  50. package/static/v3/js/core/router.584570f2.js +37 -0
  51. package/static/v3/js/core/routes.f935dd50.js +78 -0
  52. package/static/v3/js/core/routes.js +6 -1
  53. package/static/v3/js/core/shell.1b6199d6.js +363 -0
  54. package/static/v3/js/core/store.34ebd5e6.js +113 -0
  55. package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
  56. package/static/v3/js/views/admin-audit.js +1 -1
  57. package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
  58. package/static/v3/js/views/admin-permissions.js +4 -5
  59. package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
  60. package/static/v3/js/views/admin-policies.js +4 -5
  61. package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
  62. package/static/v3/js/views/admin-private-vpc.js +2 -5
  63. package/static/v3/js/views/admin-security.07c66b72.js +180 -0
  64. package/static/v3/js/views/admin-security.js +4 -5
  65. package/static/v3/js/views/admin-users.03bac88c.js +168 -0
  66. package/static/v3/js/views/admin-users.js +6 -6
  67. package/static/v3/js/views/agents.14e48bdd.js +193 -0
  68. package/static/v3/js/views/agents.js +1 -2
  69. package/static/v3/js/views/chat.718144ce.js +449 -0
  70. package/static/v3/js/views/chat.js +2 -3
  71. package/static/v3/js/views/files.4935197e.js +186 -0
  72. package/static/v3/js/views/files.js +27 -21
  73. package/static/v3/js/views/home.cdde3b32.js +119 -0
  74. package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
  75. package/static/v3/js/views/hybrid-search.js +1 -1
  76. package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
  77. package/static/v3/js/views/knowledge-graph.js +2 -3
  78. package/static/v3/js/views/models.a1ffa147.js +256 -0
  79. package/static/v3/js/views/models.js +17 -8
  80. package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
  81. package/static/v3/js/views/my-computer.js +5 -5
  82. package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
  83. package/static/v3/js/views/pipeline.js +3 -7
  84. package/static/v3/js/views/settings.4f777210.js +250 -0
  85. package/static/v3/js/views/settings.js +6 -14
  86. package/static/workflows.html +4 -4
  87. package/static/workspace.html +5 -5
  88. package/docs/images/tmp_frames/frame_00.png +0 -0
  89. package/docs/images/tmp_frames/frame_01.png +0 -0
  90. package/docs/images/tmp_frames/frame_02.png +0 -0
  91. package/docs/images/tmp_frames/frame_03.png +0 -0
  92. package/docs/images/tmp_frames/hero_00.png +0 -0
  93. package/docs/images/tmp_frames/hero_01.png +0 -0
  94. package/docs/images/tmp_frames/hero_02.png +0 -0
  95. package/docs/images/tmp_frames/hero_03.png +0 -0
  96. package/static/v3/js/core/fixtures.js +0 -171
@@ -0,0 +1,344 @@
1
+ /* ============================================================================
2
+ * Lattice AI v3 — Integration adapter
3
+ *
4
+ * Every adapter call hits the real endpoint first (including /api/index/status,
5
+ * /api/graph, /api/search/hybrid, and /chat). If that endpoint is
6
+ * missing/unavailable, it returns an unavailable source with empty data so the
7
+ * UI can render a clear unavailable state without inventing counters or health.
8
+ *
9
+ * Return shape (never throws): { ok, status, data, source, error }
10
+ * source: "live" → returned by a real backend endpoint
11
+ * "unavailable" → endpoint missing/down; no fake payload
12
+ * ========================================================================== */
13
+
14
+ import { store } from "./store.34ebd5e6.js";
15
+
16
+ const TIMEOUT_MS = 8000;
17
+ const EMPTY_INDEX_STATUS = { generated_at: null, pipelines: {}, sources: [] };
18
+ const EMPTY_GRAPH_STATS = { nodes: {}, edges: {}, total_nodes: 0, total_edges: 0 };
19
+ const EMPTY_WORKSPACE_OS = { counts: {}, models: {} };
20
+ const EMPTY_SYSINFO = { cpu_pct: null, ram_pct: null, gpu_mem_pct: null, gpu_mem_gb: null };
21
+ const EMPTY_ADMIN = {
22
+ summary: { total_users: null, active_users: null, admin_users: null, total_messages: null },
23
+ users: [],
24
+ audit: { recent_events: [] },
25
+ security: {},
26
+ roles: { roles: [] },
27
+ policies: { policies: [] },
28
+ vpc: {},
29
+ };
30
+
31
+ async function raw(path, { method = "GET", body, headers } = {}) {
32
+ const ctrl = new AbortController();
33
+ const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
34
+ try {
35
+ const ws = store.get().workspaceId;
36
+ const res = await fetch(path, {
37
+ method,
38
+ credentials: "same-origin",
39
+ signal: ctrl.signal,
40
+ headers: {
41
+ "Accept": "application/json",
42
+ ...(body ? { "Content-Type": "application/json" } : {}),
43
+ ...(ws ? { "X-Workspace-Id": ws } : {}),
44
+ ...headers,
45
+ },
46
+ body: body ? JSON.stringify(body) : undefined,
47
+ });
48
+ let data = null;
49
+ const text = await res.text();
50
+ if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } }
51
+ return { ok: res.ok, status: res.status, data };
52
+ } catch (err) {
53
+ return { ok: false, status: 0, data: null, error: err && err.name === "AbortError" ? "timeout" : String(err) };
54
+ } finally {
55
+ clearTimeout(timer);
56
+ }
57
+ }
58
+
59
+ function unavailableData(shape) {
60
+ const value = typeof shape === "function" ? shape() : shape;
61
+ if (Array.isArray(value)) return [];
62
+ if (value && typeof value === "object") return {};
63
+ return null;
64
+ }
65
+
66
+ /** Try the live endpoint; on any non-2xx/transport failure, return empty data. */
67
+ async function withFallback(path, opts, shape) {
68
+ const res = await raw(path, opts);
69
+ if (res.ok && res.data && !res.data.raw) {
70
+ return { ...res, source: "live" };
71
+ }
72
+ return { ok: false, status: res.status, data: unavailableData(shape), source: "unavailable", error: res.error };
73
+ }
74
+
75
+ export const api = {
76
+ raw,
77
+
78
+ /** Generic GET with unavailable fallback. */
79
+ async get(path, shape = null) {
80
+ return withFallback(path, {}, shape);
81
+ },
82
+
83
+ /* ── Documented future surfaces ─────────────────────────────────────── */
84
+
85
+ /** GET /api/index/status — KG + Vector + Hybrid pipeline state. */
86
+ indexStatus() {
87
+ return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
88
+ },
89
+
90
+ /** POST /api/index/rebuild — rebuild the derived vector index (real run). */
91
+ rebuildIndex(opts = {}) {
92
+ return raw("/api/index/rebuild", { method: "POST", body: { full: false, include_nodes: true, include_chunks: true, ...opts } });
93
+ },
94
+
95
+ /** GET /api/graph — knowledge graph (nodes + edges). Falls back through the
96
+ * current /knowledge-graph/graph route before reporting unavailable. */
97
+ async graph(params = {}) {
98
+ const qs = new URLSearchParams(params).toString();
99
+ const primary = await raw(`/api/graph${qs ? "?" + qs : ""}`);
100
+ if (primary.ok && primary.data && Array.isArray(primary.data.nodes)) {
101
+ return { ...primary, source: "live" };
102
+ }
103
+ const legacy = await raw("/knowledge-graph/graph");
104
+ if (legacy.ok && legacy.data && Array.isArray(legacy.data.nodes)) {
105
+ return { ...legacy, source: "live" };
106
+ }
107
+ return { ok: false, status: primary.status || legacy.status || 0, data: { nodes: [], edges: [] }, source: "unavailable", error: primary.error || legacy.error };
108
+ },
109
+
110
+ graphStats() {
111
+ return withFallback("/knowledge-graph/stats", {}, EMPTY_GRAPH_STATS);
112
+ },
113
+
114
+ /** POST /api/search/hybrid — fused KG + vector retrieval.
115
+ * The backend returns `{ matches: [...] }` where each match carries
116
+ * `source_scores: { keyword, vector, graph }`. Normalize that into the flat
117
+ * result shape the view renders (title/path/snippet/score + per-signal). A
118
+ * legacy `results` array is also accepted defensively. */
119
+ async hybridSearch(query, opts = {}) {
120
+ const res = await raw("/api/search/hybrid", { method: "POST", body: { query, ...opts } });
121
+ const live = res.ok && res.data
122
+ ? (Array.isArray(res.data.matches) ? res.data.matches
123
+ : Array.isArray(res.data.results) ? res.data.results
124
+ : null)
125
+ : null;
126
+ if (live) {
127
+ const items = live.map((m) => {
128
+ const ss = m.source_scores || {};
129
+ const meta = m.metadata || {};
130
+ return {
131
+ id: m.id || m.node_id,
132
+ title: m.title || m.id || "Untitled",
133
+ path: meta.path || meta.source || m.path || m.type || "",
134
+ snippet: m.snippet || m.summary || "",
135
+ score: typeof m.score === "number" ? m.score : 0,
136
+ vector: Number(ss.vector ?? m.vector) || 0,
137
+ lexical: Number(ss.keyword ?? m.lexical) || 0,
138
+ graph: Number(ss.graph ?? m.graph) || 0,
139
+ };
140
+ });
141
+ return { ok: true, status: res.status, data: items, source: "live", weights: res.data.weights || null };
142
+ }
143
+ return { ok: false, status: res.status, data: [], source: "unavailable", error: res.error };
144
+ },
145
+
146
+ /* ── Existing surfaces (used where helpful, all fallback-safe) ──────── */
147
+ workspaceOs() { return withFallback("/workspace/os", {}, EMPTY_WORKSPACE_OS); },
148
+ async models() {
149
+ const res = await raw("/models");
150
+ if (res.ok && res.data && !res.data.raw) {
151
+ const data = res.data;
152
+ const loadedIds = Array.isArray(data.loaded) ? data.loaded : [];
153
+ const recommended = Array.isArray(data.recommended) ? data.recommended.map((m) => ({
154
+ ...m,
155
+ name: m.name || m.display_name || m.id,
156
+ family: m.family || m.modality || "local",
157
+ state: loadedIds.includes(m.id) || data.current === m.id ? "loaded" : "available",
158
+ })) : [];
159
+ const loadedOnly = loadedIds
160
+ .filter((id) => !recommended.some((m) => m.id === id))
161
+ .map((id) => ({ id, name: id, family: "local", state: data.current === id ? "loaded" : "available" }));
162
+ return {
163
+ ok: true,
164
+ status: res.status,
165
+ source: "live",
166
+ data: { ...data, catalog: Array.isArray(data.catalog) ? data.catalog : [...recommended, ...loadedOnly] },
167
+ };
168
+ }
169
+ return { ok: false, status: res.status, data: { current: null, catalog: [] }, source: "unavailable", error: res.error };
170
+ },
171
+ loadModel(modelId, engine) {
172
+ return raw("/models/load", { method: "POST", body: { model_id: modelId, engine: engine || null } });
173
+ },
174
+ unloadModel(modelId) {
175
+ return raw(`/models/unload/${encodeURIComponent(modelId)}`, { method: "DELETE" });
176
+ },
177
+ sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
178
+
179
+ adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
180
+ adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
181
+ adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
182
+ adminSecurity() { return withFallback("/admin/security/overview", {}, EMPTY_ADMIN.security); },
183
+ adminRoles() { return withFallback("/admin/roles", {}, EMPTY_ADMIN.roles); },
184
+ adminPolicies() { return withFallback("/admin/policies", {}, EMPTY_ADMIN.policies); },
185
+ vpcStatus() { return withFallback("/vpc/status", {}, EMPTY_ADMIN.vpc); },
186
+
187
+ /* ── Embeddings (real backend: /api/embeddings/*) ───────────────────── */
188
+ /** GET /api/embeddings/status — active provider, grade, dimensions, last index. */
189
+ async embeddingsStatus() {
190
+ const res = await raw("/api/embeddings/status");
191
+ if (res.ok && res.data && res.data.provider) {
192
+ return { ok: true, status: res.status, data: res.data, source: "live" };
193
+ }
194
+ // No backend → report unavailable honestly (never fabricate a provider).
195
+ return {
196
+ ok: false, status: res.status, source: "unavailable",
197
+ data: { provider: null, active_provider: null, model: null,
198
+ model_id: null, dimensions: null, grade: "unavailable",
199
+ state: "unavailable", fell_back: false, health: { status: "unavailable", detail: "backend unavailable" },
200
+ last_indexed_at: null },
201
+ };
202
+ },
203
+ embeddingsProviders() { return withFallback("/api/embeddings/providers", {}, { active: "hash", providers: [] }); },
204
+
205
+ /* ── Agents (real backend: AgentRuntime /agents/api/runtime/*) ───────── */
206
+ /** GET /agents/api/runtime/status — roles, roster, runs, health from the runtime. */
207
+ async agentRuntime() {
208
+ const res = await raw("/agents/api/runtime/status");
209
+ if (res.ok && res.data && res.data.runtime && Array.isArray(res.data.agents)) {
210
+ return { ok: true, status: res.status, data: res.data, source: "live" };
211
+ }
212
+ // Fallback: unavailable roster, no fabricated run ledger.
213
+ return {
214
+ ok: false, status: res.status, source: "unavailable",
215
+ data: { runtime: { ready: false, total_runs: 0, active_runs: 0 },
216
+ health: { status: "unknown", checks: {} }, roles: [],
217
+ agents: [], runs: [] },
218
+ };
219
+ },
220
+ /** POST /agents/api/run — execute the multi-agent pipeline for a goal. */
221
+ runAgent(goal, roles) { return raw("/agents/api/run", { method: "POST", body: { goal, roles: roles || [] } }); },
222
+
223
+ /* ── Local computer memory (real backend: /workspace/computer-memory) ── */
224
+ computerMemory() { return raw("/workspace/computer-memory"); },
225
+ setComputerMemory(enabled) {
226
+ return raw("/workspace/computer-memory", { method: "POST", body: { enabled, consent: { approved: !!enabled } } });
227
+ },
228
+
229
+ /* ── Organization workspaces (real backend: /workspace/orgs) ────────── */
230
+ createOrg(name) { return raw("/workspace/orgs", { method: "POST", body: { name } }); },
231
+
232
+ /* ── Chat (real backend: SSE /chat + /history/*) ────────────────────── */
233
+
234
+ /** GET /history/conversations — conversation list. */
235
+ async chatHistory() {
236
+ const res = await raw("/history/conversations");
237
+ const list = res.ok && Array.isArray(res.data) ? res.data
238
+ : res.ok && res.data && Array.isArray(res.data.conversations) ? res.data.conversations
239
+ : null;
240
+ if (list) return { ok: true, status: res.status, data: list, source: "live" };
241
+ return { ok: false, status: res.status, data: [], source: "unavailable" };
242
+ },
243
+
244
+ /** GET /history/conversations/{id} — messages for one conversation. */
245
+ async conversation(id) {
246
+ const res = await raw(`/history/conversations/${encodeURIComponent(id)}`);
247
+ if (res.ok && res.data && Array.isArray(res.data.messages)) {
248
+ return { ok: true, status: res.status, data: res.data.messages, source: "live" };
249
+ }
250
+ return { ok: false, status: res.status, data: [], source: "unavailable" };
251
+ },
252
+
253
+ deleteConversation(id) {
254
+ return raw(`/history/conversations/${encodeURIComponent(id)}`, { method: "DELETE" });
255
+ },
256
+
257
+ /**
258
+ * POST /chat — streams the assistant reply over SSE.
259
+ * Parses `data: {chunk, model, trace}` events (terminator `[DONE]`), calling
260
+ * onChunk(delta, fullText) and onTrace(trace). If the endpoint is missing or
261
+ * not an event-stream, reports that chat is unavailable (no generated answer is
262
+ * invented). Resolves { source, text, trace, model, aborted }.
263
+ */
264
+ async streamChat(body, { onChunk, onTrace, signal } = {}) {
265
+ const ws = store.get().workspaceId;
266
+ let res;
267
+ try {
268
+ res = await fetch("/chat", {
269
+ method: "POST",
270
+ credentials: "same-origin",
271
+ signal,
272
+ headers: {
273
+ "Content-Type": "application/json",
274
+ "Accept": "text/event-stream",
275
+ ...(ws ? { "X-Workspace-Id": ws } : {}),
276
+ },
277
+ body: JSON.stringify({ stream: true, max_tokens: 2048, temperature: 0.2, ...body }),
278
+ });
279
+ } catch (err) {
280
+ if (err && err.name === "AbortError") return { source: "live", text: "", aborted: true };
281
+ return simulateChat(body, { onChunk, onTrace, signal });
282
+ }
283
+ const ctype = res.headers.get("content-type") || "";
284
+ if (!res.ok) {
285
+ let data = null;
286
+ try { data = await res.clone().json(); } catch {}
287
+ const detail = data && (data.detail || data.message || data.error);
288
+ const noModel = data && (data.error === "no_model_loaded" || /no .*model .*loaded/i.test(String(detail || "")));
289
+ if (noModel) {
290
+ return { source: "live", text: "", error: "no_model_loaded", errorMessage: String(detail || "No local model is loaded.") };
291
+ }
292
+ return simulateChat(body, { onChunk, onTrace, signal });
293
+ }
294
+ if (!res.body || !ctype.includes("text/event-stream")) {
295
+ return simulateChat(body, { onChunk, onTrace, signal });
296
+ }
297
+ const reader = res.body.getReader();
298
+ const decoder = new TextDecoder();
299
+ let buf = "", text = "", trace = null, model = null;
300
+ try {
301
+ for (;;) {
302
+ const { value, done } = await reader.read();
303
+ if (done) break;
304
+ buf += decoder.decode(value, { stream: true });
305
+ const parts = buf.split("\n\n");
306
+ buf = parts.pop();
307
+ for (const part of parts) {
308
+ const line = part.split("\n").find((l) => l.startsWith("data:"));
309
+ if (!line) continue;
310
+ const rawData = line.slice(5).trim();
311
+ if (rawData === "[DONE]") return { source: "live", text, trace, model };
312
+ let data; try { data = JSON.parse(rawData); } catch { continue; }
313
+ if (data.chunk) { text += data.chunk; onChunk && onChunk(data.chunk, text); }
314
+ if (data.model) model = data.model;
315
+ if (data.trace) { trace = data.trace; onTrace && onTrace(trace); }
316
+ }
317
+ }
318
+ } catch (err) {
319
+ if (err && err.name === "AbortError") return { source: "live", text, trace, model, aborted: true };
320
+ if (!text) return simulateChat(body, { onChunk, onTrace, signal });
321
+ }
322
+ return { source: "live", text, trace, model };
323
+ },
324
+ };
325
+
326
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
327
+
328
+ /** Transparent unavailable stream — used only when no chat backend is available. */
329
+ async function simulateChat(body, { onChunk, onTrace, signal } = {}) {
330
+ const q = (body && body.message) || "your question";
331
+ const reply =
332
+ `Chat is unavailable because the Lattice backend or active model is not reachable. ` +
333
+ `Start the server, load a model, and rebuild retrieval before sending “${q}”.`;
334
+ let text = "";
335
+ for (const word of reply.split(" ")) {
336
+ if (signal && signal.aborted) return { source: "unavailable", text, aborted: true };
337
+ const delta = (text ? " " : "") + word;
338
+ text += delta;
339
+ onChunk && onChunk(delta, text);
340
+ await sleep(16);
341
+ }
342
+ onTrace && onTrace(null);
343
+ return { source: "unavailable", text, trace: null };
344
+ }
@@ -2,21 +2,31 @@
2
2
  * Lattice AI v3 — Integration adapter
3
3
  *
4
4
  * Every adapter call hits the real endpoint first (including /api/index/status,
5
- * /api/graph, /api/search/hybrid, and /chat). If that
6
- * endpoint is missing/unavailable, it degrades to a clearly-labeled SAMPLE
7
- * payload from fixtures.js and reports `source: "placeholder"` so the UI can
8
- * badge it. No backend logic is implemented here — only transport + graceful
9
- * fallback, which keeps the v3 frontend resilient during local setup.
5
+ * /api/graph, /api/search/hybrid, and /chat). If that endpoint is
6
+ * missing/unavailable, it returns an unavailable source with empty data so the
7
+ * UI can render a clear unavailable state without inventing counters or health.
10
8
  *
11
9
  * Return shape (never throws): { ok, status, data, source, error }
12
10
  * source: "live" → returned by a real backend endpoint
13
- * "placeholder" → fixture fallback (backend not yet available)
11
+ * "unavailable" → endpoint missing/down; no fake payload
14
12
  * ========================================================================== */
15
13
 
16
14
  import { store } from "./store.js";
17
- import * as fx from "./fixtures.js";
18
15
 
19
16
  const TIMEOUT_MS = 8000;
17
+ const EMPTY_INDEX_STATUS = { generated_at: null, pipelines: {}, sources: [] };
18
+ const EMPTY_GRAPH_STATS = { nodes: {}, edges: {}, total_nodes: 0, total_edges: 0 };
19
+ const EMPTY_WORKSPACE_OS = { counts: {}, models: {} };
20
+ const EMPTY_SYSINFO = { cpu_pct: null, ram_pct: null, gpu_mem_pct: null, gpu_mem_gb: null };
21
+ const EMPTY_ADMIN = {
22
+ summary: { total_users: null, active_users: null, admin_users: null, total_messages: null },
23
+ users: [],
24
+ audit: { recent_events: [] },
25
+ security: {},
26
+ roles: { roles: [] },
27
+ policies: { policies: [] },
28
+ vpc: {},
29
+ };
20
30
 
21
31
  async function raw(path, { method = "GET", body, headers } = {}) {
22
32
  const ctrl = new AbortController();
@@ -46,28 +56,35 @@ async function raw(path, { method = "GET", body, headers } = {}) {
46
56
  }
47
57
  }
48
58
 
49
- /** Try the live endpoint; on any non-2xx/transport failure, use the fixture. */
50
- async function withFallback(path, opts, fixture) {
59
+ function unavailableData(shape) {
60
+ const value = typeof shape === "function" ? shape() : shape;
61
+ if (Array.isArray(value)) return [];
62
+ if (value && typeof value === "object") return {};
63
+ return null;
64
+ }
65
+
66
+ /** Try the live endpoint; on any non-2xx/transport failure, return empty data. */
67
+ async function withFallback(path, opts, shape) {
51
68
  const res = await raw(path, opts);
52
69
  if (res.ok && res.data && !res.data.raw) {
53
70
  return { ...res, source: "live" };
54
71
  }
55
- return { ok: true, status: res.status, data: typeof fixture === "function" ? fixture() : fixture, source: "placeholder", error: res.error };
72
+ return { ok: false, status: res.status, data: unavailableData(shape), source: "unavailable", error: res.error };
56
73
  }
57
74
 
58
75
  export const api = {
59
76
  raw,
60
77
 
61
- /** Generic GET with fixture fallback. */
62
- async get(path, fixture = null) {
63
- return withFallback(path, {}, fixture);
78
+ /** Generic GET with unavailable fallback. */
79
+ async get(path, shape = null) {
80
+ return withFallback(path, {}, shape);
64
81
  },
65
82
 
66
83
  /* ── Documented future surfaces ─────────────────────────────────────── */
67
84
 
68
85
  /** GET /api/index/status — KG + Vector + Hybrid pipeline state. */
69
86
  indexStatus() {
70
- return withFallback("/api/index/status", {}, fx.INDEX_STATUS);
87
+ return withFallback("/api/index/status", {}, EMPTY_INDEX_STATUS);
71
88
  },
72
89
 
73
90
  /** POST /api/index/rebuild — rebuild the derived vector index (real run). */
@@ -76,7 +93,7 @@ export const api = {
76
93
  },
77
94
 
78
95
  /** GET /api/graph — knowledge graph (nodes + edges). Falls back through the
79
- * current /knowledge-graph/graph route before the fixture. */
96
+ * current /knowledge-graph/graph route before reporting unavailable. */
80
97
  async graph(params = {}) {
81
98
  const qs = new URLSearchParams(params).toString();
82
99
  const primary = await raw(`/api/graph${qs ? "?" + qs : ""}`);
@@ -87,11 +104,11 @@ export const api = {
87
104
  if (legacy.ok && legacy.data && Array.isArray(legacy.data.nodes)) {
88
105
  return { ...legacy, source: "live" };
89
106
  }
90
- return { ok: true, status: 200, data: fx.GRAPH, source: "placeholder" };
107
+ return { ok: false, status: primary.status || legacy.status || 0, data: { nodes: [], edges: [] }, source: "unavailable", error: primary.error || legacy.error };
91
108
  },
92
109
 
93
110
  graphStats() {
94
- return withFallback("/knowledge-graph/stats", {}, fx.GRAPH_STATS);
111
+ return withFallback("/knowledge-graph/stats", {}, EMPTY_GRAPH_STATS);
95
112
  },
96
113
 
97
114
  /** POST /api/search/hybrid — fused KG + vector retrieval.
@@ -123,11 +140,11 @@ export const api = {
123
140
  });
124
141
  return { ok: true, status: res.status, data: items, source: "live", weights: res.data.weights || null };
125
142
  }
126
- return { ok: true, status: res.status, data: fx.hybridResults(query), source: "placeholder", error: res.error };
143
+ return { ok: false, status: res.status, data: [], source: "unavailable", error: res.error };
127
144
  },
128
145
 
129
146
  /* ── Existing surfaces (used where helpful, all fallback-safe) ──────── */
130
- workspaceOs() { return withFallback("/workspace/os", {}, fx.WORKSPACE_OS); },
147
+ workspaceOs() { return withFallback("/workspace/os", {}, EMPTY_WORKSPACE_OS); },
131
148
  async models() {
132
149
  const res = await raw("/models");
133
150
  if (res.ok && res.data && !res.data.raw) {
@@ -149,17 +166,23 @@ export const api = {
149
166
  data: { ...data, catalog: Array.isArray(data.catalog) ? data.catalog : [...recommended, ...loadedOnly] },
150
167
  };
151
168
  }
152
- return { ok: true, status: res.status, data: fx.MODELS, source: "placeholder", error: res.error };
169
+ return { ok: false, status: res.status, data: { current: null, catalog: [] }, source: "unavailable", error: res.error };
153
170
  },
154
- sysinfo() { return withFallback("/local/sysinfo", {}, fx.SYSINFO); },
171
+ loadModel(modelId, engine) {
172
+ return raw("/models/load", { method: "POST", body: { model_id: modelId, engine: engine || null } });
173
+ },
174
+ unloadModel(modelId) {
175
+ return raw(`/models/unload/${encodeURIComponent(modelId)}`, { method: "DELETE" });
176
+ },
177
+ sysinfo() { return withFallback("/local/sysinfo", {}, EMPTY_SYSINFO); },
155
178
 
156
- adminSummary() { return withFallback("/admin/summary", {}, fx.ADMIN.summary); },
157
- adminUsers() { return withFallback("/admin/users", {}, fx.ADMIN.users); },
158
- adminAudit() { return withFallback("/admin/audit", {}, { recent_events: fx.ADMIN.audit }); },
159
- adminSecurity() { return withFallback("/admin/security/overview", {}, fx.ADMIN.security); },
160
- adminRoles() { return withFallback("/admin/roles", {}, { roles: fx.ADMIN.roles }); },
161
- adminPolicies() { return withFallback("/admin/policies", {}, { policies: fx.ADMIN.policies }); },
162
- vpcStatus() { return withFallback("/vpc/status", {}, fx.ADMIN.vpc); },
179
+ adminSummary() { return withFallback("/admin/summary", {}, EMPTY_ADMIN.summary); },
180
+ adminUsers() { return withFallback("/admin/users", {}, EMPTY_ADMIN.users); },
181
+ adminAudit() { return withFallback("/admin/audit", {}, EMPTY_ADMIN.audit); },
182
+ adminSecurity() { return withFallback("/admin/security/overview", {}, EMPTY_ADMIN.security); },
183
+ adminRoles() { return withFallback("/admin/roles", {}, EMPTY_ADMIN.roles); },
184
+ adminPolicies() { return withFallback("/admin/policies", {}, EMPTY_ADMIN.policies); },
185
+ vpcStatus() { return withFallback("/vpc/status", {}, EMPTY_ADMIN.vpc); },
163
186
 
164
187
  /* ── Embeddings (real backend: /api/embeddings/*) ───────────────────── */
165
188
  /** GET /api/embeddings/status — active provider, grade, dimensions, last index. */
@@ -170,10 +193,10 @@ export const api = {
170
193
  }
171
194
  // No backend → report unavailable honestly (never fabricate a provider).
172
195
  return {
173
- ok: true, status: res.status, source: "placeholder",
174
- data: { provider: "hash", active_provider: "hash", model: "lattice-local-hash-v1",
175
- model_id: "lattice-local-hash-v1:384", dimensions: 384, grade: "fallback",
176
- state: "fallback", fell_back: false, health: { status: "unknown", detail: "backend unavailable" },
196
+ ok: false, status: res.status, source: "unavailable",
197
+ data: { provider: null, active_provider: null, model: null,
198
+ model_id: null, dimensions: null, grade: "unavailable",
199
+ state: "unavailable", fell_back: false, health: { status: "unavailable", detail: "backend unavailable" },
177
200
  last_indexed_at: null },
178
201
  };
179
202
  },
@@ -186,12 +209,12 @@ export const api = {
186
209
  if (res.ok && res.data && res.data.runtime && Array.isArray(res.data.agents)) {
187
210
  return { ok: true, status: res.status, data: res.data, source: "live" };
188
211
  }
189
- // Fallback: clearly-badged sample roster, no fabricated run ledger.
212
+ // Fallback: unavailable roster, no fabricated run ledger.
190
213
  return {
191
- ok: true, status: res.status, source: "placeholder",
214
+ ok: false, status: res.status, source: "unavailable",
192
215
  data: { runtime: { ready: false, total_runs: 0, active_runs: 0 },
193
216
  health: { status: "unknown", checks: {} }, roles: [],
194
- agents: fx.AGENTS.map((a) => ({ ...a, last_status: null, last_at: null })), runs: [] },
217
+ agents: [], runs: [] },
195
218
  };
196
219
  },
197
220
  /** POST /agents/api/run — execute the multi-agent pipeline for a goal. */
@@ -215,7 +238,7 @@ export const api = {
215
238
  : res.ok && res.data && Array.isArray(res.data.conversations) ? res.data.conversations
216
239
  : null;
217
240
  if (list) return { ok: true, status: res.status, data: list, source: "live" };
218
- return { ok: true, status: res.status, data: fx.CHAT.conversations, source: "placeholder" };
241
+ return { ok: false, status: res.status, data: [], source: "unavailable" };
219
242
  },
220
243
 
221
244
  /** GET /history/conversations/{id} — messages for one conversation. */
@@ -224,8 +247,7 @@ export const api = {
224
247
  if (res.ok && res.data && Array.isArray(res.data.messages)) {
225
248
  return { ok: true, status: res.status, data: res.data.messages, source: "live" };
226
249
  }
227
- const sample = (fx.CHAT.conversations.find((c) => c.id === id) || {}).messages || [];
228
- return { ok: true, status: res.status, data: sample, source: "placeholder" };
250
+ return { ok: false, status: res.status, data: [], source: "unavailable" };
229
251
  },
230
252
 
231
253
  deleteConversation(id) {
@@ -236,8 +258,8 @@ export const api = {
236
258
  * POST /chat — streams the assistant reply over SSE.
237
259
  * Parses `data: {chunk, model, trace}` events (terminator `[DONE]`), calling
238
260
  * onChunk(delta, fullText) and onTrace(trace). If the endpoint is missing or
239
- * not an event-stream, degrades to a clearly-labeled SAMPLE stream (no real
240
- * generation is invented). Resolves { source, text, trace, model, aborted }.
261
+ * not an event-stream, reports that chat is unavailable (no generated answer is
262
+ * invented). Resolves { source, text, trace, model, aborted }.
241
263
  */
242
264
  async streamChat(body, { onChunk, onTrace, signal } = {}) {
243
265
  const ws = store.get().workspaceId;
@@ -303,25 +325,20 @@ export const api = {
303
325
 
304
326
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
305
327
 
306
- /** Transparent SAMPLE stream — used only when no chat backend is available. */
328
+ /** Transparent unavailable stream — used only when no chat backend is available. */
307
329
  async function simulateChat(body, { onChunk, onTrace, signal } = {}) {
308
330
  const q = (body && body.message) || "your question";
309
331
  const reply =
310
- `This is a sample response. Connect a local model and the v3 retrieval backend ` +
311
- `(/api/search/hybrid, /api/graph) to ground answers to “${q}” in your workspace. ` +
312
- `The context panel shows the knowledge-graph entities, vector matches, and indexed ` +
313
- `files that would be used to answer — this preview does not run a model.`;
332
+ `Chat is unavailable because the Lattice backend or active model is not reachable. ` +
333
+ `Start the server, load a model, and rebuild retrieval before sending “${q}”.`;
314
334
  let text = "";
315
335
  for (const word of reply.split(" ")) {
316
- if (signal && signal.aborted) return { source: "placeholder", text, aborted: true };
336
+ if (signal && signal.aborted) return { source: "unavailable", text, aborted: true };
317
337
  const delta = (text ? " " : "") + word;
318
338
  text += delta;
319
339
  onChunk && onChunk(delta, text);
320
340
  await sleep(16);
321
341
  }
322
- const trace = fx.sampleTrace(q);
323
- onTrace && onTrace(trace);
324
- return { source: "placeholder", text, trace };
342
+ onTrace && onTrace(null);
343
+ return { source: "unavailable", text, trace: null };
325
344
  }
326
-
327
- export { fx };