ltcai 0.6.0 → 1.0.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 +20 -5
- package/docs/CHANGELOG.md +51 -0
- package/knowledge_graph.py +33 -0
- package/latticeai/__init__.py +3 -1
- package/latticeai/core/workspace_os.py +1178 -0
- package/latticeai/server_app.py +605 -6
- package/package.json +6 -3
- package/static/admin.html +1 -0
- package/static/graph.html +1 -0
- package/static/manifest.json +2 -2
- package/static/scripts/chat.js +4 -2
- package/static/scripts/graph.js +3 -3
- package/static/scripts/workspace.js +382 -0
- package/static/sw.js +5 -1
- package/static/workspace.css +515 -0
- package/static/workspace.html +199 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ltcai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lattice AI local
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lattice AI Workspace OS for local-first graph, memory, agent, workflow, and skill operations",
|
|
5
5
|
"homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "LTCAI",
|
|
19
19
|
"dev": "python3 ltcai_cli.py --reload",
|
|
20
|
+
"build": "npm run build:python",
|
|
20
21
|
"build:python": "python3 -m build",
|
|
21
|
-
"check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/core/tool_registry.py latticeai/core/agent_prompts.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
|
|
22
|
+
"check:python": "python3 -m py_compile ltcai_cli.py server.py latticeai/server_app.py latticeai/core/tool_registry.py latticeai/core/agent_prompts.py latticeai/core/workspace_os.py knowledge_graph.py knowledge_graph_api.py local_knowledge_api.py llm_router.py p_reinforce.py telegram_bot.py tools.py codex_telegram_bot.py",
|
|
22
23
|
"test": "python3 -m pytest tests/ -v",
|
|
23
24
|
"test:unit": "python3 -m pytest tests/unit/ -v",
|
|
24
25
|
"test:integration": "python3 -m pytest tests/integration/ -v",
|
|
@@ -60,9 +61,11 @@
|
|
|
60
61
|
"static/chat.html",
|
|
61
62
|
"static/admin.html",
|
|
62
63
|
"static/graph.html",
|
|
64
|
+
"static/workspace.html",
|
|
63
65
|
"static/manifest.json",
|
|
64
66
|
"static/sw.js",
|
|
65
67
|
"static/lattice-reference.css",
|
|
68
|
+
"static/workspace.css",
|
|
66
69
|
"static/scripts/",
|
|
67
70
|
"static/css/",
|
|
68
71
|
"static/icons/",
|
package/static/admin.html
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
<a href="#sso" data-admin-nav="sso"><i class="ti ti-lock-access"></i> <span data-i18n="nav_sso">SSO 관리</span></a>
|
|
29
29
|
<a href="#security" data-admin-nav="security"><i class="ti ti-shield-check"></i> <span data-i18n="nav_security">보안 모니터링</span></a>
|
|
30
30
|
<a href="#audit" data-admin-nav="audit"><i class="ti ti-report-search"></i> <span data-i18n="nav_audit">감사 로그</span></a>
|
|
31
|
+
<a href="/workspace"><i class="ti ti-layout-dashboard"></i> <span>Workspace OS</span></a>
|
|
31
32
|
<a href="/chat"><i class="ti ti-message-circle"></i> <span data-i18n="nav_chat">채팅으로</span></a>
|
|
32
33
|
</nav>
|
|
33
34
|
<div class="rail-project">
|
package/static/graph.html
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<div class="rail-brand"><i class="ti ti-chart-dots-3"></i><strong>Lattice AI</strong></div>
|
|
16
16
|
<nav>
|
|
17
17
|
<a href="/chat"><i class="ti ti-home"></i> 홈</a>
|
|
18
|
+
<a href="/workspace"><i class="ti ti-layout-dashboard"></i> Workspace OS</a>
|
|
18
19
|
<a class="active" href="/graph"><i class="ti ti-chart-dots-3"></i> 지식 그래프</a>
|
|
19
20
|
<a href="/chat"><i class="ti ti-message-circle"></i> 대화</a>
|
|
20
21
|
<a href="/chat"><i class="ti ti-file"></i> 파일</a>
|
package/static/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Lattice AI",
|
|
3
3
|
"short_name": "LatticeAI",
|
|
4
|
-
"description": "
|
|
5
|
-
"start_url": "/",
|
|
4
|
+
"description": "AI Workspace OS for local-first graph, memory, agents, workflows, and skills",
|
|
5
|
+
"start_url": "/workspace",
|
|
6
6
|
"id": "/",
|
|
7
7
|
"display": "standalone",
|
|
8
8
|
"orientation": "any",
|
package/static/scripts/chat.js
CHANGED
|
@@ -225,7 +225,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
225
225
|
// 헤더 / 사이드바
|
|
226
226
|
logout: '로그아웃', admin_dashboard: '관리자 대시보드',
|
|
227
227
|
my_status: '내 상태 보기', auto_setup: '자동 설정',
|
|
228
|
-
nav_home: '홈', nav_chat: '채팅', nav_knowledge: '지식 그래프',
|
|
228
|
+
nav_home: '홈', nav_chat: '채팅', nav_workspace: 'Workspace OS', nav_knowledge: '지식 그래프',
|
|
229
229
|
nav_pipeline: '파이프라인', nav_files: '내 컴퓨터',
|
|
230
230
|
nav_model_status: '모델 상태', nav_runtime: '런타임 설정',
|
|
231
231
|
nav_advanced_settings: '고급 설정',
|
|
@@ -304,7 +304,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
304
304
|
// Header / Sidebar
|
|
305
305
|
logout: 'Logout', admin_dashboard: 'Admin Dashboard',
|
|
306
306
|
my_status: 'My Status', auto_setup: 'Auto Setup',
|
|
307
|
-
nav_home: 'Home', nav_chat: 'Chat', nav_knowledge: 'Knowledge Graph',
|
|
307
|
+
nav_home: 'Home', nav_chat: 'Chat', nav_workspace: 'Workspace OS', nav_knowledge: 'Knowledge Graph',
|
|
308
308
|
nav_pipeline: 'Pipeline', nav_files: 'My Computer',
|
|
309
309
|
nav_model_status: 'Model Status', nav_runtime: 'Runtime Settings',
|
|
310
310
|
nav_advanced_settings: 'Advanced Settings',
|
|
@@ -559,6 +559,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
559
559
|
const BASE_NAV_ITEMS = [
|
|
560
560
|
{ id: 'home', icon: 'ti-home', labelKey: 'nav_home' },
|
|
561
561
|
{ id: 'chat', icon: 'ti-message-circle', labelKey: 'nav_chat' },
|
|
562
|
+
{ id: 'workspace-os', icon: 'ti-layout-dashboard', labelKey: 'nav_workspace' },
|
|
562
563
|
{ id: 'knowledge', icon: 'ti-chart-dots-3', labelKey: 'nav_knowledge' },
|
|
563
564
|
{ id: 'pipeline', icon: 'ti-git-branch', labelKey: 'nav_pipeline' },
|
|
564
565
|
{ id: 'files', icon: 'ti-device-desktop', labelKey: 'nav_files' },
|
|
@@ -622,6 +623,7 @@ const chatViewport = document.getElementById('chat-viewport');
|
|
|
622
623
|
closeSidebar();
|
|
623
624
|
if (id === 'home') showHome();
|
|
624
625
|
else if (id === 'chat') showChat();
|
|
626
|
+
else if (id === 'workspace-os') window.location.href = `${API_BASE}/workspace`;
|
|
625
627
|
else if (id === 'knowledge') openDataGraph();
|
|
626
628
|
else if (id === 'pipeline') openPipelineModal();
|
|
627
629
|
else if (id === 'files') openLocalBrowser();
|
package/static/scripts/graph.js
CHANGED
|
@@ -4,7 +4,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
4
4
|
|
|
5
5
|
const G18N = {
|
|
6
6
|
ko: {
|
|
7
|
-
nav_home: '홈', nav_graph: '지식 그래프', nav_chat: '대화', nav_files: '파일', nav_code: '코드', nav_settings: '설정',
|
|
7
|
+
nav_home: '홈', nav_workspace: 'Workspace OS', nav_graph: '지식 그래프', nav_chat: '대화', nav_files: '파일', nav_code: '코드', nav_settings: '설정',
|
|
8
8
|
project: '프로젝트', search_title: '그래프 탐색', search_sub: '주제, 파일, 대화, 결정, 작업을 검색하세요.',
|
|
9
9
|
ready: '준비됨', search_ph: '주제, 파일, 대화로 검색...', clear_search: '검색 지우기',
|
|
10
10
|
search_results: '{n}개 결과',
|
|
@@ -26,7 +26,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
26
26
|
open_in_chat: '채팅에서 열기', today: '오늘', day_ago: '1일 전', days_ago: '{n}일 전', months_ago: '{n}개월 전', years_ago: '{n}년 전',
|
|
27
27
|
},
|
|
28
28
|
en: {
|
|
29
|
-
nav_home: 'Home', nav_graph: 'Knowledge Graph', nav_chat: 'Chat', nav_files: 'Files', nav_code: 'Code', nav_settings: 'Settings',
|
|
29
|
+
nav_home: 'Home', nav_workspace: 'Workspace OS', nav_graph: 'Knowledge Graph', nav_chat: 'Chat', nav_files: 'Files', nav_code: 'Code', nav_settings: 'Settings',
|
|
30
30
|
project: 'Project', search_title: 'Explore the graph', search_sub: 'Search topics, files, conversations, decisions, and tasks.',
|
|
31
31
|
ready: 'Ready', search_ph: 'Search by topic, file, or conversation...', clear_search: 'Clear search',
|
|
32
32
|
search_results: '{n} result(s)',
|
|
@@ -54,7 +54,7 @@ const API_BASE = window.location.protocol === 'file:' ? 'http://localhost:4825'
|
|
|
54
54
|
|
|
55
55
|
function applyI18n() {
|
|
56
56
|
document.documentElement.lang = currentLang;
|
|
57
|
-
const navLabels = ['nav_home', 'nav_graph', 'nav_chat', 'nav_files', 'nav_code', 'nav_settings'];
|
|
57
|
+
const navLabels = ['nav_home', 'nav_workspace', 'nav_graph', 'nav_chat', 'nav_files', 'nav_code', 'nav_settings'];
|
|
58
58
|
document.querySelectorAll('.graph-rail nav a').forEach((link, index) => {
|
|
59
59
|
const icon = link.querySelector('i')?.outerHTML || '';
|
|
60
60
|
link.innerHTML = `${icon} ${t(navLabels[index])}`;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
const API_BASE = window.location.protocol === "file:" ? "http://localhost:4825" : "";
|
|
2
|
+
|
|
3
|
+
const state = {
|
|
4
|
+
os: null,
|
|
5
|
+
snapshots: [],
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function $(id) {
|
|
9
|
+
return document.getElementById(id);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function escapeHtml(value) {
|
|
13
|
+
return String(value ?? "")
|
|
14
|
+
.replaceAll("&", "&")
|
|
15
|
+
.replaceAll("<", "<")
|
|
16
|
+
.replaceAll(">", ">")
|
|
17
|
+
.replaceAll('"', """)
|
|
18
|
+
.replaceAll("'", "'");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function api(path, options = {}) {
|
|
22
|
+
const headers = { ...(options.headers || {}) };
|
|
23
|
+
if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
|
24
|
+
const response = await fetch(API_BASE + path, { credentials: "include", ...options, headers });
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
let data = {};
|
|
27
|
+
try { data = text ? JSON.parse(text) : {}; } catch { data = { detail: text }; }
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(data.detail || `${response.status} ${response.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toast(message) {
|
|
35
|
+
const node = $("toast");
|
|
36
|
+
node.textContent = message;
|
|
37
|
+
node.classList.add("show");
|
|
38
|
+
clearTimeout(node._timer);
|
|
39
|
+
node._timer = setTimeout(() => node.classList.remove("show"), 2200);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderMetrics(os) {
|
|
43
|
+
const counts = os?.counts || {};
|
|
44
|
+
const graph = os?.graph || {};
|
|
45
|
+
const nodeTotal = Object.values(graph.nodes || {}).reduce((sum, value) => sum + Number(value || 0), 0);
|
|
46
|
+
const edgeTotal = Object.values(graph.edges || {}).reduce((sum, value) => sum + Number(value || 0), 0);
|
|
47
|
+
const items = [
|
|
48
|
+
["Graph Nodes", nodeTotal, "ti-chart-dots-3"],
|
|
49
|
+
["Graph Edges", edgeTotal, "ti-git-branch"],
|
|
50
|
+
["Snapshots", counts.snapshots || 0, "ti-stack-2"],
|
|
51
|
+
["Memories", counts.memories || 0, "ti-book-2"],
|
|
52
|
+
];
|
|
53
|
+
$("metric-grid").innerHTML = items.map(([label, value, icon]) => `
|
|
54
|
+
<div class="metric-card">
|
|
55
|
+
<i class="ti ${icon}"></i>
|
|
56
|
+
<span>${escapeHtml(label)}</span>
|
|
57
|
+
<strong>${escapeHtml(value)}</strong>
|
|
58
|
+
</div>
|
|
59
|
+
`).join("");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderOnboarding(payload) {
|
|
63
|
+
const steps = payload.steps || [];
|
|
64
|
+
$("onboarding-steps").innerHTML = steps.map((step) => {
|
|
65
|
+
const status = step.status || "pending";
|
|
66
|
+
const label = step.id.replaceAll("_", " ");
|
|
67
|
+
return `
|
|
68
|
+
<button class="step-chip" data-step="${escapeHtml(step.id)}" title="Mark ${escapeHtml(label)} complete">
|
|
69
|
+
<span>${escapeHtml(label)}</span>
|
|
70
|
+
<span class="status-pill status-${escapeHtml(status)}">${escapeHtml(status)}</span>
|
|
71
|
+
</button>
|
|
72
|
+
`;
|
|
73
|
+
}).join("");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderTraces(payload) {
|
|
77
|
+
const traces = payload.traces || [];
|
|
78
|
+
$("trace-list").innerHTML = traces.length ? traces.map((trace) => `
|
|
79
|
+
<div class="list-item">
|
|
80
|
+
<div class="list-title">
|
|
81
|
+
<span>${escapeHtml(trace.question || "Trace")}</span>
|
|
82
|
+
<span class="status-pill">${Math.round((trace.confidence || 0) * 100)}%</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="meta-line">${escapeHtml(trace.created_at || "")} · ${escapeHtml(trace.conversation_id || "workspace")}</div>
|
|
85
|
+
<div class="tag-row">
|
|
86
|
+
${(trace.graph_nodes || []).slice(0, 5).map((node) => `<a class="tag" href="/graph?node=${encodeURIComponent(node.id)}">${escapeHtml(node.title || node.id)}</a>`).join("")}
|
|
87
|
+
</div>
|
|
88
|
+
<div class="mini-row">${escapeHtml((trace.source_files || []).map((source) => source.source).slice(0, 3).join(" · ") || "No source files")}</div>
|
|
89
|
+
</div>
|
|
90
|
+
`).join("") : `<div class="list-item"><div class="meta-line">No answer traces yet.</div></div>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderIndexing(payload) {
|
|
94
|
+
const sources = payload.sources || [];
|
|
95
|
+
$("indexing-list").innerHTML = sources.length ? sources.map((source) => `
|
|
96
|
+
<div class="list-item">
|
|
97
|
+
<div class="list-title">
|
|
98
|
+
<span>${escapeHtml(source.label || source.root_path)}</span>
|
|
99
|
+
<span class="status-pill ${source.watch_active ? "status-complete" : ""}">${source.watch_active ? "watching" : source.status || "idle"}</span>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="meta-line">${escapeHtml(source.root_path || "")}</div>
|
|
102
|
+
<div class="tag-row">
|
|
103
|
+
<span class="tag">${Number(source.success_count || 0)} indexed</span>
|
|
104
|
+
<span class="tag">${Number(source.failure_count || 0)} failed</span>
|
|
105
|
+
<span class="tag">${escapeHtml(source.last_run_at || "not scanned")}</span>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="item-actions">
|
|
108
|
+
<button class="small-action" data-index-action="resume" data-source="${escapeHtml(source.id)}"><i class="ti ti-player-play"></i>Resume</button>
|
|
109
|
+
<button class="small-action" data-index-action="pause" data-source="${escapeHtml(source.id)}"><i class="ti ti-player-pause"></i>Pause</button>
|
|
110
|
+
<button class="small-action danger-action" data-index-action="remove" data-source="${escapeHtml(source.id)}"><i class="ti ti-trash"></i>Remove</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
`).join("") : `<div class="list-item"><div class="meta-line">No indexed folders.</div></div>`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderSnapshots(payload) {
|
|
117
|
+
const snapshots = payload.snapshots || [];
|
|
118
|
+
state.snapshots = snapshots;
|
|
119
|
+
$("snapshot-list").innerHTML = snapshots.length ? snapshots.map((snapshot) => `
|
|
120
|
+
<div class="list-item">
|
|
121
|
+
<div class="list-title">
|
|
122
|
+
<span>${escapeHtml(snapshot.name)}</span>
|
|
123
|
+
<span class="status-pill">${escapeHtml(snapshot.node_count || 0)} nodes</span>
|
|
124
|
+
</div>
|
|
125
|
+
<div class="meta-line">${escapeHtml(snapshot.created_at)} · ${escapeHtml(snapshot.id)}</div>
|
|
126
|
+
<div class="item-actions">
|
|
127
|
+
<button class="small-action" data-export-snapshot="${escapeHtml(snapshot.id)}"><i class="ti ti-package-export"></i>Export</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
`).join("") : `<div class="list-item"><div class="meta-line">No snapshots.</div></div>`;
|
|
131
|
+
|
|
132
|
+
const options = snapshots.map((snapshot) => `<option value="${escapeHtml(snapshot.id)}">${escapeHtml(snapshot.name)}</option>`).join("");
|
|
133
|
+
$("snapshot-before").innerHTML = options;
|
|
134
|
+
$("snapshot-after").innerHTML = options;
|
|
135
|
+
if (snapshots[1]) $("snapshot-before").value = snapshots[1].id;
|
|
136
|
+
if (snapshots[0]) $("snapshot-after").value = snapshots[0].id;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderMemories(payload) {
|
|
140
|
+
const memories = payload.memories || [];
|
|
141
|
+
$("memory-list").innerHTML = memories.length ? memories.map((memory) => `
|
|
142
|
+
<div class="list-item">
|
|
143
|
+
<div class="list-title">
|
|
144
|
+
<span>${escapeHtml(memory.kind || "memory")}</span>
|
|
145
|
+
<span class="status-pill">${escapeHtml(memory.updated_at || "")}</span>
|
|
146
|
+
</div>
|
|
147
|
+
<div>${escapeHtml(memory.content || "")}</div>
|
|
148
|
+
<div class="tag-row">${(memory.tags || []).map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}</div>
|
|
149
|
+
</div>
|
|
150
|
+
`).join("") : `<div class="list-item"><div class="meta-line">No personal memory yet.</div></div>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderComputerMemory(payload) {
|
|
154
|
+
const config = payload?.computer_memory || payload || {};
|
|
155
|
+
$("computer-memory-toggle").checked = Boolean(config.enabled);
|
|
156
|
+
$("computer-memory-state").textContent = JSON.stringify({
|
|
157
|
+
enabled: Boolean(config.enabled),
|
|
158
|
+
approved: Boolean(config.approved),
|
|
159
|
+
scopes: config.scopes || [],
|
|
160
|
+
activities: (config.activities || []).length,
|
|
161
|
+
notice: config.notice,
|
|
162
|
+
}, null, 2);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderAgents(payload) {
|
|
166
|
+
const agents = payload.agents || [];
|
|
167
|
+
$("agent-list").innerHTML = agents.map((agent) => `
|
|
168
|
+
<div class="list-item">
|
|
169
|
+
<div class="list-title">
|
|
170
|
+
<span>${escapeHtml(agent.name)}</span>
|
|
171
|
+
<span class="status-pill status-complete">${escapeHtml(agent.status || "available")}</span>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="meta-line">${escapeHtml(agent.role || "")}</div>
|
|
174
|
+
<div class="tag-row">${(agent.relationships || []).map((rel) => `<span class="tag">${escapeHtml(rel)}</span>`).join("")}</div>
|
|
175
|
+
</div>
|
|
176
|
+
`).join("");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderWorkflows(payload) {
|
|
180
|
+
const workflows = payload.workflows || [];
|
|
181
|
+
$("workflow-list").innerHTML = workflows.length ? workflows.map((workflow) => `
|
|
182
|
+
<div class="list-item">
|
|
183
|
+
<div class="list-title">
|
|
184
|
+
<span>${escapeHtml(workflow.name)}</span>
|
|
185
|
+
<span class="status-pill">${(workflow.steps || []).length} steps</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="meta-line">${escapeHtml(workflow.created_at || "")}</div>
|
|
188
|
+
<div class="tag-row">${(workflow.steps || []).slice(0, 4).map((step) => `<span class="tag">${escapeHtml(step.action || step.name || "step")}</span>`).join("")}</div>
|
|
189
|
+
</div>
|
|
190
|
+
`).join("") : `<div class="list-item"><div class="meta-line">No workflows.</div></div>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function renderSkills(payload) {
|
|
194
|
+
const installed = payload.installed || [];
|
|
195
|
+
const available = (payload.available || []).filter((skill) => !installed.some((item) => item.name === (skill.skill || skill.name))).slice(0, 8);
|
|
196
|
+
const rows = [
|
|
197
|
+
...installed.map((skill) => ({ ...skill, marketplace: false })),
|
|
198
|
+
...available.map((skill) => ({ name: skill.skill || skill.name, description: skill.description, version: skill.version || "remote", enabled: skill.enabled, marketplace: true })),
|
|
199
|
+
];
|
|
200
|
+
$("skill-list").innerHTML = rows.length ? rows.map((skill) => `
|
|
201
|
+
<div class="list-item">
|
|
202
|
+
<div class="list-title">
|
|
203
|
+
<span>${escapeHtml(skill.name)}</span>
|
|
204
|
+
<span class="status-pill ${skill.enabled === false ? "status-failed" : "status-complete"}">${skill.enabled === false ? "disabled" : "enabled"}</span>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="meta-line">${escapeHtml(skill.description || "")}</div>
|
|
207
|
+
<div class="tag-row">
|
|
208
|
+
<span class="tag">${escapeHtml(skill.version || "local")}</span>
|
|
209
|
+
<span class="tag">${skill.marketplace ? "marketplace" : "installed"}</span>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="item-actions">
|
|
212
|
+
<button class="small-action" data-skill-action="enable" data-skill="${escapeHtml(skill.name)}"><i class="ti ti-toggle-right"></i>Enable</button>
|
|
213
|
+
<button class="small-action" data-skill-action="disable" data-skill="${escapeHtml(skill.name)}"><i class="ti ti-toggle-left"></i>Disable</button>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
`).join("") : `<div class="list-item"><div class="meta-line">No skills found.</div></div>`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderTimeline(payload) {
|
|
220
|
+
const events = payload.events || [];
|
|
221
|
+
$("timeline-list").innerHTML = events.length ? events.slice(0, 40).map((event) => `
|
|
222
|
+
<div class="timeline-item">
|
|
223
|
+
<div class="list-title"><span>${escapeHtml(event.event_type || "event")}</span><span class="status-pill">${escapeHtml(event.area || "workspace")}</span></div>
|
|
224
|
+
<div class="meta-line">${escapeHtml(event.timestamp || "")}</div>
|
|
225
|
+
</div>
|
|
226
|
+
`).join("") : `<div class="timeline-item"><div class="meta-line">No timeline events.</div></div>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function refreshAll() {
|
|
230
|
+
const [os, onboarding, traces, indexing, snapshots, memories, computerMemory, agents, workflows, skills, timeline] = await Promise.all([
|
|
231
|
+
api("/workspace/os"),
|
|
232
|
+
api("/workspace/onboarding/status"),
|
|
233
|
+
api("/workspace/traces"),
|
|
234
|
+
api("/workspace/indexing"),
|
|
235
|
+
api("/workspace/snapshots"),
|
|
236
|
+
api("/workspace/memories"),
|
|
237
|
+
api("/workspace/computer-memory"),
|
|
238
|
+
api("/workspace/agents"),
|
|
239
|
+
api("/workspace/workflows"),
|
|
240
|
+
api("/workspace/skills"),
|
|
241
|
+
api("/workspace/time-machine"),
|
|
242
|
+
]);
|
|
243
|
+
state.os = os;
|
|
244
|
+
renderMetrics(os);
|
|
245
|
+
renderOnboarding(onboarding);
|
|
246
|
+
renderTraces(traces);
|
|
247
|
+
renderIndexing(indexing);
|
|
248
|
+
renderSnapshots(snapshots);
|
|
249
|
+
renderMemories(memories);
|
|
250
|
+
renderComputerMemory(computerMemory);
|
|
251
|
+
renderAgents(agents);
|
|
252
|
+
renderWorkflows(workflows);
|
|
253
|
+
renderSkills(skills);
|
|
254
|
+
renderTimeline(timeline);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function createSnapshot() {
|
|
258
|
+
const name = $("snapshot-name").value || "Workspace snapshot";
|
|
259
|
+
const payload = await api("/workspace/snapshots", {
|
|
260
|
+
method: "POST",
|
|
261
|
+
body: JSON.stringify({ name }),
|
|
262
|
+
});
|
|
263
|
+
toast(`Snapshot saved: ${payload.snapshot.id}`);
|
|
264
|
+
await refreshAll();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function compareSnapshots() {
|
|
268
|
+
const beforeId = $("snapshot-before").value;
|
|
269
|
+
const afterId = $("snapshot-after").value;
|
|
270
|
+
if (!beforeId || !afterId) return;
|
|
271
|
+
const diff = await api("/workspace/snapshots/compare", {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: JSON.stringify({ before_id: beforeId, after_id: afterId }),
|
|
274
|
+
});
|
|
275
|
+
$("snapshot-diff").textContent = JSON.stringify(diff.summary, null, 2);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function saveMemory() {
|
|
279
|
+
const content = $("memory-content").value.trim();
|
|
280
|
+
if (!content) return;
|
|
281
|
+
await api("/workspace/memories", {
|
|
282
|
+
method: "POST",
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
kind: $("memory-kind").value,
|
|
285
|
+
content,
|
|
286
|
+
tags: [],
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
$("memory-content").value = "";
|
|
290
|
+
toast("Memory saved");
|
|
291
|
+
await refreshAll();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function createDemoWorkflow() {
|
|
295
|
+
await api("/workspace/workflows", {
|
|
296
|
+
method: "POST",
|
|
297
|
+
body: JSON.stringify({
|
|
298
|
+
name: "Upload -> Summarize -> Generate -> Export",
|
|
299
|
+
steps: [
|
|
300
|
+
{ action: "upload" },
|
|
301
|
+
{ action: "summarize" },
|
|
302
|
+
{ action: "generate" },
|
|
303
|
+
{ action: "export" },
|
|
304
|
+
],
|
|
305
|
+
}),
|
|
306
|
+
});
|
|
307
|
+
toast("Workflow created");
|
|
308
|
+
await refreshAll();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function configureComputerMemory(enabled) {
|
|
312
|
+
const consent = enabled
|
|
313
|
+
? { approved: true, reason: "Enabled from Workspace OS UI", approved_at: new Date().toISOString() }
|
|
314
|
+
: { approved: false };
|
|
315
|
+
await api("/workspace/computer-memory", {
|
|
316
|
+
method: "POST",
|
|
317
|
+
body: JSON.stringify({ enabled, consent }),
|
|
318
|
+
});
|
|
319
|
+
toast(enabled ? "Computer Memory enabled" : "Computer Memory disabled");
|
|
320
|
+
await refreshAll();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
document.addEventListener("click", async (event) => {
|
|
324
|
+
const step = event.target.closest("[data-step]");
|
|
325
|
+
if (step) {
|
|
326
|
+
await api("/workspace/onboarding/step", {
|
|
327
|
+
method: "POST",
|
|
328
|
+
body: JSON.stringify({ step: step.dataset.step, status: "complete" }),
|
|
329
|
+
});
|
|
330
|
+
toast("Onboarding step saved");
|
|
331
|
+
await refreshAll();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const indexBtn = event.target.closest("[data-index-action]");
|
|
336
|
+
if (indexBtn) {
|
|
337
|
+
const action = indexBtn.dataset.indexAction;
|
|
338
|
+
const source = indexBtn.dataset.source;
|
|
339
|
+
await api(`/workspace/indexing/${encodeURIComponent(source)}/${action}`, { method: "POST" });
|
|
340
|
+
toast(`Index ${action} complete`);
|
|
341
|
+
await refreshAll();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const exportBtn = event.target.closest("[data-export-snapshot]");
|
|
346
|
+
if (exportBtn) {
|
|
347
|
+
const result = await api(`/workspace/snapshots/${encodeURIComponent(exportBtn.dataset.exportSnapshot)}/export`, { method: "POST" });
|
|
348
|
+
toast(`Exported ${result.bytes} bytes`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const skillBtn = event.target.closest("[data-skill-action]");
|
|
353
|
+
if (skillBtn) {
|
|
354
|
+
await api(`/workspace/skills/${skillBtn.dataset.skillAction}`, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
body: JSON.stringify({ skill: skillBtn.dataset.skill }),
|
|
357
|
+
});
|
|
358
|
+
toast(`Skill ${skillBtn.dataset.skillAction}`);
|
|
359
|
+
await refreshAll();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
364
|
+
$("refresh-btn").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
|
|
365
|
+
$("snapshot-now").addEventListener("click", () => createSnapshot().catch((err) => toast(err.message)));
|
|
366
|
+
$("create-snapshot").addEventListener("click", () => createSnapshot().catch((err) => toast(err.message)));
|
|
367
|
+
$("complete-onboarding").addEventListener("click", async () => {
|
|
368
|
+
await api("/workspace/onboarding/complete", { method: "POST", body: JSON.stringify({ data: { ui: "workspace" } }) });
|
|
369
|
+
toast("Onboarding complete");
|
|
370
|
+
await refreshAll();
|
|
371
|
+
});
|
|
372
|
+
$("reload-traces").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
|
|
373
|
+
$("compare-snapshots").addEventListener("click", () => compareSnapshots().catch((err) => toast(err.message)));
|
|
374
|
+
$("save-memory").addEventListener("click", () => saveMemory().catch((err) => toast(err.message)));
|
|
375
|
+
$("computer-memory-toggle").addEventListener("change", (event) => configureComputerMemory(event.target.checked).catch((err) => {
|
|
376
|
+
event.target.checked = false;
|
|
377
|
+
toast(err.message);
|
|
378
|
+
}));
|
|
379
|
+
$("create-demo-workflow").addEventListener("click", () => createDemoWorkflow().catch((err) => toast(err.message)));
|
|
380
|
+
$("reload-skills").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
|
|
381
|
+
refreshAll().catch((err) => toast(err.message));
|
|
382
|
+
});
|
package/static/sw.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Lattice AI Service Worker — enables PWA install on Android/iOS
|
|
2
2
|
// Strategy: network-first for API, cache-first for static assets.
|
|
3
|
-
const CACHE = "ltcai-
|
|
3
|
+
const CACHE = "ltcai-v100";
|
|
4
4
|
const STATIC = [
|
|
5
5
|
"/",
|
|
6
|
+
"/workspace",
|
|
6
7
|
"/static/lattice-reference.css",
|
|
8
|
+
"/static/workspace.css",
|
|
7
9
|
"/static/css/tokens.css",
|
|
8
10
|
"/static/scripts/chat.js",
|
|
9
11
|
"/static/scripts/admin.js",
|
|
10
12
|
"/static/scripts/graph.js",
|
|
13
|
+
"/static/scripts/workspace.js",
|
|
11
14
|
"/static/scripts/account.js",
|
|
12
15
|
"/manifest.json",
|
|
13
16
|
"/icons/icon-192.png",
|
|
@@ -45,6 +48,7 @@ self.addEventListener("fetch", e => {
|
|
|
45
48
|
url.pathname.startsWith("/local") ||
|
|
46
49
|
url.pathname.startsWith("/tools") ||
|
|
47
50
|
url.pathname.startsWith("/knowledge") ||
|
|
51
|
+
url.pathname.startsWith("/workspace/") ||
|
|
48
52
|
url.pathname.startsWith("/history")) {
|
|
49
53
|
e.respondWith(fetch(e.request));
|
|
50
54
|
return;
|