ltcai 2.2.7 → 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.
- package/README.md +72 -34
- package/docs/CHANGELOG.md +119 -0
- package/docs/V3_BACKEND_ARCHITECTURE.md +138 -0
- package/docs/V3_FRONTEND.md +139 -0
- package/knowledge_graph.py +649 -21
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +47 -0
- package/latticeai/api/agents.py +54 -31
- package/latticeai/api/auth.py +5 -2
- package/latticeai/api/chat.py +10 -2
- package/latticeai/api/search.py +240 -0
- package/latticeai/api/static_routes.py +11 -2
- package/latticeai/core/config.py +18 -0
- package/latticeai/core/embedding_providers.py +625 -0
- package/latticeai/core/local_embeddings.py +86 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +65 -1
- package/latticeai/services/agent_runtime.py +245 -0
- package/latticeai/services/search_service.py +346 -0
- package/package.json +13 -6
- package/scripts/build_v3_assets.mjs +164 -0
- package/scripts/capture/README.md +28 -0
- package/scripts/capture/capture_enterprise.js +8 -0
- package/scripts/capture/capture_graph.js +8 -0
- package/scripts/capture/capture_onboarding.js +8 -0
- package/scripts/capture/capture_page.js +43 -0
- package/scripts/capture/capture_release_media.js +125 -0
- package/scripts/capture/capture_skills.js +8 -0
- package/scripts/capture/capture_workspace.js +8 -0
- package/scripts/generate_diagrams.py +513 -0
- package/scripts/lint_v3.mjs +33 -0
- package/scripts/release-0.3.1.sh +105 -0
- package/scripts/take_screenshots.js +69 -0
- package/scripts/validate_release_artifacts.py +167 -0
- package/static/account.html +9 -9
- package/static/activity.html +4 -4
- package/static/admin.html +8 -8
- package/static/agents.html +4 -4
- package/static/chat.html +10 -10
- package/static/css/reference/account.css +137 -1
- package/static/css/reference/chat.css +31 -37
- package/static/css/responsive.css +42 -0
- package/static/css/tokens.5a595671.css +260 -0
- package/static/css/tokens.css +125 -130
- package/static/graph.html +9 -9
- package/static/manifest.json +3 -3
- package/static/plugins.html +4 -4
- package/static/scripts/account.js +4 -4
- package/static/scripts/chat.js +40 -8
- package/static/scripts/workspace.js +78 -0
- package/static/sw.js +3 -1
- package/static/v3/asset-manifest.json +47 -0
- package/static/v3/css/lattice.base.css +128 -0
- package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
- package/static/v3/css/lattice.components.011e988b.css +447 -0
- package/static/v3/css/lattice.components.css +447 -0
- package/static/v3/css/lattice.shell.4920f42d.css +407 -0
- package/static/v3/css/lattice.shell.css +407 -0
- package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
- package/static/v3/css/lattice.tokens.css +132 -0
- package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
- package/static/v3/css/lattice.views.css +277 -0
- package/static/v3/index.html +69 -0
- package/static/v3/js/app.46fb61d9.js +26 -0
- package/static/v3/js/app.js +26 -0
- package/static/v3/js/core/api.22a41d42.js +344 -0
- package/static/v3/js/core/api.js +344 -0
- package/static/v3/js/core/components.4c83e0a9.js +222 -0
- package/static/v3/js/core/components.js +222 -0
- package/static/v3/js/core/dom.a2773eb0.js +148 -0
- package/static/v3/js/core/dom.js +148 -0
- package/static/v3/js/core/router.584570f2.js +37 -0
- package/static/v3/js/core/router.js +37 -0
- package/static/v3/js/core/routes.f935dd50.js +78 -0
- package/static/v3/js/core/routes.js +78 -0
- package/static/v3/js/core/shell.1b6199d6.js +363 -0
- package/static/v3/js/core/shell.js +363 -0
- package/static/v3/js/core/store.34ebd5e6.js +113 -0
- package/static/v3/js/core/store.js +113 -0
- package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
- package/static/v3/js/views/admin-audit.js +185 -0
- package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
- package/static/v3/js/views/admin-permissions.js +177 -0
- package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
- package/static/v3/js/views/admin-policies.js +102 -0
- package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
- package/static/v3/js/views/admin-private-vpc.js +135 -0
- package/static/v3/js/views/admin-security.07c66b72.js +180 -0
- package/static/v3/js/views/admin-security.js +180 -0
- package/static/v3/js/views/admin-users.03bac88c.js +168 -0
- package/static/v3/js/views/admin-users.js +168 -0
- package/static/v3/js/views/agents.14e48bdd.js +193 -0
- package/static/v3/js/views/agents.js +193 -0
- package/static/v3/js/views/chat.718144ce.js +449 -0
- package/static/v3/js/views/chat.js +449 -0
- package/static/v3/js/views/files.4935197e.js +186 -0
- package/static/v3/js/views/files.js +186 -0
- package/static/v3/js/views/home.cdde3b32.js +119 -0
- package/static/v3/js/views/home.js +119 -0
- package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
- package/static/v3/js/views/hybrid-search.js +195 -0
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
- package/static/v3/js/views/knowledge-graph.js +237 -0
- package/static/v3/js/views/models.a1ffa147.js +256 -0
- package/static/v3/js/views/models.js +256 -0
- package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
- package/static/v3/js/views/my-computer.js +237 -0
- package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
- package/static/v3/js/views/pipeline.js +157 -0
- package/static/v3/js/views/settings.4f777210.js +250 -0
- package/static/v3/js/views/settings.js +250 -0
- package/static/workflows.html +4 -4
- package/static/workspace.css +340 -2
- package/static/workspace.html +43 -24
- package/docs/images/tmp_frames/frame_00.png +0 -0
- package/docs/images/tmp_frames/frame_01.png +0 -0
- package/docs/images/tmp_frames/frame_02.png +0 -0
- package/docs/images/tmp_frames/frame_03.png +0 -0
- package/docs/images/tmp_frames/hero_00.png +0 -0
- package/docs/images/tmp_frames/hero_01.png +0 -0
- package/docs/images/tmp_frames/hero_02.png +0 -0
- package/docs/images/tmp_frames/hero_03.png +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — Component factories
|
|
3
|
+
* Build the shared vocabulary (cards, panels, stats, tables, states, the
|
|
4
|
+
* retrieval-lattice pillars …) on top of dom.js + the component CSS. Views
|
|
5
|
+
* compose these to stay visually consistent.
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
import { h, icon, fmtNum } from "./dom.js";
|
|
9
|
+
|
|
10
|
+
/* ── View + section headers ─────────────────────────────────────────────── */
|
|
11
|
+
export function viewHeader({ eyebrow, title, sub, actions } = {}) {
|
|
12
|
+
return h("header.lt3-vhead",
|
|
13
|
+
h("div",
|
|
14
|
+
eyebrow && h("div.lt3-eyebrow", eyebrow),
|
|
15
|
+
h("h1.lt3-vhead__title", title),
|
|
16
|
+
sub && h("p.lt3-vhead__sub", sub),
|
|
17
|
+
),
|
|
18
|
+
actions && actions.length && h("div.lt3-vhead__actions", actions),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sectionHead(title, ...actions) {
|
|
23
|
+
return h("div.lt3-section__head",
|
|
24
|
+
h("h2.lt3-section__title", title),
|
|
25
|
+
actions.length && h("div.lt3-row-2", actions),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ── Panel / card ───────────────────────────────────────────────────────── */
|
|
30
|
+
export function panel({ title, sub, actions, head, children, eyebrow, className } = {}) {
|
|
31
|
+
return h(`section.lt3-panel${className ? "." + className : ""}`,
|
|
32
|
+
(title || head || actions) && h("div.lt3-panel__head",
|
|
33
|
+
head || h("div",
|
|
34
|
+
eyebrow && h("div.lt3-eyebrow", eyebrow),
|
|
35
|
+
title && h("h3.lt3-panel__title", title),
|
|
36
|
+
sub && h("p.lt3-panel__sub", sub),
|
|
37
|
+
),
|
|
38
|
+
actions && h("div.lt3-row-2", actions),
|
|
39
|
+
),
|
|
40
|
+
children,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function card(children, opts = {}) {
|
|
45
|
+
const cls = ["lt3-card"];
|
|
46
|
+
if (opts.interactive) cls.push("lt3-card--interactive");
|
|
47
|
+
if (opts.flat) cls.push("lt3-card--flat");
|
|
48
|
+
if (opts.ghost) cls.push("lt3-card--ghost");
|
|
49
|
+
return h(`div.${cls.join(".")}`, opts.attrs || {}, children);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ── Stat tile ──────────────────────────────────────────────────────────── */
|
|
53
|
+
export function stat({ label, value, icon: ic, delta, deltaDir }) {
|
|
54
|
+
return h("div.lt3-stat",
|
|
55
|
+
h("div.lt3-stat__label", ic && icon(ic), label),
|
|
56
|
+
h("div.lt3-stat__value", value == null ? "—" : value),
|
|
57
|
+
delta && h(`div.lt3-stat__delta${deltaDir ? ".lt3-stat__delta--" + deltaDir : ""}`, delta),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Pills / badges ─────────────────────────────────────────────────────── */
|
|
62
|
+
export function pill(text, variant = "", { dot } = {}) {
|
|
63
|
+
const cls = ["lt3-pill"];
|
|
64
|
+
if (variant) cls.push("lt3-pill--" + variant);
|
|
65
|
+
if (dot) cls.push("lt3-pill--dot");
|
|
66
|
+
return h(`span.${cls.join(".")}`, text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const STATE_VARIANT = {
|
|
70
|
+
ready: "ok", active: "ok", indexed: "ok", loaded: "ok", ok: "ok", available: "info",
|
|
71
|
+
idle: "", standby: "", pending: "warn", indexing: "warn", building: "warn",
|
|
72
|
+
failed: "err", error: "err", disabled: "err", not_configured: "",
|
|
73
|
+
};
|
|
74
|
+
export function statePill(state) {
|
|
75
|
+
return pill(String(state || "unknown"), STATE_VARIANT[String(state).toLowerCase()] ?? "", { dot: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Provenance badge — makes live vs unavailable data explicit. */
|
|
79
|
+
export function sourceBadge(source) {
|
|
80
|
+
if (source === "live") return h("span.lt3-source.lt3-source--live", icon("circle-filled"), "Live");
|
|
81
|
+
if (source === "unavailable") return h("span.lt3-source.lt3-source--unavailable", icon("alert-circle"), "Unavailable");
|
|
82
|
+
return h("span.lt3-source.lt3-source--pending", "—");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── States ─────────────────────────────────────────────────────────────── */
|
|
86
|
+
export function emptyState({ icon: ic = "inbox", title, body, action } = {}) {
|
|
87
|
+
return h("div.lt3-empty",
|
|
88
|
+
h("div.lt3-empty__icon", icon(ic)),
|
|
89
|
+
title && h("div.lt3-empty__title", title),
|
|
90
|
+
body && h("div.lt3-empty__body", body),
|
|
91
|
+
action,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function loading({ lines = 3, block = false } = {}) {
|
|
96
|
+
const kids = [];
|
|
97
|
+
if (block) kids.push(h("div.lt3-skel.lt3-skel--block"));
|
|
98
|
+
for (let i = 0; i < lines; i++) {
|
|
99
|
+
kids.push(h("div.lt3-skel.lt3-skel--line", { style: { width: 100 - i * 14 + "%" } }));
|
|
100
|
+
}
|
|
101
|
+
return h("div", { "aria-busy": "true", "aria-label": "Loading" }, kids);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function errorState(message, onRetry) {
|
|
105
|
+
return h("div.lt3-banner.lt3-banner--err",
|
|
106
|
+
icon("alert-triangle"),
|
|
107
|
+
h("div", h("div", { style: { fontWeight: 600 } }, "Couldn't load"), h("div.lt3-faint", message || "Request failed")),
|
|
108
|
+
onRetry && h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { style: { "margin-left": "auto" }, on: { click: onRetry } }, icon("refresh"), "Retry"),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function banner(text, variant = "info", ic = "info-circle") {
|
|
113
|
+
return h(`div.lt3-banner.lt3-banner--${variant}`, icon(ic), h("div", text));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ── Table ──────────────────────────────────────────────────────────────── */
|
|
117
|
+
export function table(columns, rows, { empty } = {}) {
|
|
118
|
+
if (!rows || !rows.length) {
|
|
119
|
+
return empty || emptyState({ title: "Nothing here yet", body: "Data will appear once connected." });
|
|
120
|
+
}
|
|
121
|
+
return h("div.lt3-table--clip", { style: { overflow: "auto" } },
|
|
122
|
+
h("table.lt3-table",
|
|
123
|
+
h("thead", h("tr", columns.map((c) => h("th", { style: c.width ? { width: c.width } : {} }, c.label)))),
|
|
124
|
+
h("tbody", rows.map((row) => h("tr", columns.map((c) => h("td", c.render ? c.render(row) : row[c.key]))))),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* ── Tabs / segmented ───────────────────────────────────────────────────── */
|
|
130
|
+
export function tabs(items, active, onChange) {
|
|
131
|
+
return h("div.lt3-tabs", { role: "tablist" },
|
|
132
|
+
items.map((it) => h("button.lt3-tab", {
|
|
133
|
+
role: "tab", type: "button",
|
|
134
|
+
dataset: { active: String(it.key === active) },
|
|
135
|
+
"aria-selected": String(it.key === active),
|
|
136
|
+
on: { click: () => onChange(it.key) },
|
|
137
|
+
}, it.label)),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function segmented(items, active, onChange) {
|
|
142
|
+
return h("div.lt3-seg", { role: "tablist" },
|
|
143
|
+
items.map((it) => h("button", {
|
|
144
|
+
type: "button", role: "tab",
|
|
145
|
+
dataset: { active: String(it.key === active) },
|
|
146
|
+
"aria-selected": String(it.key === active),
|
|
147
|
+
on: { click: () => onChange(it.key) },
|
|
148
|
+
}, it.label)),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ── Meter ──────────────────────────────────────────────────────────────── */
|
|
153
|
+
export function meter(value, variant = "") {
|
|
154
|
+
const pct = Math.max(0, Math.min(1, Number(value) || 0)) * 100;
|
|
155
|
+
return h("div.lt3-meter",
|
|
156
|
+
h(`div.lt3-meter__fill${variant ? ".lt3-meter__fill--" + variant : ""}`, { style: { width: pct + "%" } }),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ── Retrieval lattice (signature) ──────────────────────────────────────── */
|
|
161
|
+
const PILLAR_DEFS = [
|
|
162
|
+
{ key: "knowledge_graph", kind: "graph", name: "Knowledge Graph", desc: "Entities & relations", icon: "chart-dots-3", unit: "entities", read: (p) => p?.entities },
|
|
163
|
+
{ key: "vector_index", kind: "vector", name: "Vector Index", desc: "Local embedding vectors", icon: "grid-dots", unit: "vectors", read: (p) => p?.vectors },
|
|
164
|
+
{ key: "hybrid", kind: "hybrid", name: "Hybrid Search", desc: "Fused graph + vector", icon: "arrows-join", unit: "fusion", read: (p) => p?.strategy },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
export function pillars(indexStatus) {
|
|
168
|
+
const pipes = indexStatus?.pipelines || {};
|
|
169
|
+
if (!Object.keys(pipes).length) {
|
|
170
|
+
return emptyState({
|
|
171
|
+
icon: "database-off",
|
|
172
|
+
title: "Retrieval status unavailable",
|
|
173
|
+
body: "Start the backend with Knowledge Graph enabled to see live index state.",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return h("div.lt3-pillars",
|
|
177
|
+
PILLAR_DEFS.map((def) => {
|
|
178
|
+
const p = pipes[def.key] || {};
|
|
179
|
+
const raw = def.read(p);
|
|
180
|
+
const num = typeof raw === "number" ? fmtNum(raw) : (raw || "ready");
|
|
181
|
+
return h(`article.lt3-pillar.lt3-pillar--${def.kind}`,
|
|
182
|
+
h("div.lt3-pillar__icon", icon(def.icon)),
|
|
183
|
+
h("div.lt3-row", { style: { "justify-content": "space-between" } },
|
|
184
|
+
h("div",
|
|
185
|
+
h("div.lt3-pillar__name", def.name),
|
|
186
|
+
h("div.lt3-pillar__desc", def.desc),
|
|
187
|
+
),
|
|
188
|
+
statePill(p.state || "ready"),
|
|
189
|
+
),
|
|
190
|
+
h("div.lt3-pillar__stat",
|
|
191
|
+
h("span.lt3-pillar__num", num),
|
|
192
|
+
h("span.lt3-pillar__unit", def.unit),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Compact 3-dot index chip for the topbar. */
|
|
200
|
+
export function indexChip(indexStatus) {
|
|
201
|
+
const pipes = indexStatus?.pipelines || {};
|
|
202
|
+
const dot = (kind, key) => h("span.lt3-idxchip__dot", {
|
|
203
|
+
dataset: { kind, on: String((pipes[key]?.state || "ready") === "ready") },
|
|
204
|
+
title: `${kind}: ${pipes[key]?.state || "—"}`,
|
|
205
|
+
});
|
|
206
|
+
return h("div.lt3-idxchip", { title: "Retrieval index status" },
|
|
207
|
+
h("span.lt3-idxchip__dots", dot("graph", "knowledge_graph"), dot("vector", "vector_index"), dot("hybrid", "hybrid")),
|
|
208
|
+
h("span", "Index"),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ── Toast ──────────────────────────────────────────────────────────────── */
|
|
213
|
+
export function toast(message, variant = "info") {
|
|
214
|
+
let host = document.querySelector(".lt3-toasts");
|
|
215
|
+
if (!host) { host = h("div.lt3-toasts"); document.body.append(host); }
|
|
216
|
+
const ic = variant === "ok" ? "circle-check" : variant === "err" ? "alert-circle" : "info-circle";
|
|
217
|
+
const node = h(`div.lt3-toast.lt3-toast--${variant}`, icon(ic), h("div", message));
|
|
218
|
+
host.append(node);
|
|
219
|
+
setTimeout(() => { node.style.opacity = "0"; node.style.transition = "opacity .3s"; setTimeout(() => node.remove(), 300); }, 3200);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { icon, fmtNum };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — DOM helpers
|
|
3
|
+
* A tiny, dependency-free hyperscript. Builds real DOM nodes (no innerHTML for
|
|
4
|
+
* dynamic content → no injection surface). Ergonomic enough to author views.
|
|
5
|
+
* ========================================================================== */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* h("div.card#id", { props }, ...children)
|
|
9
|
+
* - tag selector supports .class (many) and #id
|
|
10
|
+
* - props: class | className, id, on:{event:fn}, dataset:{k:v},
|
|
11
|
+
* style:{k:v} or string, html (trusted innerHTML), attrs (any other)
|
|
12
|
+
* - children: Node | string | number | falsy (skipped) | array (flattened)
|
|
13
|
+
*/
|
|
14
|
+
export function h(selector, props, ...children) {
|
|
15
|
+
const { tag, id, classes } = parseSelector(selector);
|
|
16
|
+
const el = document.createElement(tag);
|
|
17
|
+
if (id) el.id = id;
|
|
18
|
+
if (classes.length) el.classList.add(...classes);
|
|
19
|
+
|
|
20
|
+
if (props && (props.nodeType || typeof props === "string" || Array.isArray(props))) {
|
|
21
|
+
children.unshift(props);
|
|
22
|
+
props = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (props) {
|
|
26
|
+
for (const [key, val] of Object.entries(props)) {
|
|
27
|
+
if (val == null || val === false) continue;
|
|
28
|
+
if (key === "class" || key === "className") {
|
|
29
|
+
for (const c of String(val).split(/\s+/).filter(Boolean)) el.classList.add(c);
|
|
30
|
+
} else if (key === "on" && typeof val === "object") {
|
|
31
|
+
for (const [ev, fn] of Object.entries(val)) el.addEventListener(ev, fn);
|
|
32
|
+
} else if (key === "dataset" && typeof val === "object") {
|
|
33
|
+
for (const [k, v] of Object.entries(val)) { if (v != null) el.dataset[k] = v; }
|
|
34
|
+
} else if (key === "style" && typeof val === "object") {
|
|
35
|
+
for (const [k, v] of Object.entries(val)) el.style.setProperty(k, v);
|
|
36
|
+
} else if (key === "style") {
|
|
37
|
+
el.setAttribute("style", val);
|
|
38
|
+
} else if (key === "html") {
|
|
39
|
+
el.innerHTML = val;
|
|
40
|
+
} else if (key === "ref" && typeof val === "function") {
|
|
41
|
+
val(el);
|
|
42
|
+
} else if (key in el && key !== "list" && typeof val !== "object") {
|
|
43
|
+
try { el[key] = val; } catch { el.setAttribute(key, val); }
|
|
44
|
+
} else {
|
|
45
|
+
el.setAttribute(key, val === true ? "" : val);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
appendChildren(el, children);
|
|
51
|
+
return el;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseSelector(sel) {
|
|
55
|
+
if (typeof sel !== "string") return { tag: "div", id: "", classes: [] };
|
|
56
|
+
const idMatch = sel.match(/#([\w-]+)/);
|
|
57
|
+
const id = idMatch ? idMatch[1] : "";
|
|
58
|
+
const classes = (sel.match(/\.([\w-]+)/g) || []).map((c) => c.slice(1));
|
|
59
|
+
const tag = (sel.match(/^([\w-]+)/) || [, "div"])[1];
|
|
60
|
+
return { tag, id, classes };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendChildren(el, children) {
|
|
64
|
+
for (const child of children.flat(Infinity)) {
|
|
65
|
+
if (child == null || child === false || child === true) continue;
|
|
66
|
+
el.append(child.nodeType ? child : document.createTextNode(String(child)));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Fragment of many children. */
|
|
71
|
+
export function frag(...children) {
|
|
72
|
+
const f = document.createDocumentFragment();
|
|
73
|
+
appendChildren(f, children);
|
|
74
|
+
return f;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Replace the contents of a node. */
|
|
78
|
+
export function render(host, ...children) {
|
|
79
|
+
host.replaceChildren();
|
|
80
|
+
appendChildren(host, children);
|
|
81
|
+
return host;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function clear(host) { host.replaceChildren(); return host; }
|
|
85
|
+
|
|
86
|
+
export function escapeHtml(s) {
|
|
87
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, (c) =>
|
|
88
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const $ = (sel, root = document) => root.querySelector(sel);
|
|
92
|
+
export const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
|
93
|
+
|
|
94
|
+
/** Tabler icon element: icon("home") → <i class="ti ti-home"> */
|
|
95
|
+
export function icon(name, extra = "") {
|
|
96
|
+
const i = document.createElement("i");
|
|
97
|
+
i.className = `ti ti-${name}${extra ? " " + extra : ""}`;
|
|
98
|
+
i.setAttribute("aria-hidden", "true");
|
|
99
|
+
i.setAttribute("data-fallback", iconFallback(name));
|
|
100
|
+
return i;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function iconFallback(name) {
|
|
104
|
+
const map = {
|
|
105
|
+
home: "H", search: "?", menu: "=", x: "x", refresh: "R",
|
|
106
|
+
"arrow-up": "^", "arrow-up-right": ">", "arrows-join": "H",
|
|
107
|
+
"chart-dots-3": "G", "grid-dots": "V", message: "C", "message-2": "C",
|
|
108
|
+
"message-plus": "+", "message-off": "C", user: "U", users: "U",
|
|
109
|
+
settings: "S", files: "F", cpu: "M", "cpu-off": "M", "player-play": ">",
|
|
110
|
+
"player-stop": "s", "layout-sidebar": "<", "layout-sidebar-right": ">",
|
|
111
|
+
"external-link": "o", trash: "x", sparkles: "*", "alert-triangle": "!",
|
|
112
|
+
"alert-circle": "!", "info-circle": "i", "circle-check": "v",
|
|
113
|
+
"circle-filled": "*", "stack-2": "S", "ruler-2": "R", binary: "B",
|
|
114
|
+
category: "T", abc: "K", "search-off": "?"
|
|
115
|
+
};
|
|
116
|
+
if (map[name]) return map[name];
|
|
117
|
+
return String(name || "?").split("-").map((part) => part[0] || "").join("").slice(0, 2).toUpperCase() || "?";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Compact relative-time formatter. */
|
|
121
|
+
export function timeAgo(value) {
|
|
122
|
+
if (!value) return "";
|
|
123
|
+
const then = new Date(value).getTime();
|
|
124
|
+
if (Number.isNaN(then)) return String(value);
|
|
125
|
+
const diff = Math.max(0, Date.now() - then);
|
|
126
|
+
const mins = Math.floor(diff / 60000);
|
|
127
|
+
if (mins < 1) return "just now";
|
|
128
|
+
if (mins < 60) return `${mins}m ago`;
|
|
129
|
+
const hrs = Math.floor(mins / 60);
|
|
130
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
131
|
+
const days = Math.floor(hrs / 24);
|
|
132
|
+
if (days < 30) return `${days}d ago`;
|
|
133
|
+
return new Date(then).toLocaleDateString();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Format a number with thousands separators / compact suffix. */
|
|
137
|
+
export function fmtNum(n) {
|
|
138
|
+
if (n == null || Number.isNaN(Number(n))) return "—";
|
|
139
|
+
const v = Number(n);
|
|
140
|
+
if (Math.abs(v) >= 1_000_000) return (v / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
141
|
+
if (Math.abs(v) >= 1_000) return (v / 1_000).toFixed(1).replace(/\.0$/, "") + "k";
|
|
142
|
+
return v.toLocaleString();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function debounce(fn, ms = 220) {
|
|
146
|
+
let t;
|
|
147
|
+
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
|
148
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — DOM helpers
|
|
3
|
+
* A tiny, dependency-free hyperscript. Builds real DOM nodes (no innerHTML for
|
|
4
|
+
* dynamic content → no injection surface). Ergonomic enough to author views.
|
|
5
|
+
* ========================================================================== */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* h("div.card#id", { props }, ...children)
|
|
9
|
+
* - tag selector supports .class (many) and #id
|
|
10
|
+
* - props: class | className, id, on:{event:fn}, dataset:{k:v},
|
|
11
|
+
* style:{k:v} or string, html (trusted innerHTML), attrs (any other)
|
|
12
|
+
* - children: Node | string | number | falsy (skipped) | array (flattened)
|
|
13
|
+
*/
|
|
14
|
+
export function h(selector, props, ...children) {
|
|
15
|
+
const { tag, id, classes } = parseSelector(selector);
|
|
16
|
+
const el = document.createElement(tag);
|
|
17
|
+
if (id) el.id = id;
|
|
18
|
+
if (classes.length) el.classList.add(...classes);
|
|
19
|
+
|
|
20
|
+
if (props && (props.nodeType || typeof props === "string" || Array.isArray(props))) {
|
|
21
|
+
children.unshift(props);
|
|
22
|
+
props = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (props) {
|
|
26
|
+
for (const [key, val] of Object.entries(props)) {
|
|
27
|
+
if (val == null || val === false) continue;
|
|
28
|
+
if (key === "class" || key === "className") {
|
|
29
|
+
for (const c of String(val).split(/\s+/).filter(Boolean)) el.classList.add(c);
|
|
30
|
+
} else if (key === "on" && typeof val === "object") {
|
|
31
|
+
for (const [ev, fn] of Object.entries(val)) el.addEventListener(ev, fn);
|
|
32
|
+
} else if (key === "dataset" && typeof val === "object") {
|
|
33
|
+
for (const [k, v] of Object.entries(val)) { if (v != null) el.dataset[k] = v; }
|
|
34
|
+
} else if (key === "style" && typeof val === "object") {
|
|
35
|
+
for (const [k, v] of Object.entries(val)) el.style.setProperty(k, v);
|
|
36
|
+
} else if (key === "style") {
|
|
37
|
+
el.setAttribute("style", val);
|
|
38
|
+
} else if (key === "html") {
|
|
39
|
+
el.innerHTML = val;
|
|
40
|
+
} else if (key === "ref" && typeof val === "function") {
|
|
41
|
+
val(el);
|
|
42
|
+
} else if (key in el && key !== "list" && typeof val !== "object") {
|
|
43
|
+
try { el[key] = val; } catch { el.setAttribute(key, val); }
|
|
44
|
+
} else {
|
|
45
|
+
el.setAttribute(key, val === true ? "" : val);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
appendChildren(el, children);
|
|
51
|
+
return el;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseSelector(sel) {
|
|
55
|
+
if (typeof sel !== "string") return { tag: "div", id: "", classes: [] };
|
|
56
|
+
const idMatch = sel.match(/#([\w-]+)/);
|
|
57
|
+
const id = idMatch ? idMatch[1] : "";
|
|
58
|
+
const classes = (sel.match(/\.([\w-]+)/g) || []).map((c) => c.slice(1));
|
|
59
|
+
const tag = (sel.match(/^([\w-]+)/) || [, "div"])[1];
|
|
60
|
+
return { tag, id, classes };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendChildren(el, children) {
|
|
64
|
+
for (const child of children.flat(Infinity)) {
|
|
65
|
+
if (child == null || child === false || child === true) continue;
|
|
66
|
+
el.append(child.nodeType ? child : document.createTextNode(String(child)));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Fragment of many children. */
|
|
71
|
+
export function frag(...children) {
|
|
72
|
+
const f = document.createDocumentFragment();
|
|
73
|
+
appendChildren(f, children);
|
|
74
|
+
return f;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Replace the contents of a node. */
|
|
78
|
+
export function render(host, ...children) {
|
|
79
|
+
host.replaceChildren();
|
|
80
|
+
appendChildren(host, children);
|
|
81
|
+
return host;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function clear(host) { host.replaceChildren(); return host; }
|
|
85
|
+
|
|
86
|
+
export function escapeHtml(s) {
|
|
87
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, (c) =>
|
|
88
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const $ = (sel, root = document) => root.querySelector(sel);
|
|
92
|
+
export const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
|
93
|
+
|
|
94
|
+
/** Tabler icon element: icon("home") → <i class="ti ti-home"> */
|
|
95
|
+
export function icon(name, extra = "") {
|
|
96
|
+
const i = document.createElement("i");
|
|
97
|
+
i.className = `ti ti-${name}${extra ? " " + extra : ""}`;
|
|
98
|
+
i.setAttribute("aria-hidden", "true");
|
|
99
|
+
i.setAttribute("data-fallback", iconFallback(name));
|
|
100
|
+
return i;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function iconFallback(name) {
|
|
104
|
+
const map = {
|
|
105
|
+
home: "H", search: "?", menu: "=", x: "x", refresh: "R",
|
|
106
|
+
"arrow-up": "^", "arrow-up-right": ">", "arrows-join": "H",
|
|
107
|
+
"chart-dots-3": "G", "grid-dots": "V", message: "C", "message-2": "C",
|
|
108
|
+
"message-plus": "+", "message-off": "C", user: "U", users: "U",
|
|
109
|
+
settings: "S", files: "F", cpu: "M", "cpu-off": "M", "player-play": ">",
|
|
110
|
+
"player-stop": "s", "layout-sidebar": "<", "layout-sidebar-right": ">",
|
|
111
|
+
"external-link": "o", trash: "x", sparkles: "*", "alert-triangle": "!",
|
|
112
|
+
"alert-circle": "!", "info-circle": "i", "circle-check": "v",
|
|
113
|
+
"circle-filled": "*", "stack-2": "S", "ruler-2": "R", binary: "B",
|
|
114
|
+
category: "T", abc: "K", "search-off": "?"
|
|
115
|
+
};
|
|
116
|
+
if (map[name]) return map[name];
|
|
117
|
+
return String(name || "?").split("-").map((part) => part[0] || "").join("").slice(0, 2).toUpperCase() || "?";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Compact relative-time formatter. */
|
|
121
|
+
export function timeAgo(value) {
|
|
122
|
+
if (!value) return "";
|
|
123
|
+
const then = new Date(value).getTime();
|
|
124
|
+
if (Number.isNaN(then)) return String(value);
|
|
125
|
+
const diff = Math.max(0, Date.now() - then);
|
|
126
|
+
const mins = Math.floor(diff / 60000);
|
|
127
|
+
if (mins < 1) return "just now";
|
|
128
|
+
if (mins < 60) return `${mins}m ago`;
|
|
129
|
+
const hrs = Math.floor(mins / 60);
|
|
130
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
131
|
+
const days = Math.floor(hrs / 24);
|
|
132
|
+
if (days < 30) return `${days}d ago`;
|
|
133
|
+
return new Date(then).toLocaleDateString();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Format a number with thousands separators / compact suffix. */
|
|
137
|
+
export function fmtNum(n) {
|
|
138
|
+
if (n == null || Number.isNaN(Number(n))) return "—";
|
|
139
|
+
const v = Number(n);
|
|
140
|
+
if (Math.abs(v) >= 1_000_000) return (v / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
141
|
+
if (Math.abs(v) >= 1_000) return (v / 1_000).toFixed(1).replace(/\.0$/, "") + "k";
|
|
142
|
+
return v.toLocaleString();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function debounce(fn, ms = 220) {
|
|
146
|
+
let t;
|
|
147
|
+
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
|
|
148
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — Hash router
|
|
3
|
+
* Maps location.hash → { key, params }. Hash routing keeps the SPA shell
|
|
4
|
+
* served by a single static route (/app) with no server-side rewrites.
|
|
5
|
+
* #/home → { key: "home" }
|
|
6
|
+
* #/admin/users → { key: "admin/users" }
|
|
7
|
+
* #/chat?new=1 → { key: "chat", params: { new: "1" } }
|
|
8
|
+
* ========================================================================== */
|
|
9
|
+
|
|
10
|
+
export function createRouter({ onRoute, fallback = "home" }) {
|
|
11
|
+
function parse() {
|
|
12
|
+
let hash = location.hash.replace(/^#\/?/, "");
|
|
13
|
+
let query = "";
|
|
14
|
+
const qi = hash.indexOf("?");
|
|
15
|
+
if (qi >= 0) { query = hash.slice(qi + 1); hash = hash.slice(0, qi); }
|
|
16
|
+
const key = hash.replace(/\/+$/, "") || fallback;
|
|
17
|
+
const params = {};
|
|
18
|
+
if (query) new URLSearchParams(query).forEach((v, k) => { params[k] = v; });
|
|
19
|
+
return { key, params };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handle() { onRoute(parse()); }
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
start() {
|
|
26
|
+
window.addEventListener("hashchange", handle);
|
|
27
|
+
if (!location.hash) { location.replace("#/" + fallback); }
|
|
28
|
+
else { handle(); }
|
|
29
|
+
},
|
|
30
|
+
current: parse,
|
|
31
|
+
navigate(key, params) {
|
|
32
|
+
const qs = params && Object.keys(params).length ? "?" + new URLSearchParams(params).toString() : "";
|
|
33
|
+
const next = "#/" + String(key).replace(/^#?\/?/, "") + qs;
|
|
34
|
+
if (location.hash === next) handle(); else location.hash = next;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — Hash router
|
|
3
|
+
* Maps location.hash → { key, params }. Hash routing keeps the SPA shell
|
|
4
|
+
* served by a single static route (/app) with no server-side rewrites.
|
|
5
|
+
* #/home → { key: "home" }
|
|
6
|
+
* #/admin/users → { key: "admin/users" }
|
|
7
|
+
* #/chat?new=1 → { key: "chat", params: { new: "1" } }
|
|
8
|
+
* ========================================================================== */
|
|
9
|
+
|
|
10
|
+
export function createRouter({ onRoute, fallback = "home" }) {
|
|
11
|
+
function parse() {
|
|
12
|
+
let hash = location.hash.replace(/^#\/?/, "");
|
|
13
|
+
let query = "";
|
|
14
|
+
const qi = hash.indexOf("?");
|
|
15
|
+
if (qi >= 0) { query = hash.slice(qi + 1); hash = hash.slice(0, qi); }
|
|
16
|
+
const key = hash.replace(/\/+$/, "") || fallback;
|
|
17
|
+
const params = {};
|
|
18
|
+
if (query) new URLSearchParams(query).forEach((v, k) => { params[k] = v; });
|
|
19
|
+
return { key, params };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function handle() { onRoute(parse()); }
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
start() {
|
|
26
|
+
window.addEventListener("hashchange", handle);
|
|
27
|
+
if (!location.hash) { location.replace("#/" + fallback); }
|
|
28
|
+
else { handle(); }
|
|
29
|
+
},
|
|
30
|
+
current: parse,
|
|
31
|
+
navigate(key, params) {
|
|
32
|
+
const qs = params && Object.keys(params).length ? "?" + new URLSearchParams(params).toString() : "";
|
|
33
|
+
const next = "#/" + String(key).replace(/^#?\/?/, "") + qs;
|
|
34
|
+
if (location.hash === next) handle(); else location.hash = next;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|