ltcai 4.0.0 → 4.0.1
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 +37 -33
- package/docs/CHANGELOG.md +64 -0
- package/docs/REALTIME_COLLABORATION.md +3 -3
- package/docs/V3_FRONTEND.md +9 -8
- package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +86 -43
- package/docs/kg-schema.md +6 -2
- package/docs/spec-vs-impl.md +10 -10
- package/kg_schema.py +2 -603
- package/knowledge_graph.py +37 -4958
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +15 -16
- package/latticeai/api/agents.py +13 -6
- package/latticeai/api/auth.py +19 -11
- package/latticeai/api/invitations.py +100 -0
- package/latticeai/api/knowledge_graph.py +4 -11
- package/latticeai/api/plugins.py +3 -6
- package/latticeai/api/realtime.py +4 -7
- package/latticeai/api/static_routes.py +9 -12
- package/latticeai/api/ui_redirects.py +26 -0
- package/latticeai/api/workflow_designer.py +39 -6
- package/latticeai/api/workspace.py +24 -10
- package/latticeai/app_factory.py +88 -17
- package/latticeai/brain/_kg_common.py +1123 -0
- package/latticeai/brain/discovery.py +1455 -0
- package/latticeai/brain/documents.py +218 -0
- package/latticeai/brain/ingest.py +644 -0
- package/latticeai/brain/projection.py +561 -0
- package/latticeai/brain/provenance.py +401 -0
- package/latticeai/brain/retrieval.py +1316 -0
- package/latticeai/brain/schema.py +640 -0
- package/latticeai/brain/store.py +216 -0
- package/latticeai/brain/write_master.py +225 -0
- package/latticeai/core/invitations.py +131 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/multi_agent.py +1 -1
- package/latticeai/core/policy.py +54 -0
- package/latticeai/core/realtime.py +65 -44
- package/latticeai/core/sessions.py +31 -5
- package/latticeai/core/users.py +147 -0
- package/latticeai/core/workspace_os.py +420 -20
- package/latticeai/services/agent_runtime.py +242 -4
- package/latticeai/services/run_executor.py +328 -0
- package/latticeai/services/workspace_service.py +27 -19
- package/package.json +2 -14
- package/scripts/lint_v3.mjs +23 -0
- package/static/v3/asset-manifest.json +21 -14
- package/static/v3/js/{app.356e6452.js → app.c5c80c46.js} +1 -1
- package/static/v3/js/core/{api.7a308b89.js → api.ba0fbf14.js} +58 -1
- package/static/v3/js/core/api.js +57 -0
- package/static/v3/js/core/i18n.880e1fec.js +575 -0
- package/static/v3/js/core/i18n.js +575 -0
- package/static/v3/js/core/routes.37522821.js +101 -0
- package/static/v3/js/core/routes.js +71 -63
- package/static/v3/js/core/{shell.a1657f20.js → shell.e3f6bbfa.js} +67 -38
- package/static/v3/js/core/shell.js +65 -36
- package/static/v3/js/core/{store.204a08b2.js → store.7b2aa044.js} +10 -0
- package/static/v3/js/core/store.js +10 -0
- package/static/v3/js/views/account.eff40715.js +143 -0
- package/static/v3/js/views/account.js +143 -0
- package/static/v3/js/views/activity.0d271ef9.js +67 -0
- package/static/v3/js/views/activity.js +67 -0
- package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
- package/static/v3/js/views/admin-users.js +4 -6
- package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
- package/static/v3/js/views/agents.js +35 -12
- package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
- package/static/v3/js/views/chat.js +23 -0
- package/static/v3/js/views/{knowledge-graph.5e40cbeb.js → knowledge-graph.4d09c537.js} +27 -7
- package/static/v3/js/views/knowledge-graph.js +27 -7
- package/static/v3/js/views/network.52a4f181.js +97 -0
- package/static/v3/js/views/network.js +97 -0
- package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
- package/static/v3/js/views/planning.js +26 -5
- package/static/v3/js/views/runs.b63b2afa.js +144 -0
- package/static/v3/js/views/runs.js +144 -0
- package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
- package/static/v3/js/views/settings.js +7 -8
- package/static/v3/js/views/snapshots.6f5db095.js +135 -0
- package/static/v3/js/views/snapshots.js +135 -0
- package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
- package/static/v3/js/views/workflows.js +87 -2
- package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
- package/static/v3/js/views/workspace-admin.js +156 -0
- package/static/account.html +0 -113
- package/static/activity.html +0 -73
- package/static/admin.html +0 -486
- package/static/agents.html +0 -139
- package/static/chat.html +0 -841
- package/static/css/reference/account.css +0 -439
- package/static/css/reference/admin.css +0 -610
- package/static/css/reference/base.css +0 -1661
- package/static/css/reference/chat.css +0 -4623
- package/static/css/reference/graph.css +0 -1016
- package/static/css/responsive.css +0 -861
- package/static/graph.html +0 -122
- package/static/platform.css +0 -104
- package/static/plugins.html +0 -136
- package/static/scripts/account.js +0 -238
- package/static/scripts/admin.js +0 -1614
- package/static/scripts/chat.js +0 -5081
- package/static/scripts/graph.js +0 -1804
- package/static/scripts/platform.js +0 -64
- package/static/scripts/ux.js +0 -167
- package/static/scripts/workspace.js +0 -948
- package/static/v3/js/core/routes.7222343d.js +0 -93
- package/static/workflows.html +0 -146
- package/static/workspace.css +0 -1121
- package/static/workspace.html +0 -357
|
@@ -10,12 +10,13 @@ import { store } from "./store.js";
|
|
|
10
10
|
import { api } from "./api.js";
|
|
11
11
|
import * as c from "./components.js";
|
|
12
12
|
import { createRouter } from "./router.js";
|
|
13
|
-
import { GROUPS,
|
|
13
|
+
import { GROUPS, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView, groupLabel, localizeRoute } from "./routes.js";
|
|
14
|
+
import { setI18nLanguage, t } from "./i18n.js";
|
|
14
15
|
|
|
15
16
|
const MODES = [
|
|
16
|
-
{ key: "basic",
|
|
17
|
-
{ key: "advanced",
|
|
18
|
-
{ key: "admin",
|
|
17
|
+
{ key: "basic", labelKey: "shell.mode.basic", icon: "circle" },
|
|
18
|
+
{ key: "advanced", labelKey: "shell.mode.advanced", icon: "circles" },
|
|
19
|
+
{ key: "admin", labelKey: "shell.mode.admin", icon: "shield-half" },
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
const ctxBase = { h, icon, api, store, c };
|
|
@@ -27,7 +28,7 @@ let currentRoute = null;
|
|
|
27
28
|
export function boot(rootEl) {
|
|
28
29
|
rootEl.classList.add("lt3-app");
|
|
29
30
|
rootEl.append(
|
|
30
|
-
h("a.lt3-skip", { href: "#lt3-view" }, "
|
|
31
|
+
h("a.lt3-skip", { href: "#lt3-view" }, t("shell.skip")),
|
|
31
32
|
h("div.lt3-rail__scrim", { on: { click: closeDrawer } }),
|
|
32
33
|
buildRail(),
|
|
33
34
|
buildMain(),
|
|
@@ -46,19 +47,19 @@ export function boot(rootEl) {
|
|
|
46
47
|
|
|
47
48
|
/* ── Rail ────────────────────────────────────────────────────────────────── */
|
|
48
49
|
function buildRail() {
|
|
49
|
-
return h("aside.lt3-rail", { id: "lt3-rail", "aria-label": "
|
|
50
|
+
return h("aside.lt3-rail", { id: "lt3-rail", "aria-label": t("shell.primary") },
|
|
50
51
|
h("div.lt3-rail__brand",
|
|
51
52
|
h("div.lt3-rail__logo", { html: latticeMark() }),
|
|
52
|
-
h("div.lt3-rail__word", h("b", "Lattice AI"), h("small", "
|
|
53
|
-
h("button.lt3-iconbtn.lt3-iconbtn--sm.lt3-rail__close", { "aria-label": "
|
|
53
|
+
h("div.lt3-rail__word", h("b", "Lattice AI"), h("small", t("shell.privateRuntime"))),
|
|
54
|
+
h("button.lt3-iconbtn.lt3-iconbtn--sm.lt3-rail__close", { "aria-label": t("shell.closeMenu"), on: { click: closeDrawer } }, icon("x")),
|
|
54
55
|
),
|
|
55
56
|
h("div.lt3-rail__scope", { id: "lt3-scope" }),
|
|
56
57
|
h("nav.lt3-rail__nav", { id: "lt3-nav", "aria-label": "Sections" }),
|
|
57
58
|
h("div.lt3-rail__foot",
|
|
58
59
|
h("div.lt3-rail__status", { id: "lt3-rail-status" }),
|
|
59
60
|
h("div.lt3-rail__foot-row",
|
|
60
|
-
h("button.lt3-rail__user", { id: "lt3-user", "aria-label": "
|
|
61
|
-
h("button.lt3-iconbtn", { id: "lt3-theme", "aria-label": "
|
|
61
|
+
h("button.lt3-rail__user", { id: "lt3-user", "aria-label": t("shell.account"), on: { click: () => router.navigate("account") } }),
|
|
62
|
+
h("button.lt3-iconbtn", { id: "lt3-theme", "aria-label": t("shell.toggleTheme"), title: t("shell.toggleTheme"), on: { click: () => store.toggleTheme() } }, icon("moon")),
|
|
62
63
|
),
|
|
63
64
|
),
|
|
64
65
|
);
|
|
@@ -73,7 +74,7 @@ function renderNav() {
|
|
|
73
74
|
const items = routes.filter((r) => r.group === group.id);
|
|
74
75
|
if (!items.length) continue;
|
|
75
76
|
const groupEl = h("div.lt3-navgroup",
|
|
76
|
-
h("div.lt3-navgroup__label", group
|
|
77
|
+
h("div.lt3-navgroup__label", groupLabel(group)),
|
|
77
78
|
items.map((r) => navItem(r)),
|
|
78
79
|
);
|
|
79
80
|
nav.append(groupEl);
|
|
@@ -109,7 +110,7 @@ function renderScope() {
|
|
|
109
110
|
els.scope.replaceChildren(
|
|
110
111
|
h("button.lt3-scope", { "aria-haspopup": "listbox", on: { click: openScopeMenu } },
|
|
111
112
|
h("div.lt3-scope__icon", icon(ws.type === "organization" ? "building-community" : "user")),
|
|
112
|
-
h("div.lt3-scope__meta", h("b", ws.name), h("small", `${ws.type} · ${ws.your_role || "member"}`)),
|
|
113
|
+
h("div.lt3-scope__meta", h("b", ws.name), h("small", `${ws.type} · ${ws.your_role || t("shell.member")}`)),
|
|
113
114
|
icon("selector"),
|
|
114
115
|
),
|
|
115
116
|
);
|
|
@@ -120,7 +121,7 @@ function renderUser() {
|
|
|
120
121
|
const initials = (u.nickname || u.email || "U").slice(0, 2);
|
|
121
122
|
els.user.replaceChildren(
|
|
122
123
|
h("span.lt3-avatar", initials),
|
|
123
|
-
h("div.lt3-rail__user-meta", h("b", u.nickname || u.email || "
|
|
124
|
+
h("div.lt3-rail__user-meta", h("b", u.nickname || u.email || t("shell.you")), h("small", u.role || t("shell.local"))),
|
|
124
125
|
);
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -134,17 +135,17 @@ function updateThemeIcon() {
|
|
|
134
135
|
function buildMain() {
|
|
135
136
|
return h("div.lt3-main",
|
|
136
137
|
h("header.lt3-topbar",
|
|
137
|
-
h("button.lt3-iconbtn.lt3-topbar__menu", { "aria-label": "
|
|
138
|
+
h("button.lt3-iconbtn.lt3-topbar__menu", { "aria-label": t("shell.openMenu"), on: { click: openDrawer } }, icon("menu-2")),
|
|
138
139
|
h("div.lt3-topbar__crumbs", { id: "lt3-crumbs" }),
|
|
139
140
|
h("div.lt3-spacer"),
|
|
140
|
-
h("button.lt3-cmd-trigger", { "aria-label": "
|
|
141
|
-
icon("search"), h("span", "
|
|
141
|
+
h("button.lt3-cmd-trigger", { "aria-label": t("shell.searchCommands"), on: { click: openPalette } },
|
|
142
|
+
icon("search"), h("span", { id: "lt3-cmd-text" }, t("shell.searchCommands")), h("span.lt3-kbd", "⌘K")),
|
|
142
143
|
h("div", { id: "lt3-idxchip" }),
|
|
143
|
-
h("div.lt3-mode", { id: "lt3-mode", role: "tablist", "aria-label": "
|
|
144
|
+
h("div.lt3-mode", { id: "lt3-mode", role: "tablist", "aria-label": t("shell.workspaceMode") },
|
|
144
145
|
MODES.map((m) => h("button", {
|
|
145
146
|
type: "button", role: "tab", dataset: { mode: m.key },
|
|
146
147
|
on: { click: () => store.setMode(m.key) },
|
|
147
|
-
}, icon(m.icon), h("span", m.
|
|
148
|
+
}, icon(m.icon), h("span", t(m.labelKey)))),
|
|
148
149
|
),
|
|
149
150
|
),
|
|
150
151
|
h("main.lt3-view", { id: "lt3-view", tabindex: "-1" },
|
|
@@ -155,13 +156,18 @@ function buildMain() {
|
|
|
155
156
|
|
|
156
157
|
function renderMode() {
|
|
157
158
|
$$("#lt3-mode button", els.root).forEach((b) => b.dataset.active = String(b.dataset.mode === store.get().mode));
|
|
159
|
+
$$("#lt3-mode button", els.root).forEach((b) => {
|
|
160
|
+
const mode = MODES.find((m) => m.key === b.dataset.mode);
|
|
161
|
+
const span = $("span", b);
|
|
162
|
+
if (mode && span) span.textContent = t(mode.labelKey);
|
|
163
|
+
});
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
function renderCrumbs() {
|
|
161
167
|
const r = currentRoute;
|
|
162
168
|
if (!r) return;
|
|
163
169
|
const parts = [h("span.lt3-crumb", store.activeWorkspace().name)];
|
|
164
|
-
if (r.group === "admin") parts.push(icon("chevron-right"), h("span.lt3-crumb", "
|
|
170
|
+
if (r.group === "admin") parts.push(icon("chevron-right"), h("span.lt3-crumb", t("shell.adminCrumb")));
|
|
165
171
|
parts.push(icon("chevron-right"), h("span.lt3-crumb.lt3-crumb--current", r.title || r.label));
|
|
166
172
|
els.crumbs.replaceChildren(...parts);
|
|
167
173
|
}
|
|
@@ -171,6 +177,23 @@ function renderIndexChip() {
|
|
|
171
177
|
renderRailStatus();
|
|
172
178
|
}
|
|
173
179
|
|
|
180
|
+
function renderChromeText() {
|
|
181
|
+
const skip = $(".lt3-skip", els.root);
|
|
182
|
+
if (skip) skip.textContent = t("shell.skip");
|
|
183
|
+
if (els.cmdText) els.cmdText.textContent = t("shell.searchCommands");
|
|
184
|
+
const rail = $("#lt3-rail", els.root);
|
|
185
|
+
if (rail) rail.setAttribute("aria-label", t("shell.primary"));
|
|
186
|
+
const mode = $("#lt3-mode", els.root);
|
|
187
|
+
if (mode) mode.setAttribute("aria-label", t("shell.workspaceMode"));
|
|
188
|
+
const user = $("#lt3-user", els.root);
|
|
189
|
+
if (user) user.setAttribute("aria-label", t("shell.account"));
|
|
190
|
+
const theme = $("#lt3-theme", els.root);
|
|
191
|
+
if (theme) {
|
|
192
|
+
theme.setAttribute("aria-label", t("shell.toggleTheme"));
|
|
193
|
+
theme.setAttribute("title", t("shell.toggleTheme"));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
174
197
|
function renderRailStatus() {
|
|
175
198
|
if (!els.railStatus) return;
|
|
176
199
|
const status = store.get().indexStatus;
|
|
@@ -181,21 +204,21 @@ function renderRailStatus() {
|
|
|
181
204
|
els.railStatus.replaceChildren(
|
|
182
205
|
h("div.lt3-rail__status-top",
|
|
183
206
|
h("span.lt3-rail__status-dot", { dataset: { state: unavailable ? "pending" : ready === keys.length ? "ready" : "partial" } }),
|
|
184
|
-
h("span", unavailable ? "
|
|
207
|
+
h("span", unavailable ? t("shell.indexPending") : t("shell.indexReady", { ready, total: keys.length })),
|
|
185
208
|
),
|
|
186
|
-
h("div.lt3-rail__status-sub", unavailable ? "
|
|
209
|
+
h("div.lt3-rail__status-sub", unavailable ? t("shell.startBackend") : t("shell.graphVectorHybrid")),
|
|
187
210
|
);
|
|
188
211
|
}
|
|
189
212
|
|
|
190
213
|
/* ── View rendering ─────────────────────────────────────────────────────── */
|
|
191
214
|
async function renderRoute({ key, params }) {
|
|
192
|
-
let route = ROUTE_BY_KEY[key] || ROUTE_BY_KEY.home;
|
|
215
|
+
let route = localizeRoute(ROUTE_BY_KEY[key] || ROUTE_BY_KEY.home);
|
|
193
216
|
// Deep-linking into an admin area surfaces Admin mode so the rail matches.
|
|
194
217
|
if (route.admin && store.get().mode !== "admin") store.setMode("admin");
|
|
195
218
|
currentRoute = route;
|
|
196
219
|
store.setRoute({ key: route.key, params });
|
|
197
220
|
|
|
198
|
-
document.title =
|
|
221
|
+
document.title = t("shell.documentTitle", { title: route.title || route.label });
|
|
199
222
|
markActive();
|
|
200
223
|
renderCrumbs();
|
|
201
224
|
|
|
@@ -213,7 +236,7 @@ async function renderRoute({ key, params }) {
|
|
|
213
236
|
outlet.replaceChildren(node);
|
|
214
237
|
} catch (err) {
|
|
215
238
|
console.error("[shell] view render failed:", route.view, err);
|
|
216
|
-
outlet.replaceChildren(c.errorState(
|
|
239
|
+
outlet.replaceChildren(c.errorState(t("shell.viewFailed", { label: route.label }), () => renderRoute({ key: route.key, params })));
|
|
217
240
|
}
|
|
218
241
|
}
|
|
219
242
|
|
|
@@ -230,6 +253,10 @@ function onStateChange(_state, change) {
|
|
|
230
253
|
case "user": renderUser(); break;
|
|
231
254
|
case "theme": updateThemeIcon(); break;
|
|
232
255
|
case "index": renderIndexChip(); break;
|
|
256
|
+
case "language":
|
|
257
|
+
setI18nLanguage(store.get().lang);
|
|
258
|
+
renderNav(); renderScope(); renderUser(); renderMode(); renderCrumbs(); renderIndexChip(); renderChromeText(); renderCurrent();
|
|
259
|
+
break;
|
|
233
260
|
}
|
|
234
261
|
}
|
|
235
262
|
|
|
@@ -249,8 +276,8 @@ function openScopeMenu(ev) {
|
|
|
249
276
|
w.workspace_id === store.get().workspaceId ? icon("check", "") : null,
|
|
250
277
|
)),
|
|
251
278
|
h("div.lt3-menu__sep"),
|
|
252
|
-
h("button.lt3-menu__item", { on: { click: () => { c.toast("
|
|
253
|
-
icon("plus"), "
|
|
279
|
+
h("button.lt3-menu__item", { on: { click: () => { c.toast(t("shell.orgCreationOpens"), "info"); closeMenus(); router.navigate("workspace-admin"); } } },
|
|
280
|
+
icon("plus"), t("shell.newOrganization")),
|
|
254
281
|
);
|
|
255
282
|
document.body.append(menu);
|
|
256
283
|
setTimeout(() => document.addEventListener("click", closeMenusOnce, { once: true }), 0);
|
|
@@ -267,16 +294,16 @@ function paletteItems() {
|
|
|
267
294
|
const mode = store.get().mode;
|
|
268
295
|
const currentRoutes = visibleRoutes(mode);
|
|
269
296
|
const nav = currentRoutes.map((r) => ({
|
|
270
|
-
group: "
|
|
297
|
+
group: t("shell.goTo"), label: r.title || r.label, icon: r.icon, hint: r.label === r.title ? groupLabel(GROUPS.find((g) => g.id === r.group) || { labelKey: r.group }) : r.label,
|
|
271
298
|
run: () => router.navigate(r.key),
|
|
272
299
|
}));
|
|
273
300
|
const actions = [
|
|
274
|
-
{ group: "
|
|
275
|
-
{ group: "
|
|
276
|
-
{ group: "
|
|
277
|
-
{ group: "
|
|
278
|
-
{ group: "
|
|
279
|
-
{ group: "
|
|
301
|
+
{ group: t("shell.actions"), label: t("shell.toggleLightDark"), icon: "contrast", run: () => store.toggleTheme() },
|
|
302
|
+
{ group: t("shell.actions"), label: `${t("common.status")}: ${t("shell.mode.basic")}`, icon: "circle", run: () => store.setMode("basic") },
|
|
303
|
+
{ group: t("shell.actions"), label: `${t("common.status")}: ${t("shell.mode.advanced")}`, icon: "circles", run: () => store.setMode("advanced") },
|
|
304
|
+
{ group: t("shell.actions"), label: `${t("common.status")}: ${t("shell.mode.admin")}`, icon: "shield-half", run: () => store.setMode("admin") },
|
|
305
|
+
{ group: t("shell.actions"), label: t("shell.newChat"), icon: "message-plus", run: () => router.navigate("chat", { new: "1" }) },
|
|
306
|
+
{ group: t("shell.actions"), label: t("shell.runHybridSearch"), icon: "arrows-join", run: () => router.navigate("hybrid-search") },
|
|
280
307
|
];
|
|
281
308
|
return [...nav, ...actions];
|
|
282
309
|
}
|
|
@@ -287,8 +314,8 @@ function openPalette() {
|
|
|
287
314
|
let active = 0, filtered = all;
|
|
288
315
|
|
|
289
316
|
const listEl = h("div.lt3-palette__list");
|
|
290
|
-
const input = h("input", { type: "text", placeholder: "
|
|
291
|
-
const palette = h("div.lt3-palette", { id: "lt3-palette", role: "dialog", "aria-modal": "true", "aria-label": "
|
|
317
|
+
const input = h("input", { type: "text", placeholder: t("shell.palettePlaceholder"), "aria-label": t("shell.commandPalette"), autocomplete: "off" });
|
|
318
|
+
const palette = h("div.lt3-palette", { id: "lt3-palette", role: "dialog", "aria-modal": "true", "aria-label": t("shell.commandPalette") },
|
|
292
319
|
h("div.lt3-palette__input", icon("search"), input, h("span.lt3-kbd", "Esc")),
|
|
293
320
|
listEl,
|
|
294
321
|
);
|
|
@@ -298,7 +325,7 @@ function openPalette() {
|
|
|
298
325
|
|
|
299
326
|
function renderList() {
|
|
300
327
|
listEl.replaceChildren();
|
|
301
|
-
if (!filtered.length) { listEl.append(h("div.lt3-palette__empty", "
|
|
328
|
+
if (!filtered.length) { listEl.append(h("div.lt3-palette__empty", t("shell.noMatches"))); return; }
|
|
302
329
|
let lastGroup = null;
|
|
303
330
|
filtered.forEach((item, i) => {
|
|
304
331
|
if (item.group !== lastGroup) { listEl.append(h("div.lt3-palette__group-label", item.group)); lastGroup = item.group; }
|
|
@@ -366,6 +393,7 @@ function cacheEls(root) {
|
|
|
366
393
|
theme: $("#lt3-theme", root),
|
|
367
394
|
crumbs: $("#lt3-crumbs", root),
|
|
368
395
|
idxchip: $("#lt3-idxchip", root),
|
|
396
|
+
cmdText: $("#lt3-cmd-text", root),
|
|
369
397
|
railStatus: $("#lt3-rail-status", root),
|
|
370
398
|
outlet: $("#lt3-outlet", root),
|
|
371
399
|
view: $("#lt3-view", root),
|
|
@@ -377,6 +405,7 @@ function cacheEls(root) {
|
|
|
377
405
|
updateThemeIcon();
|
|
378
406
|
renderIndexChip();
|
|
379
407
|
renderRailStatus();
|
|
408
|
+
renderChromeText();
|
|
380
409
|
}
|
|
381
410
|
|
|
382
411
|
function latticeMark() {
|
|
@@ -9,6 +9,7 @@ const LS = {
|
|
|
9
9
|
theme: "lt-theme", // shared with the rest of Lattice (data-lt-theme)
|
|
10
10
|
mode: "lt3-mode",
|
|
11
11
|
workspace: "lt3-workspace",
|
|
12
|
+
lang: "lt3-lang",
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
function load(key, fallback) {
|
|
@@ -23,6 +24,7 @@ const VALID_MODES = ["basic", "advanced", "admin"];
|
|
|
23
24
|
|
|
24
25
|
const state = {
|
|
25
26
|
theme: load(LS.theme, ""), // "" → follow OS
|
|
27
|
+
lang: load(LS.lang, "en"),
|
|
26
28
|
mode: VALID_MODES.includes(load(LS.mode)) ? load(LS.mode) : "basic",
|
|
27
29
|
workspaceId: load(LS.workspace, "personal"),
|
|
28
30
|
workspaces: [
|
|
@@ -67,6 +69,14 @@ export const store = {
|
|
|
67
69
|
store.setTheme(effective === "dark" ? "light" : "dark");
|
|
68
70
|
},
|
|
69
71
|
|
|
72
|
+
setLang(lang) {
|
|
73
|
+
const value = lang === "ko" ? "ko" : "en";
|
|
74
|
+
if (value === state.lang) return;
|
|
75
|
+
state.lang = value;
|
|
76
|
+
save(LS.lang, value);
|
|
77
|
+
emit({ type: "language" });
|
|
78
|
+
},
|
|
79
|
+
|
|
70
80
|
/* ── Mode ──────────────────────────────────────────────── */
|
|
71
81
|
setMode(mode) {
|
|
72
82
|
if (!VALID_MODES.includes(mode) || mode === state.mode) return;
|
|
@@ -9,6 +9,7 @@ const LS = {
|
|
|
9
9
|
theme: "lt-theme", // shared with the rest of Lattice (data-lt-theme)
|
|
10
10
|
mode: "lt3-mode",
|
|
11
11
|
workspace: "lt3-workspace",
|
|
12
|
+
lang: "lt3-lang",
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
function load(key, fallback) {
|
|
@@ -23,6 +24,7 @@ const VALID_MODES = ["basic", "advanced", "admin"];
|
|
|
23
24
|
|
|
24
25
|
const state = {
|
|
25
26
|
theme: load(LS.theme, ""), // "" → follow OS
|
|
27
|
+
lang: load(LS.lang, "en"),
|
|
26
28
|
mode: VALID_MODES.includes(load(LS.mode)) ? load(LS.mode) : "basic",
|
|
27
29
|
workspaceId: load(LS.workspace, "personal"),
|
|
28
30
|
workspaces: [
|
|
@@ -67,6 +69,14 @@ export const store = {
|
|
|
67
69
|
store.setTheme(effective === "dark" ? "light" : "dark");
|
|
68
70
|
},
|
|
69
71
|
|
|
72
|
+
setLang(lang) {
|
|
73
|
+
const value = lang === "ko" ? "ko" : "en";
|
|
74
|
+
if (value === state.lang) return;
|
|
75
|
+
state.lang = value;
|
|
76
|
+
save(LS.lang, value);
|
|
77
|
+
emit({ type: "language" });
|
|
78
|
+
},
|
|
79
|
+
|
|
70
80
|
/* ── Mode ──────────────────────────────────────────────── */
|
|
71
81
|
setMode(mode) {
|
|
72
82
|
if (!VALID_MODES.includes(mode) || mode === state.mode) return;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { t } from "../core/i18n.880e1fec.js";
|
|
2
|
+
|
|
3
|
+
export async function render(ctx) {
|
|
4
|
+
const { h, icon, api, store, c, toast } = ctx;
|
|
5
|
+
const host = h("div.lt3-stack-6", c.loading({ lines: 4 }));
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
const [profile, sso] = await Promise.all([api.profile(), api.ssoConfig()]);
|
|
9
|
+
host.replaceChildren(
|
|
10
|
+
c.viewHeader({
|
|
11
|
+
eyebrow: t("account.eyebrow"),
|
|
12
|
+
title: t("account.title"),
|
|
13
|
+
sub: t("account.sub"),
|
|
14
|
+
actions: [c.sourceBadge(profile.ok ? "live" : "unavailable")],
|
|
15
|
+
}),
|
|
16
|
+
profile.ok && profile.data ? signedInPanel(profile.data, sso.data || {}) : authPanel(sso.data || {}),
|
|
17
|
+
);
|
|
18
|
+
if (profile.ok && profile.data) {
|
|
19
|
+
store.setUser({
|
|
20
|
+
email: profile.data.email || "",
|
|
21
|
+
nickname: profile.data.nickname || profile.data.name || profile.data.email || t("shell.you"),
|
|
22
|
+
role: profile.data.role || "user",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function authPanel(sso) {
|
|
28
|
+
const email = input("email", t("account.email"));
|
|
29
|
+
const password = input("password", t("account.password"));
|
|
30
|
+
const regEmail = input("email", t("account.email"));
|
|
31
|
+
const regName = input("text", t("account.name"));
|
|
32
|
+
const regNick = input("text", t("account.nickname"));
|
|
33
|
+
const regPassword = input("password", t("account.password"));
|
|
34
|
+
|
|
35
|
+
async function doLogin() {
|
|
36
|
+
const res = await api.login(email.value.trim(), password.value);
|
|
37
|
+
toast(resultText(res, t("account.loginOk")), res.ok ? "ok" : "err");
|
|
38
|
+
if (res.ok) load();
|
|
39
|
+
}
|
|
40
|
+
async function doRegister() {
|
|
41
|
+
const res = await api.register({
|
|
42
|
+
email: regEmail.value.trim(),
|
|
43
|
+
password: regPassword.value,
|
|
44
|
+
name: regName.value.trim(),
|
|
45
|
+
nickname: regNick.value.trim(),
|
|
46
|
+
});
|
|
47
|
+
toast(resultText(res, t("account.registerOk")), res.ok ? "ok" : "err");
|
|
48
|
+
if (res.ok) load();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return h("div.lt3-grid-2",
|
|
52
|
+
c.panel({
|
|
53
|
+
title: t("account.login"),
|
|
54
|
+
children: h("div.lt3-stack-4",
|
|
55
|
+
field(ctx, t("account.email"), email),
|
|
56
|
+
field(ctx, t("account.password"), password),
|
|
57
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: doLogin } }, icon("login"), t("account.login")),
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
c.panel({
|
|
61
|
+
title: t("account.register"),
|
|
62
|
+
sub: t("account.passwordRule"),
|
|
63
|
+
children: h("div.lt3-stack-4",
|
|
64
|
+
field(ctx, t("account.email"), regEmail),
|
|
65
|
+
field(ctx, t("account.name"), regName),
|
|
66
|
+
field(ctx, t("account.nickname"), regNick),
|
|
67
|
+
field(ctx, t("account.password"), regPassword),
|
|
68
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: doRegister } }, icon("user-plus"), t("account.register")),
|
|
69
|
+
sso && sso.enabled ? c.banner(t("account.sso"), "info") : null,
|
|
70
|
+
),
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function signedInPanel(profile, sso) {
|
|
76
|
+
const name = input("text", t("account.name"), profile.name || "");
|
|
77
|
+
const nick = input("text", t("account.nickname"), profile.nickname || "");
|
|
78
|
+
const current = input("password", t("account.currentPassword"));
|
|
79
|
+
const next = input("password", t("account.newPassword"));
|
|
80
|
+
|
|
81
|
+
async function saveProfile() {
|
|
82
|
+
const res = await api.updateProfile({ name: name.value.trim(), nickname: nick.value.trim() });
|
|
83
|
+
toast(resultText(res, t("account.profileOk")), res.ok ? "ok" : "err");
|
|
84
|
+
if (res.ok) load();
|
|
85
|
+
}
|
|
86
|
+
async function savePassword() {
|
|
87
|
+
const res = await api.changePassword(current.value, next.value);
|
|
88
|
+
toast(resultText(res, t("account.passwordOk")), res.ok ? "ok" : "err");
|
|
89
|
+
if (res.ok) { current.value = ""; next.value = ""; }
|
|
90
|
+
}
|
|
91
|
+
async function doLogout() {
|
|
92
|
+
const res = await api.logout();
|
|
93
|
+
toast(resultText(res, t("account.logoutOk")), res.ok ? "ok" : "err");
|
|
94
|
+
if (res.ok) load();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return h("div.lt3-grid-2",
|
|
98
|
+
c.panel({
|
|
99
|
+
title: t("account.profile"),
|
|
100
|
+
actions: [c.statePill(t("account.signedIn"))],
|
|
101
|
+
children: h("div.lt3-stack-4",
|
|
102
|
+
h("dl.lt3-keyval",
|
|
103
|
+
h("dt", t("account.email")), h("dd", h("span.lt3-mono", profile.email || "—")),
|
|
104
|
+
h("dt", t("common.role")), h("dd", c.pill(profile.role || "user", "info")),
|
|
105
|
+
h("dt", t("account.sso")), h("dd", c.statePill(sso && sso.enabled ? "ready" : "idle")),
|
|
106
|
+
),
|
|
107
|
+
field(ctx, t("account.name"), name),
|
|
108
|
+
field(ctx, t("account.nickname"), nick),
|
|
109
|
+
h("div.lt3-row-2",
|
|
110
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: saveProfile } }, icon("device-floppy"), t("common.save")),
|
|
111
|
+
h("button.lt3-btn.lt3-btn--ghost", { on: { click: doLogout } }, icon("logout"), t("account.logout")),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
}),
|
|
115
|
+
c.panel({
|
|
116
|
+
title: t("account.changePassword"),
|
|
117
|
+
sub: t("account.passwordRule"),
|
|
118
|
+
children: h("div.lt3-stack-4",
|
|
119
|
+
field(ctx, t("account.currentPassword"), current),
|
|
120
|
+
field(ctx, t("account.newPassword"), next),
|
|
121
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: savePassword } }, icon("key"), t("account.changePassword")),
|
|
122
|
+
),
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await load();
|
|
128
|
+
return host;
|
|
129
|
+
|
|
130
|
+
function input(type, label, value = "") {
|
|
131
|
+
return h("input.lt3-input", { type, value, autocomplete: type === "password" ? "current-password" : "on", "aria-label": label });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function field({ h }, label, control) {
|
|
136
|
+
return h("div.lt3-field", h("label.lt3-label", label), control);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resultText(res, okText) {
|
|
140
|
+
if (res && res.ok) return okText;
|
|
141
|
+
const data = (res && res.data) || {};
|
|
142
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
143
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { t } from "../core/i18n.js";
|
|
2
|
+
|
|
3
|
+
export async function render(ctx) {
|
|
4
|
+
const { h, icon, api, store, c, toast } = ctx;
|
|
5
|
+
const host = h("div.lt3-stack-6", c.loading({ lines: 4 }));
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
const [profile, sso] = await Promise.all([api.profile(), api.ssoConfig()]);
|
|
9
|
+
host.replaceChildren(
|
|
10
|
+
c.viewHeader({
|
|
11
|
+
eyebrow: t("account.eyebrow"),
|
|
12
|
+
title: t("account.title"),
|
|
13
|
+
sub: t("account.sub"),
|
|
14
|
+
actions: [c.sourceBadge(profile.ok ? "live" : "unavailable")],
|
|
15
|
+
}),
|
|
16
|
+
profile.ok && profile.data ? signedInPanel(profile.data, sso.data || {}) : authPanel(sso.data || {}),
|
|
17
|
+
);
|
|
18
|
+
if (profile.ok && profile.data) {
|
|
19
|
+
store.setUser({
|
|
20
|
+
email: profile.data.email || "",
|
|
21
|
+
nickname: profile.data.nickname || profile.data.name || profile.data.email || t("shell.you"),
|
|
22
|
+
role: profile.data.role || "user",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function authPanel(sso) {
|
|
28
|
+
const email = input("email", t("account.email"));
|
|
29
|
+
const password = input("password", t("account.password"));
|
|
30
|
+
const regEmail = input("email", t("account.email"));
|
|
31
|
+
const regName = input("text", t("account.name"));
|
|
32
|
+
const regNick = input("text", t("account.nickname"));
|
|
33
|
+
const regPassword = input("password", t("account.password"));
|
|
34
|
+
|
|
35
|
+
async function doLogin() {
|
|
36
|
+
const res = await api.login(email.value.trim(), password.value);
|
|
37
|
+
toast(resultText(res, t("account.loginOk")), res.ok ? "ok" : "err");
|
|
38
|
+
if (res.ok) load();
|
|
39
|
+
}
|
|
40
|
+
async function doRegister() {
|
|
41
|
+
const res = await api.register({
|
|
42
|
+
email: regEmail.value.trim(),
|
|
43
|
+
password: regPassword.value,
|
|
44
|
+
name: regName.value.trim(),
|
|
45
|
+
nickname: regNick.value.trim(),
|
|
46
|
+
});
|
|
47
|
+
toast(resultText(res, t("account.registerOk")), res.ok ? "ok" : "err");
|
|
48
|
+
if (res.ok) load();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return h("div.lt3-grid-2",
|
|
52
|
+
c.panel({
|
|
53
|
+
title: t("account.login"),
|
|
54
|
+
children: h("div.lt3-stack-4",
|
|
55
|
+
field(ctx, t("account.email"), email),
|
|
56
|
+
field(ctx, t("account.password"), password),
|
|
57
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: doLogin } }, icon("login"), t("account.login")),
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
c.panel({
|
|
61
|
+
title: t("account.register"),
|
|
62
|
+
sub: t("account.passwordRule"),
|
|
63
|
+
children: h("div.lt3-stack-4",
|
|
64
|
+
field(ctx, t("account.email"), regEmail),
|
|
65
|
+
field(ctx, t("account.name"), regName),
|
|
66
|
+
field(ctx, t("account.nickname"), regNick),
|
|
67
|
+
field(ctx, t("account.password"), regPassword),
|
|
68
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: doRegister } }, icon("user-plus"), t("account.register")),
|
|
69
|
+
sso && sso.enabled ? c.banner(t("account.sso"), "info") : null,
|
|
70
|
+
),
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function signedInPanel(profile, sso) {
|
|
76
|
+
const name = input("text", t("account.name"), profile.name || "");
|
|
77
|
+
const nick = input("text", t("account.nickname"), profile.nickname || "");
|
|
78
|
+
const current = input("password", t("account.currentPassword"));
|
|
79
|
+
const next = input("password", t("account.newPassword"));
|
|
80
|
+
|
|
81
|
+
async function saveProfile() {
|
|
82
|
+
const res = await api.updateProfile({ name: name.value.trim(), nickname: nick.value.trim() });
|
|
83
|
+
toast(resultText(res, t("account.profileOk")), res.ok ? "ok" : "err");
|
|
84
|
+
if (res.ok) load();
|
|
85
|
+
}
|
|
86
|
+
async function savePassword() {
|
|
87
|
+
const res = await api.changePassword(current.value, next.value);
|
|
88
|
+
toast(resultText(res, t("account.passwordOk")), res.ok ? "ok" : "err");
|
|
89
|
+
if (res.ok) { current.value = ""; next.value = ""; }
|
|
90
|
+
}
|
|
91
|
+
async function doLogout() {
|
|
92
|
+
const res = await api.logout();
|
|
93
|
+
toast(resultText(res, t("account.logoutOk")), res.ok ? "ok" : "err");
|
|
94
|
+
if (res.ok) load();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return h("div.lt3-grid-2",
|
|
98
|
+
c.panel({
|
|
99
|
+
title: t("account.profile"),
|
|
100
|
+
actions: [c.statePill(t("account.signedIn"))],
|
|
101
|
+
children: h("div.lt3-stack-4",
|
|
102
|
+
h("dl.lt3-keyval",
|
|
103
|
+
h("dt", t("account.email")), h("dd", h("span.lt3-mono", profile.email || "—")),
|
|
104
|
+
h("dt", t("common.role")), h("dd", c.pill(profile.role || "user", "info")),
|
|
105
|
+
h("dt", t("account.sso")), h("dd", c.statePill(sso && sso.enabled ? "ready" : "idle")),
|
|
106
|
+
),
|
|
107
|
+
field(ctx, t("account.name"), name),
|
|
108
|
+
field(ctx, t("account.nickname"), nick),
|
|
109
|
+
h("div.lt3-row-2",
|
|
110
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: saveProfile } }, icon("device-floppy"), t("common.save")),
|
|
111
|
+
h("button.lt3-btn.lt3-btn--ghost", { on: { click: doLogout } }, icon("logout"), t("account.logout")),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
}),
|
|
115
|
+
c.panel({
|
|
116
|
+
title: t("account.changePassword"),
|
|
117
|
+
sub: t("account.passwordRule"),
|
|
118
|
+
children: h("div.lt3-stack-4",
|
|
119
|
+
field(ctx, t("account.currentPassword"), current),
|
|
120
|
+
field(ctx, t("account.newPassword"), next),
|
|
121
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: savePassword } }, icon("key"), t("account.changePassword")),
|
|
122
|
+
),
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await load();
|
|
128
|
+
return host;
|
|
129
|
+
|
|
130
|
+
function input(type, label, value = "") {
|
|
131
|
+
return h("input.lt3-input", { type, value, autocomplete: type === "password" ? "current-password" : "on", "aria-label": label });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function field({ h }, label, control) {
|
|
136
|
+
return h("div.lt3-field", h("label.lt3-label", label), control);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resultText(res, okText) {
|
|
140
|
+
if (res && res.ok) return okText;
|
|
141
|
+
const data = (res && res.data) || {};
|
|
142
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
143
|
+
}
|