ltcai 3.1.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 +35 -8
- package/docs/CHANGELOG.md +53 -0
- package/docs/V3_2_AUDIT.md +82 -0
- package/docs/V3_FRONTEND.md +1 -1
- package/docs/architecture.md +6 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_registry.py +103 -0
- package/latticeai/api/hooks.py +113 -0
- package/latticeai/api/marketplace.py +13 -0
- package/latticeai/api/memory.py +109 -0
- package/latticeai/core/agent_registry.py +234 -0
- package/latticeai/core/hooks.py +284 -0
- package/latticeai/core/marketplace.py +87 -2
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +41 -0
- package/latticeai/services/memory_service.py +324 -0
- package/package.json +2 -2
- package/scripts/build_v3_assets.mjs +7 -1
- package/static/css/{tokens.5a595671.css → tokens.8b8e31bd.css} +1 -1
- package/static/css/tokens.css +1 -1
- package/static/v3/asset-manifest.json +22 -14
- package/static/v3/css/{lattice.views.3ee19d4e.css → lattice.views.1d326beb.css} +5 -0
- package/static/v3/css/lattice.views.css +5 -0
- package/static/v3/js/{app.46fb61d9.js → app.cf5bb712.js} +1 -1
- package/static/v3/js/core/{api.22a41d42.js → api.113660c5.js} +123 -4
- package/static/v3/js/core/api.js +123 -4
- package/static/v3/js/core/{routes.f935dd50.js → routes.07ad6696.js} +11 -0
- package/static/v3/js/core/routes.js +11 -0
- package/static/v3/js/core/{shell.1b6199d6.js → shell.9e707234.js} +2 -2
- package/static/v3/js/views/{agents.14e48bdd.js → agents.c373d48c.js} +100 -0
- package/static/v3/js/views/agents.js +100 -0
- package/static/v3/js/views/{chat.718144ce.js → chat.c48fd9e2.js} +1 -1
- package/static/v3/js/views/chat.js +1 -1
- package/static/v3/js/views/{files.4935197e.js → files.8464634a.js} +64 -18
- package/static/v3/js/views/files.js +64 -18
- package/static/v3/js/views/hooks.f3edebca.js +99 -0
- package/static/v3/js/views/hooks.js +99 -0
- package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
- package/static/v3/js/views/marketplace.js +141 -0
- package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
- package/static/v3/js/views/mcp.js +114 -0
- package/static/v3/js/views/memory.4ebdf474.js +147 -0
- package/static/v3/js/views/memory.js +147 -0
- package/static/v3/js/views/planning.9ac3e313.js +153 -0
- package/static/v3/js/views/planning.js +153 -0
- package/static/v3/js/views/{settings.4f777210.js → settings.c7b0cc05.js} +16 -2
- package/static/v3/js/views/settings.js +16 -2
- package/static/v3/js/views/skills.c6c2f965.js +109 -0
- package/static/v3/js/views/skills.js +109 -0
- package/static/v3/js/views/tools.e4f11276.js +108 -0
- package/static/v3/js/views/tools.js +108 -0
- package/static/v3/js/views/workflows.26c57290.js +128 -0
- package/static/v3/js/views/workflows.js +128 -0
|
@@ -9,8 +9,10 @@ export async function render(ctx) {
|
|
|
9
9
|
const { h, icon, c } = ctx;
|
|
10
10
|
|
|
11
11
|
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
12
|
+
const registryHost = h("div", c.loading({ lines: 3, block: true }));
|
|
12
13
|
const rosterHost = h("div", c.loading({ lines: 2, block: true }));
|
|
13
14
|
const runsHost = h("div", c.loading({ lines: 4 }));
|
|
15
|
+
const registrySrc = h("span", c.sourceBadge("pending"));
|
|
14
16
|
const rosterSrc = h("span", c.sourceBadge("pending"));
|
|
15
17
|
const runsSrc = h("span", c.sourceBadge("pending"));
|
|
16
18
|
const healthSlot = h("span", c.sourceBadge("pending"));
|
|
@@ -23,6 +25,10 @@ export async function render(ctx) {
|
|
|
23
25
|
actions: [healthSlot],
|
|
24
26
|
}),
|
|
25
27
|
statHost,
|
|
28
|
+
h("section",
|
|
29
|
+
c.sectionHead("Agent Registry", registrySrc),
|
|
30
|
+
registryHost,
|
|
31
|
+
),
|
|
26
32
|
h("section",
|
|
27
33
|
c.sectionHead("Agent roster", rosterSrc),
|
|
28
34
|
rosterHost,
|
|
@@ -40,6 +46,7 @@ export async function render(ctx) {
|
|
|
40
46
|
);
|
|
41
47
|
|
|
42
48
|
hydrate(ctx, { statHost, rosterHost, runsHost, rosterSrc, runsSrc, healthSlot });
|
|
49
|
+
loadRegistry(ctx, { registryHost, registrySrc });
|
|
43
50
|
return root;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -109,6 +116,83 @@ async function hydrate(ctx, hosts) {
|
|
|
109
116
|
);
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
async function loadRegistry(ctx, hosts) {
|
|
120
|
+
const { h, c } = ctx;
|
|
121
|
+
const { registryHost, registrySrc } = hosts;
|
|
122
|
+
const [registryRes, capsRes] = await Promise.all([ctx.api.agentRegistry(), ctx.api.agentCapabilities()]);
|
|
123
|
+
const agents = normalizeRegistry(registryRes.data);
|
|
124
|
+
const caps = (capsRes.data && capsRes.data.capabilities) || {};
|
|
125
|
+
registrySrc.replaceChildren(c.sourceBadge(registryRes.source === "live" || capsRes.source === "live" ? "live" : "unavailable"));
|
|
126
|
+
|
|
127
|
+
const nameInput = h("input.lt3-input", { type: "text", placeholder: "Custom agent name", "aria-label": "Custom agent name" });
|
|
128
|
+
const capsInput = h("input.lt3-input", { type: "text", placeholder: "capability-a, capability-b", "aria-label": "Custom agent capabilities" });
|
|
129
|
+
const registerBtn = h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: register } }, c.icon("plus"), "Register");
|
|
130
|
+
|
|
131
|
+
const capList = Object.keys(caps).sort();
|
|
132
|
+
const body = h("div.lt3-stack-4",
|
|
133
|
+
h("div.lt3-grid-2",
|
|
134
|
+
h("div.lt3-field", h("label", "Name"), nameInput),
|
|
135
|
+
h("div.lt3-field", h("label", "Capabilities"), capsInput),
|
|
136
|
+
),
|
|
137
|
+
h("div.lt3-row-2", registerBtn,
|
|
138
|
+
h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-xs)" } }, "Custom agents persist in the local registry.")),
|
|
139
|
+
capList.length
|
|
140
|
+
? h("div.lt3-cluster", capList.slice(0, 18).map((cap) => h("span.lt3-chip", c.icon("sparkles"), `${cap} (${caps[cap].length})`)))
|
|
141
|
+
: h("p.lt3-faint", { style: { margin: 0 } }, "Capabilities appear here when the registry is live."),
|
|
142
|
+
agents.length
|
|
143
|
+
? h("div.lt3-grid-auto", agents.map((agent) => registryCard(ctx, agent)))
|
|
144
|
+
: c.emptyState({ icon: "robot-off", title: "Agent registry unavailable", body: "Start the local server to register and configure agents." }),
|
|
145
|
+
);
|
|
146
|
+
registryHost.replaceChildren(c.panel({ title: "Registry controls", sub: "Register, discover, and configure built-in or custom agents.", children: body }));
|
|
147
|
+
|
|
148
|
+
async function register() {
|
|
149
|
+
const name = nameInput.value.trim();
|
|
150
|
+
if (!name) { ctx.toast("Enter an agent name", "info"); return; }
|
|
151
|
+
const capabilities = capsInput.value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
152
|
+
registerBtn.disabled = true;
|
|
153
|
+
const res = await ctx.api.registerAgent({ name, type: "custom", capabilities });
|
|
154
|
+
registerBtn.disabled = false;
|
|
155
|
+
if (res && res.ok) {
|
|
156
|
+
ctx.toast(`Registered ${name}`, "ok");
|
|
157
|
+
loadRegistry(ctx, hosts);
|
|
158
|
+
} else {
|
|
159
|
+
ctx.toast("Register unavailable", "err");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function registryCard(ctx, agent) {
|
|
165
|
+
const { h, c } = ctx;
|
|
166
|
+
return c.card(h("div.lt3-stack-3",
|
|
167
|
+
h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start" } },
|
|
168
|
+
h("div",
|
|
169
|
+
h("b", agent.name),
|
|
170
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, agent.id),
|
|
171
|
+
),
|
|
172
|
+
c.pill(agent.source === "builtin" ? "built-in" : "custom", agent.source === "builtin" ? "info" : "warn"),
|
|
173
|
+
),
|
|
174
|
+
h("p.lt3-muted", { style: { "font-size": "var(--lt3-text-sm)", margin: 0 } }, agent.description || "No description."),
|
|
175
|
+
h("div.lt3-cluster", [c.statePill(agent.enabled ? "ready" : "idle"), c.pill(agent.type), c.pill(`v${agent.version || "1.0.0"}`)]),
|
|
176
|
+
agent.capabilities.length ? h("div.lt3-cluster", agent.capabilities.slice(0, 8).map((cap) => h("span.lt3-chip", cap))) : null,
|
|
177
|
+
h("div.lt3-row-2",
|
|
178
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggleAgent(ctx, agent) } }, c.icon(agent.enabled ? "toggle-right" : "toggle-left"), agent.enabled ? "Disable" : "Enable"),
|
|
179
|
+
agent.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => removeAgent(ctx, agent) } }, c.icon("trash"), "Remove") : null,
|
|
180
|
+
),
|
|
181
|
+
), { interactive: false });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function toggleAgent(ctx, agent) {
|
|
185
|
+
const res = await ctx.api.updateAgent(agent.id, { config: agent.config || {}, enabled: !agent.enabled });
|
|
186
|
+
ctx.toast(res && res.ok ? `${agent.name}: ${agent.enabled ? "disabled" : "enabled"}` : "Agent update unavailable", res && res.ok ? "ok" : "err");
|
|
187
|
+
if (res && res.ok) ctx.navigate("agents");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function removeAgent(ctx, agent) {
|
|
191
|
+
const res = await ctx.api.removeAgent(agent.id);
|
|
192
|
+
ctx.toast(res && res.ok ? `Removed ${agent.name}` : "Agent remove unavailable", res && res.ok ? "ok" : "err");
|
|
193
|
+
if (res && res.ok) ctx.navigate("agents");
|
|
194
|
+
}
|
|
195
|
+
|
|
112
196
|
/* ── Agent card ──────────────────────────────────────────────────────────── */
|
|
113
197
|
function agentCard(ctx, agent, byId) {
|
|
114
198
|
const { h, icon, c } = ctx;
|
|
@@ -158,6 +242,22 @@ function normalize(data) {
|
|
|
158
242
|
}));
|
|
159
243
|
}
|
|
160
244
|
|
|
245
|
+
function normalizeRegistry(data) {
|
|
246
|
+
const list = Array.isArray(data) ? data : (data && Array.isArray(data.agents) ? data.agents : []);
|
|
247
|
+
return list.map((agent, i) => ({
|
|
248
|
+
id: agent.id || `agent:${i}`,
|
|
249
|
+
name: agent.name || agent.id || `Agent ${i + 1}`,
|
|
250
|
+
type: agent.type || "custom",
|
|
251
|
+
version: agent.version || "1.0.0",
|
|
252
|
+
description: agent.description || "",
|
|
253
|
+
capabilities: Array.isArray(agent.capabilities) ? agent.capabilities : [],
|
|
254
|
+
source: agent.source || "user",
|
|
255
|
+
enabled: agent.enabled !== false,
|
|
256
|
+
removable: !!agent.removable,
|
|
257
|
+
config: agent.config || {},
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
161
261
|
const AVAILABLE_STATES = new Set(["available", "ready", "active", "ok", "idle"]);
|
|
162
262
|
function isAvailable(state) {
|
|
163
263
|
return AVAILABLE_STATES.has(String(state).toLowerCase());
|
|
@@ -52,7 +52,7 @@ export async function render(ctx) {
|
|
|
52
52
|
|
|
53
53
|
const groundChip = (key, label, icn) => h("button.lt3-chip", {
|
|
54
54
|
type: "button", dataset: { active: String(state.grounding[key]) }, "aria-pressed": String(state.grounding[key]),
|
|
55
|
-
title: `
|
|
55
|
+
title: `Show the ${label} signal in the retrieval-context panel`,
|
|
56
56
|
on: { click: (e) => { state.grounding[key] = !state.grounding[key]; const b = e.currentTarget; b.dataset.active = String(state.grounding[key]); b.setAttribute("aria-pressed", String(state.grounding[key])); } },
|
|
57
57
|
}, icon(icn), label);
|
|
58
58
|
|
|
@@ -52,7 +52,7 @@ export async function render(ctx) {
|
|
|
52
52
|
|
|
53
53
|
const groundChip = (key, label, icn) => h("button.lt3-chip", {
|
|
54
54
|
type: "button", dataset: { active: String(state.grounding[key]) }, "aria-pressed": String(state.grounding[key]),
|
|
55
|
-
title: `
|
|
55
|
+
title: `Show the ${label} signal in the retrieval-context panel`,
|
|
56
56
|
on: { click: (e) => { state.grounding[key] = !state.grounding[key]; const b = e.currentTarget; b.dataset.active = String(state.grounding[key]); b.setAttribute("aria-pressed", String(state.grounding[key])); } },
|
|
57
57
|
}, icon(icn), label);
|
|
58
58
|
|
|
@@ -61,11 +61,15 @@ function normalize(data) {
|
|
|
61
61
|
}));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Document types the backend accepts (latticeai/services/upload_service.py). */
|
|
65
|
+
const UPLOAD_ACCEPT = ".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv";
|
|
66
|
+
|
|
64
67
|
export async function render(ctx) {
|
|
65
68
|
const { h, icon, api, c, navigate, toast } = ctx;
|
|
66
69
|
|
|
67
|
-
//
|
|
68
|
-
// not enabled in this build. Say so plainly
|
|
70
|
+
// Connecting/watching a *folder* needs the desktop local-agent connector,
|
|
71
|
+
// which is not enabled in this build. Say so plainly. Manual document upload
|
|
72
|
+
// below works without it.
|
|
69
73
|
const unavailableToast = () =>
|
|
70
74
|
toast("Connecting a folder requires the Lattice desktop local agent — not available in this build.", "warn");
|
|
71
75
|
|
|
@@ -73,6 +77,56 @@ export async function render(ctx) {
|
|
|
73
77
|
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
74
78
|
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
75
79
|
|
|
80
|
+
// ── Manual upload (works in this build; no desktop agent required) ─────────
|
|
81
|
+
let busy = false;
|
|
82
|
+
const fileInput = h("input", {
|
|
83
|
+
type: "file", multiple: true, accept: UPLOAD_ACCEPT,
|
|
84
|
+
style: { display: "none" }, "aria-hidden": "true",
|
|
85
|
+
on: { change: (e) => uploadFiles(e.target.files) },
|
|
86
|
+
});
|
|
87
|
+
const pickFiles = () => { if (!busy) fileInput.click(); };
|
|
88
|
+
const slots = { statHost, srcSlot, tableHost, pickFiles };
|
|
89
|
+
|
|
90
|
+
async function uploadFiles(fileList) {
|
|
91
|
+
const files = Array.from(fileList || []);
|
|
92
|
+
if (!files.length || busy) return;
|
|
93
|
+
busy = true;
|
|
94
|
+
let ok = 0;
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
toast(`Uploading “${file.name}”…`, "info");
|
|
97
|
+
const res = await api.uploadDocument(file);
|
|
98
|
+
if (res.ok && res.data && !res.data.detail && !res.data.error) {
|
|
99
|
+
ok++;
|
|
100
|
+
} else {
|
|
101
|
+
const detail = (res.data && (res.data.detail || res.data.error)) || "the backend is unavailable";
|
|
102
|
+
toast(`Could not ingest “${file.name}” — ${detail}.`, "warn");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
fileInput.value = "";
|
|
106
|
+
busy = false;
|
|
107
|
+
if (ok) {
|
|
108
|
+
toast(`Indexed ${ok} document${ok === 1 ? "" : "s"} into the knowledge graph — now searchable in Chat and Hybrid Search.`, "ok");
|
|
109
|
+
}
|
|
110
|
+
hydrate(ctx, slots);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const dropZone = h("div.lt3-drop", {
|
|
114
|
+
on: {
|
|
115
|
+
dragover: (e) => { e.preventDefault(); dropZone.classList.add("is-dragover"); },
|
|
116
|
+
dragleave: () => dropZone.classList.remove("is-dragover"),
|
|
117
|
+
drop: (e) => { e.preventDefault(); dropZone.classList.remove("is-dragover"); uploadFiles(e.dataTransfer && e.dataTransfer.files); },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
fileInput,
|
|
121
|
+
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
122
|
+
h("div",
|
|
123
|
+
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag documents here, or upload manually"),
|
|
124
|
+
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
125
|
+
"Lattice parses each file, chunks it, embeds it, and links it into the knowledge graph. PDF · DOCX · XLSX · PPTX · TXT · MD · CSV, up to 10 MB each."),
|
|
126
|
+
),
|
|
127
|
+
h("button.lt3-btn.lt3-btn--primary", { type: "button", on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
128
|
+
);
|
|
129
|
+
|
|
76
130
|
const root = h("div.lt3-stack-6",
|
|
77
131
|
c.viewHeader({
|
|
78
132
|
eyebrow: "Data",
|
|
@@ -80,19 +134,12 @@ export async function render(ctx) {
|
|
|
80
134
|
sub: "Connected sources and the documents Lattice has indexed for retrieval. Everything stays on this machine.",
|
|
81
135
|
actions: [
|
|
82
136
|
h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => navigate("knowledge-graph") } }, icon("chart-dots-3"), "View graph"),
|
|
137
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
83
138
|
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Connect folder"),
|
|
84
139
|
],
|
|
85
140
|
}),
|
|
86
141
|
statHost,
|
|
87
|
-
|
|
88
|
-
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
89
|
-
h("div",
|
|
90
|
-
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag files or connect a folder"),
|
|
91
|
-
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
92
|
-
"Lattice watches the source, chunks it, embeds it, and links it into the knowledge graph."),
|
|
93
|
-
),
|
|
94
|
-
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Choose folder"),
|
|
95
|
-
),
|
|
142
|
+
dropZone,
|
|
96
143
|
c.panel({
|
|
97
144
|
head: h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", width: "100%" } },
|
|
98
145
|
h("div",
|
|
@@ -105,13 +152,13 @@ export async function render(ctx) {
|
|
|
105
152
|
}),
|
|
106
153
|
);
|
|
107
154
|
|
|
108
|
-
hydrate(ctx,
|
|
155
|
+
hydrate(ctx, slots);
|
|
109
156
|
return root;
|
|
110
157
|
}
|
|
111
158
|
|
|
112
159
|
async function hydrate(ctx, slots) {
|
|
113
160
|
const { h, icon, api, c, toast } = ctx;
|
|
114
|
-
const { statHost, srcSlot, tableHost } = slots;
|
|
161
|
+
const { statHost, srcSlot, tableHost, pickFiles } = slots;
|
|
115
162
|
|
|
116
163
|
const probe = await api.get("/workspace/indexing", { sources: [], totals: {} });
|
|
117
164
|
const liveFiles = probe.ok && probe.data ? normalize(probe.data) : null;
|
|
@@ -137,11 +184,10 @@ async function hydrate(ctx, slots) {
|
|
|
137
184
|
tableHost.replaceChildren(c.emptyState({
|
|
138
185
|
icon: "folder-off",
|
|
139
186
|
title: "No documents indexed yet",
|
|
140
|
-
body: "
|
|
141
|
-
action: h("button.lt3-btn.lt3-btn--
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
icon("folder-plus"), "Connect folder"),
|
|
187
|
+
body: "Upload a document and Lattice will parse, embed, and link it into the knowledge graph for hybrid retrieval.",
|
|
188
|
+
action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm",
|
|
189
|
+
{ on: { click: () => (pickFiles ? pickFiles() : null) } },
|
|
190
|
+
icon("upload"), "Upload files"),
|
|
145
191
|
}));
|
|
146
192
|
return;
|
|
147
193
|
}
|
|
@@ -61,11 +61,15 @@ function normalize(data) {
|
|
|
61
61
|
}));
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Document types the backend accepts (latticeai/services/upload_service.py). */
|
|
65
|
+
const UPLOAD_ACCEPT = ".pdf,.docx,.xlsx,.pptx,.txt,.md,.csv";
|
|
66
|
+
|
|
64
67
|
export async function render(ctx) {
|
|
65
68
|
const { h, icon, api, c, navigate, toast } = ctx;
|
|
66
69
|
|
|
67
|
-
//
|
|
68
|
-
// not enabled in this build. Say so plainly
|
|
70
|
+
// Connecting/watching a *folder* needs the desktop local-agent connector,
|
|
71
|
+
// which is not enabled in this build. Say so plainly. Manual document upload
|
|
72
|
+
// below works without it.
|
|
69
73
|
const unavailableToast = () =>
|
|
70
74
|
toast("Connecting a folder requires the Lattice desktop local agent — not available in this build.", "warn");
|
|
71
75
|
|
|
@@ -73,6 +77,56 @@ export async function render(ctx) {
|
|
|
73
77
|
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
74
78
|
const tableHost = h("div", c.loading({ lines: 4 }));
|
|
75
79
|
|
|
80
|
+
// ── Manual upload (works in this build; no desktop agent required) ─────────
|
|
81
|
+
let busy = false;
|
|
82
|
+
const fileInput = h("input", {
|
|
83
|
+
type: "file", multiple: true, accept: UPLOAD_ACCEPT,
|
|
84
|
+
style: { display: "none" }, "aria-hidden": "true",
|
|
85
|
+
on: { change: (e) => uploadFiles(e.target.files) },
|
|
86
|
+
});
|
|
87
|
+
const pickFiles = () => { if (!busy) fileInput.click(); };
|
|
88
|
+
const slots = { statHost, srcSlot, tableHost, pickFiles };
|
|
89
|
+
|
|
90
|
+
async function uploadFiles(fileList) {
|
|
91
|
+
const files = Array.from(fileList || []);
|
|
92
|
+
if (!files.length || busy) return;
|
|
93
|
+
busy = true;
|
|
94
|
+
let ok = 0;
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
toast(`Uploading “${file.name}”…`, "info");
|
|
97
|
+
const res = await api.uploadDocument(file);
|
|
98
|
+
if (res.ok && res.data && !res.data.detail && !res.data.error) {
|
|
99
|
+
ok++;
|
|
100
|
+
} else {
|
|
101
|
+
const detail = (res.data && (res.data.detail || res.data.error)) || "the backend is unavailable";
|
|
102
|
+
toast(`Could not ingest “${file.name}” — ${detail}.`, "warn");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
fileInput.value = "";
|
|
106
|
+
busy = false;
|
|
107
|
+
if (ok) {
|
|
108
|
+
toast(`Indexed ${ok} document${ok === 1 ? "" : "s"} into the knowledge graph — now searchable in Chat and Hybrid Search.`, "ok");
|
|
109
|
+
}
|
|
110
|
+
hydrate(ctx, slots);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const dropZone = h("div.lt3-drop", {
|
|
114
|
+
on: {
|
|
115
|
+
dragover: (e) => { e.preventDefault(); dropZone.classList.add("is-dragover"); },
|
|
116
|
+
dragleave: () => dropZone.classList.remove("is-dragover"),
|
|
117
|
+
drop: (e) => { e.preventDefault(); dropZone.classList.remove("is-dragover"); uploadFiles(e.dataTransfer && e.dataTransfer.files); },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
fileInput,
|
|
121
|
+
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
122
|
+
h("div",
|
|
123
|
+
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag documents here, or upload manually"),
|
|
124
|
+
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
125
|
+
"Lattice parses each file, chunks it, embeds it, and links it into the knowledge graph. PDF · DOCX · XLSX · PPTX · TXT · MD · CSV, up to 10 MB each."),
|
|
126
|
+
),
|
|
127
|
+
h("button.lt3-btn.lt3-btn--primary", { type: "button", on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
128
|
+
);
|
|
129
|
+
|
|
76
130
|
const root = h("div.lt3-stack-6",
|
|
77
131
|
c.viewHeader({
|
|
78
132
|
eyebrow: "Data",
|
|
@@ -80,19 +134,12 @@ export async function render(ctx) {
|
|
|
80
134
|
sub: "Connected sources and the documents Lattice has indexed for retrieval. Everything stays on this machine.",
|
|
81
135
|
actions: [
|
|
82
136
|
h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => navigate("knowledge-graph") } }, icon("chart-dots-3"), "View graph"),
|
|
137
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: pickFiles } }, icon("upload"), "Upload files"),
|
|
83
138
|
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Connect folder"),
|
|
84
139
|
],
|
|
85
140
|
}),
|
|
86
141
|
statHost,
|
|
87
|
-
|
|
88
|
-
h("div.lt3-pillar__icon", icon("cloud-upload")),
|
|
89
|
-
h("div",
|
|
90
|
-
h("div", { style: { "font-weight": "var(--lt3-weight-semi)" } }, "Drag files or connect a folder"),
|
|
91
|
-
h("p.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)", "margin-top": "var(--lt3-space-1)" } },
|
|
92
|
-
"Lattice watches the source, chunks it, embeds it, and links it into the knowledge graph."),
|
|
93
|
-
),
|
|
94
|
-
h("button.lt3-btn.lt3-btn--ghost", { title: "Requires the desktop local agent (not in this build)", on: { click: unavailableToast } }, icon("folder-plus"), "Choose folder"),
|
|
95
|
-
),
|
|
142
|
+
dropZone,
|
|
96
143
|
c.panel({
|
|
97
144
|
head: h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", width: "100%" } },
|
|
98
145
|
h("div",
|
|
@@ -105,13 +152,13 @@ export async function render(ctx) {
|
|
|
105
152
|
}),
|
|
106
153
|
);
|
|
107
154
|
|
|
108
|
-
hydrate(ctx,
|
|
155
|
+
hydrate(ctx, slots);
|
|
109
156
|
return root;
|
|
110
157
|
}
|
|
111
158
|
|
|
112
159
|
async function hydrate(ctx, slots) {
|
|
113
160
|
const { h, icon, api, c, toast } = ctx;
|
|
114
|
-
const { statHost, srcSlot, tableHost } = slots;
|
|
161
|
+
const { statHost, srcSlot, tableHost, pickFiles } = slots;
|
|
115
162
|
|
|
116
163
|
const probe = await api.get("/workspace/indexing", { sources: [], totals: {} });
|
|
117
164
|
const liveFiles = probe.ok && probe.data ? normalize(probe.data) : null;
|
|
@@ -137,11 +184,10 @@ async function hydrate(ctx, slots) {
|
|
|
137
184
|
tableHost.replaceChildren(c.emptyState({
|
|
138
185
|
icon: "folder-off",
|
|
139
186
|
title: "No documents indexed yet",
|
|
140
|
-
body: "
|
|
141
|
-
action: h("button.lt3-btn.lt3-btn--
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
icon("folder-plus"), "Connect folder"),
|
|
187
|
+
body: "Upload a document and Lattice will parse, embed, and link it into the knowledge graph for hybrid retrieval.",
|
|
188
|
+
action: h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm",
|
|
189
|
+
{ on: { click: () => (pickFiles ? pickFiles() : null) } },
|
|
190
|
+
icon("upload"), "Upload files"),
|
|
145
191
|
}));
|
|
146
192
|
return;
|
|
147
193
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* View: Hooks — the lifecycle hooks registry.
|
|
3
|
+
* Reads /api/hooks (built-in + user hooks across pre_run/post_run/pre_tool/
|
|
4
|
+
* post_tool/agent/pipeline/workflow), toggles enabled state, reorders, and
|
|
5
|
+
* registers custom hooks. Built-in hooks are platform-managed and labelled.
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
const KIND_LABEL = {
|
|
9
|
+
pre_run: "Pre-run", post_run: "Post-run", pre_tool: "Pre-tool", post_tool: "Post-tool",
|
|
10
|
+
agent: "Agent", pipeline: "Pipeline", workflow: "Workflow",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function render(ctx) {
|
|
14
|
+
const { h, c } = ctx;
|
|
15
|
+
const src = h("span", c.sourceBadge("pending"));
|
|
16
|
+
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
17
|
+
const groupsHost = h("div", c.loading({ lines: 4, block: true }));
|
|
18
|
+
|
|
19
|
+
const nameInput = h("input.lt3-input", { type: "text", placeholder: "Hook name" });
|
|
20
|
+
const kindSelect = h("select.lt3-select", Object.keys(KIND_LABEL).map((k) => h("option", { value: k }, KIND_LABEL[k])));
|
|
21
|
+
const descInput = h("input.lt3-input", { type: "text", placeholder: "What it does (optional)" });
|
|
22
|
+
|
|
23
|
+
const root = h("div.lt3-stack-6",
|
|
24
|
+
c.viewHeader({
|
|
25
|
+
eyebrow: "Platform",
|
|
26
|
+
title: "Hooks",
|
|
27
|
+
sub: "Lifecycle extension points across runs, tools, agents, pipelines, and workflows — visible, ordered, and individually toggleable.",
|
|
28
|
+
actions: [src],
|
|
29
|
+
}),
|
|
30
|
+
statHost,
|
|
31
|
+
c.panel({
|
|
32
|
+
title: "Register a hook", sub: "Custom hooks are listed, ordered, and inspectable.",
|
|
33
|
+
children: h("div.lt3-stack-3",
|
|
34
|
+
h("div.lt3-grid-2", h("div.lt3-field", h("label", "Name"), nameInput), h("div.lt3-field", h("label", "Kind"), kindSelect)),
|
|
35
|
+
h("div.lt3-field", h("label", "Description"), descInput),
|
|
36
|
+
h("div.lt3-row-2", h("button.lt3-btn.lt3-btn--primary", { on: { click: register } }, c.icon("plus"), "Register hook")),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
groupsHost,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
load();
|
|
43
|
+
return root;
|
|
44
|
+
|
|
45
|
+
async function load() {
|
|
46
|
+
const res = await ctx.api.hooks();
|
|
47
|
+
src.replaceChildren(c.sourceBadge(res.source));
|
|
48
|
+
const hooks = (res.data && res.data.hooks) || [];
|
|
49
|
+
if (!hooks.length) {
|
|
50
|
+
statHost.replaceChildren(c.stat({ label: "Hooks", value: "—", icon: "webhook" }));
|
|
51
|
+
groupsHost.replaceChildren(c.emptyState({ icon: "webhook-off", title: "Hooks unavailable", body: "Start the backend to read the hooks registry." }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const en = hooks.filter((x) => x.enabled).length;
|
|
55
|
+
statHost.replaceChildren(
|
|
56
|
+
c.stat({ label: "Hooks", value: c.fmtNum(hooks.length), icon: "webhook" }),
|
|
57
|
+
c.stat({ label: "Enabled", value: c.fmtNum(en), icon: "circle-check" }),
|
|
58
|
+
c.stat({ label: "Kinds", value: c.fmtNum((res.data.kinds || []).length), icon: "layers" }),
|
|
59
|
+
);
|
|
60
|
+
const byKind = {};
|
|
61
|
+
for (const hk of hooks) (byKind[hk.kind] = byKind[hk.kind] || []).push(hk);
|
|
62
|
+
groupsHost.replaceChildren(h("div.lt3-stack-6", Object.keys(byKind).map((kind) =>
|
|
63
|
+
h("section", c.sectionHead(KIND_LABEL[kind] || kind, c.pill(String(byKind[kind].length))),
|
|
64
|
+
h("div.lt3-stack-2", byKind[kind].map((hk) => hookRow(ctx, hk)))))));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hookRow(ctx2, hk) {
|
|
68
|
+
return c.card(h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center", gap: "var(--lt3-space-3)" } },
|
|
69
|
+
h("div", { style: { "min-width": 0 } },
|
|
70
|
+
h("div.lt3-row-2", h("b", hk.name), c.pill(hk.source === "builtin" ? "built-in" : "custom", hk.source === "builtin" ? "info" : ""), hk.managed === "platform" ? c.pill("managed", "") : null),
|
|
71
|
+
h("p.lt3-muted", { style: { margin: "2px 0 0", "font-size": "var(--lt3-text-sm)" } }, hk.description || ""),
|
|
72
|
+
hk.binding ? h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, hk.binding) : null,
|
|
73
|
+
),
|
|
74
|
+
h("div.lt3-row-2", { style: { "flex-shrink": 0 } },
|
|
75
|
+
h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, `#${hk.order}`),
|
|
76
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggle(ctx2, hk) } }, c.icon(hk.enabled ? "toggle-right" : "toggle-left"), hk.enabled ? "On" : "Off"),
|
|
77
|
+
hk.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => remove(ctx2, hk) } }, c.icon("trash")) : null,
|
|
78
|
+
),
|
|
79
|
+
), { flat: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function toggle(ctx2, hk) {
|
|
83
|
+
const res = hk.enabled ? await ctx2.api.hookDisable(hk.id) : await ctx2.api.hookEnable(hk.id, true);
|
|
84
|
+
ctx2.toast(res && res.ok ? `${hk.name}: ${hk.enabled ? "disabled" : "enabled"}` : "Action unavailable", res && res.ok ? "ok" : "err");
|
|
85
|
+
load();
|
|
86
|
+
}
|
|
87
|
+
async function remove(ctx2, hk) {
|
|
88
|
+
const res = await ctx2.api.hookRemove(hk.id);
|
|
89
|
+
ctx2.toast(res && res.ok ? `Removed ${hk.name}` : "Remove unavailable", res && res.ok ? "ok" : "err");
|
|
90
|
+
load();
|
|
91
|
+
}
|
|
92
|
+
async function register() {
|
|
93
|
+
const name = nameInput.value.trim();
|
|
94
|
+
if (!name) { ctx.toast("Enter a hook name", "info"); return; }
|
|
95
|
+
const res = await ctx.api.hookRegister({ name, kind: kindSelect.value, description: descInput.value.trim() });
|
|
96
|
+
if (res && res.ok) { ctx.toast(`Registered ${name}`, "ok"); nameInput.value = ""; descInput.value = ""; load(); }
|
|
97
|
+
else { ctx.toast("Register unavailable", "err"); }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* View: Hooks — the lifecycle hooks registry.
|
|
3
|
+
* Reads /api/hooks (built-in + user hooks across pre_run/post_run/pre_tool/
|
|
4
|
+
* post_tool/agent/pipeline/workflow), toggles enabled state, reorders, and
|
|
5
|
+
* registers custom hooks. Built-in hooks are platform-managed and labelled.
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
const KIND_LABEL = {
|
|
9
|
+
pre_run: "Pre-run", post_run: "Post-run", pre_tool: "Pre-tool", post_tool: "Post-tool",
|
|
10
|
+
agent: "Agent", pipeline: "Pipeline", workflow: "Workflow",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export async function render(ctx) {
|
|
14
|
+
const { h, c } = ctx;
|
|
15
|
+
const src = h("span", c.sourceBadge("pending"));
|
|
16
|
+
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
17
|
+
const groupsHost = h("div", c.loading({ lines: 4, block: true }));
|
|
18
|
+
|
|
19
|
+
const nameInput = h("input.lt3-input", { type: "text", placeholder: "Hook name" });
|
|
20
|
+
const kindSelect = h("select.lt3-select", Object.keys(KIND_LABEL).map((k) => h("option", { value: k }, KIND_LABEL[k])));
|
|
21
|
+
const descInput = h("input.lt3-input", { type: "text", placeholder: "What it does (optional)" });
|
|
22
|
+
|
|
23
|
+
const root = h("div.lt3-stack-6",
|
|
24
|
+
c.viewHeader({
|
|
25
|
+
eyebrow: "Platform",
|
|
26
|
+
title: "Hooks",
|
|
27
|
+
sub: "Lifecycle extension points across runs, tools, agents, pipelines, and workflows — visible, ordered, and individually toggleable.",
|
|
28
|
+
actions: [src],
|
|
29
|
+
}),
|
|
30
|
+
statHost,
|
|
31
|
+
c.panel({
|
|
32
|
+
title: "Register a hook", sub: "Custom hooks are listed, ordered, and inspectable.",
|
|
33
|
+
children: h("div.lt3-stack-3",
|
|
34
|
+
h("div.lt3-grid-2", h("div.lt3-field", h("label", "Name"), nameInput), h("div.lt3-field", h("label", "Kind"), kindSelect)),
|
|
35
|
+
h("div.lt3-field", h("label", "Description"), descInput),
|
|
36
|
+
h("div.lt3-row-2", h("button.lt3-btn.lt3-btn--primary", { on: { click: register } }, c.icon("plus"), "Register hook")),
|
|
37
|
+
),
|
|
38
|
+
}),
|
|
39
|
+
groupsHost,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
load();
|
|
43
|
+
return root;
|
|
44
|
+
|
|
45
|
+
async function load() {
|
|
46
|
+
const res = await ctx.api.hooks();
|
|
47
|
+
src.replaceChildren(c.sourceBadge(res.source));
|
|
48
|
+
const hooks = (res.data && res.data.hooks) || [];
|
|
49
|
+
if (!hooks.length) {
|
|
50
|
+
statHost.replaceChildren(c.stat({ label: "Hooks", value: "—", icon: "webhook" }));
|
|
51
|
+
groupsHost.replaceChildren(c.emptyState({ icon: "webhook-off", title: "Hooks unavailable", body: "Start the backend to read the hooks registry." }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const en = hooks.filter((x) => x.enabled).length;
|
|
55
|
+
statHost.replaceChildren(
|
|
56
|
+
c.stat({ label: "Hooks", value: c.fmtNum(hooks.length), icon: "webhook" }),
|
|
57
|
+
c.stat({ label: "Enabled", value: c.fmtNum(en), icon: "circle-check" }),
|
|
58
|
+
c.stat({ label: "Kinds", value: c.fmtNum((res.data.kinds || []).length), icon: "layers" }),
|
|
59
|
+
);
|
|
60
|
+
const byKind = {};
|
|
61
|
+
for (const hk of hooks) (byKind[hk.kind] = byKind[hk.kind] || []).push(hk);
|
|
62
|
+
groupsHost.replaceChildren(h("div.lt3-stack-6", Object.keys(byKind).map((kind) =>
|
|
63
|
+
h("section", c.sectionHead(KIND_LABEL[kind] || kind, c.pill(String(byKind[kind].length))),
|
|
64
|
+
h("div.lt3-stack-2", byKind[kind].map((hk) => hookRow(ctx, hk)))))));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hookRow(ctx2, hk) {
|
|
68
|
+
return c.card(h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "center", gap: "var(--lt3-space-3)" } },
|
|
69
|
+
h("div", { style: { "min-width": 0 } },
|
|
70
|
+
h("div.lt3-row-2", h("b", hk.name), c.pill(hk.source === "builtin" ? "built-in" : "custom", hk.source === "builtin" ? "info" : ""), hk.managed === "platform" ? c.pill("managed", "") : null),
|
|
71
|
+
h("p.lt3-muted", { style: { margin: "2px 0 0", "font-size": "var(--lt3-text-sm)" } }, hk.description || ""),
|
|
72
|
+
hk.binding ? h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, hk.binding) : null,
|
|
73
|
+
),
|
|
74
|
+
h("div.lt3-row-2", { style: { "flex-shrink": 0 } },
|
|
75
|
+
h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, `#${hk.order}`),
|
|
76
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => toggle(ctx2, hk) } }, c.icon(hk.enabled ? "toggle-right" : "toggle-left"), hk.enabled ? "On" : "Off"),
|
|
77
|
+
hk.removable ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => remove(ctx2, hk) } }, c.icon("trash")) : null,
|
|
78
|
+
),
|
|
79
|
+
), { flat: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function toggle(ctx2, hk) {
|
|
83
|
+
const res = hk.enabled ? await ctx2.api.hookDisable(hk.id) : await ctx2.api.hookEnable(hk.id, true);
|
|
84
|
+
ctx2.toast(res && res.ok ? `${hk.name}: ${hk.enabled ? "disabled" : "enabled"}` : "Action unavailable", res && res.ok ? "ok" : "err");
|
|
85
|
+
load();
|
|
86
|
+
}
|
|
87
|
+
async function remove(ctx2, hk) {
|
|
88
|
+
const res = await ctx2.api.hookRemove(hk.id);
|
|
89
|
+
ctx2.toast(res && res.ok ? `Removed ${hk.name}` : "Remove unavailable", res && res.ok ? "ok" : "err");
|
|
90
|
+
load();
|
|
91
|
+
}
|
|
92
|
+
async function register() {
|
|
93
|
+
const name = nameInput.value.trim();
|
|
94
|
+
if (!name) { ctx.toast("Enter a hook name", "info"); return; }
|
|
95
|
+
const res = await ctx.api.hookRegister({ name, kind: kindSelect.value, description: descInput.value.trim() });
|
|
96
|
+
if (res && res.ok) { ctx.toast(`Registered ${name}`, "ok"); nameInput.value = ""; descInput.value = ""; load(); }
|
|
97
|
+
else { ctx.toast("Register unavailable", "err"); }
|
|
98
|
+
}
|
|
99
|
+
}
|