ltcai 1.7.0 → 2.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 +32 -21
- package/docs/CHANGELOG.md +119 -0
- package/docs/EDITION_STRATEGY.md +10 -4
- package/docs/ENTERPRISE.md +3 -1
- package/docs/MULTI_AGENT_RUNTIME.md +428 -0
- package/docs/PLUGIN_SDK.md +664 -0
- package/docs/REALTIME_COLLABORATION.md +423 -0
- package/docs/V2_ARCHITECTURE.md +540 -0
- package/docs/WORKFLOW_DESIGNER.md +485 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agents.py +154 -0
- package/latticeai/api/marketplace.py +81 -0
- package/latticeai/api/plugins.py +115 -0
- package/latticeai/api/realtime.py +91 -0
- package/latticeai/api/workflow_designer.py +216 -0
- package/latticeai/core/marketplace.py +178 -0
- package/latticeai/core/multi_agent.py +561 -0
- package/latticeai/core/plugins.py +416 -0
- package/latticeai/core/realtime.py +190 -0
- package/latticeai/core/workflow_engine.py +329 -0
- package/latticeai/core/workspace_os.py +406 -6
- package/latticeai/server_app.py +88 -2
- package/latticeai/services/platform_runtime.py +204 -0
- package/package.json +8 -2
- package/plugins/README.md +35 -0
- package/plugins/git-insights/plugin.json +15 -0
- package/plugins/hello-world/plugin.json +16 -0
- package/plugins/hello-world/skills/hello_skill/SKILL.md +15 -0
- package/static/activity.html +70 -0
- package/static/agents.html +136 -0
- package/static/platform.css +75 -0
- package/static/plugins.html +133 -0
- package/static/scripts/platform.js +64 -0
- package/static/workflows.html +143 -0
- package/static/workspace.html +5 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* Lattice AI v2.0 — shared styling for the Agentic Workspace Platform pages
|
|
2
|
+
(Plugin SDK, Workflow Designer, Multi-Agent Runtime, Realtime Activity). */
|
|
3
|
+
:root {
|
|
4
|
+
--bg: #0f1115;
|
|
5
|
+
--panel: #16191f;
|
|
6
|
+
--panel-2: #1c2027;
|
|
7
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
8
|
+
--text: #e7ecf3;
|
|
9
|
+
--muted: #94a3b8;
|
|
10
|
+
--accent: #378ADD;
|
|
11
|
+
--accent-2: #5ea7ec;
|
|
12
|
+
--ok: #34d399;
|
|
13
|
+
--warn: #fbbf24;
|
|
14
|
+
--err: #f87171;
|
|
15
|
+
}
|
|
16
|
+
* { box-sizing: border-box; }
|
|
17
|
+
body {
|
|
18
|
+
margin: 0;
|
|
19
|
+
background: var(--bg);
|
|
20
|
+
color: var(--text);
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
22
|
+
line-height: 1.5;
|
|
23
|
+
}
|
|
24
|
+
a { color: var(--accent-2); text-decoration: none; }
|
|
25
|
+
header.app {
|
|
26
|
+
display: flex; align-items: center; gap: 18px;
|
|
27
|
+
padding: 14px 24px; border-bottom: 1px solid var(--border);
|
|
28
|
+
background: rgba(22, 25, 31, 0.8); position: sticky; top: 0; backdrop-filter: blur(8px); z-index: 5;
|
|
29
|
+
}
|
|
30
|
+
header.app .brand { font-weight: 700; font-size: 16px; color: #fff; letter-spacing: .3px; }
|
|
31
|
+
header.app .brand small { color: var(--accent); font-weight: 600; margin-left: 6px; }
|
|
32
|
+
header.app nav { display: flex; gap: 14px; flex-wrap: wrap; }
|
|
33
|
+
header.app nav a { color: var(--muted); font-size: 13px; padding: 4px 6px; border-radius: 6px; }
|
|
34
|
+
header.app nav a:hover, header.app nav a.active { color: #fff; background: var(--panel-2); }
|
|
35
|
+
main { max-width: 1080px; margin: 0 auto; padding: 28px 24px 80px; }
|
|
36
|
+
h1 { font-size: 22px; margin: 0 0 4px; }
|
|
37
|
+
.sub { color: var(--muted); font-size: 13px; margin: 0 0 24px; }
|
|
38
|
+
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
|
|
39
|
+
.card {
|
|
40
|
+
background: var(--panel); border: 1px solid var(--border); border-radius: 14px;
|
|
41
|
+
padding: 16px 18px;
|
|
42
|
+
}
|
|
43
|
+
.card h3 { margin: 0 0 6px; font-size: 15px; }
|
|
44
|
+
.card .meta { color: var(--muted); font-size: 12px; }
|
|
45
|
+
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
46
|
+
.spacer { flex: 1; }
|
|
47
|
+
.badge {
|
|
48
|
+
display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px;
|
|
49
|
+
background: var(--panel-2); color: var(--muted); border: 1px solid var(--border);
|
|
50
|
+
}
|
|
51
|
+
.badge.ok { color: var(--ok); border-color: rgba(52,211,153,.4); }
|
|
52
|
+
.badge.warn { color: var(--warn); border-color: rgba(251,191,36,.4); }
|
|
53
|
+
.badge.err { color: var(--err); border-color: rgba(248,113,113,.4); }
|
|
54
|
+
button, .btn {
|
|
55
|
+
background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
|
56
|
+
padding: 7px 14px; font-size: 13px; cursor: pointer; font-weight: 600;
|
|
57
|
+
}
|
|
58
|
+
button.ghost { background: var(--panel-2); color: var(--text); border: 1px solid var(--border); }
|
|
59
|
+
button:hover { filter: brightness(1.08); }
|
|
60
|
+
button:disabled { opacity: .5; cursor: not-allowed; }
|
|
61
|
+
textarea, input, select {
|
|
62
|
+
width: 100%; background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
|
|
63
|
+
border-radius: 8px; padding: 9px 11px; font-size: 13px; font-family: inherit;
|
|
64
|
+
}
|
|
65
|
+
textarea { min-height: 90px; resize: vertical; }
|
|
66
|
+
label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 4px; }
|
|
67
|
+
pre {
|
|
68
|
+
background: #0b0d11; border: 1px solid var(--border); border-radius: 10px;
|
|
69
|
+
padding: 12px; overflow: auto; font-size: 12px; color: #cbd5e1; max-height: 360px;
|
|
70
|
+
}
|
|
71
|
+
.empty { color: var(--muted); text-align: center; padding: 50px 0; }
|
|
72
|
+
.section { margin-top: 28px; }
|
|
73
|
+
.timeline-item { border-left: 2px solid var(--border); padding: 6px 0 6px 14px; margin-left: 6px; font-size: 13px; }
|
|
74
|
+
.timeline-item .t-meta { color: var(--muted); font-size: 11px; }
|
|
75
|
+
.toast { position: fixed; bottom: 20px; right: 20px; background: var(--panel-2); border: 1px solid var(--border); padding: 12px 16px; border-radius: 10px; font-size: 13px; max-width: 360px; }
|
|
@@ -0,0 +1,133 @@
|
|
|
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.0" />
|
|
6
|
+
<title>Plugin SDK — Lattice AI</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/platform.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Plugin SDK</h1>
|
|
12
|
+
<p class="sub" id="sub">Versioned, permissioned plugins that extend skills, tools, and workflows.</p>
|
|
13
|
+
<div id="list" class="grid"><div class="empty">Loading plugins…</div></div>
|
|
14
|
+
|
|
15
|
+
<div class="section">
|
|
16
|
+
<h3>Template foundation</h3>
|
|
17
|
+
<div id="templates" class="grid"><div class="empty">Loading templates…</div></div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="section">
|
|
21
|
+
<h3>Plugin execution viewer</h3>
|
|
22
|
+
<div id="pluginEvents"><div class="empty">Loading plugin events…</div></div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="section">
|
|
26
|
+
<h3>Validate a manifest</h3>
|
|
27
|
+
<p class="sub">Paste a <code>plugin.json</code> to check it against the SDK schema and permission allow-list.</p>
|
|
28
|
+
<textarea id="manifest" spellcheck="false">{
|
|
29
|
+
"id": "my-plugin",
|
|
30
|
+
"name": "My Plugin",
|
|
31
|
+
"version": "1.0.0",
|
|
32
|
+
"lattice_version": ">=2.0.0",
|
|
33
|
+
"permissions": ["read_workspace"],
|
|
34
|
+
"provides": { "skills": [] }
|
|
35
|
+
}</textarea>
|
|
36
|
+
<div class="row" style="margin-top:10px"><button id="validate">Validate</button></div>
|
|
37
|
+
<pre id="validateOut" style="display:none"></pre>
|
|
38
|
+
</div>
|
|
39
|
+
</main>
|
|
40
|
+
|
|
41
|
+
<script type="module">
|
|
42
|
+
import { mountHeader, api, escapeHtml, badge, toast } from "/static/scripts/platform.js";
|
|
43
|
+
mountHeader("/plugins/sdk");
|
|
44
|
+
|
|
45
|
+
async function load() {
|
|
46
|
+
const data = await api("/plugins/registry");
|
|
47
|
+
document.getElementById("sub").textContent =
|
|
48
|
+
`SDK v${data.sdk_version} · ${data.total} plugin(s) discovered in ${data.plugins_dir}`;
|
|
49
|
+
const list = document.getElementById("list");
|
|
50
|
+
if (!data.plugins.length) { list.innerHTML = `<div class="empty">No plugins found.</div>`; return; }
|
|
51
|
+
list.innerHTML = data.plugins.map((p) => `
|
|
52
|
+
<div class="card">
|
|
53
|
+
<div class="row"><h3>${escapeHtml(p.name)}</h3><div class="spacer"></div>
|
|
54
|
+
${badge(p.installed ? (p.enabled ? "ready" : "available") : "available")}</div>
|
|
55
|
+
<div class="meta">v${escapeHtml(p.version)} · ${escapeHtml(p.author || "unknown")} · ${p.compatible ? "compatible" : "<span class='badge err'>incompatible</span>"}</div>
|
|
56
|
+
<p style="font-size:13px;color:#cbd5e1">${escapeHtml(p.description)}</p>
|
|
57
|
+
<div class="meta">Permissions: ${(p.permissions||[]).map(x=>`<span class="badge">${escapeHtml(x)}</span>`).join(" ") || "none"}</div>
|
|
58
|
+
<div class="meta" style="margin-top:6px">Provides: ${Object.entries(p.provides||{}).map(([k,v])=>`${k}(${(v||[]).length})`).join(", ") || "—"}</div>
|
|
59
|
+
<div class="row" style="margin-top:12px">
|
|
60
|
+
${p.installed
|
|
61
|
+
? `<button class="ghost" data-act="${p.enabled?"disable":"enable"}" data-id="${p.id}">${p.enabled?"Disable":"Enable"}</button>
|
|
62
|
+
<button class="ghost" data-act="uninstall" data-id="${p.id}">Uninstall</button>`
|
|
63
|
+
: `<button data-act="install" data-id="${p.id}" ${p.compatible?"":"disabled"}>Install</button>`}
|
|
64
|
+
</div>
|
|
65
|
+
</div>`).join("");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function loadTemplates() {
|
|
69
|
+
const data = await api("/marketplace/templates");
|
|
70
|
+
const box = document.getElementById("templates");
|
|
71
|
+
if (!data.templates.length) { box.innerHTML = `<div class="empty">No templates available.</div>`; return; }
|
|
72
|
+
box.innerHTML = data.templates.map((t) => `
|
|
73
|
+
<div class="card">
|
|
74
|
+
<div class="row"><h3>${escapeHtml(t.name)}</h3><div class="spacer"></div>${badge(t.kind)}</div>
|
|
75
|
+
<div class="meta">v${escapeHtml(t.version)} · ${escapeHtml((t.metadata||{}).category || "foundation")}</div>
|
|
76
|
+
<p style="font-size:13px;color:#cbd5e1">${escapeHtml(t.description || "")}</p>
|
|
77
|
+
<div class="row" style="margin-top:12px">
|
|
78
|
+
<a class="btn ghost" target="_blank" href="/marketplace/templates/${t.kind}/${t.id}/export">Export</a>
|
|
79
|
+
<button data-template="${t.kind}:${t.id}">Install</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>`).join("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function loadPluginEvents() {
|
|
85
|
+
const data = await api("/realtime/feed?limit=80");
|
|
86
|
+
const events = (data.events || []).filter((e) => e.area === "plugins" || e.event_type?.startsWith("plugin_"));
|
|
87
|
+
const box = document.getElementById("pluginEvents");
|
|
88
|
+
if (!events.length) { box.innerHTML = `<div class="empty">No plugin executions yet.</div>`; return; }
|
|
89
|
+
box.innerHTML = events.slice(0, 20).map((ev) => `<div class="timeline-item">
|
|
90
|
+
<div class="row"><strong>${escapeHtml(ev.event_type)}</strong><div class="spacer"></div>${badge((ev.payload||{}).status || ev.area)}</div>
|
|
91
|
+
<div class="t-meta">${escapeHtml((ev.payload||{}).plugin_id || (ev.payload||{}).plugin || "")} · ${escapeHtml(ev.received_at || ev.timestamp || "")}</div>
|
|
92
|
+
</div>`).join("");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
document.getElementById("templates").addEventListener("click", async (e) => {
|
|
96
|
+
const btn = e.target.closest("button[data-template]");
|
|
97
|
+
if (!btn) return;
|
|
98
|
+
btn.disabled = true;
|
|
99
|
+
const [kind, id] = btn.dataset.template.split(":");
|
|
100
|
+
try {
|
|
101
|
+
const exported = await api(`/marketplace/templates/${kind}/${id}/export`);
|
|
102
|
+
await api("/marketplace/templates/install", { method: "POST", body: JSON.stringify({ data: exported }) });
|
|
103
|
+
toast(`Installed template: ${id}`);
|
|
104
|
+
} catch (err) { toast(err.message); } finally { btn.disabled = false; }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
document.getElementById("list").addEventListener("click", async (e) => {
|
|
108
|
+
const btn = e.target.closest("button[data-act]");
|
|
109
|
+
if (!btn) return;
|
|
110
|
+
btn.disabled = true;
|
|
111
|
+
try {
|
|
112
|
+
await api(`/plugins/${btn.dataset.act}`, { method: "POST", body: JSON.stringify({ plugin_id: btn.dataset.id }) });
|
|
113
|
+
toast(`${btn.dataset.act}: ${btn.dataset.id}`);
|
|
114
|
+
await load();
|
|
115
|
+
} catch (err) { toast(err.message); btn.disabled = false; }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
document.getElementById("validate").addEventListener("click", async () => {
|
|
119
|
+
const out = document.getElementById("validateOut");
|
|
120
|
+
out.style.display = "block";
|
|
121
|
+
try {
|
|
122
|
+
const manifest = JSON.parse(document.getElementById("manifest").value);
|
|
123
|
+
const res = await api("/plugins/validate", { method: "POST", body: JSON.stringify({ manifest }) });
|
|
124
|
+
out.textContent = JSON.stringify(res, null, 2);
|
|
125
|
+
} catch (err) { out.textContent = "Error: " + err.message; }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
load().catch((e) => toast(e.message));
|
|
129
|
+
loadTemplates().catch((e) => toast(e.message));
|
|
130
|
+
loadPluginEvents().catch(() => {});
|
|
131
|
+
</script>
|
|
132
|
+
</body>
|
|
133
|
+
</html>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Lattice AI v2.0 — shared helpers for the Agentic Workspace Platform pages.
|
|
2
|
+
export const NAV = [
|
|
3
|
+
{ href: "/workspace", label: "Dashboard" },
|
|
4
|
+
{ href: "/plugins/sdk", label: "Plugins" },
|
|
5
|
+
{ href: "/workflows", label: "Workflows" },
|
|
6
|
+
{ href: "/agents", label: "Agents" },
|
|
7
|
+
{ href: "/activity", label: "Activity" },
|
|
8
|
+
{ href: "/chat", label: "Chat" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export function mountHeader(active) {
|
|
12
|
+
const links = NAV.map(
|
|
13
|
+
(n) => `<a href="${n.href}" class="${n.href === active ? "active" : ""}">${n.label}</a>`
|
|
14
|
+
).join("");
|
|
15
|
+
document.body.insertAdjacentHTML(
|
|
16
|
+
"afterbegin",
|
|
17
|
+
`<header class="app"><div class="brand">Lattice AI<small>v2.0 Platform</small></div><nav>${links}</nav></header>`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function api(path, opts = {}) {
|
|
22
|
+
const res = await fetch(path, {
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
credentials: "same-origin",
|
|
25
|
+
...opts,
|
|
26
|
+
});
|
|
27
|
+
if (res.status === 401) {
|
|
28
|
+
location.href = "/account";
|
|
29
|
+
throw new Error("unauthorized");
|
|
30
|
+
}
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
let body;
|
|
33
|
+
try { body = text ? JSON.parse(text) : {}; } catch { body = { raw: text }; }
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const detail = body && body.detail ? (typeof body.detail === "string" ? body.detail : JSON.stringify(body.detail)) : res.statusText;
|
|
36
|
+
throw new Error(detail);
|
|
37
|
+
}
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function el(html) {
|
|
42
|
+
const t = document.createElement("template");
|
|
43
|
+
t.innerHTML = html.trim();
|
|
44
|
+
return t.content.firstElementChild;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function escapeHtml(s) {
|
|
48
|
+
return String(s == null ? "" : s).replace(/[&<>"']/g, (c) =>
|
|
49
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function toast(msg) {
|
|
54
|
+
const node = el(`<div class="toast">${escapeHtml(msg)}</div>`);
|
|
55
|
+
document.body.appendChild(node);
|
|
56
|
+
setTimeout(() => node.remove(), 4000);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function badge(status) {
|
|
60
|
+
const cls = { ok: "ok", ready: "ok", valid: "ok", retried_ok: "ok",
|
|
61
|
+
partial: "warn", retry: "warn", skipped: "warn", available: "warn",
|
|
62
|
+
failed: "err", error: "err", blocked: "err" }[status] || "";
|
|
63
|
+
return `<span class="badge ${cls}">${escapeHtml(status || "?")}</span>`;
|
|
64
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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.0" />
|
|
6
|
+
<title>Workflow Designer — Lattice AI</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/platform.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<main>
|
|
11
|
+
<h1>Workflow Designer</h1>
|
|
12
|
+
<p class="sub">Compose triggers, tools, skills, plugins, agents, conditions, and outputs into runnable workflows.</p>
|
|
13
|
+
|
|
14
|
+
<div class="row">
|
|
15
|
+
<button id="newBtn">+ New from template</button>
|
|
16
|
+
<button class="ghost" id="validateBtn">Validate</button>
|
|
17
|
+
<button class="ghost" id="saveBtn">Save</button>
|
|
18
|
+
<div class="spacer"></div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="section">
|
|
22
|
+
<label>Workflow name</label>
|
|
23
|
+
<input id="wfName" value="My workflow" />
|
|
24
|
+
<label>Nodes (JSON) — trigger → … → output</label>
|
|
25
|
+
<textarea id="wfNodes" spellcheck="false" style="min-height:240px"></textarea>
|
|
26
|
+
<pre id="validateOut" style="display:none"></pre>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="section">
|
|
30
|
+
<h3>Saved workflows</h3>
|
|
31
|
+
<div id="list" class="grid"><div class="empty">Loading…</div></div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="section">
|
|
35
|
+
<h3>Run history</h3>
|
|
36
|
+
<div id="runs"><div class="empty">No runs yet.</div></div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="section">
|
|
40
|
+
<h3>Replay viewer</h3>
|
|
41
|
+
<div id="replay"><div class="empty">Select a workflow run.</div></div>
|
|
42
|
+
</div>
|
|
43
|
+
</main>
|
|
44
|
+
|
|
45
|
+
<script type="module">
|
|
46
|
+
import { mountHeader, api, escapeHtml, badge, toast } from "/static/scripts/platform.js";
|
|
47
|
+
mountHeader("/workflows");
|
|
48
|
+
|
|
49
|
+
const TEMPLATE = [
|
|
50
|
+
{ id: "trigger", type: "trigger", name: "Manual start", config: { trigger: "manual" }, next: "review" },
|
|
51
|
+
{ id: "review", type: "agent", name: "Multi-agent review", config: { goal: "Review the latest workspace changes", roles: ["planner", "executor", "reviewer"] }, next: "decide" },
|
|
52
|
+
{ id: "decide", type: "condition", name: "Passed?", config: { left: "last_output", op: "truthy" }, branches: { true: "done", false: "done" } },
|
|
53
|
+
{ id: "done", type: "output", name: "Output", config: {}, next: null },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function loadTemplate() {
|
|
57
|
+
document.getElementById("wfName").value = "My workflow";
|
|
58
|
+
document.getElementById("wfNodes").value = JSON.stringify(TEMPLATE, null, 2);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readNodes() { return JSON.parse(document.getElementById("wfNodes").value); }
|
|
62
|
+
|
|
63
|
+
document.getElementById("newBtn").addEventListener("click", loadTemplate);
|
|
64
|
+
|
|
65
|
+
document.getElementById("validateBtn").addEventListener("click", async () => {
|
|
66
|
+
const out = document.getElementById("validateOut");
|
|
67
|
+
out.style.display = "block";
|
|
68
|
+
try {
|
|
69
|
+
const res = await api("/workflows/api/validate", { method: "POST", body: JSON.stringify({ name: document.getElementById("wfName").value, nodes: readNodes() }) });
|
|
70
|
+
out.textContent = res.ok ? "✓ Valid workflow" : "Errors:\n" + res.errors.join("\n");
|
|
71
|
+
} catch (err) { out.textContent = "Error: " + err.message; }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
document.getElementById("saveBtn").addEventListener("click", async () => {
|
|
75
|
+
try {
|
|
76
|
+
await api("/workflows/api/definitions", { method: "POST", body: JSON.stringify({ name: document.getElementById("wfName").value, nodes: readNodes() }) });
|
|
77
|
+
toast("Workflow saved"); await loadList();
|
|
78
|
+
} catch (err) { toast(err.message); }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
async function loadList() {
|
|
82
|
+
const data = await api("/workflows/api/definitions");
|
|
83
|
+
const list = document.getElementById("list");
|
|
84
|
+
const items = data.workflows || [];
|
|
85
|
+
if (!items.length) { list.innerHTML = `<div class="empty">No workflows yet.</div>`; return; }
|
|
86
|
+
list.innerHTML = items.map((w) => `
|
|
87
|
+
<div class="card">
|
|
88
|
+
<h3>${escapeHtml(w.name)}</h3>
|
|
89
|
+
<div class="meta">${(w.nodes||w.steps||[]).length} node(s) · ${escapeHtml(w.updated_at||w.created_at||"")}</div>
|
|
90
|
+
<div class="row" style="margin-top:12px">
|
|
91
|
+
<button data-run="${w.id}">Run</button>
|
|
92
|
+
<a class="btn ghost" href="/workflows/api/export/${w.id}" target="_blank">Export</a>
|
|
93
|
+
</div>
|
|
94
|
+
</div>`).join("");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
document.getElementById("list").addEventListener("click", async (e) => {
|
|
98
|
+
const btn = e.target.closest("button[data-run]");
|
|
99
|
+
if (!btn) return;
|
|
100
|
+
btn.disabled = true;
|
|
101
|
+
try {
|
|
102
|
+
const res = await api(`/workflows/api/definitions/${btn.dataset.run}/run`, { method: "POST", body: JSON.stringify({ inputs: {} }) });
|
|
103
|
+
toast(`Run ${res.result.status} · ${res.result.step_count} steps`);
|
|
104
|
+
await loadRuns();
|
|
105
|
+
} catch (err) { toast(err.message); } finally { btn.disabled = false; }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
async function loadRuns() {
|
|
109
|
+
const data = await api("/workflows/api/runs");
|
|
110
|
+
const runs = data.runs || [];
|
|
111
|
+
const box = document.getElementById("runs");
|
|
112
|
+
if (!runs.length) { box.innerHTML = `<div class="empty">No runs yet.</div>`; return; }
|
|
113
|
+
box.innerHTML = runs.map((r) => `
|
|
114
|
+
<div class="card" style="margin-bottom:10px">
|
|
115
|
+
<div class="row"><h3>${escapeHtml(r.name)}</h3><div class="spacer"></div>${badge(r.status)}</div>
|
|
116
|
+
<div class="meta">${escapeHtml(r.created_at)} · ${ (r.timeline||[]).length } steps</div>
|
|
117
|
+
<div class="row" style="margin-top:10px"><button class="ghost" data-replay="${r.id}">Replay</button></div>
|
|
118
|
+
<pre>${escapeHtml(JSON.stringify(r.timeline, null, 2))}</pre>
|
|
119
|
+
</div>`).join("");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
document.getElementById("runs").addEventListener("click", async (e) => {
|
|
123
|
+
const btn = e.target.closest("button[data-replay]");
|
|
124
|
+
if (!btn) return;
|
|
125
|
+
const out = document.getElementById("replay");
|
|
126
|
+
out.innerHTML = `<div class="empty">Loading replay…</div>`;
|
|
127
|
+
try {
|
|
128
|
+
const data = await api(`/workflows/api/runs/${btn.dataset.replay}/replay`);
|
|
129
|
+
const frames = data.replay.frames || [];
|
|
130
|
+
out.innerHTML = frames.map((f) => `<div class="timeline-item">
|
|
131
|
+
<div class="row"><strong>${escapeHtml(f.event)}</strong><div class="spacer"></div>${badge(f.decision || "event")}</div>
|
|
132
|
+
<div class="t-meta">${escapeHtml(String(f.actor||""))} · ${escapeHtml(f.when||"")}</div>
|
|
133
|
+
<pre>${escapeHtml(JSON.stringify({ why: f.why, input: f.input, output: f.output }, null, 2))}</pre>
|
|
134
|
+
</div>`).join("") || `<div class="empty">No replay frames.</div>`;
|
|
135
|
+
} catch (err) { out.innerHTML = `<div class="empty">${escapeHtml(err.message)}</div>`; }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
loadTemplate();
|
|
139
|
+
loadList().catch((e) => toast(e.message));
|
|
140
|
+
loadRuns().catch(() => {});
|
|
141
|
+
</script>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
package/static/workspace.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png">
|
|
9
9
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap">
|
|
10
10
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
|
|
11
|
-
<link rel="stylesheet" href="/static/workspace.css?v=
|
|
11
|
+
<link rel="stylesheet" href="/static/workspace.css?v=2.0.0">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div class="workspace-shell">
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
<a href="#enterprise"><i class="ti ti-building-skyscraper"></i><span>Editions</span></a>
|
|
32
32
|
</nav>
|
|
33
33
|
<div class="rail-links">
|
|
34
|
+
<a href="/plugins/sdk"><i class="ti ti-plug"></i><span>Plugins</span></a>
|
|
35
|
+
<a href="/workflows"><i class="ti ti-sitemap"></i><span>Designer</span></a>
|
|
36
|
+
<a href="/agents"><i class="ti ti-robot"></i><span>Agents</span></a>
|
|
37
|
+
<a href="/activity"><i class="ti ti-broadcast"></i><span>Activity</span></a>
|
|
34
38
|
<a href="/chat"><i class="ti ti-message-circle"></i><span>Chat</span></a>
|
|
35
39
|
<a href="/graph"><i class="ti ti-network"></i><span>Graph Canvas</span></a>
|
|
36
40
|
<a href="/admin"><i class="ti ti-shield-lock"></i><span>Admin</span></a>
|