ltcai 3.2.0 → 3.4.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 +87 -67
- package/docs/CHANGELOG.md +36 -0
- package/docs/architecture.md +2 -1
- package/docs/assets/v3.4.0/agent-run.png +0 -0
- package/docs/assets/v3.4.0/agents.png +0 -0
- package/docs/assets/v3.4.0/before/chat-before.png +0 -0
- package/docs/assets/v3.4.0/before/files-before.png +0 -0
- package/docs/assets/v3.4.0/chat.png +0 -0
- package/docs/assets/v3.4.0/connect-folder.png +0 -0
- package/docs/assets/v3.4.0/files.png +0 -0
- package/docs/assets/v3.4.0/home.png +0 -0
- package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
- package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
- package/docs/assets/v3.4.0/local-agent.png +0 -0
- package/docs/assets/v3.4.0/memory.png +0 -0
- package/docs/assets/v3.4.0/settings.png +0 -0
- package/docs/assets/v3.4.0/vision-input.png +0 -0
- package/docs/assets/v3.4.0/workflows.png +0 -0
- package/knowledge_graph.py +45 -0
- package/knowledge_graph_api.py +10 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +3 -0
- package/latticeai/api/hooks.py +39 -0
- package/latticeai/api/local_files.py +41 -0
- package/latticeai/api/models.py +36 -1
- package/latticeai/api/tools.py +16 -1
- package/latticeai/api/workflow_designer.py +2 -1
- package/latticeai/core/hooks.py +398 -2
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/workflow_engine.py +21 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +40 -0
- package/latticeai/services/agent_runtime.py +46 -1
- package/latticeai/services/upload_service.py +17 -0
- package/package.json +1 -1
- package/scripts/build_v3_assets.mjs +7 -1
- package/scripts/capture/capture_v340.js +88 -0
- package/static/css/{tokens.5a595671.css → tokens.3ba22e37.css} +109 -109
- package/static/css/tokens.css +109 -109
- package/static/v3/asset-manifest.json +25 -25
- package/static/v3/css/{lattice.components.011e988b.css → lattice.components.9b49d614.css} +57 -32
- package/static/v3/css/lattice.components.css +57 -32
- package/static/v3/css/{lattice.shell.4920f42d.css → lattice.shell.6ceea7c8.css} +75 -31
- package/static/v3/css/lattice.shell.css +75 -31
- package/static/v3/css/lattice.tokens.css +13 -13
- package/static/v3/css/{lattice.tokens.c597ff81.css → lattice.tokens.e7018963.css} +13 -13
- package/static/v3/css/{lattice.views.3ee19d4e.css → lattice.views.22f69117.css} +98 -15
- package/static/v3/css/lattice.views.css +98 -15
- package/static/v3/js/{app.a5adc0f3.js → app.c4acfdd8.js} +1 -1
- package/static/v3/js/core/{api.603b978f.js → api.12b568ad.js} +126 -4
- package/static/v3/js/core/api.js +126 -4
- package/static/v3/js/core/{components.4c83e0a9.js → components.35f02e4c.js} +8 -0
- package/static/v3/js/core/components.js +8 -0
- package/static/v3/js/core/{routes.07ad6696.js → routes.d214b399.js} +16 -12
- package/static/v3/js/core/routes.js +16 -12
- package/static/v3/js/core/{shell.ea0b9ae5.js → shell.80a6ad82.js} +37 -9
- package/static/v3/js/core/shell.js +34 -6
- package/static/v3/js/views/agents.014d0b74.js +541 -0
- package/static/v3/js/views/agents.js +305 -57
- package/static/v3/js/views/{chat.718144ce.js → chat.e6dd7dd0.js} +162 -10
- package/static/v3/js/views/chat.js +162 -10
- package/static/v3/js/views/files.adad14c1.js +365 -0
- package/static/v3/js/views/files.js +269 -90
- package/static/v3/js/views/home.24f8b8ae.js +200 -0
- package/static/v3/js/views/home.js +96 -15
- package/static/v3/js/views/hooks.13845954.js +215 -0
- package/static/v3/js/views/hooks.js +117 -1
- 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/{my-computer.1b2ff621.js → my-computer.c3ef5283.js} +224 -1
- package/static/v3/js/views/my-computer.js +224 -1
- package/static/v3/js/views/{settings.4f777210.js → settings.8631fa5e.js} +70 -2
- package/static/v3/js/views/settings.js +70 -2
- package/static/v3/js/views/agents.c373d48c.js +0 -293
- package/static/v3/js/views/files.4935197e.js +0 -186
- package/static/v3/js/views/home.cdde3b32.js +0 -119
- package/static/v3/js/views/hooks.f3edebca.js +0 -99
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
|
-
* View: Files — connected
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* View: Files — uploaded documents, connected folders & folder watch.
|
|
3
|
+
* The headline table lists the documents Lattice has actually ingested
|
|
4
|
+
* (/knowledge-graph/documents, live) with a per-doc index-state pill. A manual
|
|
5
|
+
* upload drop zone ingests files on-device, and Connect Folder indexes a local
|
|
6
|
+
* directory (+ watches it for changes) over the on-device runtime. When a
|
|
7
|
+
* surface is unavailable its panel renders an honest empty/unavailable state —
|
|
8
|
+
* no counts or statuses are fabricated.
|
|
6
9
|
*
|
|
7
10
|
* View contract (shared by all views):
|
|
8
11
|
* export async function render(ctx) -> single DOM node
|
|
@@ -11,15 +14,15 @@
|
|
|
11
14
|
|
|
12
15
|
import { timeAgo } from "../core/dom.js";
|
|
13
16
|
|
|
14
|
-
/** Tabler glyph per
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
/** Tabler glyph per uploaded-document extension. */
|
|
18
|
+
const EXT_ICON = {
|
|
19
|
+
pdf: "file-type-pdf", docx: "file-type-docx", doc: "file-text",
|
|
20
|
+
xlsx: "file-spreadsheet", xls: "file-spreadsheet", csv: "table",
|
|
21
|
+
pptx: "presentation", ppt: "presentation",
|
|
22
|
+
md: "file-text", txt: "file-text", json: "file-code",
|
|
23
|
+
png: "photo", jpg: "photo", jpeg: "photo", gif: "photo",
|
|
21
24
|
};
|
|
22
|
-
const
|
|
25
|
+
const iconForExt = (ext) => EXT_ICON[String(ext || "").replace(/^\./, "").toLowerCase()] || "file";
|
|
23
26
|
|
|
24
27
|
/** Bytes → compact human string (1.0 KB / 4.7 KB / 180 KB / 1.2 MB). */
|
|
25
28
|
function humanSize(bytes) {
|
|
@@ -33,45 +36,95 @@ function humanSize(bytes) {
|
|
|
33
36
|
return `${v.toFixed(v >= 100 || Number.isInteger(v) ? 0 : 1)} ${units[i]}`;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
if (data && Array.isArray(data.sources)) {
|
|
39
|
-
return data.sources.map((source) => ({
|
|
40
|
-
name: source.label || source.id || "local source",
|
|
41
|
-
kind: "default",
|
|
42
|
-
size: null,
|
|
43
|
-
path: source.root_path || source.id || "",
|
|
44
|
-
indexed: Number(source.success_count || 0) > 0,
|
|
45
|
-
updated: source.last_run_at || source.updated_at || null,
|
|
46
|
-
count: Number(source.success_count || 0),
|
|
47
|
-
status: source.status || (source.watch_active ? "watching" : "idle"),
|
|
48
|
-
}));
|
|
49
|
-
}
|
|
50
|
-
const list = Array.isArray(data) ? data : (data && Array.isArray(data.files) ? data.files : null);
|
|
51
|
-
if (!list) return null;
|
|
52
|
-
return list.map((f) => ({
|
|
53
|
-
name: f.name || (f.path ? String(f.path).split("/").pop() : "untitled"),
|
|
54
|
-
kind: f.kind || "default",
|
|
55
|
-
size: Number(f.size) || 0,
|
|
56
|
-
path: f.path || f.name || "",
|
|
57
|
-
indexed: f.indexed === true,
|
|
58
|
-
updated: f.updated || f.modified || f.mtime || null,
|
|
59
|
-
count: Number(f.count || 0),
|
|
60
|
-
status: f.status || null,
|
|
61
|
-
}));
|
|
62
|
-
}
|
|
39
|
+
/** Document types the backend accepts (latticeai/services/upload_service.py). */
|
|
40
|
+
const UPLOAD_ACCEPT = ".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv";
|
|
63
41
|
|
|
64
42
|
export async function render(ctx) {
|
|
65
43
|
const { h, icon, api, c, navigate, toast } = ctx;
|
|
66
44
|
|
|
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.
|
|
69
|
-
const unavailableToast = () =>
|
|
70
|
-
toast("Connecting a folder requires the Lattice desktop local agent — not available in this build.", "warn");
|
|
71
|
-
|
|
72
45
|
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
73
46
|
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
74
47
|
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
48
|
+
const foldersSrc = h("span", c.sourceBadge("pending"));
|
|
49
|
+
const foldersHost = h("div", c.loading({ lines: 3 }));
|
|
50
|
+
|
|
51
|
+
// ── Manual upload (works in this build; no desktop agent required) ─────────
|
|
52
|
+
let busy = false;
|
|
53
|
+
const fileInput = h("input", {
|
|
54
|
+
type: "file", multiple: true, accept: UPLOAD_ACCEPT,
|
|
55
|
+
style: { display: "none" }, "aria-hidden": "true",
|
|
56
|
+
on: { change: (e) => uploadFiles(e.target.files) },
|
|
57
|
+
});
|
|
58
|
+
const pickFiles = () => { if (!busy) fileInput.click(); };
|
|
59
|
+
const slots = { statHost, srcSlot, tableHost, foldersSrc, foldersHost, pickFiles, connectFolder };
|
|
60
|
+
|
|
61
|
+
// ── Connect Folder — index a local directory on-device and watch it ────────
|
|
62
|
+
// Available now via the on-device runtime (one call does
|
|
63
|
+
// request → self-approve → index + watch). No desktop agent required.
|
|
64
|
+
async function connectFolder() {
|
|
65
|
+
if (busy) return;
|
|
66
|
+
const path = window.prompt("Connect a local folder to index (absolute path)", "~/Documents");
|
|
67
|
+
if (!path || !String(path).trim()) return;
|
|
68
|
+
const target = String(path).trim();
|
|
69
|
+
busy = true;
|
|
70
|
+
toast(`Connecting “${target}” — indexing on-device…`, "info");
|
|
71
|
+
const res = await api.connectFolder(target, { watch: true });
|
|
72
|
+
busy = false;
|
|
73
|
+
if (res.ok) {
|
|
74
|
+
toast(`Connected and indexing ${target} — now watched for changes.`, "ok");
|
|
75
|
+
hydrate(ctx, slots);
|
|
76
|
+
} else {
|
|
77
|
+
toast(res.error || "Could not connect the folder.", "warn");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function uploadFiles(fileList) {
|
|
82
|
+
const files = Array.from(fileList || []);
|
|
83
|
+
if (!files.length || busy) return;
|
|
84
|
+
busy = true;
|
|
85
|
+
let ok = 0;
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
toast(`Uploading “${file.name}”…`, "info");
|
|
88
|
+
const res = await api.uploadDocument(file);
|
|
89
|
+
if (res.ok && res.data && !res.data.detail && !res.data.error) {
|
|
90
|
+
ok++;
|
|
91
|
+
} else {
|
|
92
|
+
const detail = (res.data && (res.data.detail || res.data.error)) || "the backend is unavailable";
|
|
93
|
+
toast(`Could not ingest “${file.name}” — ${detail}.`, "warn");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
fileInput.value = "";
|
|
97
|
+
busy = false;
|
|
98
|
+
if (ok) {
|
|
99
|
+
toast(`Indexed ${ok} document${ok === 1 ? "" : "s"} into the knowledge graph — now searchable in Chat and Hybrid Search.`, "ok");
|
|
100
|
+
}
|
|
101
|
+
hydrate(ctx, slots);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const dropZone = h("div.lt3-drop", {
|
|
105
|
+
on: {
|
|
106
|
+
dragover: (e) => { e.preventDefault(); dropZone.classList.add("is-dragover"); },
|
|
107
|
+
dragleave: () => dropZone.classList.remove("is-dragover"),
|
|
108
|
+
drop: (e) => { e.preventDefault(); dropZone.classList.remove("is-dragover"); uploadFiles(e.dataTransfer && e.dataTransfer.files); },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
fileInput,
|
|
112
|
+
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
113
|
+
h("div",
|
|
114
|
+
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag documents here, or upload manually"),
|
|
115
|
+
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
116
|
+
"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."),
|
|
117
|
+
),
|
|
118
|
+
h("div.lt3-drop__meta",
|
|
119
|
+
c.pill("Manual upload available", "ok", { dot: true }),
|
|
120
|
+
c.pill("Connect a local folder — indexed & watched on-device", "info", { dot: true }),
|
|
121
|
+
c.pill("Search + Chat ready after indexing", "info", { dot: true }),
|
|
122
|
+
),
|
|
123
|
+
h("div.lt3-row-2",
|
|
124
|
+
h("button.lt3-btn.lt3-btn--primary", { type: "button", on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
125
|
+
h("button.lt3-btn.lt3-btn--ghost", { type: "button", on: { click: connectFolder } }, icon("folder-plus"), "Connect folder"),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
75
128
|
|
|
76
129
|
const root = h("div.lt3-stack-6",
|
|
77
130
|
c.viewHeader({
|
|
@@ -80,107 +133,233 @@ export async function render(ctx) {
|
|
|
80
133
|
sub: "Connected sources and the documents Lattice has indexed for retrieval. Everything stays on this machine.",
|
|
81
134
|
actions: [
|
|
82
135
|
h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => navigate("knowledge-graph") } }, icon("chart-dots-3"), "View graph"),
|
|
83
|
-
h("button.lt3-btn.lt3-btn--
|
|
136
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
137
|
+
h("button.lt3-btn.lt3-btn--ghost", { title: "Index a local folder on-device and watch it for changes", on: { click: connectFolder } }, icon("folder-plus"), "Connect folder"),
|
|
84
138
|
],
|
|
85
139
|
}),
|
|
86
140
|
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
|
-
),
|
|
141
|
+
dropZone,
|
|
96
142
|
c.panel({
|
|
97
143
|
head: h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", width: "100%" } },
|
|
98
144
|
h("div",
|
|
99
145
|
h("div.lt3-eyebrow", "Index"),
|
|
100
|
-
h("h3.lt3-panel__title", "
|
|
146
|
+
h("h3.lt3-panel__title", "Uploaded documents"),
|
|
147
|
+
h("p.lt3-panel__sub", "Every file Lattice has parsed, chunked, embedded and linked into the knowledge graph."),
|
|
101
148
|
),
|
|
102
149
|
srcSlot,
|
|
103
150
|
),
|
|
104
151
|
children: tableHost,
|
|
105
152
|
}),
|
|
153
|
+
c.panel({
|
|
154
|
+
head: h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", width: "100%" } },
|
|
155
|
+
h("div",
|
|
156
|
+
h("div.lt3-eyebrow", "Local sources"),
|
|
157
|
+
h("h3.lt3-panel__title", "Connected folders & folder watch"),
|
|
158
|
+
h("p.lt3-panel__sub", "Local directories Lattice indexes on-device and re-indexes when their files change."),
|
|
159
|
+
),
|
|
160
|
+
foldersSrc,
|
|
161
|
+
),
|
|
162
|
+
children: foldersHost,
|
|
163
|
+
}),
|
|
106
164
|
);
|
|
107
165
|
|
|
108
|
-
hydrate(ctx,
|
|
166
|
+
hydrate(ctx, slots);
|
|
109
167
|
return root;
|
|
110
168
|
}
|
|
111
169
|
|
|
112
170
|
async function hydrate(ctx, slots) {
|
|
113
|
-
const {
|
|
114
|
-
|
|
171
|
+
const { statHost, srcSlot, tableHost, foldersSrc, foldersHost, pickFiles } = slots;
|
|
172
|
+
|
|
173
|
+
// Fetch the documents (headline table) and connected local sources in parallel.
|
|
174
|
+
const [docsRes, sourcesRes] = await Promise.all([
|
|
175
|
+
ctx.api.documents(200),
|
|
176
|
+
ctx.api.localSources(),
|
|
177
|
+
]);
|
|
115
178
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
179
|
+
hydrateDocuments(ctx, { statHost, srcSlot, tableHost, pickFiles }, docsRes);
|
|
180
|
+
hydrateFolders(ctx, { foldersSrc, foldersHost, slots }, sourcesRes);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Headline "Uploaded documents" table + stat roll-up. Data: api.documents(). */
|
|
184
|
+
function hydrateDocuments(ctx, { statHost, srcSlot, tableHost, pickFiles }, docsRes) {
|
|
185
|
+
const { h, icon, c, toast } = ctx;
|
|
186
|
+
const docs = Array.isArray(docsRes.data) ? docsRes.data : [];
|
|
187
|
+
const source = docsRes.source || (docsRes.ok ? "live" : "unavailable");
|
|
120
188
|
srcSlot.replaceChildren(c.sourceBadge(source));
|
|
121
189
|
|
|
122
|
-
// ── Stat roll-up
|
|
123
|
-
const indexedCount =
|
|
190
|
+
// ── Stat roll-up (driven by the real documents list) ──────────────────────
|
|
191
|
+
const indexedCount = docs.filter((d) => d.indexed === true || d.ingest_state === "indexed").length;
|
|
124
192
|
const sourceCount = new Set(
|
|
125
|
-
|
|
193
|
+
docs.map((d) => (d.uploader ? `u:${d.uploader}` : `e:${String(d.ext || "").toLowerCase()}`)),
|
|
126
194
|
).size;
|
|
127
|
-
const totalBytes =
|
|
195
|
+
const totalBytes = docs.reduce((sum, d) => sum + (Number(d.bytes) || 0), 0);
|
|
128
196
|
statHost.replaceChildren(
|
|
129
|
-
c.stat({ label: "Total files", value: c.fmtNum(
|
|
197
|
+
c.stat({ label: "Total files", value: c.fmtNum(docs.length), icon: "files" }),
|
|
130
198
|
c.stat({ label: "Indexed", value: c.fmtNum(indexedCount), icon: "circle-check" }),
|
|
131
199
|
c.stat({ label: "Sources", value: c.fmtNum(sourceCount), icon: "database" }),
|
|
132
200
|
c.stat({ label: "Total size", value: humanSize(totalBytes), icon: "weight" }),
|
|
133
201
|
);
|
|
134
202
|
|
|
135
|
-
// ── Empty state
|
|
136
|
-
if (!
|
|
203
|
+
// ── Empty / unavailable state ─────────────────────────────────────────────
|
|
204
|
+
if (!docs.length) {
|
|
205
|
+
if (!docsRes.ok) {
|
|
206
|
+
tableHost.replaceChildren(c.errorState(
|
|
207
|
+
docsRes.error || "The document index is unavailable. Start the backend with the knowledge graph enabled.",
|
|
208
|
+
));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
137
211
|
tableHost.replaceChildren(c.emptyState({
|
|
138
212
|
icon: "folder-off",
|
|
139
213
|
title: "No documents indexed yet",
|
|
140
|
-
body: "
|
|
141
|
-
action: h("button.lt3-btn.lt3-btn--
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
icon("folder-plus"), "Connect folder"),
|
|
214
|
+
body: "Upload a document and Lattice will parse, embed, and link it into the knowledge graph for hybrid retrieval.",
|
|
215
|
+
action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm",
|
|
216
|
+
{ on: { click: () => (pickFiles ? pickFiles() : null) } },
|
|
217
|
+
icon("upload"), "Upload files"),
|
|
145
218
|
}));
|
|
146
219
|
return;
|
|
147
220
|
}
|
|
148
221
|
|
|
149
|
-
// ── Table
|
|
222
|
+
// ── Table ─────────────────────────────────────────────────────────────────
|
|
150
223
|
const columns = [
|
|
151
224
|
{
|
|
152
|
-
key: "
|
|
225
|
+
key: "filename", label: "Name",
|
|
153
226
|
render: (row) => h("div.lt3-row-2",
|
|
154
|
-
h("span.lt3-filerow__icon", icon(
|
|
155
|
-
h("span", { style: { "font-weight": "var(--lt3-weight-medium)" } }, row.
|
|
227
|
+
h("span.lt3-filerow__icon", icon(iconForExt(row.ext))),
|
|
228
|
+
h("span", { style: { "font-weight": "var(--lt3-weight-medium)" } }, row.filename || "untitled"),
|
|
156
229
|
),
|
|
157
230
|
},
|
|
158
231
|
{
|
|
159
|
-
key: "
|
|
160
|
-
render: (row) => h("span.lt3-mono.lt3-faint", row.
|
|
232
|
+
key: "uploader", label: "Uploaded by", width: "26%",
|
|
233
|
+
render: (row) => h("span.lt3-mono.lt3-faint", row.uploader || "—"),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
key: "chars", label: "Size", width: "100px",
|
|
237
|
+
render: (row) => h("span.lt3-mono",
|
|
238
|
+
Number(row.chars) > 0 ? `${c.fmtNum(row.chars)} chars` : humanSize(row.bytes)),
|
|
161
239
|
},
|
|
162
240
|
{
|
|
163
|
-
key: "
|
|
164
|
-
render: (row) => h("span.lt3-mono", row.
|
|
241
|
+
key: "chunks", label: "Chunks", width: "84px",
|
|
242
|
+
render: (row) => h("span.lt3-mono", Number(row.chunks) > 0 ? c.fmtNum(row.chunks) : "—"),
|
|
165
243
|
},
|
|
166
244
|
{
|
|
167
|
-
key: "
|
|
168
|
-
|
|
245
|
+
key: "ingest_state", label: "Index", width: "120px",
|
|
246
|
+
// "indexed" → green, "ingested" → warn (via components STATE_VARIANT).
|
|
247
|
+
render: (row) => c.statePill(row.ingest_state || (row.indexed ? "indexed" : "ingested")),
|
|
169
248
|
},
|
|
170
249
|
{
|
|
171
|
-
key: "
|
|
250
|
+
key: "updated_at", label: "Updated", width: "104px",
|
|
172
251
|
render: (row) => h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } },
|
|
173
|
-
row.
|
|
252
|
+
(row.updated_at || row.created_at) ? timeAgo(row.updated_at || row.created_at) : "—"),
|
|
174
253
|
},
|
|
175
254
|
{
|
|
176
255
|
key: "_actions", label: "", width: "44px",
|
|
256
|
+
// Per-file management is limited — say so honestly rather than implying delete/re-index.
|
|
177
257
|
render: (row) => h("button.lt3-iconbtn.lt3-iconbtn--sm", {
|
|
178
|
-
"aria-label": `
|
|
179
|
-
title: "
|
|
180
|
-
on: { click: () => toast(
|
|
258
|
+
"aria-label": `Document info for ${row.filename || "file"}`,
|
|
259
|
+
title: "Per-document management isn't available yet",
|
|
260
|
+
on: { click: () => toast("Per-document management (delete / re-index) isn't available yet — re-upload to refresh a file.", "info") },
|
|
181
261
|
}, icon("dots-vertical")),
|
|
182
262
|
},
|
|
183
263
|
];
|
|
184
264
|
|
|
185
|
-
tableHost.replaceChildren(c.table(columns,
|
|
265
|
+
tableHost.replaceChildren(c.table(columns, docs));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Connected local folders + folder-watch state. Data: api.localSources(). */
|
|
269
|
+
function hydrateFolders(ctx, { foldersSrc, foldersHost, slots }, res) {
|
|
270
|
+
const { h, icon, c, toast } = ctx;
|
|
271
|
+
const data = res.data || {};
|
|
272
|
+
const sources = Array.isArray(data.sources) ? data.sources : [];
|
|
273
|
+
const watch = data.watch || {};
|
|
274
|
+
const source = res.source || (res.ok ? "live" : "unavailable");
|
|
275
|
+
foldersSrc.replaceChildren(c.sourceBadge(source));
|
|
276
|
+
|
|
277
|
+
const kids = [];
|
|
278
|
+
|
|
279
|
+
// Honest note when filesystem watching can't run (watchdog dependency missing).
|
|
280
|
+
if (watch.available === false) {
|
|
281
|
+
kids.push(c.banner(
|
|
282
|
+
watch.error
|
|
283
|
+
? `Folder watch is off: ${watch.error}`
|
|
284
|
+
: "Folder watch needs the watchdog dependency — connected folders index once but won't re-index automatically until it's installed.",
|
|
285
|
+
"warn",
|
|
286
|
+
"alert-triangle",
|
|
287
|
+
));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!sources.length) {
|
|
291
|
+
if (!res.ok) {
|
|
292
|
+
kids.push(c.errorState("Local sources are unavailable — the on-device runtime isn't reachable."));
|
|
293
|
+
} else {
|
|
294
|
+
kids.push(c.emptyState({
|
|
295
|
+
icon: "folder-plus",
|
|
296
|
+
title: "No folders connected",
|
|
297
|
+
body: "Connect a local folder — Lattice indexes it on-device and watches it for changes.",
|
|
298
|
+
action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm",
|
|
299
|
+
{ on: { click: () => (slots.connectFolder ? slots.connectFolder() : null) } },
|
|
300
|
+
icon("folder-plus"), "Connect folder"),
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
foldersHost.replaceChildren(...kids);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Connected-folders table ───────────────────────────────────────────────
|
|
308
|
+
async function stopWatching(id) {
|
|
309
|
+
toast("Stopping folder watch…", "info");
|
|
310
|
+
const stop = await ctx.api.localWatchStop(id);
|
|
311
|
+
if (stop.ok && stop.data && !stop.data.detail && !stop.data.error) {
|
|
312
|
+
toast("Stopped watching that folder.", "ok");
|
|
313
|
+
} else {
|
|
314
|
+
const detail = (stop.data && (stop.data.detail || stop.data.error)) || "the runtime is unavailable";
|
|
315
|
+
toast(`Could not stop watching — ${detail}.`, "warn");
|
|
316
|
+
}
|
|
317
|
+
hydrate(ctx, slots);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const columns = [
|
|
321
|
+
{
|
|
322
|
+
key: "label", label: "Folder",
|
|
323
|
+
render: (row) => h("div",
|
|
324
|
+
h("div.lt3-row-2",
|
|
325
|
+
h("span.lt3-filerow__icon", icon("folder")),
|
|
326
|
+
h("span", { style: { "font-weight": "var(--lt3-weight-medium)" } }, row.label || "Local folder"),
|
|
327
|
+
),
|
|
328
|
+
h("div.lt3-mono.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)", "margin-top": "var(--lt3-space-1)" } },
|
|
329
|
+
row.root_path || row.id || "—"),
|
|
330
|
+
),
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
key: "success_count", label: "Indexed", width: "92px",
|
|
334
|
+
render: (row) => h("span.lt3-mono", c.fmtNum(Number(row.success_count) || 0)),
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
key: "watch_active", label: "Watch", width: "120px",
|
|
338
|
+
render: (row) => c.statePill(row.watch_active ? "watching" : "idle"),
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
key: "watch_status", label: "Last activity", width: "150px",
|
|
342
|
+
render: (row) => {
|
|
343
|
+
const ws = row.watch_status || {};
|
|
344
|
+
if (ws.last_error) {
|
|
345
|
+
return h("span", { style: { color: "var(--danger)", "font-size": "var(--lt3-text-xs)" } }, ws.last_error);
|
|
346
|
+
}
|
|
347
|
+
const at = ws.last_event_at || ws.last_indexed_at;
|
|
348
|
+
const label = ws.last_event_at ? "event" : (ws.last_indexed_at ? "indexed" : "");
|
|
349
|
+
return h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } },
|
|
350
|
+
at ? `${timeAgo(at)}${label ? ` · ${label}` : ""}` : "—");
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
key: "_stop", label: "", width: "120px",
|
|
355
|
+
render: (row) => row.watch_active
|
|
356
|
+
? h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", {
|
|
357
|
+
on: { click: () => stopWatching(row.id) },
|
|
358
|
+
}, icon("player-stop"), "Stop watching")
|
|
359
|
+
: h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Not watching"),
|
|
360
|
+
},
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
kids.push(c.table(columns, sources));
|
|
364
|
+
foldersHost.replaceChildren(...kids);
|
|
186
365
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* View: Home — the workspace command center.
|
|
3
|
+
* Leads with the product identity (the retrieval lattice: Knowledge Graph +
|
|
4
|
+
* Vector Index + Hybrid Search) and routes into every primary area.
|
|
5
|
+
*
|
|
6
|
+
* View contract (shared by all views):
|
|
7
|
+
* export async function render(ctx) -> single DOM node
|
|
8
|
+
* ctx = { h, icon, api, store, c, route, params, navigate, toast }
|
|
9
|
+
* ========================================================================== */
|
|
10
|
+
|
|
11
|
+
export async function render(ctx) {
|
|
12
|
+
const { h, icon, api, store, c, navigate } = ctx;
|
|
13
|
+
const ws = store.activeWorkspace();
|
|
14
|
+
const readinessHost = h("div.lt3-readiness", c.loading({ lines: 4 }));
|
|
15
|
+
const activityHost = h("div", c.loading({ lines: 3 }));
|
|
16
|
+
|
|
17
|
+
const root = h("div.lt3-stack-6",
|
|
18
|
+
c.viewHeader({
|
|
19
|
+
eyebrow: "Local-first AI workspace",
|
|
20
|
+
title: `Welcome to ${ws.name}`,
|
|
21
|
+
sub: "Everything you index stays on this machine. Ask questions, explore the graph, and fuse structure with semantics — no data leaves your computer.",
|
|
22
|
+
actions: [
|
|
23
|
+
h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => navigate("hybrid-search") } }, icon("arrows-join"), "Hybrid search"),
|
|
24
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: () => navigate("chat", { new: "1" }) } }, icon("message-plus"), "New chat"),
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
buildHero(ctx, readinessHost),
|
|
28
|
+
h("section",
|
|
29
|
+
c.sectionHead("Retrieval lattice", h("span", { id: "home-idx-src" }, c.sourceBadge("pending"))),
|
|
30
|
+
h("div", { id: "home-pillars" }, c.loading({ lines: 2, block: true })),
|
|
31
|
+
),
|
|
32
|
+
h("section",
|
|
33
|
+
c.sectionHead("Jump back in"),
|
|
34
|
+
buildQuickGrid(ctx),
|
|
35
|
+
),
|
|
36
|
+
h("div.lt3-grid-2",
|
|
37
|
+
c.panel({ eyebrow: "Index", title: "Connected sources", children: h("div", { id: "home-sources" }, c.loading({ lines: 3 })) }),
|
|
38
|
+
c.panel({ eyebrow: "Workspace", title: "At a glance", children: h("div", { id: "home-stats" }, c.loading({ lines: 3 })) }),
|
|
39
|
+
),
|
|
40
|
+
c.panel({
|
|
41
|
+
eyebrow: "Activity",
|
|
42
|
+
title: "Recent activity",
|
|
43
|
+
sub: "Shown only when the local backend provides trace history.",
|
|
44
|
+
children: activityHost,
|
|
45
|
+
className: "lt3-panel--activity",
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
hydrate(ctx, root, { readinessHost, activityHost });
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildHero({ h, icon, navigate }, readinessHost) {
|
|
54
|
+
return h("div.lt3-hero",
|
|
55
|
+
h("div",
|
|
56
|
+
h("div.lt3-eyebrow.lt3-hero__eyebrow", icon("sparkles"), "Knowledge Graph · Vector Index · Hybrid Search"),
|
|
57
|
+
h("h2.lt3-hero__title", "Local workspace status, without pretending."),
|
|
58
|
+
h("p.lt3-hero__sub", "Lattice shows what is ready, what is unavailable, and what needs a local runtime before Chat, Search, Knowledge, and Memory can work together."),
|
|
59
|
+
h("div.lt3-hero__actions",
|
|
60
|
+
h("button.lt3-btn.lt3-btn--primary.lt3-btn--lg", { on: { click: () => navigate("chat", { new: "1" }) } }, icon("message-plus"), "Start chat"),
|
|
61
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--lg", { on: { click: () => navigate("files") } }, icon("upload"), "Upload files"),
|
|
62
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--lg", { on: { click: () => navigate("models") } }, icon("cpu"), "Check models"),
|
|
63
|
+
),
|
|
64
|
+
h("div.lt3-mini-lattice", { style: { "margin-top": "var(--lt3-space-6)" } },
|
|
65
|
+
h("div.lt3-mini-lattice__node", h("b", "Knowledge"), h("span", "Entities and relations")),
|
|
66
|
+
h("div.lt3-mini-lattice__node", h("b", "Vectors"), h("span", "Local semantic recall")),
|
|
67
|
+
h("div.lt3-mini-lattice__node", h("b", "Hybrid"), h("span", "Fused answer grounding")),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
h("aside.lt3-hero__aside",
|
|
71
|
+
h("div.lt3-eyebrow", "Readiness"),
|
|
72
|
+
readinessHost,
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const QUICK = [
|
|
78
|
+
{ key: "chat", icon: "message-2", title: "Chat", desc: "Grounded conversation." },
|
|
79
|
+
{ key: "files", icon: "folders", title: "Files", desc: "Sources and indexing." },
|
|
80
|
+
{ key: "hybrid-search", icon: "arrows-join", title: "Search", desc: "Fuse graph + vector recall." },
|
|
81
|
+
{ key: "knowledge-graph", icon: "chart-dots-3", title: "Knowledge", desc: "Browse entities and relations." },
|
|
82
|
+
{ key: "memory", icon: "brain", title: "Memory", desc: "Inspect long-term recall." },
|
|
83
|
+
{ key: "models", icon: "cpu", title: "Models", desc: "Local MLX runtime." },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function buildQuickGrid({ h, icon, navigate }) {
|
|
87
|
+
return h("div.lt3-quickgrid",
|
|
88
|
+
QUICK.map((q) => h("button.lt3-quick", { style: { "text-align": "left" }, on: { click: () => navigate(q.key) } },
|
|
89
|
+
h("div.lt3-quick__icon", icon(q.icon)),
|
|
90
|
+
h("div.lt3-quick__title", q.title),
|
|
91
|
+
h("div.lt3-quick__desc", q.desc),
|
|
92
|
+
)),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function hydrate(ctx, root, hosts) {
|
|
97
|
+
const { h, icon, api, store, c } = ctx;
|
|
98
|
+
const { readinessHost, activityHost } = hosts;
|
|
99
|
+
const numFmt = c.fmtNum;
|
|
100
|
+
|
|
101
|
+
// Index status → pillars + sources + topbar chip.
|
|
102
|
+
const idx = store.get().indexStatus
|
|
103
|
+
? { data: store.get().indexStatus, source: "live" }
|
|
104
|
+
: await api.indexStatus().then((r) => { store.setIndexStatus(r.data); return r; });
|
|
105
|
+
|
|
106
|
+
root.querySelector("#home-idx-src")?.replaceChildren(c.sourceBadge(idx.source));
|
|
107
|
+
root.querySelector("#home-pillars")?.replaceChildren(c.pillars(idx.data));
|
|
108
|
+
|
|
109
|
+
const sources = (idx.data && idx.data.sources) || [];
|
|
110
|
+
const srcHost = root.querySelector("#home-sources");
|
|
111
|
+
if (srcHost) {
|
|
112
|
+
srcHost.replaceChildren(
|
|
113
|
+
sources.length
|
|
114
|
+
? h("div.lt3-stack-3", sources.map((s) => h("div.lt3-stack-2",
|
|
115
|
+
h("div.lt3-row", { style: { "justify-content": "space-between" } },
|
|
116
|
+
h("div.lt3-row-2", icon("database"), h("b", { style: { "font-size": "var(--lt3-text-sm)" } }, s.label)),
|
|
117
|
+
c.statePill(s.state),
|
|
118
|
+
),
|
|
119
|
+
c.meter(s.progress ?? (s.state === "indexed" ? 1 : 0.5), s.state === "indexing" ? "warn" : "vector"),
|
|
120
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, `${numFmt(s.files)} files`),
|
|
121
|
+
)))
|
|
122
|
+
: c.emptyState({ icon: "database-off", title: "No sources connected", body: "Upload documents to start indexing. Folder watching requires the desktop local agent." }),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const [os, models, memory, traces] = await Promise.all([
|
|
127
|
+
api.workspaceOs(),
|
|
128
|
+
api.models(),
|
|
129
|
+
api.memoryManager(),
|
|
130
|
+
api.get("/workspace/traces", { traces: [] }),
|
|
131
|
+
]);
|
|
132
|
+
renderReadiness({ h, icon, c, readinessHost, idx, models, os, memory });
|
|
133
|
+
|
|
134
|
+
// Workspace counts.
|
|
135
|
+
const counts = (os.data && os.data.counts) || {};
|
|
136
|
+
const statHost = root.querySelector("#home-stats");
|
|
137
|
+
if (statHost) {
|
|
138
|
+
statHost.replaceChildren(
|
|
139
|
+
h("div.lt3-statrow",
|
|
140
|
+
c.stat({ label: "Memories", value: numFmt(counts.memories), icon: "brain" }),
|
|
141
|
+
c.stat({ label: "Traces", value: numFmt(counts.traces), icon: "route" }),
|
|
142
|
+
c.stat({ label: "Workflows", value: numFmt(counts.workflows), icon: "git-branch" }),
|
|
143
|
+
c.stat({ label: "Skills", value: numFmt(counts.skills), icon: "puzzle" }),
|
|
144
|
+
),
|
|
145
|
+
h("div", { style: { "margin-top": "var(--lt3-space-3)" } }, c.sourceBadge(os.source)),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
renderActivity({ h, icon, c, activityHost, traces });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderReadiness({ h, icon, c, readinessHost, idx, models, os, memory }) {
|
|
153
|
+
const pipes = (idx.data && idx.data.pipelines) || {};
|
|
154
|
+
const vectorReady = String(pipes.vector_index?.state || "").toLowerCase() === "ready";
|
|
155
|
+
const graphReady = String(pipes.knowledge_graph?.state || "").toLowerCase() === "ready";
|
|
156
|
+
const modelName = models.data && models.data.current;
|
|
157
|
+
const counts = (os.data && os.data.counts) || {};
|
|
158
|
+
const memSources = (memory.data && memory.data.sources) || [];
|
|
159
|
+
readinessHost.replaceChildren(
|
|
160
|
+
readinessRow({ h, icon, c, ic: "server", title: "Backend", meta: idx.source === "live" ? "Live local API" : "Local API unavailable", state: idx.source === "live" ? "ready" : "pending" }),
|
|
161
|
+
readinessRow({ h, icon, c, ic: "cpu", title: "Model", meta: modelName ? shortModel(modelName) : "No model loaded", state: modelName ? "ready" : "pending" }),
|
|
162
|
+
readinessRow({ h, icon, c, ic: "database", title: "Retrieval", meta: graphReady && vectorReady ? "Graph and vector ready" : "Index needs data or rebuild", state: graphReady && vectorReady ? "ready" : "pending" }),
|
|
163
|
+
readinessRow({ h, icon, c, ic: "brain", title: "Memory", meta: memSources.length ? `${c.fmtNum(counts.memories)} memories across ${memSources.length} tiers` : "Memory backend unavailable or empty", state: memSources.length ? "ready" : "idle" }),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readinessRow({ h, icon, c, ic, title, meta, state }) {
|
|
168
|
+
return h("div.lt3-readiness__row",
|
|
169
|
+
h("div.lt3-readiness__icon", icon(ic)),
|
|
170
|
+
h("div", h("div.lt3-readiness__title", title), h("div.lt3-readiness__meta", meta)),
|
|
171
|
+
c.statePill(state),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderActivity({ h, icon, c, activityHost, traces }) {
|
|
176
|
+
const rows = (traces.data && Array.isArray(traces.data.traces)) ? traces.data.traces : [];
|
|
177
|
+
if (!rows.length) {
|
|
178
|
+
activityHost.replaceChildren(c.emptyState({
|
|
179
|
+
icon: "history-off",
|
|
180
|
+
title: "No recent activity available",
|
|
181
|
+
body: traces.source === "live" ? "The backend returned no trace history yet." : "Start the backend to show recent local workspace activity.",
|
|
182
|
+
}));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
activityHost.replaceChildren(h("div.lt3-list", rows.slice(0, 6).map((tr) =>
|
|
186
|
+
h("div.lt3-list__item",
|
|
187
|
+
h("span.lt3-avatar", { style: { width: "28px", height: "28px" } }, icon("route")),
|
|
188
|
+
h("div.lt3-list__body",
|
|
189
|
+
h("div.lt3-list__title", tr.question || tr.event_type || "Workspace event"),
|
|
190
|
+
h("div.lt3-list__meta", [tr.confidence != null ? `${Math.round(Number(tr.confidence) * 100)}% confidence` : null, tr.created_at || tr.timestamp || null].filter(Boolean).join(" · ")),
|
|
191
|
+
),
|
|
192
|
+
c.sourceBadge(traces.source),
|
|
193
|
+
))));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function shortModel(id) {
|
|
197
|
+
const s = String(id || "");
|
|
198
|
+
const tail = s.includes("/") ? s.split("/").pop() : s;
|
|
199
|
+
return tail.length > 28 ? tail.slice(0, 27) + "…" : tail;
|
|
200
|
+
}
|