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
|
@@ -11,6 +11,7 @@ export async function render(ctx) {
|
|
|
11
11
|
const { h, c } = ctx;
|
|
12
12
|
const defsHost = h("div", c.loading({ lines: 3, block: true }));
|
|
13
13
|
const runsHost = h("div", c.loading({ lines: 3 }));
|
|
14
|
+
const triggerHost = h("div", c.loading({ lines: 2 }));
|
|
14
15
|
const defsSrc = h("span", c.sourceBadge("pending"));
|
|
15
16
|
const runsSrc = h("span", c.sourceBadge("pending"));
|
|
16
17
|
|
|
@@ -21,6 +22,12 @@ export async function render(ctx) {
|
|
|
21
22
|
sub: "Repeatable automation: a trigger fires an agent chain that calls tools, reads and writes memory, and produces a result.",
|
|
22
23
|
}),
|
|
23
24
|
stageLegend(ctx),
|
|
25
|
+
c.panel({
|
|
26
|
+
eyebrow: "Triggers",
|
|
27
|
+
title: "Trigger status",
|
|
28
|
+
sub: "Interval and brain-event triggers armed from saved workflow definitions.",
|
|
29
|
+
children: triggerHost,
|
|
30
|
+
}),
|
|
24
31
|
h("section", c.sectionHead("Workflow definitions", defsSrc), defsHost),
|
|
25
32
|
c.panel({
|
|
26
33
|
head: h("div.lt3-row", { style: { "justify-content": "space-between", width: "100%" } },
|
|
@@ -30,6 +37,7 @@ export async function render(ctx) {
|
|
|
30
37
|
);
|
|
31
38
|
|
|
32
39
|
loadDefs();
|
|
40
|
+
loadTriggers();
|
|
33
41
|
loadRuns();
|
|
34
42
|
return root;
|
|
35
43
|
|
|
@@ -59,6 +67,7 @@ export async function render(ctx) {
|
|
|
59
67
|
),
|
|
60
68
|
w.description ? h("p.lt3-muted", { style: { margin: 0, "font-size": "var(--lt3-text-sm)" } }, w.description) : null,
|
|
61
69
|
h("div.lt3-cluster", (nodes.slice(0, 6)).map((n) => h("span.lt3-chip", c.icon(nodeIcon(n.type)), n.name || n.type))),
|
|
70
|
+
triggerControls(ctx2, w, nodes),
|
|
62
71
|
h("div.lt3-row-2",
|
|
63
72
|
h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => runDef(ctx2, w) } }, c.icon("player-play"), "Run"),
|
|
64
73
|
h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)" } }, `${triggers} trigger${triggers === 1 ? "" : "s"}`),
|
|
@@ -85,7 +94,13 @@ export async function render(ctx) {
|
|
|
85
94
|
{ key: "status", label: "Status", width: "1%", render: (r) => c.statePill(mapStatus(r.status)) },
|
|
86
95
|
{ key: "name", label: "Workflow", render: (r) => h("b", { style: { "font-size": "var(--lt3-text-sm)" } }, r.workflow_name || r.workflow_id || r.id) },
|
|
87
96
|
{ key: "when", label: "When", width: "1%", render: (r) => h("span.lt3-faint", { style: { "white-space": "nowrap", "font-size": "var(--lt3-text-2xs)" } }, fmtTime(r.created_at || r.started_at)) },
|
|
88
|
-
{ key: "act", label: "", width: "1%", render: (r) => h("
|
|
97
|
+
{ key: "act", label: "", width: "1%", render: (r) => h("div.lt3-row-2",
|
|
98
|
+
isActiveStatus(r.status) ? h("button.lt3-btn.lt3-btn--danger.lt3-btn--sm", { on: { click: () => stop(ctx, r) } }, c.icon("player-stop"), "Stop") : null,
|
|
99
|
+
String(r.status || "").toLowerCase() === "awaiting_approval"
|
|
100
|
+
? h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => decide(ctx, r, true) } }, c.icon("circle-check"), "Approve")
|
|
101
|
+
: null,
|
|
102
|
+
h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => replay(ctx, r) } }, c.icon("player-track-next"), "Replay"),
|
|
103
|
+
) },
|
|
89
104
|
],
|
|
90
105
|
runs.slice(0, 20),
|
|
91
106
|
));
|
|
@@ -96,6 +111,71 @@ export async function render(ctx) {
|
|
|
96
111
|
const res = await ctx2.api.workflowReplay(id);
|
|
97
112
|
ctx2.toast(res && res.ok ? `Replay ready for ${id}` : "Replay unavailable", res && res.ok ? "ok" : "err");
|
|
98
113
|
}
|
|
114
|
+
|
|
115
|
+
async function stop(ctx2, r) {
|
|
116
|
+
const id = r.id || r.run_id;
|
|
117
|
+
const res = await ctx2.api.stopWorkflowRun(id);
|
|
118
|
+
ctx2.toast(res && res.ok ? `Stop requested for ${id}` : "Stop unavailable", res && res.ok ? "ok" : "err");
|
|
119
|
+
loadRuns();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function decide(ctx2, r, approved) {
|
|
123
|
+
const id = r.id || r.run_id;
|
|
124
|
+
const res = await ctx2.api.resumeWorkflowRun(id, approved);
|
|
125
|
+
ctx2.toast(res && res.ok ? `Decision recorded for ${id}` : "Decision unavailable", res && res.ok ? "ok" : "err");
|
|
126
|
+
loadRuns();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function loadTriggers() {
|
|
130
|
+
const res = await ctx.api.workflowTriggers();
|
|
131
|
+
const armed = Array.isArray(res.data?.armed) ? res.data.armed : [];
|
|
132
|
+
triggerHost.replaceChildren(
|
|
133
|
+
h("div.lt3-stack-3",
|
|
134
|
+
h("div.lt3-row-2", c.sourceBadge(res.source), c.statePill(res.data?.running ? "running" : "idle")),
|
|
135
|
+
armed.length ? c.table([
|
|
136
|
+
{ key: "name", label: "Workflow", render: (r) => h("b", r.name || r.workflow_id) },
|
|
137
|
+
{ key: "kind", label: "Trigger", width: "1%", render: (r) => c.pill(r.kind) },
|
|
138
|
+
{ key: "last", label: "Last fired", width: "1%", render: (r) => fmtTime(r.last_fired_at ? Number(r.last_fired_at) * 1000 : null) },
|
|
139
|
+
{ key: "events", label: "Recent", render: (r) => (r.recent_events || []).slice(-2).map((e) => e.type || e.trigger).join(", ") || "—" },
|
|
140
|
+
], armed) : c.emptyState({ icon: "bolt-off", title: "No triggers armed", body: "Set a workflow trigger below to arm it." }),
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function triggerControls(ctx2, w, nodes) {
|
|
146
|
+
const trigger = nodes.find((n) => n.type === "trigger") || {};
|
|
147
|
+
const cfg = trigger.config || {};
|
|
148
|
+
const kind = h("select.lt3-select", { "aria-label": "Trigger type" },
|
|
149
|
+
["manual", "interval", "brain_event"].map((value) => h("option", { value, selected: String(cfg.trigger || "manual") === value }, value)));
|
|
150
|
+
const seconds = h("input.lt3-input", { type: "number", min: "60", step: "60", value: String(cfg.interval_seconds || 300), "aria-label": "Interval seconds" });
|
|
151
|
+
const sourceType = h("input.lt3-input", { type: "text", value: cfg.source_type || "", placeholder: "source_type", "aria-label": "source_type" });
|
|
152
|
+
async function save() {
|
|
153
|
+
const updated = ensureTrigger(nodes, kind.value, Number(seconds.value) || 300, sourceType.value.trim());
|
|
154
|
+
const res = await ctx2.api.updateWorkflow(w.id, { nodes: updated, metadata: { trigger_updated_at: new Date().toISOString() } });
|
|
155
|
+
ctx2.toast(res && res.ok ? "Trigger saved" : "Trigger update unavailable", res && res.ok ? "ok" : "err");
|
|
156
|
+
if (res && res.ok) { loadDefs(); loadTriggers(); }
|
|
157
|
+
}
|
|
158
|
+
return h("div.lt3-stack-2",
|
|
159
|
+
h("div.lt3-eyebrow", "Trigger configuration"),
|
|
160
|
+
h("div.lt3-row-2", kind, seconds, sourceType, h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: save } }, c.icon("device-floppy"), "Save")),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ensureTrigger(nodes, triggerKind, intervalSeconds, sourceType) {
|
|
166
|
+
const clone = (nodes || []).map((node) => ({ ...node, config: { ...(node.config || {}) } }));
|
|
167
|
+
let trigger = clone.find((n) => n.type === "trigger");
|
|
168
|
+
if (!trigger) {
|
|
169
|
+
trigger = { id: "trigger", type: "trigger", name: "Trigger", next: clone[0]?.id || "output", config: {} };
|
|
170
|
+
clone.unshift(trigger);
|
|
171
|
+
if (!clone.some((n) => n.id === "output")) clone.push({ id: "output", type: "output", name: "Output", config: {}, next: null });
|
|
172
|
+
}
|
|
173
|
+
trigger.config.trigger = triggerKind;
|
|
174
|
+
delete trigger.config.interval_seconds;
|
|
175
|
+
delete trigger.config.source_type;
|
|
176
|
+
if (triggerKind === "interval") trigger.config.interval_seconds = Math.max(60, intervalSeconds || 300);
|
|
177
|
+
if (triggerKind === "brain_event" && sourceType) trigger.config.source_type = sourceType;
|
|
178
|
+
return clone;
|
|
99
179
|
}
|
|
100
180
|
|
|
101
181
|
function normalizeDefs(data) {
|
|
@@ -118,9 +198,14 @@ function mapStatus(s) {
|
|
|
118
198
|
const v = String(s || "").toLowerCase();
|
|
119
199
|
if (v === "ok" || v === "completed" || v === "success") return "ready";
|
|
120
200
|
if (v === "failed" || v === "error") return "failed";
|
|
121
|
-
if (v === "running") return "active";
|
|
201
|
+
if (v === "running" || v === "queued" || v === "cancelling") return "active";
|
|
202
|
+
if (v === "awaiting_approval") return "pending";
|
|
203
|
+
if (v === "cancelled" || v === "interrupted") return "warn";
|
|
122
204
|
return v || "idle";
|
|
123
205
|
}
|
|
206
|
+
function isActiveStatus(status) {
|
|
207
|
+
return ["running", "queued", "in_progress", "cancelling"].includes(String(status || "").toLowerCase());
|
|
208
|
+
}
|
|
124
209
|
function fmtTime(ts) {
|
|
125
210
|
if (!ts) return "—";
|
|
126
211
|
try { const d = new Date(ts); return Number.isNaN(d.getTime()) ? String(ts) : d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); }
|
|
@@ -0,0 +1,156 @@
|
|
|
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: 5, block: true }));
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
const [registry, invites] = await Promise.all([api.workspaceRegistry(), api.invitations()]);
|
|
9
|
+
const data = registry.data || {};
|
|
10
|
+
const workspaces = Array.isArray(data.workspaces) ? data.workspaces : [];
|
|
11
|
+
store.setWorkspaces(workspaces.length ? workspaces : store.get().workspaces);
|
|
12
|
+
host.replaceChildren(
|
|
13
|
+
c.viewHeader({
|
|
14
|
+
eyebrow: t("workspace.eyebrow"),
|
|
15
|
+
title: t("workspace.title"),
|
|
16
|
+
sub: t("workspace.sub"),
|
|
17
|
+
actions: [c.sourceBadge(registry.source)],
|
|
18
|
+
}),
|
|
19
|
+
createOrgPanel(),
|
|
20
|
+
workspaceGrid(workspaces, data),
|
|
21
|
+
invitationsPanel(invites),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createOrgPanel() {
|
|
26
|
+
const name = h("input.lt3-input", { type: "text", "aria-label": t("workspace.orgName"), placeholder: t("workspace.orgName") });
|
|
27
|
+
return c.panel({
|
|
28
|
+
title: t("workspace.createOrg"),
|
|
29
|
+
children: h("div.lt3-row-2",
|
|
30
|
+
name,
|
|
31
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: async () => {
|
|
32
|
+
if (!name.value.trim()) return;
|
|
33
|
+
const res = await api.createOrg(name.value.trim());
|
|
34
|
+
toast(resultText(res, t("workspace.createOrg")), res.ok ? "ok" : "err");
|
|
35
|
+
if (res.ok) { name.value = ""; load(); }
|
|
36
|
+
} } }, icon("plus"), t("workspace.createOrg")),
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function workspaceGrid(workspaces, registry) {
|
|
42
|
+
if (!workspaces.length) {
|
|
43
|
+
return c.emptyState({ icon: "building-community", title: t("workspace.title"), body: t("common.unavailable") });
|
|
44
|
+
}
|
|
45
|
+
return h("div.lt3-grid-auto", workspaces.map((ws) => workspaceCard(ws, registry)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function workspaceCard(ws, registry) {
|
|
49
|
+
const roleOptions = registry.roles || ["owner", "admin", "member", "viewer"];
|
|
50
|
+
const members = Array.isArray(ws.members) ? ws.members : [];
|
|
51
|
+
const userId = h("input.lt3-input", { type: "text", placeholder: t("workspace.userId"), "aria-label": t("workspace.userId") });
|
|
52
|
+
const role = h("select.lt3-select", { "aria-label": t("common.role") }, roleOptions.map((r) => h("option", { value: r }, roleLabel(r))));
|
|
53
|
+
return c.card(h("div.lt3-stack-4",
|
|
54
|
+
h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", gap: "var(--lt3-space-3)" } },
|
|
55
|
+
h("div",
|
|
56
|
+
h("b", ws.name || ws.workspace_id),
|
|
57
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, ws.workspace_id),
|
|
58
|
+
),
|
|
59
|
+
c.pill(roleLabel(ws.your_role || "member"), "info"),
|
|
60
|
+
),
|
|
61
|
+
h("dl.lt3-keyval",
|
|
62
|
+
h("dt", t("common.type")), h("dd", ws.type || "—"),
|
|
63
|
+
h("dt", t("common.status")), h("dd", c.statePill(ws.status || "active")),
|
|
64
|
+
h("dt", t("workspace.members")), h("dd", String(ws.member_count ?? members.length)),
|
|
65
|
+
),
|
|
66
|
+
h("div.lt3-row-2",
|
|
67
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => activate(ws.workspace_id) } }, icon("selector"), t("workspace.activate")),
|
|
68
|
+
ws.type === "organization" ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => archive(ws.workspace_id) } }, icon("archive"), t("workspace.archive")) : null,
|
|
69
|
+
),
|
|
70
|
+
ws.type === "organization" ? h("div.lt3-stack-3",
|
|
71
|
+
h("div.lt3-eyebrow", t("workspace.members")),
|
|
72
|
+
members.length ? c.table([
|
|
73
|
+
{ key: "user", label: t("workspace.userId"), render: (m) => h("span.lt3-mono", m.user_id || "—") },
|
|
74
|
+
{ key: "role", label: t("common.role"), width: "1%", render: (m) => c.pill(roleLabel(m.role)) },
|
|
75
|
+
{ key: "act", label: "", width: "1%", render: (m) => h("button.lt3-iconbtn.lt3-iconbtn--sm", { "aria-label": t("common.cancel"), on: { click: () => removeMember(ws.workspace_id, m.user_id) } }, icon("trash")) },
|
|
76
|
+
], members) : c.emptyState({ icon: "users", title: t("workspace.members"), body: t("common.none") }),
|
|
77
|
+
h("div.lt3-row-2", userId, role, h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => addMember(ws.workspace_id, userId.value.trim(), role.value) } }, icon("user-plus"), t("workspace.addMember"))),
|
|
78
|
+
) : null,
|
|
79
|
+
), { interactive: false });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function invitationsPanel(invites) {
|
|
83
|
+
const rows = Array.isArray(invites.data?.invitations) ? invites.data.invitations : [];
|
|
84
|
+
const email = h("input.lt3-input", { type: "email", placeholder: t("workspace.inviteEmail"), "aria-label": t("workspace.inviteEmail") });
|
|
85
|
+
const workspace = h("input.lt3-input", { type: "text", placeholder: "workspace_id", "aria-label": "workspace_id", value: store.get().workspaceId || "personal" });
|
|
86
|
+
const role = h("select.lt3-select", { "aria-label": t("common.role") },
|
|
87
|
+
["member", "viewer", "admin"].map((r) => h("option", { value: r }, roleLabel(r))));
|
|
88
|
+
const token = h("input.lt3-input", { type: "text", placeholder: t("workspace.inviteToken"), "aria-label": t("workspace.inviteToken") });
|
|
89
|
+
|
|
90
|
+
return c.panel({
|
|
91
|
+
title: t("workspace.invitations"),
|
|
92
|
+
actions: [c.sourceBadge(invites.source)],
|
|
93
|
+
children: h("div.lt3-stack-4",
|
|
94
|
+
h("div.lt3-grid-2",
|
|
95
|
+
h("div.lt3-field", h("label.lt3-label", t("workspace.inviteEmail")), email),
|
|
96
|
+
h("div.lt3-field", h("label.lt3-label", "workspace_id"), workspace),
|
|
97
|
+
),
|
|
98
|
+
h("div.lt3-row-2", role, h("button.lt3-btn.lt3-btn--primary", { on: { click: () => createInvite(email.value.trim(), workspace.value.trim(), role.value) } }, icon("mail-plus"), t("workspace.invitations"))),
|
|
99
|
+
rows.length ? c.table([
|
|
100
|
+
{ key: "email", label: t("account.email"), render: (r) => r.email || "—" },
|
|
101
|
+
{ key: "role", label: t("common.role"), width: "1%", render: (r) => c.pill(roleLabel(r.role)) },
|
|
102
|
+
{ key: "token", label: t("workspace.inviteToken"), render: (r) => h("span.lt3-mono", r.token || r.id || "—") },
|
|
103
|
+
{ key: "status", label: t("common.status"), width: "1%", render: (r) => c.statePill(r.status || (r.accepted_at ? "ready" : "pending")) },
|
|
104
|
+
], rows.slice(0, 20)) : c.emptyState({ icon: "mail", title: t("workspace.invitations"), body: t("common.none") }),
|
|
105
|
+
h("hr.lt3-divider"),
|
|
106
|
+
h("div.lt3-row-2", token, h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => acceptInvite(token.value.trim()) } }, icon("circle-check"), t("workspace.acceptInvite"))),
|
|
107
|
+
),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function activate(workspace_id) {
|
|
112
|
+
const res = await api.activateWorkspace(workspace_id);
|
|
113
|
+
toast(resultText(res, t("workspace.activated")), res.ok ? "ok" : "err");
|
|
114
|
+
if (res.ok) { store.setWorkspace(workspace_id); load(); }
|
|
115
|
+
}
|
|
116
|
+
async function archive(workspace_id) {
|
|
117
|
+
const res = await api.archiveWorkspace(workspace_id);
|
|
118
|
+
toast(resultText(res, t("workspace.archived")), res.ok ? "ok" : "err");
|
|
119
|
+
if (res.ok) load();
|
|
120
|
+
}
|
|
121
|
+
async function addMember(workspace_id, user_id, role) {
|
|
122
|
+
if (!user_id) return;
|
|
123
|
+
const res = await api.addWorkspaceMember(workspace_id, user_id, role);
|
|
124
|
+
toast(resultText(res, t("workspace.memberAdded")), res.ok ? "ok" : "err");
|
|
125
|
+
if (res.ok) load();
|
|
126
|
+
}
|
|
127
|
+
async function removeMember(workspace_id, user_id) {
|
|
128
|
+
const res = await api.removeWorkspaceMember(workspace_id, user_id);
|
|
129
|
+
toast(resultText(res, t("workspace.memberAdded")), res.ok ? "ok" : "err");
|
|
130
|
+
if (res.ok) load();
|
|
131
|
+
}
|
|
132
|
+
async function createInvite(email, workspace_id, role) {
|
|
133
|
+
const res = await api.createInvitation({ email: email || null, workspace_id: workspace_id || null, role, expires_hours: 168 });
|
|
134
|
+
toast(resultText(res, t("workspace.inviteCreated")), res.ok ? "ok" : "err");
|
|
135
|
+
if (res.ok) load();
|
|
136
|
+
}
|
|
137
|
+
async function acceptInvite(token) {
|
|
138
|
+
if (!token) return;
|
|
139
|
+
const res = await api.acceptInvitation(token);
|
|
140
|
+
toast(resultText(res, t("workspace.inviteAccepted")), res.ok ? "ok" : "err");
|
|
141
|
+
if (res.ok) load();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await load();
|
|
145
|
+
return host;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function roleLabel(role) {
|
|
149
|
+
return t(`common.${String(role || "member")}`) || String(role || "member");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resultText(res, okText) {
|
|
153
|
+
if (res && res.ok) return okText;
|
|
154
|
+
const data = (res && res.data) || {};
|
|
155
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
156
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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: 5, block: true }));
|
|
6
|
+
|
|
7
|
+
async function load() {
|
|
8
|
+
const [registry, invites] = await Promise.all([api.workspaceRegistry(), api.invitations()]);
|
|
9
|
+
const data = registry.data || {};
|
|
10
|
+
const workspaces = Array.isArray(data.workspaces) ? data.workspaces : [];
|
|
11
|
+
store.setWorkspaces(workspaces.length ? workspaces : store.get().workspaces);
|
|
12
|
+
host.replaceChildren(
|
|
13
|
+
c.viewHeader({
|
|
14
|
+
eyebrow: t("workspace.eyebrow"),
|
|
15
|
+
title: t("workspace.title"),
|
|
16
|
+
sub: t("workspace.sub"),
|
|
17
|
+
actions: [c.sourceBadge(registry.source)],
|
|
18
|
+
}),
|
|
19
|
+
createOrgPanel(),
|
|
20
|
+
workspaceGrid(workspaces, data),
|
|
21
|
+
invitationsPanel(invites),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createOrgPanel() {
|
|
26
|
+
const name = h("input.lt3-input", { type: "text", "aria-label": t("workspace.orgName"), placeholder: t("workspace.orgName") });
|
|
27
|
+
return c.panel({
|
|
28
|
+
title: t("workspace.createOrg"),
|
|
29
|
+
children: h("div.lt3-row-2",
|
|
30
|
+
name,
|
|
31
|
+
h("button.lt3-btn.lt3-btn--primary", { on: { click: async () => {
|
|
32
|
+
if (!name.value.trim()) return;
|
|
33
|
+
const res = await api.createOrg(name.value.trim());
|
|
34
|
+
toast(resultText(res, t("workspace.createOrg")), res.ok ? "ok" : "err");
|
|
35
|
+
if (res.ok) { name.value = ""; load(); }
|
|
36
|
+
} } }, icon("plus"), t("workspace.createOrg")),
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function workspaceGrid(workspaces, registry) {
|
|
42
|
+
if (!workspaces.length) {
|
|
43
|
+
return c.emptyState({ icon: "building-community", title: t("workspace.title"), body: t("common.unavailable") });
|
|
44
|
+
}
|
|
45
|
+
return h("div.lt3-grid-auto", workspaces.map((ws) => workspaceCard(ws, registry)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function workspaceCard(ws, registry) {
|
|
49
|
+
const roleOptions = registry.roles || ["owner", "admin", "member", "viewer"];
|
|
50
|
+
const members = Array.isArray(ws.members) ? ws.members : [];
|
|
51
|
+
const userId = h("input.lt3-input", { type: "text", placeholder: t("workspace.userId"), "aria-label": t("workspace.userId") });
|
|
52
|
+
const role = h("select.lt3-select", { "aria-label": t("common.role") }, roleOptions.map((r) => h("option", { value: r }, roleLabel(r))));
|
|
53
|
+
return c.card(h("div.lt3-stack-4",
|
|
54
|
+
h("div.lt3-row", { style: { "justify-content": "space-between", "align-items": "flex-start", gap: "var(--lt3-space-3)" } },
|
|
55
|
+
h("div",
|
|
56
|
+
h("b", ws.name || ws.workspace_id),
|
|
57
|
+
h("div.lt3-faint", { style: { "font-size": "var(--lt3-text-2xs)", "font-family": "var(--lt3-font-mono)" } }, ws.workspace_id),
|
|
58
|
+
),
|
|
59
|
+
c.pill(roleLabel(ws.your_role || "member"), "info"),
|
|
60
|
+
),
|
|
61
|
+
h("dl.lt3-keyval",
|
|
62
|
+
h("dt", t("common.type")), h("dd", ws.type || "—"),
|
|
63
|
+
h("dt", t("common.status")), h("dd", c.statePill(ws.status || "active")),
|
|
64
|
+
h("dt", t("workspace.members")), h("dd", String(ws.member_count ?? members.length)),
|
|
65
|
+
),
|
|
66
|
+
h("div.lt3-row-2",
|
|
67
|
+
h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", { on: { click: () => activate(ws.workspace_id) } }, icon("selector"), t("workspace.activate")),
|
|
68
|
+
ws.type === "organization" ? h("button.lt3-btn.lt3-btn--ghost.lt3-btn--sm", { on: { click: () => archive(ws.workspace_id) } }, icon("archive"), t("workspace.archive")) : null,
|
|
69
|
+
),
|
|
70
|
+
ws.type === "organization" ? h("div.lt3-stack-3",
|
|
71
|
+
h("div.lt3-eyebrow", t("workspace.members")),
|
|
72
|
+
members.length ? c.table([
|
|
73
|
+
{ key: "user", label: t("workspace.userId"), render: (m) => h("span.lt3-mono", m.user_id || "—") },
|
|
74
|
+
{ key: "role", label: t("common.role"), width: "1%", render: (m) => c.pill(roleLabel(m.role)) },
|
|
75
|
+
{ key: "act", label: "", width: "1%", render: (m) => h("button.lt3-iconbtn.lt3-iconbtn--sm", { "aria-label": t("common.cancel"), on: { click: () => removeMember(ws.workspace_id, m.user_id) } }, icon("trash")) },
|
|
76
|
+
], members) : c.emptyState({ icon: "users", title: t("workspace.members"), body: t("common.none") }),
|
|
77
|
+
h("div.lt3-row-2", userId, role, h("button.lt3-btn.lt3-btn--primary.lt3-btn--sm", { on: { click: () => addMember(ws.workspace_id, userId.value.trim(), role.value) } }, icon("user-plus"), t("workspace.addMember"))),
|
|
78
|
+
) : null,
|
|
79
|
+
), { interactive: false });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function invitationsPanel(invites) {
|
|
83
|
+
const rows = Array.isArray(invites.data?.invitations) ? invites.data.invitations : [];
|
|
84
|
+
const email = h("input.lt3-input", { type: "email", placeholder: t("workspace.inviteEmail"), "aria-label": t("workspace.inviteEmail") });
|
|
85
|
+
const workspace = h("input.lt3-input", { type: "text", placeholder: "workspace_id", "aria-label": "workspace_id", value: store.get().workspaceId || "personal" });
|
|
86
|
+
const role = h("select.lt3-select", { "aria-label": t("common.role") },
|
|
87
|
+
["member", "viewer", "admin"].map((r) => h("option", { value: r }, roleLabel(r))));
|
|
88
|
+
const token = h("input.lt3-input", { type: "text", placeholder: t("workspace.inviteToken"), "aria-label": t("workspace.inviteToken") });
|
|
89
|
+
|
|
90
|
+
return c.panel({
|
|
91
|
+
title: t("workspace.invitations"),
|
|
92
|
+
actions: [c.sourceBadge(invites.source)],
|
|
93
|
+
children: h("div.lt3-stack-4",
|
|
94
|
+
h("div.lt3-grid-2",
|
|
95
|
+
h("div.lt3-field", h("label.lt3-label", t("workspace.inviteEmail")), email),
|
|
96
|
+
h("div.lt3-field", h("label.lt3-label", "workspace_id"), workspace),
|
|
97
|
+
),
|
|
98
|
+
h("div.lt3-row-2", role, h("button.lt3-btn.lt3-btn--primary", { on: { click: () => createInvite(email.value.trim(), workspace.value.trim(), role.value) } }, icon("mail-plus"), t("workspace.invitations"))),
|
|
99
|
+
rows.length ? c.table([
|
|
100
|
+
{ key: "email", label: t("account.email"), render: (r) => r.email || "—" },
|
|
101
|
+
{ key: "role", label: t("common.role"), width: "1%", render: (r) => c.pill(roleLabel(r.role)) },
|
|
102
|
+
{ key: "token", label: t("workspace.inviteToken"), render: (r) => h("span.lt3-mono", r.token || r.id || "—") },
|
|
103
|
+
{ key: "status", label: t("common.status"), width: "1%", render: (r) => c.statePill(r.status || (r.accepted_at ? "ready" : "pending")) },
|
|
104
|
+
], rows.slice(0, 20)) : c.emptyState({ icon: "mail", title: t("workspace.invitations"), body: t("common.none") }),
|
|
105
|
+
h("hr.lt3-divider"),
|
|
106
|
+
h("div.lt3-row-2", token, h("button.lt3-btn.lt3-btn--ghost", { on: { click: () => acceptInvite(token.value.trim()) } }, icon("circle-check"), t("workspace.acceptInvite"))),
|
|
107
|
+
),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function activate(workspace_id) {
|
|
112
|
+
const res = await api.activateWorkspace(workspace_id);
|
|
113
|
+
toast(resultText(res, t("workspace.activated")), res.ok ? "ok" : "err");
|
|
114
|
+
if (res.ok) { store.setWorkspace(workspace_id); load(); }
|
|
115
|
+
}
|
|
116
|
+
async function archive(workspace_id) {
|
|
117
|
+
const res = await api.archiveWorkspace(workspace_id);
|
|
118
|
+
toast(resultText(res, t("workspace.archived")), res.ok ? "ok" : "err");
|
|
119
|
+
if (res.ok) load();
|
|
120
|
+
}
|
|
121
|
+
async function addMember(workspace_id, user_id, role) {
|
|
122
|
+
if (!user_id) return;
|
|
123
|
+
const res = await api.addWorkspaceMember(workspace_id, user_id, role);
|
|
124
|
+
toast(resultText(res, t("workspace.memberAdded")), res.ok ? "ok" : "err");
|
|
125
|
+
if (res.ok) load();
|
|
126
|
+
}
|
|
127
|
+
async function removeMember(workspace_id, user_id) {
|
|
128
|
+
const res = await api.removeWorkspaceMember(workspace_id, user_id);
|
|
129
|
+
toast(resultText(res, t("workspace.memberAdded")), res.ok ? "ok" : "err");
|
|
130
|
+
if (res.ok) load();
|
|
131
|
+
}
|
|
132
|
+
async function createInvite(email, workspace_id, role) {
|
|
133
|
+
const res = await api.createInvitation({ email: email || null, workspace_id: workspace_id || null, role, expires_hours: 168 });
|
|
134
|
+
toast(resultText(res, t("workspace.inviteCreated")), res.ok ? "ok" : "err");
|
|
135
|
+
if (res.ok) load();
|
|
136
|
+
}
|
|
137
|
+
async function acceptInvite(token) {
|
|
138
|
+
if (!token) return;
|
|
139
|
+
const res = await api.acceptInvitation(token);
|
|
140
|
+
toast(resultText(res, t("workspace.inviteAccepted")), res.ok ? "ok" : "err");
|
|
141
|
+
if (res.ok) load();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await load();
|
|
145
|
+
return host;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function roleLabel(role) {
|
|
149
|
+
return t(`common.${String(role || "member")}`) || String(role || "member");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resultText(res, okText) {
|
|
153
|
+
if (res && res.ok) return okText;
|
|
154
|
+
const data = (res && res.data) || {};
|
|
155
|
+
return String(data.detail || data.error || res?.error || t("common.unavailable"));
|
|
156
|
+
}
|
package/static/account.html
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="ko">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
|
6
|
-
<title>Lattice AI</title>
|
|
7
|
-
<script src="/static/scripts/ux.js"></script>
|
|
8
|
-
<link rel="manifest" href="/manifest.json">
|
|
9
|
-
<meta name="theme-color" content="#f3ecff">
|
|
10
|
-
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
11
|
-
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
|
12
|
-
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
|
|
13
|
-
<link rel="stylesheet" href="/static/vendor/fonts/inter.css">
|
|
14
|
-
<link rel="stylesheet" href="/static/vendor/icons/tabler-icons.min.css">
|
|
15
|
-
<link rel="stylesheet" href="/static/css/tokens.css">
|
|
16
|
-
<link rel="stylesheet" href="/static/css/reference/base.css">
|
|
17
|
-
<link rel="stylesheet" href="/static/css/reference/account.css">
|
|
18
|
-
<link rel="stylesheet" href="/static/css/reference/admin.css">
|
|
19
|
-
<link rel="stylesheet" href="/static/css/reference/graph.css">
|
|
20
|
-
<link rel="stylesheet" href="/static/css/reference/chat.css">
|
|
21
|
-
<link rel="stylesheet" href="/static/css/responsive.css">
|
|
22
|
-
</head>
|
|
23
|
-
<body class="lattice-ref-auth">
|
|
24
|
-
<div class="orb orb-1"></div>
|
|
25
|
-
<div class="orb orb-2"></div>
|
|
26
|
-
<div class="auth-titlebar" aria-hidden="true">
|
|
27
|
-
<div class="auth-window-brand"><i class="ti ti-cube-3d-sphere"></i><span>Lattice AI</span></div>
|
|
28
|
-
<div class="auth-window-controls"><span></span><span></span><span></span></div>
|
|
29
|
-
</div>
|
|
30
|
-
<div class="auth-wave auth-wave-left" aria-hidden="true"></div>
|
|
31
|
-
<div class="auth-wave auth-wave-right" aria-hidden="true"></div>
|
|
32
|
-
<div class="auth-network" aria-hidden="true">
|
|
33
|
-
<span></span><span></span><span></span><span></span><span></span><span></span>
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<div class="login-shell">
|
|
37
|
-
<div class="brand-preview" aria-hidden="true">
|
|
38
|
-
<div class="preview-node n1"></div>
|
|
39
|
-
<div class="preview-node n2"></div>
|
|
40
|
-
<div class="preview-node n3"></div>
|
|
41
|
-
<div class="preview-node n4"></div>
|
|
42
|
-
<div class="preview-line l1"></div>
|
|
43
|
-
<div class="preview-line l2"></div>
|
|
44
|
-
<div class="preview-line l3"></div>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="card">
|
|
47
|
-
<div class="lang-wrap">
|
|
48
|
-
<button class="lang-btn" id="lang-btn" onclick="toggleLang()">🌐 Language</button>
|
|
49
|
-
<div class="lang-menu" id="lang-menu">
|
|
50
|
-
<div class="lang-opt" id="opt-ko" onclick="setLang('ko')">🇰🇷 한국어</div>
|
|
51
|
-
<div class="lang-opt" id="opt-en" onclick="setLang('en')">🇺🇸 English</div>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
<!-- Login form -->
|
|
55
|
-
<div id="login-section">
|
|
56
|
-
<div class="hero-logo">
|
|
57
|
-
<div class="hero-logo-mark"><i class="ti ti-cube-3d-sphere"></i></div>
|
|
58
|
-
<h2 class="title" id="login-title">Lattice AI</h2>
|
|
59
|
-
</div>
|
|
60
|
-
<p class="subtitle" id="login-sub">내 PC에서 시작하는<br>개인 AI 워크스페이스</p>
|
|
61
|
-
<label class="auth-field">
|
|
62
|
-
<i class="ti ti-mail"></i>
|
|
63
|
-
<input class="input" type="email" id="login-email" placeholder="이메일 주소">
|
|
64
|
-
</label>
|
|
65
|
-
<label class="auth-field">
|
|
66
|
-
<i class="ti ti-lock"></i>
|
|
67
|
-
<input class="input" type="password" id="login-pw" placeholder="비밀번호" onkeydown="if(event.key==='Enter')doLogin()">
|
|
68
|
-
<button type="button" class="field-eye" onclick="togglePasswordVisibility()" title="비밀번호 보기"><i class="ti ti-eye"></i></button>
|
|
69
|
-
</label>
|
|
70
|
-
<div class="msg" id="login-msg"></div>
|
|
71
|
-
<button class="submit" id="login-btn" onclick="doLogin()" data-ko="로그인" data-en="Log in">로그인</button>
|
|
72
|
-
<button class="register-cta" id="go-register-link" onclick="showSection('register');return false;">회원가입</button>
|
|
73
|
-
<div id="sso-section">
|
|
74
|
-
<div class="sso-divider" id="sso-divider-text">조직 계정으로 로그인</div>
|
|
75
|
-
<button class="sso-btn sso-ms" onclick="doSSOLogin('microsoft')">
|
|
76
|
-
<span class="ms-logo" aria-hidden="true"><b></b><b></b><b></b><b></b></span>
|
|
77
|
-
<span id="sso-ms-label">Microsoft Entra ID로 계속하기</span>
|
|
78
|
-
</button>
|
|
79
|
-
<button class="sso-btn sso-okta" onclick="doSSOLogin('okta')">
|
|
80
|
-
<span class="okta-logo" aria-hidden="true"></span>
|
|
81
|
-
<span id="sso-okta-label">Okta SSO로 계속하기</span>
|
|
82
|
-
</button>
|
|
83
|
-
</div>
|
|
84
|
-
<button class="local-start" onclick="showSection('register');return false;"><i class="ti ti-device-desktop"></i> <span id="local-start-label">로컬 계정으로 시작</span></button>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<!-- Register form -->
|
|
88
|
-
<div id="register-section" style="display:none;">
|
|
89
|
-
<div class="logo"><i class="ti ti-user-plus"></i></div>
|
|
90
|
-
<h2 class="title" id="reg-title">계정 만들기</h2>
|
|
91
|
-
<p class="subtitle" id="reg-sub">Lattice AI 워크스페이스에 참여하세요</p>
|
|
92
|
-
<input class="input" type="email" id="reg-email" placeholder="이메일 주소">
|
|
93
|
-
<input class="input" type="password" id="reg-pw" placeholder="비밀번호 (4자 이상)">
|
|
94
|
-
<input class="input" type="password" id="reg-pw2" placeholder="비밀번호 확인">
|
|
95
|
-
<input class="input" type="text" id="reg-name" placeholder="이름">
|
|
96
|
-
<input class="input" type="text" id="reg-nick" placeholder="닉네임">
|
|
97
|
-
<div class="msg" id="reg-msg"></div>
|
|
98
|
-
<button class="submit" id="reg-btn" onclick="doRegister()">가입하기</button>
|
|
99
|
-
<p class="switch" id="reg-switch">
|
|
100
|
-
<span id="have-account-text">이미 계정이 있나요?</span>
|
|
101
|
-
<a href="#" onclick="showSection('login');return false;" id="go-login-link">로그인</a>
|
|
102
|
-
</p>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
<footer class="auth-footer">
|
|
107
|
-
<a href="#" onclick="return false;" id="help-link">도움말</a>
|
|
108
|
-
<a href="#" onclick="return false;" id="privacy-link">개인정보 처리방침</a>
|
|
109
|
-
</footer>
|
|
110
|
-
|
|
111
|
-
<script src="/static/scripts/account.js"></script>
|
|
112
|
-
</body>
|
|
113
|
-
</html>
|
package/static/activity.html
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
|
6
|
-
<title>Realtime Activity — Lattice AI</title>
|
|
7
|
-
<script src="/static/scripts/ux.js"></script>
|
|
8
|
-
<link rel="stylesheet" href="/static/css/tokens.css" />
|
|
9
|
-
<link rel="stylesheet" href="/static/platform.css" />
|
|
10
|
-
<link rel="stylesheet" href="/static/css/responsive.css" />
|
|
11
|
-
</head>
|
|
12
|
-
<body>
|
|
13
|
-
<main>
|
|
14
|
-
<h1>Realtime Activity</h1>
|
|
15
|
-
<p class="sub" id="sub">Live presence + activity feed across workspace, graph, agent, and workflow events (SSE).</p>
|
|
16
|
-
|
|
17
|
-
<div class="row">
|
|
18
|
-
<span class="badge" id="connBadge">connecting…</span>
|
|
19
|
-
<span class="badge" id="presenceBadge">presence: 0</span>
|
|
20
|
-
<div class="spacer"></div>
|
|
21
|
-
<button class="ghost" id="clearBtn">Clear view</button>
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<div class="section">
|
|
25
|
-
<h3>Live feed</h3>
|
|
26
|
-
<div id="feed"><div class="empty">Waiting for events…</div></div>
|
|
27
|
-
</div>
|
|
28
|
-
</main>
|
|
29
|
-
|
|
30
|
-
<script type="module">
|
|
31
|
-
import { mountHeader, api, escapeHtml, badge } from "/static/scripts/platform.js";
|
|
32
|
-
mountHeader("/activity");
|
|
33
|
-
|
|
34
|
-
const feed = document.getElementById("feed");
|
|
35
|
-
const seen = new Set();
|
|
36
|
-
|
|
37
|
-
function renderEvent(ev) {
|
|
38
|
-
if (ev.seq && seen.has(ev.seq)) return;
|
|
39
|
-
if (ev.seq) seen.add(ev.seq);
|
|
40
|
-
if (feed.querySelector(".empty")) feed.innerHTML = "";
|
|
41
|
-
const node = document.createElement("div");
|
|
42
|
-
node.className = "timeline-item";
|
|
43
|
-
node.innerHTML = `<div class="row">${badge(ev.area || "event")} <strong>${escapeHtml(ev.event_type || "")}</strong>
|
|
44
|
-
<span class="t-meta">${escapeHtml(ev.workspace_id || "personal")} · ${escapeHtml(ev.received_at || ev.timestamp || "")}</span></div>
|
|
45
|
-
<pre style="max-height:160px">${escapeHtml(JSON.stringify(ev.payload || {}, null, 2))}</pre>`;
|
|
46
|
-
feed.prepend(node);
|
|
47
|
-
while (feed.children.length > 100) feed.lastChild.remove();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function refreshPresence() {
|
|
51
|
-
try {
|
|
52
|
-
const data = await api("/realtime/presence");
|
|
53
|
-
document.getElementById("presenceBadge").textContent = `presence: ${data.presence.length}`;
|
|
54
|
-
document.getElementById("sub").textContent =
|
|
55
|
-
`Transport: ${data.stats.transport.toUpperCase()} · subscribers: ${data.stats.subscribers} · feed: ${data.stats.feed_size}`;
|
|
56
|
-
} catch {}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
document.getElementById("clearBtn").addEventListener("click", () => { feed.innerHTML = `<div class="empty">Cleared.</div>`; });
|
|
60
|
-
|
|
61
|
-
// Seed with recent feed, then connect to the SSE stream.
|
|
62
|
-
api("/realtime/feed?limit=30").then((d) => (d.events || []).reverse().forEach(renderEvent)).catch(() => {});
|
|
63
|
-
|
|
64
|
-
const es = new EventSource("/realtime/stream");
|
|
65
|
-
es.onopen = () => { document.getElementById("connBadge").textContent = "● live"; document.getElementById("connBadge").className = "badge ok"; };
|
|
66
|
-
es.onerror = () => { document.getElementById("connBadge").textContent = "○ reconnecting"; document.getElementById("connBadge").className = "badge warn"; };
|
|
67
|
-
es.onmessage = (e) => { try { renderEvent(JSON.parse(e.data)); } catch {} };
|
|
68
|
-
|
|
69
|
-
refreshPresence();
|
|
70
|
-
setInterval(refreshPresence, 8000);
|
|
71
|
-
</script>
|
|
72
|
-
</body>
|
|
73
|
-
</html>
|