ltcai 1.4.0 → 1.6.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 +125 -88
- package/docs/CHANGELOG.md +101 -0
- package/docs/images/architecture.png +0 -0
- package/docs/images/enterprise.png +0 -0
- package/docs/images/graph.png +0 -0
- package/docs/images/hero.gif +0 -0
- package/docs/images/model-recommendation.png +0 -0
- package/docs/images/onboarding.png +0 -0
- package/docs/images/organization.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images/tmp_frames/frame_00.png +0 -0
- package/docs/images/tmp_frames/frame_01.png +0 -0
- package/docs/images/tmp_frames/frame_02.png +0 -0
- package/docs/images/tmp_frames/frame_03.png +0 -0
- package/docs/images/tmp_frames/hero_00.png +0 -0
- package/docs/images/tmp_frames/hero_01.png +0 -0
- package/docs/images/tmp_frames/hero_02.png +0 -0
- package/docs/images/tmp_frames/hero_03.png +0 -0
- package/docs/images/workspace.png +0 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +17 -0
- package/latticeai/api/models.py +16 -0
- package/latticeai/api/workspace.py +11 -0
- package/latticeai/core/enterprise_admin.py +158 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/model_catalog.py +289 -0
- package/latticeai/services/model_recommendation.py +183 -0
- package/latticeai/services/model_runtime.py +11 -263
- package/package.json +2 -2
- package/static/scripts/chat.js +66 -0
- package/static/scripts/workspace.js +260 -18
- package/static/workspace.css +67 -0
- package/static/workspace.html +83 -2
|
@@ -6,8 +6,15 @@ const state = {
|
|
|
6
6
|
activeWorkspace: null,
|
|
7
7
|
registry: null,
|
|
8
8
|
managingWorkspace: null,
|
|
9
|
+
skillsPayload: null,
|
|
10
|
+
skillTab: "recommended",
|
|
11
|
+
entities: [],
|
|
12
|
+
activeEntity: null,
|
|
9
13
|
};
|
|
10
14
|
|
|
15
|
+
// Skills that match common workspace needs are surfaced under "Recommended".
|
|
16
|
+
const RECOMMENDED_SKILL_HINTS = ["code", "review", "doc", "test", "security", "research", "changelog", "refactor", "debug"];
|
|
17
|
+
|
|
11
18
|
function $(id) {
|
|
12
19
|
return document.getElementById(id);
|
|
13
20
|
}
|
|
@@ -194,30 +201,74 @@ function renderWorkflows(payload) {
|
|
|
194
201
|
`).join("") : `<div class="list-item"><div class="meta-line">No workflows.</div></div>`;
|
|
195
202
|
}
|
|
196
203
|
|
|
197
|
-
function
|
|
204
|
+
function skillName(skill) {
|
|
205
|
+
return skill.skill || skill.name || "skill";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Compute the four marketplace tabs from the registry payload (machine-global
|
|
209
|
+
// registry + locally-installed state). "Updates" = installed skills whose
|
|
210
|
+
// registry version differs from the installed version.
|
|
211
|
+
function computeSkillTabs(payload) {
|
|
198
212
|
const installed = payload.installed || [];
|
|
199
|
-
const available =
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
const available = payload.available || [];
|
|
214
|
+
const installedNames = new Set(installed.map(skillName));
|
|
215
|
+
const notInstalled = available.filter((s) => !installedNames.has(skillName(s)));
|
|
216
|
+
const availByName = new Map(available.map((s) => [skillName(s), s]));
|
|
217
|
+
const updates = installed.filter((s) => {
|
|
218
|
+
const remote = availByName.get(skillName(s));
|
|
219
|
+
return remote && remote.version && s.version && remote.version !== s.version;
|
|
220
|
+
});
|
|
221
|
+
const recommended = notInstalled.filter((s) => {
|
|
222
|
+
const hay = `${skillName(s)} ${s.category || ""} ${s.description || ""}`.toLowerCase();
|
|
223
|
+
return RECOMMENDED_SKILL_HINTS.some((h) => hay.includes(h));
|
|
224
|
+
});
|
|
225
|
+
return { installed, popular: notInstalled, recommended, updates };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderSkillRow(skill, { installed }) {
|
|
229
|
+
const name = skillName(skill);
|
|
230
|
+
const enabled = skill.enabled !== false;
|
|
231
|
+
const version = skill.version || (installed ? "local" : "registry");
|
|
232
|
+
const source = skill.plugin || skill.source || (installed ? "installed" : "marketplace");
|
|
233
|
+
const actions = installed
|
|
234
|
+
? `<button class="small-action" data-skill-action="${enabled ? "disable" : "enable"}" data-skill="${escapeHtml(name)}"><i class="ti ti-${enabled ? "toggle-left" : "toggle-right"}"></i>${enabled ? "Disable" : "Enable"}</button>`
|
|
235
|
+
: `<button class="small-action" data-skill-action="install" data-skill="${escapeHtml(name)}"><i class="ti ti-download"></i>Install</button>`;
|
|
236
|
+
return `
|
|
205
237
|
<div class="list-item">
|
|
206
238
|
<div class="list-title">
|
|
207
|
-
<span>${escapeHtml(
|
|
208
|
-
<span class="status-pill ${
|
|
239
|
+
<span>${escapeHtml(name)}</span>
|
|
240
|
+
<span class="status-pill ${installed ? (enabled ? "status-complete" : "status-failed") : ""}">${installed ? (enabled ? "enabled" : "disabled") : "available"}</span>
|
|
209
241
|
</div>
|
|
210
|
-
<div class="meta-line">${escapeHtml(skill.description || "")}</div>
|
|
242
|
+
<div class="meta-line">${escapeHtml(skill.description || "No description")}</div>
|
|
211
243
|
<div class="tag-row">
|
|
212
|
-
<span class="tag"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
<div class="item-actions">
|
|
216
|
-
<button class="small-action" data-skill-action="enable" data-skill="${escapeHtml(skill.name)}"><i class="ti ti-toggle-right"></i>Enable</button>
|
|
217
|
-
<button class="small-action" data-skill-action="disable" data-skill="${escapeHtml(skill.name)}"><i class="ti ti-toggle-left"></i>Disable</button>
|
|
244
|
+
<span class="tag">v${escapeHtml(version)}</span>
|
|
245
|
+
${skill.category ? `<span class="tag">${escapeHtml(skill.category)}</span>` : ""}
|
|
246
|
+
<span class="tag">${escapeHtml(source)}</span>
|
|
218
247
|
</div>
|
|
219
|
-
|
|
220
|
-
|
|
248
|
+
<div class="item-actions">${actions}</div>
|
|
249
|
+
</div>`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderSkills(payload) {
|
|
253
|
+
if (payload) state.skillsPayload = payload;
|
|
254
|
+
const data = state.skillsPayload || { installed: [], available: [] };
|
|
255
|
+
const tabs = computeSkillTabs(data);
|
|
256
|
+
const updatesCount = $("skill-updates-count");
|
|
257
|
+
if (updatesCount) updatesCount.textContent = tabs.updates.length ? String(tabs.updates.length) : "";
|
|
258
|
+
document.querySelectorAll("[data-skill-tab]").forEach((btn) => {
|
|
259
|
+
btn.classList.toggle("active", btn.dataset.skillTab === state.skillTab);
|
|
260
|
+
});
|
|
261
|
+
const tab = state.skillTab;
|
|
262
|
+
const rows = (tab === "installed" || tab === "updates")
|
|
263
|
+
? (tabs[tab] || []).map((s) => renderSkillRow(s, { installed: true }))
|
|
264
|
+
: (tabs[tab] || []).slice(0, 24).map((s) => renderSkillRow(s, { installed: false }));
|
|
265
|
+
const empty = {
|
|
266
|
+
recommended: "No recommended skills right now.",
|
|
267
|
+
popular: "Marketplace is empty.",
|
|
268
|
+
installed: "No skills installed yet.",
|
|
269
|
+
updates: "All installed skills are up to date.",
|
|
270
|
+
}[tab];
|
|
271
|
+
$("skill-list").innerHTML = rows.length ? rows.join("") : `<div class="list-item"><div class="meta-line">${escapeHtml(empty)}</div></div>`;
|
|
221
272
|
}
|
|
222
273
|
|
|
223
274
|
function renderTimeline(payload) {
|
|
@@ -327,6 +378,173 @@ async function addMember(workspaceId) {
|
|
|
327
378
|
await refreshAll();
|
|
328
379
|
}
|
|
329
380
|
|
|
381
|
+
// ── Workspace summary (Phase 3) ──────────────────────────────────────────────
|
|
382
|
+
function renderWorkspaceSummary(os) {
|
|
383
|
+
const reg = os?.workspace_registry || {};
|
|
384
|
+
const workspaces = reg.workspaces || [];
|
|
385
|
+
const activeId = state.activeWorkspace || reg.active_workspace;
|
|
386
|
+
const active = workspaces.find((w) => w.workspace_id === activeId) || workspaces[0] || { name: "Personal Workspace", type: "personal", your_role: "owner", member_count: 1 };
|
|
387
|
+
const counts = os?.counts || {};
|
|
388
|
+
const scopePill = $("summary-scope-pill");
|
|
389
|
+
if (scopePill) scopePill.textContent = active.type || "personal";
|
|
390
|
+
const summary = $("workspace-summary");
|
|
391
|
+
if (summary) {
|
|
392
|
+
const stats = [["Snapshots", counts.snapshots], ["Memories", counts.memories], ["Agent runs", counts.agent_runs], ["Workflows", counts.workflows], ["Traces", counts.traces], ["Timeline", counts.timeline]];
|
|
393
|
+
summary.innerHTML = `
|
|
394
|
+
<div class="summary-main">
|
|
395
|
+
<div class="summary-icon"><i class="ti ${active.type === "organization" ? "ti-building-community" : "ti-user"}"></i></div>
|
|
396
|
+
<div class="summary-id">
|
|
397
|
+
<div class="summary-name">${escapeHtml(active.name || "Personal Workspace")}</div>
|
|
398
|
+
<div class="meta-line">${escapeHtml(active.type || "personal")} workspace · your role <strong>${escapeHtml(active.your_role || "owner")}</strong> · ${escapeHtml(active.member_count ?? 1)} member(s)</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
<div class="summary-stats">
|
|
402
|
+
${stats.map(([l, v]) => `<div class="summary-stat"><strong>${escapeHtml(v || 0)}</strong><span>${escapeHtml(l)}</span></div>`).join("")}
|
|
403
|
+
</div>`;
|
|
404
|
+
}
|
|
405
|
+
const quick = $("workspace-quickswitch");
|
|
406
|
+
if (quick) {
|
|
407
|
+
quick.innerHTML = workspaces.map((w) => `
|
|
408
|
+
<button class="switch-chip ${w.workspace_id === activeId ? "active" : ""}" data-ws-action="activate" data-ws="${escapeHtml(w.workspace_id)}">
|
|
409
|
+
<i class="ti ${w.type === "organization" ? "ti-building-community" : "ti-user"}"></i>
|
|
410
|
+
<span>${escapeHtml(w.name)}</span>${w.workspace_id === activeId ? ' <i class="ti ti-check"></i>' : ""}
|
|
411
|
+
</button>`).join("");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Knowledge Graph explorer (Phase 2) ───────────────────────────────────────
|
|
416
|
+
const ENTITY_ICONS = { Person: "ti-user", Concept: "ti-bulb", Document: "ti-file-text", File: "ti-file", Code: "ti-code", Chat: "ti-message", Conversation: "ti-messages", Message: "ti-message-dots", Task: "ti-checklist", Decision: "ti-gavel", Error: "ti-alert-triangle", Model: "ti-cpu", Tool: "ti-tool", Project: "ti-folders", Feature: "ti-star", AIResponse: "ti-robot", Chunk: "ti-file-stack" };
|
|
417
|
+
function entityIcon(type) { return ENTITY_ICONS[type] || "ti-point"; }
|
|
418
|
+
function prettyId(id) { return String(id || "").split(":").slice(1).join(":") || String(id || ""); }
|
|
419
|
+
|
|
420
|
+
async function loadGraphExplorer() {
|
|
421
|
+
try {
|
|
422
|
+
const data = await api("/knowledge-graph/graph?limit=150");
|
|
423
|
+
const nodes = (data.nodes || []).slice();
|
|
424
|
+
nodes.sort((a, b) => (b.importance ?? b.metadata?.graph_metrics?.importance_raw ?? 0) - (a.importance ?? a.metadata?.graph_metrics?.importance_raw ?? 0));
|
|
425
|
+
state.entities = nodes;
|
|
426
|
+
renderEntities();
|
|
427
|
+
} catch (e) {
|
|
428
|
+
const el = $("entity-list");
|
|
429
|
+
if (el) el.innerHTML = `<div class="list-item"><div class="meta-line">Knowledge graph unavailable: ${escapeHtml(e.message)}</div></div>`;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function renderEntities() {
|
|
434
|
+
const el = $("entity-list");
|
|
435
|
+
if (!el) return;
|
|
436
|
+
const q = ($("entity-search")?.value || "").toLowerCase().trim();
|
|
437
|
+
const filtered = q ? state.entities.filter((n) => `${n.title || ""} ${n.type || ""} ${n.id || ""}`.toLowerCase().includes(q)) : state.entities;
|
|
438
|
+
const list = filtered.slice(0, 40);
|
|
439
|
+
el.innerHTML = list.length ? list.map((n) => {
|
|
440
|
+
const m = n.metadata?.graph_metrics || {};
|
|
441
|
+
const imp = Math.round((n.importance_norm ?? m.importance_norm ?? 0) * 100);
|
|
442
|
+
return `
|
|
443
|
+
<button class="list-item entity-card ${n.id === state.activeEntity ? "selected" : ""}" data-entity="${escapeHtml(n.id)}">
|
|
444
|
+
<div class="list-title"><span><i class="ti ${entityIcon(n.type)}"></i> ${escapeHtml(n.title || prettyId(n.id))}</span><span class="status-pill">${escapeHtml(n.type || "node")}</span></div>
|
|
445
|
+
${n.summary ? `<div class="meta-line">${escapeHtml(String(n.summary).slice(0, 110))}</div>` : ""}
|
|
446
|
+
<div class="tag-row"><span class="tag">${escapeHtml(m.degree ?? 0)} links</span><span class="tag">importance ${imp}%</span></div>
|
|
447
|
+
<div class="importance-bar"><span style="width:${imp}%"></span></div>
|
|
448
|
+
</button>`;
|
|
449
|
+
}).join("") : `<div class="list-item"><div class="meta-line">No matching entities.</div></div>`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function selectEntity(id) {
|
|
453
|
+
state.activeEntity = id;
|
|
454
|
+
renderEntities();
|
|
455
|
+
const detail = $("entity-detail");
|
|
456
|
+
const title = $("entity-detail-title");
|
|
457
|
+
if (title) title.textContent = "Loading…";
|
|
458
|
+
try {
|
|
459
|
+
const d = await api(`/workspace/relationships/${encodeURIComponent(id)}`);
|
|
460
|
+
const node = d.node || {};
|
|
461
|
+
const related = d.related_entities || [];
|
|
462
|
+
const relMap = new Map(related.map((r) => [r.id, r]));
|
|
463
|
+
const labelFor = (nodeId) => { const r = relMap.get(nodeId); return r ? (r.title || prettyId(nodeId)) : prettyId(nodeId); };
|
|
464
|
+
const edgeRow = (e, dir) => {
|
|
465
|
+
const other = dir === "out" ? e.to : e.from;
|
|
466
|
+
return `<div class="rel-row"><span class="rel-dir">${dir === "out" ? "→" : "←"}</span><span class="tag">${escapeHtml(e.type || "related")}</span><span class="rel-node">${escapeHtml(labelFor(other))}</span></div>`;
|
|
467
|
+
};
|
|
468
|
+
const inbound = (d.inbound || []).slice(0, 8);
|
|
469
|
+
const outbound = (d.outbound || []).slice(0, 8);
|
|
470
|
+
const path = Array.isArray(d.shortest_path) ? d.shortest_path : [];
|
|
471
|
+
if (title) title.textContent = node.title || prettyId(id);
|
|
472
|
+
detail.innerHTML = `
|
|
473
|
+
<div class="list-item">
|
|
474
|
+
<div class="list-title"><span><i class="ti ${entityIcon(node.type)}"></i> ${escapeHtml(node.title || prettyId(id))}</span><span class="status-pill">${escapeHtml(node.type || "node")}</span></div>
|
|
475
|
+
${node.summary ? `<div class="meta-line">${escapeHtml(node.summary)}</div>` : ""}
|
|
476
|
+
<div class="tag-row"><span class="tag">importance ${Math.round((node.importance_norm || 0) * 100)}%</span><span class="tag">${inbound.length + outbound.length} relationships</span></div>
|
|
477
|
+
</div>
|
|
478
|
+
<div class="list-item"><div class="list-title"><span>Outbound</span><span class="status-pill">${outbound.length}</span></div>${outbound.map((e) => edgeRow(e, "out")).join("") || '<div class="meta-line">None</div>'}</div>
|
|
479
|
+
<div class="list-item"><div class="list-title"><span>Inbound</span><span class="status-pill">${inbound.length}</span></div>${inbound.map((e) => edgeRow(e, "in")).join("") || '<div class="meta-line">None</div>'}</div>
|
|
480
|
+
${related.length ? `<div class="list-item"><div class="list-title"><span>Related entities</span><span class="status-pill">${related.length}</span></div><div class="tag-row">${related.slice(0, 10).map((r) => `<span class="tag"><i class="ti ${entityIcon(r.type)}"></i> ${escapeHtml(r.title || prettyId(r.id))}</span>`).join("")}</div></div>` : ""}
|
|
481
|
+
${path.length ? `<div class="list-item"><div class="list-title"><span>Path to you</span><span class="status-pill">${path.length} hops</span></div><div class="meta-line">${path.map((p) => escapeHtml(typeof p === "string" ? prettyId(p) : (p.title || prettyId(p.id)))).join(" → ")}</div></div>` : ""}
|
|
482
|
+
<div class="item-actions"><a class="small-action" href="/graph?node=${encodeURIComponent(id)}"><i class="ti ti-network"></i>Open in Graph Canvas</a></div>`;
|
|
483
|
+
} catch (e) {
|
|
484
|
+
if (title) title.textContent = "Relationships";
|
|
485
|
+
detail.innerHTML = `<div class="list-item"><div class="meta-line">No relationships available: ${escapeHtml(e.message)}</div></div>`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── Recent activity feed (Phase 2), built from already-fetched data ───────────
|
|
490
|
+
function renderActivity({ traces, snapshots, memories, workflows, timeline }) {
|
|
491
|
+
const items = [];
|
|
492
|
+
(traces.traces || []).forEach((t) => items.push({ ts: t.created_at, icon: "ti-search", label: `Answer trace: ${t.question || "query"}`, tag: "graph rag" }));
|
|
493
|
+
(snapshots.snapshots || []).forEach((s) => items.push({ ts: s.created_at, icon: "ti-stack-2", label: `Snapshot: ${s.name}`, tag: "snapshot" }));
|
|
494
|
+
(memories.memories || []).forEach((m) => items.push({ ts: m.updated_at, icon: "ti-book-2", label: `Memory: ${(m.content || m.kind || "").slice(0, 60)}`, tag: m.kind || "memory" }));
|
|
495
|
+
(workflows.workflows || []).forEach((w) => items.push({ ts: w.created_at, icon: "ti-git-branch", label: `Workflow: ${w.name}`, tag: "workflow" }));
|
|
496
|
+
(timeline.events || []).forEach((e) => items.push({ ts: e.timestamp, icon: "ti-timeline-event", label: e.event_type || "event", tag: e.area || "workspace" }));
|
|
497
|
+
items.sort((a, b) => String(b.ts || "").localeCompare(String(a.ts || "")));
|
|
498
|
+
const el = $("activity-list");
|
|
499
|
+
if (!el) return;
|
|
500
|
+
el.innerHTML = items.length ? items.slice(0, 18).map((it) => `
|
|
501
|
+
<div class="list-item activity-item">
|
|
502
|
+
<div class="list-title"><span><i class="ti ${it.icon}"></i> ${escapeHtml(it.label)}</span><span class="status-pill">${escapeHtml(it.tag)}</span></div>
|
|
503
|
+
<div class="meta-line">${escapeHtml(it.ts || "")}</div>
|
|
504
|
+
</div>`).join("") : `<div class="list-item"><div class="meta-line">No recent activity yet — index a folder or ask a question to get started.</div></div>`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function renderMemoryFeed(payload) {
|
|
508
|
+
const memories = payload.memories || [];
|
|
509
|
+
const el = $("memory-feed");
|
|
510
|
+
if (!el) return;
|
|
511
|
+
el.innerHTML = memories.length ? memories.slice(0, 8).map((m) => `
|
|
512
|
+
<div class="list-item">
|
|
513
|
+
<div class="list-title"><span><i class="ti ti-book-2"></i> ${escapeHtml(m.kind || "memory")}</span><span class="status-pill">${escapeHtml(m.updated_at || "")}</span></div>
|
|
514
|
+
<div class="meta-line">${escapeHtml(String(m.content || "").slice(0, 140))}</div>
|
|
515
|
+
</div>`).join("") : `<div class="list-item"><div class="meta-line">No workspace memory yet.</div></div>`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ── Enterprise capability panel (Phase 6) ─────────────────────────────────────
|
|
519
|
+
const CAPABILITY_LABELS = {
|
|
520
|
+
sso_advanced: "Advanced SSO", idp_provisioning: "IdP Provisioning", scim: "SCIM",
|
|
521
|
+
rbac_abac_advanced: "Advanced RBAC/ABAC", tenant_isolation: "Tenant Isolation",
|
|
522
|
+
compliance_retention: "Compliance Retention", siem_export: "SIEM Export",
|
|
523
|
+
private_vpc: "Private VPC", air_gapped_deployment: "Air-gapped Deploy",
|
|
524
|
+
dlp_policy: "DLP Policy", ediscovery: "eDiscovery", admin_policy_packs: "Admin Policy Packs",
|
|
525
|
+
};
|
|
526
|
+
function renderEnterprise(edition) {
|
|
527
|
+
edition = edition || {};
|
|
528
|
+
const caps = edition.capabilities || {};
|
|
529
|
+
const editionName = edition.edition || "community";
|
|
530
|
+
const pill = $("enterprise-edition-pill");
|
|
531
|
+
if (pill) { pill.textContent = editionName; pill.className = `status-pill ${edition.is_enterprise ? "status-complete" : ""}`; }
|
|
532
|
+
const note = $("enterprise-note");
|
|
533
|
+
if (note) note.textContent = edition.community_notice || "Community edition: every Enterprise capability below is an extension point and is disabled. Nothing here gates a Community feature.";
|
|
534
|
+
const grid = $("capability-grid");
|
|
535
|
+
if (!grid) return;
|
|
536
|
+
const keys = Object.keys(caps).length ? Object.keys(caps) : Object.keys(CAPABILITY_LABELS);
|
|
537
|
+
grid.innerHTML = keys.map((k) => {
|
|
538
|
+
const on = Boolean(caps[k]);
|
|
539
|
+
return `
|
|
540
|
+
<div class="capability-card ${on ? "on" : "off"}">
|
|
541
|
+
<i class="ti ${on ? "ti-circle-check" : "ti-lock"}"></i>
|
|
542
|
+
<span class="cap-name">${escapeHtml(CAPABILITY_LABELS[k] || k)}</span>
|
|
543
|
+
<span class="status-pill ${on ? "status-complete" : "status-failed"}">${on ? "enabled" : "disabled"}</span>
|
|
544
|
+
</div>`;
|
|
545
|
+
}).join("");
|
|
546
|
+
}
|
|
547
|
+
|
|
330
548
|
async function refreshAll() {
|
|
331
549
|
const [os, onboarding, traces, indexing, snapshots, memories, computerMemory, agents, workflows, skills, timeline] = await Promise.all([
|
|
332
550
|
api("/workspace/os"),
|
|
@@ -354,6 +572,11 @@ async function refreshAll() {
|
|
|
354
572
|
renderWorkflows(workflows);
|
|
355
573
|
renderSkills(skills);
|
|
356
574
|
renderTimeline(timeline);
|
|
575
|
+
renderWorkspaceSummary(os);
|
|
576
|
+
renderEnterprise(os.edition);
|
|
577
|
+
renderActivity({ traces, snapshots, memories, workflows, timeline });
|
|
578
|
+
renderMemoryFeed(memories);
|
|
579
|
+
loadGraphExplorer();
|
|
357
580
|
}
|
|
358
581
|
|
|
359
582
|
async function createSnapshot() {
|
|
@@ -423,6 +646,19 @@ async function configureComputerMemory(enabled) {
|
|
|
423
646
|
}
|
|
424
647
|
|
|
425
648
|
document.addEventListener("click", async (event) => {
|
|
649
|
+
const entityBtn = event.target.closest("[data-entity]");
|
|
650
|
+
if (entityBtn) {
|
|
651
|
+
selectEntity(entityBtn.dataset.entity).catch((err) => toast(err.message));
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const skillTab = event.target.closest("[data-skill-tab]");
|
|
656
|
+
if (skillTab) {
|
|
657
|
+
state.skillTab = skillTab.dataset.skillTab;
|
|
658
|
+
renderSkills();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
426
662
|
const step = event.target.closest("[data-step]");
|
|
427
663
|
if (step) {
|
|
428
664
|
await api("/workspace/onboarding/step", {
|
|
@@ -521,6 +757,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
521
757
|
}));
|
|
522
758
|
$("create-demo-workflow").addEventListener("click", () => createDemoWorkflow().catch((err) => toast(err.message)));
|
|
523
759
|
$("reload-skills").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
|
|
760
|
+
const entitySearch = $("entity-search");
|
|
761
|
+
if (entitySearch) entitySearch.addEventListener("input", () => renderEntities());
|
|
762
|
+
const reloadEntities = $("reload-entities");
|
|
763
|
+
if (reloadEntities) reloadEntities.addEventListener("click", () => loadGraphExplorer().catch((err) => toast(err.message)));
|
|
764
|
+
const reloadActivity = $("reload-activity");
|
|
765
|
+
if (reloadActivity) reloadActivity.addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
|
|
524
766
|
$("workspace-select").addEventListener("change", (event) => activateWorkspace(event.target.value).catch((err) => toast(err.message)));
|
|
525
767
|
$("create-org").addEventListener("click", () => createOrg().catch((err) => toast(err.message)));
|
|
526
768
|
$("new-org-btn").addEventListener("click", () => $("org-name").focus());
|
package/static/workspace.css
CHANGED
|
@@ -544,3 +544,70 @@ textarea {
|
|
|
544
544
|
#org-create-form {
|
|
545
545
|
margin-bottom: 12px;
|
|
546
546
|
}
|
|
547
|
+
|
|
548
|
+
/* ── Product Experience Deepening (v1.6.0) ──────────────────────────────── */
|
|
549
|
+
|
|
550
|
+
/* Workspace summary */
|
|
551
|
+
.summary-card {
|
|
552
|
+
display: flex;
|
|
553
|
+
flex-wrap: wrap;
|
|
554
|
+
justify-content: space-between;
|
|
555
|
+
gap: 16px;
|
|
556
|
+
align-items: center;
|
|
557
|
+
}
|
|
558
|
+
.summary-main { display: flex; align-items: center; gap: 14px; }
|
|
559
|
+
.summary-icon {
|
|
560
|
+
width: 48px; height: 48px; border-radius: 12px;
|
|
561
|
+
display: grid; place-items: center;
|
|
562
|
+
background: #eef2ff; color: var(--blue); font-size: 24px;
|
|
563
|
+
}
|
|
564
|
+
.summary-name { font-weight: 800; font-size: 18px; color: var(--ink); }
|
|
565
|
+
.summary-stats { display: flex; flex-wrap: wrap; gap: 18px; }
|
|
566
|
+
.summary-stat { display: grid; text-align: center; }
|
|
567
|
+
.summary-stat strong { font-size: 20px; color: var(--ink); }
|
|
568
|
+
.summary-stat span { font-size: 11px; color: var(--muted); font-weight: 700; }
|
|
569
|
+
.quickswitch-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
|
|
570
|
+
.switch-chip {
|
|
571
|
+
display: inline-flex; align-items: center; gap: 6px;
|
|
572
|
+
border: 1px solid var(--line); background: #fbfcfe; color: var(--ink);
|
|
573
|
+
border-radius: 999px; padding: 6px 12px; font-weight: 700; font-size: 13px; cursor: pointer;
|
|
574
|
+
}
|
|
575
|
+
.switch-chip.active { border-color: var(--blue); background: #eef2ff; color: var(--blue); }
|
|
576
|
+
|
|
577
|
+
/* Entity explorer */
|
|
578
|
+
.entity-card { text-align: left; cursor: pointer; width: 100%; font: inherit; }
|
|
579
|
+
.entity-card.selected { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15); }
|
|
580
|
+
.importance-bar { height: 4px; border-radius: 999px; background: #edf2f7; overflow: hidden; }
|
|
581
|
+
.importance-bar span { display: block; height: 100%; background: linear-gradient(90deg, #2563eb, #7c3aed); }
|
|
582
|
+
.rel-row { display: flex; align-items: center; gap: 8px; font-size: 13px; padding: 2px 0; }
|
|
583
|
+
.rel-dir { color: var(--muted); font-weight: 800; width: 16px; }
|
|
584
|
+
.rel-node { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
585
|
+
.activity-item .list-title span { font-weight: 700; }
|
|
586
|
+
|
|
587
|
+
/* Skill marketplace tabs */
|
|
588
|
+
.tab-bar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
|
589
|
+
.tab {
|
|
590
|
+
border: 1px solid var(--line); background: #fbfcfe; color: var(--muted);
|
|
591
|
+
border-radius: 999px; padding: 6px 14px; font-weight: 700; font-size: 13px; cursor: pointer;
|
|
592
|
+
}
|
|
593
|
+
.tab.active { border-color: var(--blue); background: #eef2ff; color: var(--blue); }
|
|
594
|
+
.tab-count {
|
|
595
|
+
display: inline-block; min-width: 16px; padding: 0 5px; margin-left: 4px;
|
|
596
|
+
border-radius: 999px; background: var(--red); color: #fff; font-size: 11px;
|
|
597
|
+
}
|
|
598
|
+
.tab-count:empty { display: none; }
|
|
599
|
+
|
|
600
|
+
/* Enterprise capability grid */
|
|
601
|
+
.capability-grid {
|
|
602
|
+
display: grid; gap: 10px;
|
|
603
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
604
|
+
margin-top: 12px;
|
|
605
|
+
}
|
|
606
|
+
.capability-card {
|
|
607
|
+
display: flex; align-items: center; gap: 10px;
|
|
608
|
+
border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: #fbfcfe;
|
|
609
|
+
}
|
|
610
|
+
.capability-card i { font-size: 18px; }
|
|
611
|
+
.capability-card.off i { color: var(--muted); }
|
|
612
|
+
.capability-card.on i { color: var(--green); }
|
|
613
|
+
.capability-card .cap-name { flex: 1; font-weight: 700; font-size: 13px; color: var(--ink); }
|
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=1.
|
|
11
|
+
<link rel="stylesheet" href="/static/workspace.css?v=1.6.0">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div class="workspace-shell">
|
|
@@ -20,12 +20,14 @@
|
|
|
20
20
|
<nav>
|
|
21
21
|
<a class="active" href="#overview"><i class="ti ti-layout-dashboard"></i><span>Overview</span></a>
|
|
22
22
|
<a href="#graph"><i class="ti ti-chart-dots-3"></i><span>Graph</span></a>
|
|
23
|
+
<a href="#graph-explorer"><i class="ti ti-affiliate"></i><span>Explorer</span></a>
|
|
23
24
|
<a href="#snapshots"><i class="ti ti-stack-2"></i><span>Snapshots</span></a>
|
|
24
25
|
<a href="#memory"><i class="ti ti-book-2"></i><span>Memory</span></a>
|
|
25
26
|
<a href="#agents"><i class="ti ti-route-alt-left"></i><span>Agents</span></a>
|
|
26
27
|
<a href="#workflows"><i class="ti ti-git-branch"></i><span>Workflow</span></a>
|
|
27
28
|
<a href="#skills"><i class="ti ti-puzzle"></i><span>Skills</span></a>
|
|
28
29
|
<a href="#timeline"><i class="ti ti-timeline-event"></i><span>Timeline</span></a>
|
|
30
|
+
<a href="#enterprise"><i class="ti ti-building-skyscraper"></i><span>Editions</span></a>
|
|
29
31
|
</nav>
|
|
30
32
|
<div class="rail-links">
|
|
31
33
|
<a href="/chat"><i class="ti ti-message-circle"></i><span>Chat</span></a>
|
|
@@ -54,6 +56,18 @@
|
|
|
54
56
|
|
|
55
57
|
<section class="metric-grid" id="metric-grid"></section>
|
|
56
58
|
|
|
59
|
+
<section class="workspace-band" id="workspace-summary-band">
|
|
60
|
+
<div class="section-head">
|
|
61
|
+
<div>
|
|
62
|
+
<div class="eyebrow">You are here</div>
|
|
63
|
+
<h2>Current Workspace</h2>
|
|
64
|
+
</div>
|
|
65
|
+
<span class="status-pill" id="summary-scope-pill">personal</span>
|
|
66
|
+
</div>
|
|
67
|
+
<div id="workspace-summary" class="summary-card"></div>
|
|
68
|
+
<div id="workspace-quickswitch" class="quickswitch-row"></div>
|
|
69
|
+
</section>
|
|
70
|
+
|
|
57
71
|
<section class="workspace-band" id="organization">
|
|
58
72
|
<div class="section-head">
|
|
59
73
|
<div>
|
|
@@ -120,6 +134,55 @@
|
|
|
120
134
|
</div>
|
|
121
135
|
</section>
|
|
122
136
|
|
|
137
|
+
<section class="workspace-grid two-col" id="graph-explorer">
|
|
138
|
+
<div class="workspace-panel">
|
|
139
|
+
<div class="section-head">
|
|
140
|
+
<div>
|
|
141
|
+
<div class="eyebrow">Knowledge Graph</div>
|
|
142
|
+
<h2>Entity Explorer</h2>
|
|
143
|
+
</div>
|
|
144
|
+
<button class="icon-action" id="reload-entities" title="Reload entities"><i class="ti ti-refresh"></i></button>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="inline-form">
|
|
147
|
+
<input id="entity-search" placeholder="Search entities (concept, person, file…)">
|
|
148
|
+
</div>
|
|
149
|
+
<div id="entity-list" class="list-stack"></div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="workspace-panel">
|
|
152
|
+
<div class="section-head">
|
|
153
|
+
<div>
|
|
154
|
+
<div class="eyebrow">Relationships</div>
|
|
155
|
+
<h2 id="entity-detail-title">Select an entity</h2>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div id="entity-detail" class="list-stack">
|
|
159
|
+
<div class="list-item"><div class="meta-line">Pick an entity on the left to see its relationships, related entities, and a path back to you.</div></div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</section>
|
|
163
|
+
|
|
164
|
+
<section class="workspace-grid two-col" id="activity">
|
|
165
|
+
<div class="workspace-panel">
|
|
166
|
+
<div class="section-head">
|
|
167
|
+
<div>
|
|
168
|
+
<div class="eyebrow">Workspace</div>
|
|
169
|
+
<h2>Recent Activity</h2>
|
|
170
|
+
</div>
|
|
171
|
+
<button class="icon-action" id="reload-activity" title="Reload activity"><i class="ti ti-refresh"></i></button>
|
|
172
|
+
</div>
|
|
173
|
+
<div id="activity-list" class="list-stack"></div>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="workspace-panel">
|
|
176
|
+
<div class="section-head">
|
|
177
|
+
<div>
|
|
178
|
+
<div class="eyebrow">Memory</div>
|
|
179
|
+
<h2>Workspace Memory Feed</h2>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div id="memory-feed" class="list-stack"></div>
|
|
183
|
+
</div>
|
|
184
|
+
</section>
|
|
185
|
+
|
|
123
186
|
<section class="workspace-grid two-col" id="snapshots">
|
|
124
187
|
<div class="workspace-panel">
|
|
125
188
|
<div class="section-head">
|
|
@@ -217,6 +280,12 @@
|
|
|
217
280
|
</div>
|
|
218
281
|
<button class="icon-action" id="reload-skills" title="Reload skills"><i class="ti ti-refresh"></i></button>
|
|
219
282
|
</div>
|
|
283
|
+
<div class="tab-bar" id="skill-tabs">
|
|
284
|
+
<button class="tab active" data-skill-tab="recommended">Recommended</button>
|
|
285
|
+
<button class="tab" data-skill-tab="popular">Popular</button>
|
|
286
|
+
<button class="tab" data-skill-tab="installed">Installed</button>
|
|
287
|
+
<button class="tab" data-skill-tab="updates">Updates <span class="tab-count" id="skill-updates-count"></span></button>
|
|
288
|
+
</div>
|
|
220
289
|
<div id="skill-list" class="list-stack"></div>
|
|
221
290
|
</div>
|
|
222
291
|
<div class="workspace-panel" id="timeline">
|
|
@@ -229,10 +298,22 @@
|
|
|
229
298
|
<div id="timeline-list" class="timeline-list"></div>
|
|
230
299
|
</div>
|
|
231
300
|
</section>
|
|
301
|
+
|
|
302
|
+
<section class="workspace-band" id="enterprise">
|
|
303
|
+
<div class="section-head">
|
|
304
|
+
<div>
|
|
305
|
+
<div class="eyebrow">Editions</div>
|
|
306
|
+
<h2>Enterprise Capabilities</h2>
|
|
307
|
+
</div>
|
|
308
|
+
<span class="status-pill" id="enterprise-edition-pill">community</span>
|
|
309
|
+
</div>
|
|
310
|
+
<p class="meta-line" id="enterprise-note"></p>
|
|
311
|
+
<div id="capability-grid" class="capability-grid"></div>
|
|
312
|
+
</section>
|
|
232
313
|
</main>
|
|
233
314
|
</div>
|
|
234
315
|
|
|
235
316
|
<div class="toast" id="toast"></div>
|
|
236
|
-
<script src="/static/scripts/workspace.js?v=1.
|
|
317
|
+
<script src="/static/scripts/workspace.js?v=1.6.0"></script>
|
|
237
318
|
</body>
|
|
238
319
|
</html>
|