zubo 0.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/.github/workflows/ci.yml +35 -0
- package/README.md +149 -0
- package/bun.lock +216 -0
- package/desktop/README.md +57 -0
- package/desktop/package.json +12 -0
- package/desktop/src-tauri/Cargo.toml +25 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/icons/README.md +17 -0
- package/desktop/src-tauri/icons/icon.png +0 -0
- package/desktop/src-tauri/src/main.rs +189 -0
- package/desktop/src-tauri/tauri.conf.json +68 -0
- package/docs/ROADMAP.md +490 -0
- package/migrations/001_init.sql +9 -0
- package/migrations/002_memory.sql +33 -0
- package/migrations/003_cron.sql +24 -0
- package/migrations/004_usage.sql +12 -0
- package/migrations/005_secrets.sql +8 -0
- package/migrations/006_agents.sql +1 -0
- package/migrations/007_workflows.sql +22 -0
- package/migrations/008_proactive.sql +24 -0
- package/migrations/009_uploads.sql +9 -0
- package/migrations/010_observability.sql +22 -0
- package/migrations/011_api_keys.sql +7 -0
- package/migrations/012_indexes.sql +5 -0
- package/migrations/013_budget.sql +11 -0
- package/migrations/014_usage_session_idx.sql +2 -0
- package/package.json +39 -0
- package/site/404.html +156 -0
- package/site/CNAME +1 -0
- package/site/docs/agents.html +294 -0
- package/site/docs/api.html +446 -0
- package/site/docs/channels.html +345 -0
- package/site/docs/cli.html +238 -0
- package/site/docs/config.html +1034 -0
- package/site/docs/index.html +433 -0
- package/site/docs/integrations.html +381 -0
- package/site/docs/memory.html +254 -0
- package/site/docs/security.html +375 -0
- package/site/docs/skills.html +322 -0
- package/site/docs.css +412 -0
- package/site/index.html +638 -0
- package/site/install.sh +98 -0
- package/site/logo.svg +1 -0
- package/site/og-image.png +0 -0
- package/site/robots.txt +4 -0
- package/site/script.js +361 -0
- package/site/sitemap.xml +63 -0
- package/site/skills.html +532 -0
- package/site/style.css +1686 -0
- package/src/agent/agents.ts +159 -0
- package/src/agent/compaction.ts +53 -0
- package/src/agent/context.ts +18 -0
- package/src/agent/delegate.ts +118 -0
- package/src/agent/loop.ts +318 -0
- package/src/agent/prompts.ts +111 -0
- package/src/agent/session.ts +87 -0
- package/src/agent/teams.ts +116 -0
- package/src/agent/workflow-executor.ts +192 -0
- package/src/agent/workflow.ts +175 -0
- package/src/channels/adapter.ts +21 -0
- package/src/channels/dashboard.html.ts +2969 -0
- package/src/channels/discord.ts +137 -0
- package/src/channels/optional-deps.d.ts +17 -0
- package/src/channels/router.ts +199 -0
- package/src/channels/signal.ts +133 -0
- package/src/channels/slack.ts +101 -0
- package/src/channels/telegram.ts +102 -0
- package/src/channels/utils.ts +18 -0
- package/src/channels/webchat.ts +1797 -0
- package/src/channels/whatsapp.ts +119 -0
- package/src/config/loader.ts +22 -0
- package/src/config/paths.ts +43 -0
- package/src/config/schema.ts +121 -0
- package/src/db/connection.ts +20 -0
- package/src/db/export.ts +148 -0
- package/src/db/migrations.ts +42 -0
- package/src/index.ts +261 -0
- package/src/llm/claude.ts +193 -0
- package/src/llm/factory.ts +115 -0
- package/src/llm/failover.ts +101 -0
- package/src/llm/openai-compat.ts +409 -0
- package/src/llm/provider.ts +83 -0
- package/src/llm/smart-router.ts +241 -0
- package/src/logs.ts +53 -0
- package/src/memory/chunker.ts +58 -0
- package/src/memory/document-parser.ts +115 -0
- package/src/memory/embedder.ts +235 -0
- package/src/memory/engine.ts +170 -0
- package/src/memory/fts-index.ts +55 -0
- package/src/memory/hybrid-search.ts +72 -0
- package/src/memory/store.ts +56 -0
- package/src/memory/vector-index.ts +72 -0
- package/src/model.ts +118 -0
- package/src/registry/cli.ts +43 -0
- package/src/registry/client.ts +54 -0
- package/src/registry/installer.ts +67 -0
- package/src/scheduler/briefing.ts +71 -0
- package/src/scheduler/cron.ts +258 -0
- package/src/scheduler/heartbeat.ts +58 -0
- package/src/scheduler/memory-triggers.ts +100 -0
- package/src/scheduler/natural-cron.ts +163 -0
- package/src/scheduler/proactive.ts +25 -0
- package/src/scheduler/recipes.ts +110 -0
- package/src/secrets/store.ts +64 -0
- package/src/setup.ts +413 -0
- package/src/skills.ts +293 -0
- package/src/start.ts +373 -0
- package/src/status.ts +165 -0
- package/src/tools/builtin/connect-service.ts +205 -0
- package/src/tools/builtin/cron.ts +126 -0
- package/src/tools/builtin/datetime.ts +36 -0
- package/src/tools/builtin/delegate-task.ts +81 -0
- package/src/tools/builtin/delegate.ts +42 -0
- package/src/tools/builtin/diagnose.ts +41 -0
- package/src/tools/builtin/google-oauth.ts +379 -0
- package/src/tools/builtin/manage-agents.ts +149 -0
- package/src/tools/builtin/manage-skills.ts +294 -0
- package/src/tools/builtin/manage-teams.ts +89 -0
- package/src/tools/builtin/manage-triggers.ts +94 -0
- package/src/tools/builtin/manage-workflows.ts +119 -0
- package/src/tools/builtin/memory-search.ts +38 -0
- package/src/tools/builtin/memory-write.ts +30 -0
- package/src/tools/builtin/run-workflow.ts +36 -0
- package/src/tools/builtin/secrets.ts +122 -0
- package/src/tools/builtin/skill-registry.ts +75 -0
- package/src/tools/builtin-integrations/api-helpers.ts +26 -0
- package/src/tools/builtin-integrations/github/github_issues/SKILL.md +56 -0
- package/src/tools/builtin-integrations/github/github_issues/handler.ts +108 -0
- package/src/tools/builtin-integrations/github/github_prs/SKILL.md +57 -0
- package/src/tools/builtin-integrations/github/github_prs/handler.ts +113 -0
- package/src/tools/builtin-integrations/github/github_repos/SKILL.md +37 -0
- package/src/tools/builtin-integrations/github/github_repos/handler.ts +88 -0
- package/src/tools/builtin-integrations/google/gmail/SKILL.md +51 -0
- package/src/tools/builtin-integrations/google/gmail/handler.ts +125 -0
- package/src/tools/builtin-integrations/google/google_calendar/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_calendar/handler.ts +105 -0
- package/src/tools/builtin-integrations/google/google_docs/SKILL.md +35 -0
- package/src/tools/builtin-integrations/google/google_docs/handler.ts +108 -0
- package/src/tools/builtin-integrations/google/google_drive/SKILL.md +39 -0
- package/src/tools/builtin-integrations/google/google_drive/handler.ts +106 -0
- package/src/tools/builtin-integrations/google/google_sheets/SKILL.md +36 -0
- package/src/tools/builtin-integrations/google/google_sheets/handler.ts +116 -0
- package/src/tools/builtin-integrations/jira/jira_boards/SKILL.md +21 -0
- package/src/tools/builtin-integrations/jira/jira_boards/handler.ts +74 -0
- package/src/tools/builtin-integrations/jira/jira_issues/SKILL.md +28 -0
- package/src/tools/builtin-integrations/jira/jira_issues/handler.ts +140 -0
- package/src/tools/builtin-integrations/linear/linear_issues/SKILL.md +30 -0
- package/src/tools/builtin-integrations/linear/linear_issues/handler.ts +75 -0
- package/src/tools/builtin-integrations/linear/linear_projects/SKILL.md +21 -0
- package/src/tools/builtin-integrations/linear/linear_projects/handler.ts +43 -0
- package/src/tools/builtin-integrations/notion/notion_databases/SKILL.md +39 -0
- package/src/tools/builtin-integrations/notion/notion_databases/handler.ts +83 -0
- package/src/tools/builtin-integrations/notion/notion_pages/SKILL.md +43 -0
- package/src/tools/builtin-integrations/notion/notion_pages/handler.ts +130 -0
- package/src/tools/builtin-integrations/notion/notion_search/SKILL.md +27 -0
- package/src/tools/builtin-integrations/notion/notion_search/handler.ts +69 -0
- package/src/tools/builtin-integrations/slack/slack_messages/SKILL.md +42 -0
- package/src/tools/builtin-integrations/slack/slack_messages/handler.ts +72 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/SKILL.md +24 -0
- package/src/tools/builtin-integrations/twitter/twitter_posts/handler.ts +133 -0
- package/src/tools/builtin-skills/file-read/SKILL.md +26 -0
- package/src/tools/builtin-skills/file-read/handler.ts +66 -0
- package/src/tools/builtin-skills/file-write/SKILL.md +30 -0
- package/src/tools/builtin-skills/file-write/handler.ts +64 -0
- package/src/tools/builtin-skills/http-request/SKILL.md +34 -0
- package/src/tools/builtin-skills/http-request/handler.ts +87 -0
- package/src/tools/builtin-skills/shell/SKILL.md +26 -0
- package/src/tools/builtin-skills/shell/handler.ts +96 -0
- package/src/tools/builtin-skills/url-fetch/SKILL.md +26 -0
- package/src/tools/builtin-skills/url-fetch/handler.ts +37 -0
- package/src/tools/builtin-skills/web-search/SKILL.md +26 -0
- package/src/tools/builtin-skills/web-search/handler.ts +50 -0
- package/src/tools/executor.ts +205 -0
- package/src/tools/integration-installer.ts +106 -0
- package/src/tools/permissions.ts +45 -0
- package/src/tools/registry.ts +39 -0
- package/src/tools/sandbox-runner.ts +56 -0
- package/src/tools/sandbox.ts +82 -0
- package/src/tools/skill-installer.ts +52 -0
- package/src/tools/skill-loader.ts +259 -0
- package/src/types/optional-deps.d.ts +23 -0
- package/src/util/auth.ts +121 -0
- package/src/util/costs.ts +59 -0
- package/src/util/error-buffer.ts +32 -0
- package/src/util/google-tokens.ts +180 -0
- package/src/util/logger.ts +73 -0
- package/src/util/perf-collector.ts +35 -0
- package/src/util/rate-limiter.ts +70 -0
- package/src/util/tokens.ts +17 -0
- package/src/voice/stt.ts +57 -0
- package/src/voice/tts.ts +103 -0
- package/tests/agent/session.test.ts +109 -0
- package/tests/agent-loop.test.ts +54 -0
- package/tests/auth.test.ts +89 -0
- package/tests/channels.test.ts +67 -0
- package/tests/compaction.test.ts +44 -0
- package/tests/config.test.ts +51 -0
- package/tests/costs.test.ts +19 -0
- package/tests/cron.test.ts +55 -0
- package/tests/db/export.test.ts +219 -0
- package/tests/executor.test.ts +144 -0
- package/tests/export.test.ts +137 -0
- package/tests/helpers/mock-llm.ts +34 -0
- package/tests/helpers/test-db.ts +74 -0
- package/tests/integration/chat-flow.test.ts +48 -0
- package/tests/integrations.test.ts +97 -0
- package/tests/memory/engine.test.ts +114 -0
- package/tests/memory-engine.test.ts +57 -0
- package/tests/permissions.test.ts +21 -0
- package/tests/rate-limiter.test.ts +70 -0
- package/tests/registry.test.ts +67 -0
- package/tests/router.test.ts +36 -0
- package/tests/session.test.ts +58 -0
- package/tests/skill-loader.test.ts +44 -0
- package/tests/tokens.test.ts +30 -0
- package/tests/tools/executor.test.ts +130 -0
- package/tests/util/auth.test.ts +75 -0
- package/tests/util/rate-limiter.test.ts +73 -0
- package/tests/voice.test.ts +60 -0
- package/tests/webchat.test.ts +88 -0
- package/tests/workflow.test.ts +38 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,2969 @@
|
|
|
1
|
+
// Unified dashboard + agent chat HTML — no build step
|
|
2
|
+
export const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
|
+
<title>Zubo</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
11
|
+
<style>
|
|
12
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
13
|
+
|
|
14
|
+
:root {
|
|
15
|
+
--bg: #060608;
|
|
16
|
+
--bg-raised: #0e0e12;
|
|
17
|
+
--bg-surface: #111116;
|
|
18
|
+
--bg-hover: #18181f;
|
|
19
|
+
--border: #1e1e26;
|
|
20
|
+
--border-subtle: #18181f;
|
|
21
|
+
--text: #f0f0f5;
|
|
22
|
+
--text-secondary: #9595a8;
|
|
23
|
+
--text-muted: #5f5f73;
|
|
24
|
+
--text-faint: #52525b;
|
|
25
|
+
--accent: #7c3aed;
|
|
26
|
+
--accent-hover: #6d28d9;
|
|
27
|
+
--accent-bg: rgba(124,58,237,0.08);
|
|
28
|
+
--accent-border: rgba(124,58,237,0.2);
|
|
29
|
+
--green: #10b981;
|
|
30
|
+
--green-bg: rgba(16,185,129,0.1);
|
|
31
|
+
--yellow: #f59e0b;
|
|
32
|
+
--yellow-bg: rgba(245,158,11,0.1);
|
|
33
|
+
--red: #ef4444;
|
|
34
|
+
--fuchsia: #d946ef;
|
|
35
|
+
--indigo: #6366f1;
|
|
36
|
+
--gradient: linear-gradient(135deg, #7c3aed, #d946ef);
|
|
37
|
+
--gradient-text: linear-gradient(135deg, #fbbf24 0%, #f97316 25%, #ec4899 50%, #a855f7 75%, #6366f1 100%);
|
|
38
|
+
--radius: 10px;
|
|
39
|
+
--radius-lg: 14px;
|
|
40
|
+
--font: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
41
|
+
--display: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
42
|
+
--mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
|
43
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.3);
|
|
44
|
+
--shadow-lg: 0 4px 12px rgba(0,0,0,0.5);
|
|
45
|
+
--shadow-glow: 0 0 80px rgba(124,58,237,0.15);
|
|
46
|
+
--transition: 150ms cubic-bezier(0.4,0,0.2,1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
body { font-family: var(--font); background: var(--bg); color: var(--text); height: 100vh; display: flex; overflow: hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
|
50
|
+
|
|
51
|
+
/* Display font for headings & titles */
|
|
52
|
+
h1, h2, h3, h4,
|
|
53
|
+
.settings-title,
|
|
54
|
+
#topbar-title,
|
|
55
|
+
.modal h2,
|
|
56
|
+
.card .value,
|
|
57
|
+
.qa-label { font-family: var(--display); }
|
|
58
|
+
|
|
59
|
+
.gradient-text {
|
|
60
|
+
background: var(--gradient-text);
|
|
61
|
+
-webkit-background-clip: text;
|
|
62
|
+
-webkit-text-fill-color: transparent;
|
|
63
|
+
background-clip: text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Sidebar */
|
|
67
|
+
#sidebar {
|
|
68
|
+
width: 220px; background: var(--bg-raised); border-right: 1px solid var(--border);
|
|
69
|
+
display: flex; flex-direction: column; flex-shrink: 0;
|
|
70
|
+
}
|
|
71
|
+
.sidebar-logo {
|
|
72
|
+
padding: 20px 20px 16px; display: flex; align-items: center; gap: 10px;
|
|
73
|
+
border-bottom: 1px solid var(--border);
|
|
74
|
+
}
|
|
75
|
+
.sidebar-logo .logo-icon { display: none; }
|
|
76
|
+
.sidebar-logo span { font-family: var(--display); font-weight: 700; font-size: 15px; color: var(--text); letter-spacing: -0.3px; }
|
|
77
|
+
|
|
78
|
+
.sidebar-section {
|
|
79
|
+
padding: 12px 10px 6px;
|
|
80
|
+
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
81
|
+
letter-spacing: 0.8px; color: var(--text-faint);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#sidebar nav { display: flex; flex-direction: column; gap: 2px; padding: 4px 8px; flex: 1; overflow-y: auto; }
|
|
85
|
+
|
|
86
|
+
#sidebar nav a {
|
|
87
|
+
display: flex; align-items: center; gap: 10px;
|
|
88
|
+
padding: 9px 12px; color: var(--text-secondary); text-decoration: none;
|
|
89
|
+
font-size: 13px; font-weight: 500; border-radius: 8px;
|
|
90
|
+
transition: all var(--transition); cursor: pointer; position: relative;
|
|
91
|
+
}
|
|
92
|
+
#sidebar nav a .nav-icon {
|
|
93
|
+
width: 18px; height: 18px; display: flex; align-items: center; justify-content: center;
|
|
94
|
+
font-size: 14px; opacity: 0.7; flex-shrink: 0;
|
|
95
|
+
}
|
|
96
|
+
#sidebar nav a .channel-dot {
|
|
97
|
+
width: 6px; height: 6px; border-radius: 50%; margin-left: auto; flex-shrink: 0;
|
|
98
|
+
}
|
|
99
|
+
#sidebar nav a .channel-dot.connected { background: var(--green); box-shadow: 0 0 4px rgba(34,197,94,0.5); }
|
|
100
|
+
#sidebar nav a .channel-dot.disconnected { background: var(--text-faint); }
|
|
101
|
+
#sidebar nav a:hover { color: var(--text); background: var(--bg-hover); }
|
|
102
|
+
#sidebar nav a.active {
|
|
103
|
+
color: var(--accent); background: var(--accent-bg);
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
}
|
|
106
|
+
#sidebar nav a.active .nav-icon { opacity: 1; }
|
|
107
|
+
#sidebar nav a.active::before {
|
|
108
|
+
content: ''; position: absolute; left: 0; top: 6px; bottom: 6px;
|
|
109
|
+
width: 3px; border-radius: 2px; background: var(--gradient);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.sidebar-divider { height: 1px; background: var(--border); margin: 8px 16px; }
|
|
113
|
+
|
|
114
|
+
.sidebar-footer {
|
|
115
|
+
padding: 12px 16px; border-top: 1px solid var(--border);
|
|
116
|
+
font-size: 11px; color: var(--text-faint); display: flex; align-items: center; gap: 6px;
|
|
117
|
+
}
|
|
118
|
+
.sidebar-footer .conn-badge {
|
|
119
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
120
|
+
font-size: 10px; padding: 2px 8px; border-radius: 10px;
|
|
121
|
+
background: var(--green-bg); color: var(--green);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Main area */
|
|
125
|
+
#main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
|
126
|
+
|
|
127
|
+
#topbar {
|
|
128
|
+
padding: 14px 24px; background: var(--bg-raised); border-bottom: 1px solid var(--border);
|
|
129
|
+
display: flex; justify-content: space-between; align-items: center; flex-shrink: 0;
|
|
130
|
+
}
|
|
131
|
+
#topbar-title { font-size: 15px; font-weight: 600; color: var(--text); letter-spacing: -0.2px; }
|
|
132
|
+
#topbar-badge {
|
|
133
|
+
font-size: 11px; color: var(--text-faint); background: var(--bg-surface);
|
|
134
|
+
padding: 3px 10px; border-radius: 20px; border: 1px solid var(--border);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#content { flex: 1; overflow-y: auto; overflow-x: hidden; }
|
|
138
|
+
|
|
139
|
+
/* Panels */
|
|
140
|
+
.panel { display: none; }
|
|
141
|
+
.panel.active { display: flex; flex-direction: column; height: 100%; animation: panelIn 200ms ease-out; }
|
|
142
|
+
@keyframes panelIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
143
|
+
|
|
144
|
+
.panel-body { padding: 24px; flex: 1; overflow-y: auto; }
|
|
145
|
+
|
|
146
|
+
/* ===== AGENT CHAT ===== */
|
|
147
|
+
#panel-agent { display: none; }
|
|
148
|
+
#panel-agent.active { display: flex; flex-direction: column; height: 100%; }
|
|
149
|
+
|
|
150
|
+
#chat-messages {
|
|
151
|
+
flex: 1; overflow-y: auto; padding: 20px 24px;
|
|
152
|
+
display: flex; flex-direction: column; gap: 16px;
|
|
153
|
+
}
|
|
154
|
+
.chat-msg {
|
|
155
|
+
max-width: 72%; padding: 12px 16px; border-radius: var(--radius-lg);
|
|
156
|
+
font-size: 14px; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word;
|
|
157
|
+
animation: msgIn 200ms ease-out;
|
|
158
|
+
}
|
|
159
|
+
@keyframes msgIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
|
|
160
|
+
|
|
161
|
+
.chat-msg.user {
|
|
162
|
+
align-self: flex-end; background: var(--accent); color: white;
|
|
163
|
+
border-bottom-right-radius: 4px;
|
|
164
|
+
}
|
|
165
|
+
.chat-msg.bot {
|
|
166
|
+
align-self: flex-start; background: var(--bg-surface); border: 1px solid var(--border);
|
|
167
|
+
border-bottom-left-radius: 4px; color: var(--text);
|
|
168
|
+
}
|
|
169
|
+
.chat-msg.bot.thinking { color: var(--text-muted); }
|
|
170
|
+
.chat-msg.bot code {
|
|
171
|
+
background: rgba(255,255,255,0.06); padding: 2px 5px; border-radius: 4px;
|
|
172
|
+
font-family: var(--mono); font-size: 13px;
|
|
173
|
+
}
|
|
174
|
+
.chat-msg.bot pre { margin: 8px 0; }
|
|
175
|
+
.chat-msg.bot pre code {
|
|
176
|
+
display: block; background: var(--bg); padding: 12px; border-radius: 6px;
|
|
177
|
+
overflow-x: auto; white-space: pre-wrap;
|
|
178
|
+
}
|
|
179
|
+
.chat-msg.bot strong { font-weight: 700; }
|
|
180
|
+
.chat-msg.bot em { font-style: italic; }
|
|
181
|
+
.chat-msg.bot.thinking::after {
|
|
182
|
+
content: ''; display: inline-block; width: 4px; height: 14px;
|
|
183
|
+
background: var(--text-muted); margin-left: 4px; vertical-align: middle;
|
|
184
|
+
animation: blink 1s steps(1) infinite;
|
|
185
|
+
}
|
|
186
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
187
|
+
|
|
188
|
+
.chat-empty {
|
|
189
|
+
flex: 1; display: flex; flex-direction: column; align-items: center;
|
|
190
|
+
justify-content: center; gap: 12px; color: var(--text-faint);
|
|
191
|
+
}
|
|
192
|
+
.chat-empty-icon { font-size: 36px; opacity: 0.3; }
|
|
193
|
+
.chat-empty-text { font-size: 14px; color: var(--text-muted); }
|
|
194
|
+
|
|
195
|
+
.chat-welcome { animation: fadeIn 400ms ease-out; gap: 16px; }
|
|
196
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
|
197
|
+
.chat-welcome-icon {
|
|
198
|
+
width: 56px; height: 56px; background: var(--gradient); border-radius: 16px;
|
|
199
|
+
display: flex; align-items: center; justify-content: center;
|
|
200
|
+
font-family: var(--display); font-weight: 800; font-size: 22px; color: #fff;
|
|
201
|
+
box-shadow: 0 0 30px rgba(124,58,237,0.3), 0 0 60px rgba(124,58,237,0.1);
|
|
202
|
+
}
|
|
203
|
+
.chat-welcome h3 {
|
|
204
|
+
font-family: var(--display); font-size: 22px; font-weight: 700; letter-spacing: -0.5px;
|
|
205
|
+
}
|
|
206
|
+
.suggestion-chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 4px; }
|
|
207
|
+
.suggestion-chip {
|
|
208
|
+
padding: 8px 16px; border-radius: 20px; border: 1px solid var(--border);
|
|
209
|
+
background: var(--bg-surface); color: var(--text-secondary); font-size: 13px;
|
|
210
|
+
font-family: var(--font); cursor: pointer; transition: all var(--transition);
|
|
211
|
+
}
|
|
212
|
+
.suggestion-chip:hover {
|
|
213
|
+
border-color: var(--accent); color: var(--text); background: var(--accent-bg);
|
|
214
|
+
box-shadow: 0 0 12px rgba(124,58,237,0.15);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#chat-input-bar {
|
|
218
|
+
padding: 16px 24px 20px; background: var(--bg-raised); border-top: 1px solid var(--border);
|
|
219
|
+
display: flex; gap: 10px; flex-shrink: 0; align-items: center;
|
|
220
|
+
}
|
|
221
|
+
.chat-attach-btn, .chat-mic-btn {
|
|
222
|
+
width: 40px; height: 40px; border: 1px solid var(--border); background: var(--bg-surface);
|
|
223
|
+
border-radius: var(--radius); color: var(--text-muted); font-size: 16px;
|
|
224
|
+
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
225
|
+
transition: all var(--transition); flex-shrink: 0;
|
|
226
|
+
}
|
|
227
|
+
.chat-attach-btn:hover, .chat-mic-btn:hover { color: var(--text); border-color: var(--accent); background: var(--accent-bg); }
|
|
228
|
+
.chat-mic-btn.recording { color: var(--red); border-color: var(--red); background: rgba(239,68,68,0.1); animation: pulse 1.5s infinite; }
|
|
229
|
+
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
|
|
230
|
+
|
|
231
|
+
#chat-input {
|
|
232
|
+
flex: 1; padding: 12px 16px; background: var(--bg-surface); border: 1px solid var(--border);
|
|
233
|
+
border-radius: var(--radius); color: var(--text); font-size: 14px; outline: none;
|
|
234
|
+
font-family: var(--font); transition: border-color var(--transition);
|
|
235
|
+
}
|
|
236
|
+
#chat-input:focus { border-color: var(--accent); }
|
|
237
|
+
#chat-input::placeholder { color: var(--text-faint); }
|
|
238
|
+
#chat-send {
|
|
239
|
+
padding: 12px 22px; background: var(--accent); color: white; border: none;
|
|
240
|
+
border-radius: var(--radius); font-size: 14px; font-weight: 600; cursor: pointer;
|
|
241
|
+
transition: background var(--transition); flex-shrink: 0;
|
|
242
|
+
}
|
|
243
|
+
#chat-send:hover { background: var(--accent-hover); }
|
|
244
|
+
#chat-send:disabled { opacity: 0.4; cursor: default; }
|
|
245
|
+
|
|
246
|
+
.file-pill {
|
|
247
|
+
display: flex; align-items: center; gap: 6px; padding: 4px 10px;
|
|
248
|
+
background: var(--accent-bg); border: 1px solid var(--accent-border);
|
|
249
|
+
border-radius: 16px; font-size: 11px; color: var(--accent); font-weight: 500;
|
|
250
|
+
}
|
|
251
|
+
.file-pill .remove { cursor: pointer; opacity: 0.7; }
|
|
252
|
+
.file-pill .remove:hover { opacity: 1; }
|
|
253
|
+
|
|
254
|
+
.drop-overlay {
|
|
255
|
+
display: none; position: absolute; inset: 0; background: rgba(124,58,237,0.08);
|
|
256
|
+
border: 2px dashed var(--accent); border-radius: var(--radius-lg);
|
|
257
|
+
z-index: 10; align-items: center; justify-content: center;
|
|
258
|
+
font-size: 15px; color: var(--accent); font-weight: 600;
|
|
259
|
+
}
|
|
260
|
+
.drop-overlay.visible { display: flex; }
|
|
261
|
+
|
|
262
|
+
/* ===== CARDS ===== */
|
|
263
|
+
.cards {
|
|
264
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
265
|
+
gap: 14px;
|
|
266
|
+
}
|
|
267
|
+
.card {
|
|
268
|
+
background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
|
269
|
+
padding: 18px 20px; transition: border-color var(--transition); overflow: hidden;
|
|
270
|
+
border-top: 2px solid var(--accent-border);
|
|
271
|
+
}
|
|
272
|
+
.card:nth-child(2) { border-top-color: rgba(16,185,129,0.3); }
|
|
273
|
+
.card:nth-child(3) { border-top-color: rgba(99,102,241,0.3); }
|
|
274
|
+
.card:nth-child(4) { border-top-color: rgba(217,70,239,0.3); }
|
|
275
|
+
.card:hover { border-color: rgba(124,58,237,0.25); box-shadow: 0 0 20px rgba(124,58,237,0.06); }
|
|
276
|
+
.card .label {
|
|
277
|
+
font-size: 11px; color: var(--text-faint); text-transform: uppercase;
|
|
278
|
+
letter-spacing: 0.6px; font-weight: 600; margin-bottom: 8px;
|
|
279
|
+
}
|
|
280
|
+
.card .value { font-size: clamp(18px, 3vw, 22px); font-weight: 700; color: var(--text); letter-spacing: -0.5px; word-break: break-word; }
|
|
281
|
+
.card .value.ok { color: var(--green); border-left: 3px solid var(--green); padding-left: 8px; }
|
|
282
|
+
.card .value.warn { color: var(--yellow); border-left: 3px solid var(--yellow); padding-left: 8px; }
|
|
283
|
+
|
|
284
|
+
/* Budget controls */
|
|
285
|
+
.budget-bar { width: 100%; height: 8px; background: var(--bg-hover); border-radius: 4px; overflow: hidden; margin-top: 8px; }
|
|
286
|
+
.budget-bar-fill { height: 100%; border-radius: 4px; transition: width 0.5s ease; }
|
|
287
|
+
.budget-bar-fill.ok { background: var(--green); }
|
|
288
|
+
.budget-bar-fill.warn { background: var(--yellow); }
|
|
289
|
+
.budget-bar-fill.danger { background: var(--red); }
|
|
290
|
+
.budget-card-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
|
291
|
+
.budget-card-value { font-family: var(--display); font-size: 28px; font-weight: 700; margin: 4px 0; }
|
|
292
|
+
.budget-card-sub { font-size: 12px; color: var(--text-secondary); }
|
|
293
|
+
|
|
294
|
+
/* Quick action cards */
|
|
295
|
+
.quick-actions {
|
|
296
|
+
display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
297
|
+
gap: 12px; margin-top: 24px;
|
|
298
|
+
}
|
|
299
|
+
.quick-action {
|
|
300
|
+
background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
|
301
|
+
padding: 18px; cursor: pointer; transition: all var(--transition); text-align: center;
|
|
302
|
+
}
|
|
303
|
+
.quick-action:hover { border-color: var(--accent); background: var(--accent-bg); transform: translateY(-2px); box-shadow: 0 4px 16px rgba(124,58,237,0.1), 0 0 20px rgba(124,58,237,0.08); }
|
|
304
|
+
.quick-action .qa-icon { font-size: 24px; margin-bottom: 8px; }
|
|
305
|
+
.quick-action .qa-label { font-size: 13px; font-weight: 600; color: var(--text); }
|
|
306
|
+
.quick-action .qa-desc { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
307
|
+
|
|
308
|
+
/* ===== EDITOR ===== */
|
|
309
|
+
.editor-wrap { display: flex; flex-direction: column; height: 100%; }
|
|
310
|
+
.editor-toolbar {
|
|
311
|
+
padding: 0 0 16px; display: flex; gap: 10px; align-items: center; flex-shrink: 0;
|
|
312
|
+
}
|
|
313
|
+
textarea.editor {
|
|
314
|
+
flex: 1; width: 100%; background: var(--bg-surface); border: 1px solid var(--border);
|
|
315
|
+
border-radius: var(--radius); color: var(--text); font-family: var(--mono);
|
|
316
|
+
font-size: 13px; padding: 16px; resize: none; outline: none; line-height: 1.7;
|
|
317
|
+
transition: border-color var(--transition);
|
|
318
|
+
}
|
|
319
|
+
textarea.editor:focus { border-color: var(--accent); }
|
|
320
|
+
|
|
321
|
+
/* ===== BUTTONS ===== */
|
|
322
|
+
.btn {
|
|
323
|
+
padding: 8px 16px; border: none; border-radius: 8px; font-size: 12px;
|
|
324
|
+
cursor: pointer; font-weight: 600; transition: all var(--transition);
|
|
325
|
+
font-family: var(--font);
|
|
326
|
+
}
|
|
327
|
+
.btn-primary { background: var(--accent); color: white; }
|
|
328
|
+
.btn-primary:hover { background: var(--accent-hover); }
|
|
329
|
+
.btn-ghost { background: transparent; color: var(--text-secondary); border: 1px solid var(--border); }
|
|
330
|
+
.btn-ghost:hover { color: var(--text); border-color: #555; background: var(--bg-hover); }
|
|
331
|
+
.btn-sm { padding: 5px 10px; font-size: 11px; }
|
|
332
|
+
.btn-purple { background: var(--fuchsia); color: white; }
|
|
333
|
+
.btn-purple:hover { background: #c026d3; }
|
|
334
|
+
|
|
335
|
+
/* ===== STATUS LABEL ===== */
|
|
336
|
+
.status-text { font-size: 12px; color: var(--text-faint); font-weight: 500; }
|
|
337
|
+
|
|
338
|
+
/* ===== MEMORY ===== */
|
|
339
|
+
.memory-section-title {
|
|
340
|
+
font-size: 13px; font-weight: 600; color: var(--text-secondary);
|
|
341
|
+
margin: 24px 0 14px; display: flex; align-items: center; gap: 8px;
|
|
342
|
+
}
|
|
343
|
+
.memory-section-title .badge {
|
|
344
|
+
font-size: 10px; background: var(--bg-hover); color: var(--text-faint);
|
|
345
|
+
padding: 2px 8px; border-radius: 10px; font-weight: 600;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.memory-list { display: flex; flex-direction: column; gap: 10px; }
|
|
349
|
+
.memory-item {
|
|
350
|
+
background: var(--bg-surface); border: 1px solid var(--border);
|
|
351
|
+
border-radius: var(--radius); padding: 14px 16px;
|
|
352
|
+
transition: border-color var(--transition);
|
|
353
|
+
}
|
|
354
|
+
.memory-item:hover { border-color: rgba(124,58,237,0.2); }
|
|
355
|
+
.memory-item .source {
|
|
356
|
+
font-size: 11px; color: var(--accent); font-weight: 600;
|
|
357
|
+
margin-bottom: 6px; font-family: var(--mono);
|
|
358
|
+
}
|
|
359
|
+
.memory-item .content { font-size: 13px; line-height: 1.6; color: var(--text-secondary); white-space: pre-wrap; }
|
|
360
|
+
|
|
361
|
+
/* ===== SEARCH ===== */
|
|
362
|
+
.search-bar { display: flex; gap: 10px; margin-bottom: 16px; }
|
|
363
|
+
.search-bar input {
|
|
364
|
+
flex: 1; padding: 10px 14px; background: var(--bg-surface); border: 1px solid var(--border);
|
|
365
|
+
border-radius: 8px; color: var(--text); font-size: 13px; outline: none;
|
|
366
|
+
font-family: var(--font); transition: border-color var(--transition);
|
|
367
|
+
}
|
|
368
|
+
.search-bar input:focus { border-color: var(--accent); }
|
|
369
|
+
.search-bar input::placeholder { color: var(--text-faint); }
|
|
370
|
+
|
|
371
|
+
/* ===== LOGS ===== */
|
|
372
|
+
.log-view {
|
|
373
|
+
background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius);
|
|
374
|
+
padding: 16px; font-family: var(--mono); font-size: 12px; line-height: 1.7;
|
|
375
|
+
white-space: pre-wrap; flex: 1; overflow-y: auto; color: var(--text-muted);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/* ===== TABLE ===== */
|
|
379
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
380
|
+
th {
|
|
381
|
+
text-align: left; padding: 10px 16px; color: var(--text-faint); font-size: 11px;
|
|
382
|
+
text-transform: uppercase; letter-spacing: 0.8px; font-weight: 600;
|
|
383
|
+
border-bottom: 2px solid var(--border); background: var(--bg-surface);
|
|
384
|
+
position: sticky; top: 0;
|
|
385
|
+
}
|
|
386
|
+
td { padding: 12px 16px; border-bottom: 1px solid var(--border-subtle); color: var(--text-secondary); }
|
|
387
|
+
tbody tr:nth-child(odd) td { background: rgba(255,255,255,0.015); }
|
|
388
|
+
tbody tr:hover td { background: var(--bg-hover); }
|
|
389
|
+
|
|
390
|
+
.status-dot {
|
|
391
|
+
display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px;
|
|
392
|
+
}
|
|
393
|
+
.status-dot.ok { background: var(--green); box-shadow: 0 0 6px rgba(34,197,94,0.4); }
|
|
394
|
+
.status-dot.error { background: var(--red); box-shadow: 0 0 6px rgba(239,68,68,0.4); }
|
|
395
|
+
|
|
396
|
+
/* ===== EMPTY STATE ===== */
|
|
397
|
+
.empty-state {
|
|
398
|
+
color: var(--text-faint); padding: 40px 20px; text-align: center;
|
|
399
|
+
font-size: 13px;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ===== TOAST ===== */
|
|
403
|
+
.toast {
|
|
404
|
+
position: fixed; bottom: 24px; right: 24px; background: var(--green);
|
|
405
|
+
color: #000; padding: 12px 20px; border-radius: var(--radius); font-size: 13px;
|
|
406
|
+
font-weight: 600; opacity: 0; transition: opacity 200ms, transform 200ms;
|
|
407
|
+
pointer-events: none; transform: translateY(8px); box-shadow: var(--shadow-lg); z-index: 100;
|
|
408
|
+
}
|
|
409
|
+
.toast.show { opacity: 1; transform: translateY(0); }
|
|
410
|
+
|
|
411
|
+
/* ===== TOOLTIP ===== */
|
|
412
|
+
[data-tooltip] { position: relative; }
|
|
413
|
+
[data-tooltip]:hover::after {
|
|
414
|
+
content: attr(data-tooltip);
|
|
415
|
+
position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
|
|
416
|
+
background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border);
|
|
417
|
+
padding: 4px 10px; border-radius: 6px; font-size: 11px; white-space: nowrap;
|
|
418
|
+
z-index: 50; pointer-events: none; box-shadow: var(--shadow);
|
|
419
|
+
animation: ttIn 100ms ease-out;
|
|
420
|
+
}
|
|
421
|
+
@keyframes ttIn { from { opacity: 0; transform: translateX(-50%) translateY(2px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
|
|
422
|
+
|
|
423
|
+
/* ===== SETTINGS ENHANCED ===== */
|
|
424
|
+
.settings-section {
|
|
425
|
+
max-width: 600px; margin-bottom: 24px; background: var(--bg-raised);
|
|
426
|
+
border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 24px;
|
|
427
|
+
}
|
|
428
|
+
.settings-title { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 6px; display: flex; align-items: center; gap: 8px; }
|
|
429
|
+
.settings-title .conn-dot { width: 8px; height: 8px; border-radius: 50%; }
|
|
430
|
+
.settings-title .conn-dot.on { background: var(--green); box-shadow: 0 0 6px rgba(34,197,94,0.4); }
|
|
431
|
+
.settings-title .conn-dot.off { background: var(--text-faint); }
|
|
432
|
+
.settings-desc { font-size: 13px; color: var(--text-muted); line-height: 1.5; margin-bottom: 20px; }
|
|
433
|
+
.settings-desc code {
|
|
434
|
+
background: var(--bg-surface); padding: 2px 6px; border-radius: 4px;
|
|
435
|
+
font-family: var(--mono); font-size: 12px; color: var(--text-secondary);
|
|
436
|
+
}
|
|
437
|
+
.settings-grid { display: flex; flex-direction: column; gap: 16px; }
|
|
438
|
+
.settings-field { display: flex; flex-direction: column; gap: 6px; }
|
|
439
|
+
.settings-label { font-size: 12px; font-weight: 600; color: var(--text-secondary); }
|
|
440
|
+
.settings-select, .settings-input {
|
|
441
|
+
padding: 10px 14px; background: var(--bg-surface); border: 1px solid var(--border);
|
|
442
|
+
border-radius: 8px; color: var(--text); font-size: 13px; outline: none;
|
|
443
|
+
font-family: var(--font); transition: border-color var(--transition);
|
|
444
|
+
width: 100%;
|
|
445
|
+
}
|
|
446
|
+
.settings-select:focus, .settings-input:focus { border-color: var(--accent); }
|
|
447
|
+
.settings-select { cursor: pointer; appearance: auto; }
|
|
448
|
+
.settings-input::placeholder { color: var(--text-faint); }
|
|
449
|
+
|
|
450
|
+
/* Perf grid */
|
|
451
|
+
.perf-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
|
452
|
+
.perf-card {
|
|
453
|
+
background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
|
454
|
+
padding: 18px 20px; transition: border-color var(--transition);
|
|
455
|
+
}
|
|
456
|
+
.perf-card:hover { border-color: rgba(124,58,237,0.25); }
|
|
457
|
+
.perf-card .perf-label { font-size: 11px; color: var(--text-faint); text-transform: uppercase; letter-spacing: 0.6px; font-weight: 600; margin-bottom: 8px; }
|
|
458
|
+
.perf-card .perf-value { font-family: var(--display); font-size: 22px; font-weight: 700; color: var(--text); letter-spacing: -0.5px; }
|
|
459
|
+
|
|
460
|
+
.cost-bar-wrap { display: flex; align-items: center; gap: 8px; }
|
|
461
|
+
.cost-bar { height: 8px; border-radius: 4px; background: var(--gradient); min-width: 2px; transition: width 300ms; }
|
|
462
|
+
.cost-pct { font-size: 11px; color: var(--text-faint); white-space: nowrap; }
|
|
463
|
+
|
|
464
|
+
/* Analytics CSS bar chart */
|
|
465
|
+
.bar-chart { display: flex; align-items: flex-end; gap: 8px; height: 120px; padding: 0 8px; position: relative; }
|
|
466
|
+
.bar-col { display: flex; flex-direction: column; align-items: center; flex: 1; max-width: 80px; gap: 4px; }
|
|
467
|
+
.bar-col .bar { background: var(--gradient); border-radius: 4px 4px 0 0; width: 100%; min-height: 2px; transition: height 300ms, opacity var(--transition); opacity: 0.85; }
|
|
468
|
+
.bar-col .bar:hover { opacity: 1; }
|
|
469
|
+
.bar-col .bar-label { font-size: 10px; color: var(--text-faint); }
|
|
470
|
+
.chart-y-label { position: absolute; top: -4px; left: 0; font-size: 10px; color: var(--text-faint); font-family: var(--mono); }
|
|
471
|
+
|
|
472
|
+
/* Workflow DAG */
|
|
473
|
+
.dag-container { background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; min-height: 200px; }
|
|
474
|
+
.dag-container svg text { fill: var(--text-secondary); font-size: 11px; font-family: var(--font); }
|
|
475
|
+
.dag-container svg .dag-node { fill: var(--accent-bg); stroke: var(--accent); stroke-width: 1.5; rx: 8; }
|
|
476
|
+
.dag-container svg .dag-edge { stroke: var(--border); stroke-width: 1.5; fill: none; marker-end: url(#arrowhead); }
|
|
477
|
+
|
|
478
|
+
/* Registry items */
|
|
479
|
+
.registry-item {
|
|
480
|
+
background: var(--bg-surface); border: 1px solid var(--border); border-radius: var(--radius);
|
|
481
|
+
padding: 14px 16px; display: flex; justify-content: space-between; align-items: center;
|
|
482
|
+
transition: border-color var(--transition);
|
|
483
|
+
}
|
|
484
|
+
.registry-item:hover { border-color: rgba(124,58,237,0.2); }
|
|
485
|
+
.registry-item .ri-name { font-weight: 600; font-size: 13px; color: var(--text); }
|
|
486
|
+
.registry-item .ri-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
|
|
487
|
+
|
|
488
|
+
/* ===== ONBOARDING MODAL ===== */
|
|
489
|
+
.modal-overlay {
|
|
490
|
+
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.7);
|
|
491
|
+
z-index: 200; align-items: center; justify-content: center;
|
|
492
|
+
backdrop-filter: blur(4px);
|
|
493
|
+
}
|
|
494
|
+
.modal-overlay.visible { display: flex; }
|
|
495
|
+
.modal {
|
|
496
|
+
background: var(--bg-raised); border: 1px solid var(--border); border-radius: 16px;
|
|
497
|
+
padding: 40px; max-width: 480px; width: 90%; box-shadow: var(--shadow-lg);
|
|
498
|
+
animation: modalIn 250ms ease-out;
|
|
499
|
+
}
|
|
500
|
+
@keyframes modalIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
|
501
|
+
.modal h2 { font-size: 22px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 8px; }
|
|
502
|
+
.modal p { font-size: 14px; color: var(--text-secondary); line-height: 1.6; margin-bottom: 20px; }
|
|
503
|
+
.modal .steps { display: flex; gap: 6px; margin-bottom: 24px; }
|
|
504
|
+
.modal .steps .step-dot {
|
|
505
|
+
width: 8px; height: 8px; border-radius: 50%; background: var(--border);
|
|
506
|
+
transition: background 200ms;
|
|
507
|
+
}
|
|
508
|
+
.modal .steps .step-dot.active { background: var(--accent); }
|
|
509
|
+
.modal .steps .step-dot.done { background: var(--green); }
|
|
510
|
+
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
|
511
|
+
|
|
512
|
+
/* Scrollbar */
|
|
513
|
+
::-webkit-scrollbar { width: 6px; }
|
|
514
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
515
|
+
::-webkit-scrollbar-thumb { background: #2a2a35; border-radius: 3px; }
|
|
516
|
+
::-webkit-scrollbar-thumb:hover { background: #3f3f4d; }
|
|
517
|
+
|
|
518
|
+
/* Send button gradient */
|
|
519
|
+
#chat-send { background: var(--gradient); }
|
|
520
|
+
#chat-send:hover { filter: brightness(1.1); }
|
|
521
|
+
|
|
522
|
+
/* Sidebar gradient border */
|
|
523
|
+
#sidebar { border-right: none; position: relative; }
|
|
524
|
+
#sidebar::after { content: ''; position: absolute; top: 0; right: 0; width: 1px; height: 100%; background: linear-gradient(to bottom, var(--border) 0%, rgba(124,58,237,0.3) 50%, var(--border) 100%); }
|
|
525
|
+
|
|
526
|
+
/* Topbar gradient border */
|
|
527
|
+
#topbar { border-bottom: none; position: relative; }
|
|
528
|
+
#topbar::after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 1px; background: linear-gradient(to right, var(--border) 0%, rgba(124,58,237,0.3) 50%, var(--border) 100%); }
|
|
529
|
+
|
|
530
|
+
.topbar-breadcrumb { color: var(--text-faint); font-weight: 500; }
|
|
531
|
+
|
|
532
|
+
/* Skeleton loading */
|
|
533
|
+
.skeleton {
|
|
534
|
+
background: linear-gradient(90deg, var(--bg-surface) 25%, var(--bg-hover) 50%, var(--bg-surface) 75%);
|
|
535
|
+
background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 6px;
|
|
536
|
+
}
|
|
537
|
+
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
|
538
|
+
.skeleton-card { height: 80px; }
|
|
539
|
+
.skeleton-row { height: 16px; margin-bottom: 10px; }
|
|
540
|
+
|
|
541
|
+
/* ===== COMMAND PALETTE ===== */
|
|
542
|
+
.cmd-palette-overlay {
|
|
543
|
+
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6);
|
|
544
|
+
z-index: 300; align-items: flex-start; justify-content: center; padding-top: 20vh;
|
|
545
|
+
backdrop-filter: blur(4px);
|
|
546
|
+
}
|
|
547
|
+
.cmd-palette-overlay.visible { display: flex; }
|
|
548
|
+
.cmd-palette {
|
|
549
|
+
background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
|
550
|
+
width: 400px; max-width: 90vw; box-shadow: var(--shadow-lg); overflow: hidden;
|
|
551
|
+
}
|
|
552
|
+
.cmd-palette input {
|
|
553
|
+
width: 100%; padding: 16px 20px; background: transparent; border: none;
|
|
554
|
+
border-bottom: 1px solid var(--border); color: var(--text); font-size: 15px;
|
|
555
|
+
font-family: var(--font); outline: none;
|
|
556
|
+
}
|
|
557
|
+
.cmd-palette input::placeholder { color: var(--text-faint); }
|
|
558
|
+
.cmd-result {
|
|
559
|
+
padding: 10px 20px; cursor: pointer; font-size: 14px; color: var(--text-secondary);
|
|
560
|
+
display: flex; align-items: center; gap: 10px; transition: background var(--transition);
|
|
561
|
+
}
|
|
562
|
+
.cmd-result:hover, .cmd-result.selected { background: var(--accent-bg); color: var(--text); }
|
|
563
|
+
.cmd-result .cmd-shortcut { margin-left: auto; font-size: 11px; color: var(--text-faint); }
|
|
564
|
+
|
|
565
|
+
/* ===== THREAD BAR ===== */
|
|
566
|
+
.thread-bar {
|
|
567
|
+
width: 220px; background: var(--bg-raised); border-right: 1px solid var(--border);
|
|
568
|
+
display: flex; flex-direction: column; flex-shrink: 0; overflow: hidden;
|
|
569
|
+
}
|
|
570
|
+
.thread-bar-header {
|
|
571
|
+
padding: 12px 14px; display: flex; justify-content: space-between; align-items: center;
|
|
572
|
+
border-bottom: 1px solid var(--border);
|
|
573
|
+
}
|
|
574
|
+
.thread-bar-header span { font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
575
|
+
.thread-list { flex: 1; overflow-y: auto; padding: 6px; display: flex; flex-direction: column; gap: 2px; }
|
|
576
|
+
.thread-item {
|
|
577
|
+
padding: 10px 12px; border-radius: 8px; cursor: pointer; font-size: 13px;
|
|
578
|
+
color: var(--text-secondary); transition: all var(--transition); position: relative;
|
|
579
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
580
|
+
}
|
|
581
|
+
.thread-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
582
|
+
.thread-item.active { background: var(--accent-bg); color: var(--accent); font-weight: 600; }
|
|
583
|
+
.thread-item .thread-delete {
|
|
584
|
+
display: none; position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
|
|
585
|
+
background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: 14px;
|
|
586
|
+
}
|
|
587
|
+
.thread-item:hover .thread-delete { display: block; }
|
|
588
|
+
.new-thread-btn {
|
|
589
|
+
padding: 6px 12px; font-size: 12px; background: var(--accent); color: white;
|
|
590
|
+
border: none; border-radius: 6px; cursor: pointer; font-weight: 600;
|
|
591
|
+
}
|
|
592
|
+
.new-thread-btn:hover { background: var(--accent-hover); }
|
|
593
|
+
|
|
594
|
+
/* ===== MOBILE RESPONSIVE ===== */
|
|
595
|
+
@media (max-width: 768px) {
|
|
596
|
+
#sidebar {
|
|
597
|
+
position: fixed; left: -240px; top: 0; bottom: 0; z-index: 100;
|
|
598
|
+
transition: left 200ms ease; width: 240px;
|
|
599
|
+
}
|
|
600
|
+
#sidebar.open { left: 0; }
|
|
601
|
+
#sidebar::after { display: none; }
|
|
602
|
+
.mobile-menu-btn {
|
|
603
|
+
display: flex; width: 36px; height: 36px; align-items: center; justify-content: center;
|
|
604
|
+
background: var(--bg-surface); border: 1px solid var(--border); border-radius: 8px;
|
|
605
|
+
color: var(--text); font-size: 18px; cursor: pointer; flex-shrink: 0;
|
|
606
|
+
}
|
|
607
|
+
.mobile-overlay {
|
|
608
|
+
display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 99;
|
|
609
|
+
}
|
|
610
|
+
.mobile-overlay.visible { display: block; }
|
|
611
|
+
.cards { grid-template-columns: 1fr; }
|
|
612
|
+
.perf-grid { grid-template-columns: 1fr; }
|
|
613
|
+
.quick-actions { grid-template-columns: repeat(2, 1fr); }
|
|
614
|
+
.panel-body { padding: 16px; }
|
|
615
|
+
#topbar { padding: 12px 16px; }
|
|
616
|
+
.chat-msg { max-width: 90%; }
|
|
617
|
+
#chat-input-bar { padding: 12px 16px 16px; }
|
|
618
|
+
.settings-section { max-width: 100%; }
|
|
619
|
+
.thread-bar { display: none; }
|
|
620
|
+
}
|
|
621
|
+
@media (min-width: 769px) {
|
|
622
|
+
.mobile-menu-btn { display: none; }
|
|
623
|
+
.mobile-overlay { display: none !important; }
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* ===== RECIPE CARDS ===== */
|
|
627
|
+
.recipe-card {
|
|
628
|
+
background: var(--bg-surface);
|
|
629
|
+
border: 1px solid var(--border);
|
|
630
|
+
border-radius: var(--radius-lg);
|
|
631
|
+
padding: 20px;
|
|
632
|
+
transition: all var(--transition);
|
|
633
|
+
}
|
|
634
|
+
.recipe-card:hover {
|
|
635
|
+
border-color: var(--accent-border);
|
|
636
|
+
transform: translateY(-2px);
|
|
637
|
+
box-shadow: var(--shadow-lg);
|
|
638
|
+
}
|
|
639
|
+
.recipe-card-header {
|
|
640
|
+
display: flex;
|
|
641
|
+
align-items: center;
|
|
642
|
+
gap: 10px;
|
|
643
|
+
margin-bottom: 8px;
|
|
644
|
+
}
|
|
645
|
+
.recipe-card-icon { font-size: 24px; }
|
|
646
|
+
.recipe-card-title { font-family: var(--display); font-weight: 700; font-size: 15px; }
|
|
647
|
+
.recipe-card-desc { font-size: 13px; color: var(--text-secondary); line-height: 1.5; margin-bottom: 12px; }
|
|
648
|
+
.recipe-card-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
649
|
+
.recipe-card-schedule { font-size: 11px; color: var(--text-muted); }
|
|
650
|
+
.recipe-card-requires { font-size: 10px; color: var(--yellow); }
|
|
651
|
+
.recipe-card .btn { font-size: 12px; padding: 5px 14px; }
|
|
652
|
+
.recipe-card .btn.installed { background: var(--green-bg); color: var(--green); border: 1px solid rgba(16,185,129,0.3); }
|
|
653
|
+
</style>
|
|
654
|
+
</head>
|
|
655
|
+
<body>
|
|
656
|
+
|
|
657
|
+
<div id="sidebar">
|
|
658
|
+
<div class="sidebar-logo">
|
|
659
|
+
<div class="logo-icon"></div>
|
|
660
|
+
<span>Zubo</span>
|
|
661
|
+
</div>
|
|
662
|
+
<nav>
|
|
663
|
+
<div class="sidebar-section">Agent</div>
|
|
664
|
+
<a href="#agent" class="active" onclick="showPanel('agent')">
|
|
665
|
+
<span class="nav-icon">\u{1F4AC}</span> Chat
|
|
666
|
+
</a>
|
|
667
|
+
<div class="sidebar-divider"></div>
|
|
668
|
+
<div class="sidebar-section">Dashboard</div>
|
|
669
|
+
<a href="#status" onclick="showPanel('status')">
|
|
670
|
+
<span class="nav-icon">\u{1F4CA}</span> Status
|
|
671
|
+
</a>
|
|
672
|
+
<a href="#analytics" onclick="showPanel('analytics')">
|
|
673
|
+
<span class="nav-icon">\u{1F4C8}</span> Analytics
|
|
674
|
+
</a>
|
|
675
|
+
<a href="#system" onclick="showPanel('system')">
|
|
676
|
+
<span class="nav-icon">\u{2699}\u{FE0F}</span> System Prompt
|
|
677
|
+
</a>
|
|
678
|
+
<a href="#memory" onclick="showPanel('memory')">
|
|
679
|
+
<span class="nav-icon">\u{1F9E0}</span> Memory
|
|
680
|
+
</a>
|
|
681
|
+
<a href="#skills" onclick="showPanel('skills')">
|
|
682
|
+
<span class="nav-icon">\u{26A1}</span> Skills
|
|
683
|
+
</a>
|
|
684
|
+
<a href="#registry" onclick="showPanel('registry')">
|
|
685
|
+
<span class="nav-icon">\u{1F50D}</span> Registry
|
|
686
|
+
</a>
|
|
687
|
+
<a href="#workflows" onclick="showPanel('workflows')">
|
|
688
|
+
<span class="nav-icon">\u{1F500}</span> Workflows
|
|
689
|
+
</a>
|
|
690
|
+
<a href="#cron" onclick="showPanel('cron')">
|
|
691
|
+
<span class="nav-icon">\u{1F504}</span> Cron Jobs
|
|
692
|
+
</a>
|
|
693
|
+
<a href="#logs" onclick="showPanel('logs')">
|
|
694
|
+
<span class="nav-icon">\u{1F4DD}</span> Logs
|
|
695
|
+
</a>
|
|
696
|
+
<div class="sidebar-divider"></div>
|
|
697
|
+
<a href="#privacy" onclick="showPanel('privacy')">
|
|
698
|
+
<span class="nav-icon">\u{1F512}</span> Privacy
|
|
699
|
+
</a>
|
|
700
|
+
<a href="#budget" onclick="showPanel('budget')">
|
|
701
|
+
<span class="nav-icon">\u{1F4B0}</span> Budget
|
|
702
|
+
</a>
|
|
703
|
+
<a href="#settings" onclick="showPanel('settings')">
|
|
704
|
+
<span class="nav-icon">\u{2699}\u{FE0F}</span> Settings
|
|
705
|
+
</a>
|
|
706
|
+
</nav>
|
|
707
|
+
<div class="sidebar-footer">
|
|
708
|
+
<span>Zubo</span>
|
|
709
|
+
<span class="conn-badge" id="sidebar-conn-badge"></span>
|
|
710
|
+
</div>
|
|
711
|
+
</div>
|
|
712
|
+
|
|
713
|
+
<div id="main">
|
|
714
|
+
<div id="topbar">
|
|
715
|
+
<button class="mobile-menu-btn" onclick="toggleMobileMenu()">☰</button>
|
|
716
|
+
<span id="topbar-title"><span class="topbar-breadcrumb">Dashboard › </span><span id="topbar-title-text">Agent</span></span>
|
|
717
|
+
<span id="topbar-badge">Zubo</span>
|
|
718
|
+
</div>
|
|
719
|
+
<div id="content">
|
|
720
|
+
|
|
721
|
+
<!-- AGENT CHAT PANEL -->
|
|
722
|
+
<div id="panel-agent" class="panel active" style="position:relative;">
|
|
723
|
+
<div style="display:flex;height:100%;">
|
|
724
|
+
<div class="thread-bar" id="thread-bar">
|
|
725
|
+
<div class="thread-bar-header">
|
|
726
|
+
<span>Threads</span>
|
|
727
|
+
<div style="display:flex;gap:6px;">
|
|
728
|
+
<button class="new-thread-btn" onclick="createThread()">+ New</button>
|
|
729
|
+
<button class="btn btn-ghost btn-sm" onclick="exportThread()" style="font-size:11px;" data-tooltip="Export as Markdown">Export</button>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
<div class="thread-list" id="thread-list"></div>
|
|
733
|
+
</div>
|
|
734
|
+
<div style="flex:1;display:flex;flex-direction:column;position:relative;min-width:0;">
|
|
735
|
+
<div class="drop-overlay" id="drop-overlay">Drop file to upload</div>
|
|
736
|
+
<div id="chat-messages">
|
|
737
|
+
<div class="chat-empty chat-welcome">
|
|
738
|
+
<div class="chat-welcome-icon"></div>
|
|
739
|
+
<h3 class="gradient-text">What can I help you with?</h3>
|
|
740
|
+
<div class="chat-empty-text">Ask me anything, or try a suggestion below</div>
|
|
741
|
+
<div class="suggestion-chips">
|
|
742
|
+
<button class="suggestion-chip" onclick="useSuggestion(this)">Summarize my day</button>
|
|
743
|
+
<button class="suggestion-chip" onclick="useSuggestion(this)">Check the weather</button>
|
|
744
|
+
<button class="suggestion-chip" onclick="useSuggestion(this)">What can you do?</button>
|
|
745
|
+
<button class="suggestion-chip" onclick="useSuggestion(this)">Set a reminder</button>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
<div id="file-pill-bar" style="display:none; padding: 4px 24px 0;">
|
|
750
|
+
<div class="file-pill" id="file-pill"><span id="file-pill-name"></span><span class="remove" onclick="clearAttachedFile()">\u{2715}</span></div>
|
|
751
|
+
</div>
|
|
752
|
+
<div id="chat-input-bar">
|
|
753
|
+
<button class="chat-attach-btn" data-tooltip="Attach file" onclick="triggerFileUpload()">\u{1F4CE}</button>
|
|
754
|
+
<button class="chat-mic-btn" id="mic-btn" data-tooltip="Voice input" onclick="toggleMic()">\u{1F3A4}</button>
|
|
755
|
+
<input id="chat-input" type="text" placeholder="Message Zubo..." autocomplete="off">
|
|
756
|
+
<button id="chat-send">Send</button>
|
|
757
|
+
</div>
|
|
758
|
+
<input type="file" id="file-input" style="display:none" accept=".pdf,.docx,.txt,.md,.csv,.json,.html">
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
|
|
763
|
+
<!-- STATUS PANEL -->
|
|
764
|
+
<div id="panel-status" class="panel">
|
|
765
|
+
<div class="panel-body">
|
|
766
|
+
<div class="cards" id="status-cards"></div>
|
|
767
|
+
<div class="quick-actions" id="quick-actions">
|
|
768
|
+
<div class="quick-action" onclick="showPanel('agent')">
|
|
769
|
+
<div class="qa-icon">\u{1F4AC}</div>
|
|
770
|
+
<div class="qa-label">Chat</div>
|
|
771
|
+
<div class="qa-desc">Start a conversation</div>
|
|
772
|
+
</div>
|
|
773
|
+
<div class="quick-action" onclick="showPanel('skills')">
|
|
774
|
+
<div class="qa-icon">\u{26A1}</div>
|
|
775
|
+
<div class="qa-label">Skills</div>
|
|
776
|
+
<div class="qa-desc">View installed skills</div>
|
|
777
|
+
</div>
|
|
778
|
+
<div class="quick-action" onclick="showPanel('registry')">
|
|
779
|
+
<div class="qa-icon">\u{1F50D}</div>
|
|
780
|
+
<div class="qa-label">Registry</div>
|
|
781
|
+
<div class="qa-desc">Browse skill registry</div>
|
|
782
|
+
</div>
|
|
783
|
+
<div class="quick-action" onclick="showPanel('cron')">
|
|
784
|
+
<div class="qa-icon">\u{1F504}</div>
|
|
785
|
+
<div class="qa-label">Cron Jobs</div>
|
|
786
|
+
<div class="qa-desc">Schedule tasks</div>
|
|
787
|
+
</div>
|
|
788
|
+
<div class="quick-action" onclick="triggerFileUpload()">
|
|
789
|
+
<div class="qa-icon">\u{1F4C4}</div>
|
|
790
|
+
<div class="qa-label">Upload File</div>
|
|
791
|
+
<div class="qa-desc">Add a document</div>
|
|
792
|
+
</div>
|
|
793
|
+
<div class="quick-action" onclick="showPanel('workflows')">
|
|
794
|
+
<div class="qa-icon">\u{1F500}</div>
|
|
795
|
+
<div class="qa-label">Workflows</div>
|
|
796
|
+
<div class="qa-desc">Multi-agent pipelines</div>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
</div>
|
|
800
|
+
</div>
|
|
801
|
+
|
|
802
|
+
<!-- ANALYTICS PANEL -->
|
|
803
|
+
<div id="panel-analytics" class="panel">
|
|
804
|
+
<div class="panel-body">
|
|
805
|
+
<div class="cards" id="analytics-summary"></div>
|
|
806
|
+
<div class="memory-section-title" style="margin-top:28px;">Token Usage (Last 7 Days)</div>
|
|
807
|
+
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;">
|
|
808
|
+
<div class="bar-chart" id="usage-chart"></div>
|
|
809
|
+
</div>
|
|
810
|
+
<div class="memory-section-title" style="margin-top:28px;">
|
|
811
|
+
<span>Tool Usage</span>
|
|
812
|
+
<span class="badge" id="tool-count"></span>
|
|
813
|
+
</div>
|
|
814
|
+
<table id="tools-table">
|
|
815
|
+
<thead><tr><th>Tool</th><th>Calls</th><th>Avg Time</th><th>Errors</th></tr></thead>
|
|
816
|
+
<tbody id="tools-body"></tbody>
|
|
817
|
+
</table>
|
|
818
|
+
<div class="memory-section-title" style="margin-top:28px;">Sessions</div>
|
|
819
|
+
<table id="sessions-table">
|
|
820
|
+
<thead><tr><th>Session</th><th>Provider</th><th>Tokens</th><th>Cost</th><th>Last Used</th></tr></thead>
|
|
821
|
+
<tbody id="sessions-body"></tbody>
|
|
822
|
+
</table>
|
|
823
|
+
|
|
824
|
+
<div class="memory-section-title" style="margin-top:28px;">System Health</div>
|
|
825
|
+
<div class="perf-grid" id="perf-health"></div>
|
|
826
|
+
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-top:14px;">
|
|
827
|
+
<div style="font-size:11px;color:var(--text-faint);margin-bottom:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">RSS Memory (7 Days)</div>
|
|
828
|
+
<div class="bar-chart" id="rss-chart"></div>
|
|
829
|
+
</div>
|
|
830
|
+
|
|
831
|
+
<div class="memory-section-title" style="margin-top:28px;">Cost Breakdown by Model</div>
|
|
832
|
+
<table id="cost-table">
|
|
833
|
+
<thead><tr><th>Provider</th><th>Model</th><th>Tokens</th><th>Cost</th><th style="width:30%;">Share</th></tr></thead>
|
|
834
|
+
<tbody id="cost-body"></tbody>
|
|
835
|
+
</table>
|
|
836
|
+
|
|
837
|
+
<div class="memory-section-title" style="margin-top:28px;">Response Time Trend (7 Days)</div>
|
|
838
|
+
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;">
|
|
839
|
+
<div class="bar-chart" id="response-chart"></div>
|
|
840
|
+
</div>
|
|
841
|
+
|
|
842
|
+
<div class="memory-section-title" style="margin-top:28px;">Top Models</div>
|
|
843
|
+
<table id="top-models-table">
|
|
844
|
+
<thead><tr><th>Model</th><th>Requests</th><th>Tokens</th><th>Cost</th><th>Avg Response</th></tr></thead>
|
|
845
|
+
<tbody id="top-models-body"></tbody>
|
|
846
|
+
</table>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
|
|
850
|
+
<!-- SYSTEM PROMPT PANEL -->
|
|
851
|
+
<div id="panel-system" class="panel">
|
|
852
|
+
<div class="panel-body">
|
|
853
|
+
<div class="editor-wrap">
|
|
854
|
+
<div class="editor-toolbar">
|
|
855
|
+
<button class="btn btn-primary" onclick="saveSystem()">Save</button>
|
|
856
|
+
<button class="btn btn-ghost" onclick="loadSystem()">Reload</button>
|
|
857
|
+
<span id="system-status" class="status-text"></span>
|
|
858
|
+
</div>
|
|
859
|
+
<textarea class="editor" id="system-editor" spellcheck="false"></textarea>
|
|
860
|
+
</div>
|
|
861
|
+
</div>
|
|
862
|
+
</div>
|
|
863
|
+
|
|
864
|
+
<!-- MEMORY PANEL -->
|
|
865
|
+
<div id="panel-memory" class="panel">
|
|
866
|
+
<div class="panel-body">
|
|
867
|
+
<div class="editor-wrap">
|
|
868
|
+
<div class="editor-toolbar">
|
|
869
|
+
<button class="btn btn-primary" onclick="saveMemory()">Save MEMORY.md</button>
|
|
870
|
+
<button class="btn btn-ghost" onclick="loadMemory()">Reload</button>
|
|
871
|
+
<span id="memory-status" class="status-text"></span>
|
|
872
|
+
</div>
|
|
873
|
+
<textarea class="editor" id="memory-editor" spellcheck="false" style="height: 35vh; flex: none;"></textarea>
|
|
874
|
+
</div>
|
|
875
|
+
<div class="memory-section-title">
|
|
876
|
+
<span>Memory Chunks</span>
|
|
877
|
+
<span class="badge" id="memory-count"></span>
|
|
878
|
+
</div>
|
|
879
|
+
<div class="search-bar">
|
|
880
|
+
<input id="memory-search" type="text" placeholder="Search memories...">
|
|
881
|
+
<button class="btn btn-primary" onclick="searchMemories()">Search</button>
|
|
882
|
+
</div>
|
|
883
|
+
<div class="memory-list" id="memory-results"></div>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
|
|
887
|
+
<!-- SKILLS PANEL -->
|
|
888
|
+
<div id="panel-skills" class="panel">
|
|
889
|
+
<div class="panel-body">
|
|
890
|
+
<table>
|
|
891
|
+
<thead><tr><th>Name</th><th>Description</th><th>Status</th></tr></thead>
|
|
892
|
+
<tbody id="skills-body"></tbody>
|
|
893
|
+
</table>
|
|
894
|
+
<p id="skills-empty" class="empty-state" style="display:none;">No skills installed. <a href="#registry" onclick="showPanel('registry')" style="color:var(--accent);">Browse the registry</a></p>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
|
|
898
|
+
<!-- REGISTRY PANEL -->
|
|
899
|
+
<div id="panel-registry" class="panel">
|
|
900
|
+
<div class="panel-body">
|
|
901
|
+
<div class="search-bar">
|
|
902
|
+
<input id="registry-search" type="text" placeholder="Search skills (e.g. email, calendar, weather...)">
|
|
903
|
+
<button class="btn btn-primary" onclick="searchRegistry()">Search</button>
|
|
904
|
+
</div>
|
|
905
|
+
<div id="registry-results" style="display:flex;flex-direction:column;gap:10px;">
|
|
906
|
+
<p class="empty-state">Search the skill registry to find and install new skills.</p>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
</div>
|
|
910
|
+
|
|
911
|
+
<!-- WORKFLOWS PANEL -->
|
|
912
|
+
<div id="panel-workflows" class="panel">
|
|
913
|
+
<div class="panel-body">
|
|
914
|
+
<div class="memory-section-title">
|
|
915
|
+
<span>Workflow Recipes</span>
|
|
916
|
+
<span class="badge" id="recipe-count"></span>
|
|
917
|
+
</div>
|
|
918
|
+
<p class="settings-desc" style="margin-bottom:16px;">Pre-built automations you can activate with one click.</p>
|
|
919
|
+
<div id="recipes-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;margin-bottom:32px;"></div>
|
|
920
|
+
|
|
921
|
+
<div class="memory-section-title" style="margin-top:28px;">
|
|
922
|
+
<span>Custom Workflows</span>
|
|
923
|
+
</div>
|
|
924
|
+
<div class="editor-toolbar">
|
|
925
|
+
<button class="btn btn-ghost" onclick="loadWorkflows()">Refresh</button>
|
|
926
|
+
<span id="workflows-status" class="status-text"></span>
|
|
927
|
+
</div>
|
|
928
|
+
<div id="workflows-list" style="display:flex;flex-direction:column;gap:14px;"></div>
|
|
929
|
+
<p id="workflows-empty" class="empty-state" style="display:none;">No custom workflows defined. Ask Zubo to create a workflow in chat.</p>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
|
|
933
|
+
<!-- CRON PANEL -->
|
|
934
|
+
<div id="panel-cron" class="panel">
|
|
935
|
+
<div class="panel-body">
|
|
936
|
+
<table>
|
|
937
|
+
<thead><tr><th>Name</th><th>Schedule</th><th>Task</th><th>Enabled</th><th>Last Run</th></tr></thead>
|
|
938
|
+
<tbody id="cron-body"></tbody>
|
|
939
|
+
</table>
|
|
940
|
+
<p id="cron-empty" class="empty-state" style="display:none;">No cron jobs configured.</p>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
|
|
944
|
+
<!-- LOGS PANEL -->
|
|
945
|
+
<div id="panel-logs" class="panel">
|
|
946
|
+
<div class="panel-body" style="display:flex; flex-direction:column; height:100%;">
|
|
947
|
+
<div class="editor-toolbar">
|
|
948
|
+
<button class="btn btn-ghost" onclick="loadLogs()">Refresh</button>
|
|
949
|
+
<span id="logs-status" class="status-text"></span>
|
|
950
|
+
</div>
|
|
951
|
+
<div class="log-view" id="log-content"></div>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
|
|
955
|
+
<!-- SETTINGS PANEL -->
|
|
956
|
+
<div id="panel-settings" class="panel">
|
|
957
|
+
<div class="panel-body">
|
|
958
|
+
<div class="settings-section">
|
|
959
|
+
<h3 class="settings-title" data-tooltip="Select which AI model powers Zubo">LLM Provider</h3>
|
|
960
|
+
<p class="settings-desc">Select which provider and model Zubo uses.</p>
|
|
961
|
+
<div class="settings-grid">
|
|
962
|
+
<div class="settings-field">
|
|
963
|
+
<label class="settings-label" data-tooltip="Cloud AI service" for="settings-provider">Provider</label>
|
|
964
|
+
<select id="settings-provider" class="settings-select" onchange="onProviderChange()"></select>
|
|
965
|
+
</div>
|
|
966
|
+
<div class="settings-field">
|
|
967
|
+
<label class="settings-label" for="settings-model">Model</label>
|
|
968
|
+
<input id="settings-model" type="text" class="settings-input" placeholder="e.g. claude-sonnet-4-5-20250929">
|
|
969
|
+
</div>
|
|
970
|
+
</div>
|
|
971
|
+
<div style="margin-top: 16px; display: flex; gap: 10px; align-items: center;">
|
|
972
|
+
<button class="btn btn-primary" onclick="saveModelConfig()">Save</button>
|
|
973
|
+
<button class="btn btn-ghost" onclick="testLlm()">Test Connection</button>
|
|
974
|
+
<span id="settings-status" class="status-text"></span>
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
|
|
978
|
+
<div class="settings-section">
|
|
979
|
+
<h3 class="settings-title" data-tooltip="Connected messaging channels">Channels
|
|
980
|
+
<span id="channel-count-badge" class="conn-badge" style="font-size:10px;"></span>
|
|
981
|
+
</h3>
|
|
982
|
+
<p class="settings-desc">Status of connected messaging channels.</p>
|
|
983
|
+
<div id="channel-status-list" style="display:flex;flex-direction:column;gap:8px;"></div>
|
|
984
|
+
</div>
|
|
985
|
+
|
|
986
|
+
<div class="settings-section">
|
|
987
|
+
<h3 class="settings-title" data-tooltip="Background task frequency">Heartbeat Interval</h3>
|
|
988
|
+
<p class="settings-desc">How often the background heartbeat runs. Default: 30 minutes.</p>
|
|
989
|
+
<div class="settings-grid">
|
|
990
|
+
<div class="settings-field">
|
|
991
|
+
<label class="settings-label" data-tooltip="Minutes between heartbeats" for="settings-heartbeat">Interval (minutes)</label>
|
|
992
|
+
<input id="settings-heartbeat" type="number" class="settings-input" min="1" max="1440" step="1" placeholder="30">
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
<div style="margin-top: 16px; display: flex; gap: 10px; align-items: center;">
|
|
996
|
+
<button class="btn btn-primary" onclick="saveHeartbeat()">Save</button>
|
|
997
|
+
<span id="heartbeat-status" class="status-text"></span>
|
|
998
|
+
</div>
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
<div class="settings-section">
|
|
1002
|
+
<h3 class="settings-title" data-tooltip="Route simple queries to a cheaper/faster model">Smart Routing</h3>
|
|
1003
|
+
<p class="settings-desc">Automatically route simple queries to a fast, cheap model and complex ones to your primary model. Saves cost without sacrificing quality.</p>
|
|
1004
|
+
<div class="settings-grid">
|
|
1005
|
+
<div class="settings-field">
|
|
1006
|
+
<label class="settings-label" for="sr-enabled">Enabled</label>
|
|
1007
|
+
<select id="sr-enabled" class="settings-select">
|
|
1008
|
+
<option value="false">Disabled</option>
|
|
1009
|
+
<option value="true">Enabled</option>
|
|
1010
|
+
</select>
|
|
1011
|
+
</div>
|
|
1012
|
+
<div class="settings-field">
|
|
1013
|
+
<label class="settings-label" for="sr-fast-provider">Fast Provider</label>
|
|
1014
|
+
<select id="sr-fast-provider" class="settings-select"></select>
|
|
1015
|
+
</div>
|
|
1016
|
+
<div class="settings-field">
|
|
1017
|
+
<label class="settings-label" for="sr-fast-model">Fast Model</label>
|
|
1018
|
+
<input id="sr-fast-model" type="text" class="settings-input" placeholder="e.g. gpt-4.1-mini">
|
|
1019
|
+
</div>
|
|
1020
|
+
</div>
|
|
1021
|
+
<div style="margin-top: 16px; display: flex; gap: 10px; align-items: center;">
|
|
1022
|
+
<button class="btn btn-primary" onclick="saveSmartRouting()">Save</button>
|
|
1023
|
+
<span id="sr-status" class="status-text"></span>
|
|
1024
|
+
</div>
|
|
1025
|
+
</div>
|
|
1026
|
+
|
|
1027
|
+
<div class="settings-section">
|
|
1028
|
+
<h3 class="settings-title">Data</h3>
|
|
1029
|
+
<p class="settings-desc">Export, backup, or import your Zubo database.</p>
|
|
1030
|
+
<div id="db-stats" style="font-size:12px;color:var(--text-muted);margin-bottom:16px;"></div>
|
|
1031
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
|
1032
|
+
<button class="btn btn-primary" onclick="exportJson()">Export JSON</button>
|
|
1033
|
+
<button class="btn btn-ghost" onclick="backupDb()">Backup SQLite</button>
|
|
1034
|
+
<button class="btn btn-ghost" onclick="document.getElementById('import-file').click()">Import JSON</button>
|
|
1035
|
+
<input type="file" id="import-file" style="display:none" accept=".json" onchange="importJson(event)">
|
|
1036
|
+
</div>
|
|
1037
|
+
<span id="data-status" class="status-text" style="display:block;margin-top:10px;"></span>
|
|
1038
|
+
</div>
|
|
1039
|
+
|
|
1040
|
+
<div class="settings-section" style="max-width:700px;">
|
|
1041
|
+
<h3 class="settings-title">Secrets & API Keys</h3>
|
|
1042
|
+
<p class="settings-desc">Manage API keys and credentials for integrations. Values are stored encrypted in your local database and never sent to external services by Zubo.</p>
|
|
1043
|
+
<div style="display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;">
|
|
1044
|
+
<button class="btn btn-primary" onclick="showAddSecretForm()">Add Secret</button>
|
|
1045
|
+
<button class="btn btn-ghost" onclick="loadSecrets()">Refresh</button>
|
|
1046
|
+
</div>
|
|
1047
|
+
<div id="secret-add-form" style="display:none;margin-bottom:16px;padding:16px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);">
|
|
1048
|
+
<div style="display:flex;flex-direction:column;gap:12px;">
|
|
1049
|
+
<div class="settings-field">
|
|
1050
|
+
<label class="settings-label" for="secret-name-input">Name</label>
|
|
1051
|
+
<input id="secret-name-input" type="text" class="settings-input" placeholder="e.g. github_token" pattern="[a-z0-9_]+" style="max-width:260px;">
|
|
1052
|
+
</div>
|
|
1053
|
+
<div class="settings-field">
|
|
1054
|
+
<label class="settings-label" for="secret-value-input">Value</label>
|
|
1055
|
+
<input id="secret-value-input" type="password" class="settings-input" placeholder="API key or token">
|
|
1056
|
+
</div>
|
|
1057
|
+
<div class="settings-field">
|
|
1058
|
+
<label class="settings-label" for="secret-service-input">Service (optional)</label>
|
|
1059
|
+
<input id="secret-service-input" type="text" class="settings-input" placeholder="e.g. github, openai" style="max-width:260px;">
|
|
1060
|
+
</div>
|
|
1061
|
+
<div style="display:flex;gap:10px;">
|
|
1062
|
+
<button class="btn btn-primary" onclick="saveSecret()">Save</button>
|
|
1063
|
+
<button class="btn btn-ghost" onclick="hideAddSecretForm()">Cancel</button>
|
|
1064
|
+
</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div id="secrets-list" style="display:flex;flex-direction:column;gap:6px;"></div>
|
|
1068
|
+
<p id="secrets-empty" class="empty-state" style="display:none;">No secrets stored. Add one to connect integrations.</p>
|
|
1069
|
+
</div>
|
|
1070
|
+
|
|
1071
|
+
<div class="settings-section">
|
|
1072
|
+
<h3 class="settings-title">Configuration</h3>
|
|
1073
|
+
<p class="settings-desc">Manage your full config by editing <code>~/.zubo/config.json</code> directly, or re-run <code>zubo setup</code>.</p>
|
|
1074
|
+
</div>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
|
|
1078
|
+
<!-- BUDGET PANEL -->
|
|
1079
|
+
<div id="panel-budget" class="panel">
|
|
1080
|
+
<div class="panel-body">
|
|
1081
|
+
<div class="cards" id="budget-summary-cards"></div>
|
|
1082
|
+
|
|
1083
|
+
<div class="settings-section" style="margin-top:24px;">
|
|
1084
|
+
<h3 class="settings-title">Budget Limits</h3>
|
|
1085
|
+
<p class="settings-desc">Set spending limits to control costs. The agent will pause when limits are reached.</p>
|
|
1086
|
+
<div class="settings-grid">
|
|
1087
|
+
<div class="settings-field">
|
|
1088
|
+
<label class="settings-label" for="budget-daily">Daily Limit (USD)</label>
|
|
1089
|
+
<input id="budget-daily" type="number" class="settings-input" min="0" step="0.01" placeholder="e.g. 5.00">
|
|
1090
|
+
</div>
|
|
1091
|
+
<div class="settings-field">
|
|
1092
|
+
<label class="settings-label" for="budget-monthly">Monthly Limit (USD)</label>
|
|
1093
|
+
<input id="budget-monthly" type="number" class="settings-input" min="0" step="0.01" placeholder="e.g. 50.00">
|
|
1094
|
+
</div>
|
|
1095
|
+
<div class="settings-field">
|
|
1096
|
+
<label class="settings-label" for="budget-alert">Alert Threshold</label>
|
|
1097
|
+
<select id="budget-alert" class="settings-select">
|
|
1098
|
+
<option value="0.5">50%</option>
|
|
1099
|
+
<option value="0.7">70%</option>
|
|
1100
|
+
<option value="0.8" selected>80%</option>
|
|
1101
|
+
<option value="0.9">90%</option>
|
|
1102
|
+
</select>
|
|
1103
|
+
</div>
|
|
1104
|
+
</div>
|
|
1105
|
+
<div style="margin-top: 16px; display: flex; gap: 10px; align-items: center;">
|
|
1106
|
+
<button class="btn btn-primary" onclick="saveBudget()">Save Limits</button>
|
|
1107
|
+
<button class="btn btn-ghost" onclick="clearBudget()">Remove Limits</button>
|
|
1108
|
+
<span id="budget-status" class="status-text"></span>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
|
|
1112
|
+
<div class="memory-section-title" style="margin-top:28px;">Daily Spend (Last 7 Days)</div>
|
|
1113
|
+
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;">
|
|
1114
|
+
<div class="bar-chart" id="budget-chart"></div>
|
|
1115
|
+
</div>
|
|
1116
|
+
</div>
|
|
1117
|
+
</div>
|
|
1118
|
+
|
|
1119
|
+
<!-- PRIVACY PANEL -->
|
|
1120
|
+
<div id="panel-privacy" class="panel">
|
|
1121
|
+
<div class="panel-body">
|
|
1122
|
+
<div style="margin-bottom:20px;">
|
|
1123
|
+
<h3 style="font-family:var(--display);font-weight:700;margin-bottom:4px;">Privacy & Data</h3>
|
|
1124
|
+
<p class="settings-desc">See exactly what data Zubo stores and what has been sent to AI providers. You own your data.</p>
|
|
1125
|
+
</div>
|
|
1126
|
+
|
|
1127
|
+
<div class="cards" id="privacy-summary-cards"></div>
|
|
1128
|
+
|
|
1129
|
+
<div class="settings-section" style="margin-top:24px;">
|
|
1130
|
+
<h3 class="settings-title">Data Sent to AI Providers</h3>
|
|
1131
|
+
<p class="settings-desc">Every API call Zubo makes to LLM providers (Anthropic, OpenAI, etc.) is logged here.</p>
|
|
1132
|
+
<div id="privacy-providers" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px;"></div>
|
|
1133
|
+
<table id="api-log-table">
|
|
1134
|
+
<thead><tr><th>Time</th><th>Provider</th><th>Model</th><th>Tokens Sent</th><th>Tokens Received</th><th>Cost</th></tr></thead>
|
|
1135
|
+
<tbody id="api-log-body"></tbody>
|
|
1136
|
+
</table>
|
|
1137
|
+
<div style="margin-top:10px;display:flex;gap:10px;">
|
|
1138
|
+
<button class="btn btn-ghost" id="api-log-more" onclick="loadMoreApiLog()" style="display:none;">Load More</button>
|
|
1139
|
+
</div>
|
|
1140
|
+
</div>
|
|
1141
|
+
|
|
1142
|
+
<div class="settings-section" style="margin-top:24px;">
|
|
1143
|
+
<h3 class="settings-title">Tool Executions</h3>
|
|
1144
|
+
<p class="settings-desc">Log of every tool/skill Zubo has executed on your behalf.</p>
|
|
1145
|
+
<table id="tool-log-table">
|
|
1146
|
+
<thead><tr><th>Time</th><th>Tool</th><th>Duration</th><th>Status</th></tr></thead>
|
|
1147
|
+
<tbody id="tool-log-body"></tbody>
|
|
1148
|
+
</table>
|
|
1149
|
+
<div style="margin-top:10px;display:flex;gap:10px;">
|
|
1150
|
+
<button class="btn btn-ghost" id="tool-log-more" onclick="loadMoreToolLog()" style="display:none;">Load More</button>
|
|
1151
|
+
</div>
|
|
1152
|
+
</div>
|
|
1153
|
+
|
|
1154
|
+
<div class="settings-section" style="margin-top:24px;border-color:rgba(239,68,68,0.2);">
|
|
1155
|
+
<h3 class="settings-title" style="color:var(--red);">Data Controls</h3>
|
|
1156
|
+
<p class="settings-desc">Delete stored data. These actions cannot be undone.</p>
|
|
1157
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
|
1158
|
+
<button class="btn btn-ghost" onclick="wipeData('memories')" style="color:var(--yellow);">Delete All Memories</button>
|
|
1159
|
+
<button class="btn btn-ghost" onclick="wipeData('messages')" style="color:var(--yellow);">Delete All Messages</button>
|
|
1160
|
+
<button class="btn btn-ghost" onclick="wipeData('usage')" style="color:var(--yellow);">Delete Usage Data</button>
|
|
1161
|
+
<button class="btn btn-ghost" onclick="wipeData('all')" style="color:var(--red);border-color:rgba(239,68,68,0.3);">Delete Everything</button>
|
|
1162
|
+
</div>
|
|
1163
|
+
<span id="wipe-status" class="status-text" style="display:block;margin-top:10px;"></span>
|
|
1164
|
+
</div>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
|
|
1168
|
+
</div>
|
|
1169
|
+
</div>
|
|
1170
|
+
|
|
1171
|
+
<!-- Onboarding Modal -->
|
|
1172
|
+
<div class="modal-overlay" id="onboarding-modal">
|
|
1173
|
+
<div class="modal">
|
|
1174
|
+
<div class="steps" id="onboarding-steps">
|
|
1175
|
+
<div class="step-dot active"></div>
|
|
1176
|
+
<div class="step-dot"></div>
|
|
1177
|
+
<div class="step-dot"></div>
|
|
1178
|
+
<div class="step-dot"></div>
|
|
1179
|
+
</div>
|
|
1180
|
+
<div id="onboarding-content">
|
|
1181
|
+
<h2>Welcome to Zubo</h2>
|
|
1182
|
+
<p>Your personal AI agent that remembers you, runs tasks, and connects to your favorite services. Let's get you set up.</p>
|
|
1183
|
+
</div>
|
|
1184
|
+
<div class="modal-actions">
|
|
1185
|
+
<button class="btn btn-ghost" onclick="skipOnboarding()">Skip</button>
|
|
1186
|
+
<button class="btn btn-primary" id="onboarding-next" onclick="nextOnboardingStep()">Get Started</button>
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
|
|
1191
|
+
<div class="cmd-palette-overlay" id="cmd-palette">
|
|
1192
|
+
<div class="cmd-palette">
|
|
1193
|
+
<input type="text" id="cmd-input" placeholder="Go to..." autocomplete="off">
|
|
1194
|
+
<div id="cmd-results"></div>
|
|
1195
|
+
</div>
|
|
1196
|
+
</div>
|
|
1197
|
+
|
|
1198
|
+
<div class="toast" id="toast"></div>
|
|
1199
|
+
|
|
1200
|
+
<div class="mobile-overlay" id="mobile-overlay" onclick="closeMobileMenu()"></div>
|
|
1201
|
+
|
|
1202
|
+
<script>
|
|
1203
|
+
// --- Panel routing ---
|
|
1204
|
+
var panelNames = ['agent','status','analytics','system','memory','skills','registry','workflows','cron','logs','privacy','budget','settings'];
|
|
1205
|
+
var panelTitles = { agent:'Agent', status:'Status', analytics:'Analytics', system:'System Prompt', memory:'Memory', skills:'Skills', registry:'Skill Registry', workflows:'Workflows', cron:'Cron Jobs', logs:'Logs', privacy:'Privacy & Data', budget:'Budget', settings:'Settings' };
|
|
1206
|
+
|
|
1207
|
+
function showPanel(name) {
|
|
1208
|
+
if (panelNames.indexOf(name) === -1) name = 'agent';
|
|
1209
|
+
document.querySelectorAll('.panel').forEach(function(p) { p.classList.remove('active'); });
|
|
1210
|
+
document.querySelectorAll('#sidebar nav a').forEach(function(a) { a.classList.remove('active'); });
|
|
1211
|
+
var panel = document.getElementById('panel-' + name);
|
|
1212
|
+
if (panel) panel.classList.add('active');
|
|
1213
|
+
var link = document.querySelector('#sidebar nav a[href="#' + name + '"]');
|
|
1214
|
+
if (link) link.classList.add('active');
|
|
1215
|
+
var titleText = document.getElementById('topbar-title-text');
|
|
1216
|
+
if (titleText) titleText.textContent = panelTitles[name] || name;
|
|
1217
|
+
window.location.hash = name;
|
|
1218
|
+
|
|
1219
|
+
if (name === 'agent') { document.getElementById('chat-input').focus(); }
|
|
1220
|
+
if (name === 'status') loadStatus();
|
|
1221
|
+
if (name === 'analytics') loadAnalytics();
|
|
1222
|
+
if (name === 'system') loadSystem();
|
|
1223
|
+
if (name === 'memory') loadMemory();
|
|
1224
|
+
if (name === 'skills') loadSkills();
|
|
1225
|
+
if (name === 'cron') loadCron();
|
|
1226
|
+
if (name === 'logs') loadLogs();
|
|
1227
|
+
if (name === 'settings') loadSettings();
|
|
1228
|
+
if (name === 'workflows') loadWorkflows();
|
|
1229
|
+
if (name === 'budget') loadBudget();
|
|
1230
|
+
if (name === 'privacy') loadPrivacy();
|
|
1231
|
+
closeMobileMenu();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function routeFromHash() {
|
|
1235
|
+
var hash = window.location.hash.replace('#', '') || 'agent';
|
|
1236
|
+
showPanel(hash);
|
|
1237
|
+
}
|
|
1238
|
+
window.addEventListener('hashchange', routeFromHash);
|
|
1239
|
+
|
|
1240
|
+
// --- Toast ---
|
|
1241
|
+
function toast(msg) {
|
|
1242
|
+
var t = document.getElementById('toast');
|
|
1243
|
+
t.textContent = msg;
|
|
1244
|
+
t.classList.add('show');
|
|
1245
|
+
setTimeout(function() { t.classList.remove('show'); }, 2200);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// --- API helper ---
|
|
1249
|
+
function api(path, opts) {
|
|
1250
|
+
return fetch('/api/dashboard' + path, opts).then(function(r) { return r.json(); });
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// --- HTML escaping (XSS prevention) ---
|
|
1254
|
+
function esc(text) {
|
|
1255
|
+
var d = document.createElement('div');
|
|
1256
|
+
d.textContent = String(text);
|
|
1257
|
+
return d.innerHTML;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// --- AGENT CHAT ---
|
|
1261
|
+
var chatMessages = document.getElementById('chat-messages');
|
|
1262
|
+
var chatInput = document.getElementById('chat-input');
|
|
1263
|
+
var chatSend = document.getElementById('chat-send');
|
|
1264
|
+
var chatBusy = false;
|
|
1265
|
+
var chatHistory = [];
|
|
1266
|
+
var attachedFile = null;
|
|
1267
|
+
|
|
1268
|
+
function clearEmptyState() {
|
|
1269
|
+
var empty = chatMessages.querySelector('.chat-empty');
|
|
1270
|
+
if (empty) empty.remove();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function useSuggestion(btn) {
|
|
1274
|
+
chatInput.value = btn.textContent;
|
|
1275
|
+
chatInput.focus();
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function renderMd(text) {
|
|
1279
|
+
var tmp = document.createElement('div');
|
|
1280
|
+
tmp.textContent = text;
|
|
1281
|
+
var s = tmp.innerHTML;
|
|
1282
|
+
s = s.replace(/\\\`\\\`\\\`([\\s\\S]*?)\\\`\\\`\\\`/g, '<pre><code>$1</code></pre>');
|
|
1283
|
+
s = s.replace(/\\\`([^\\\`]+)\\\`/g, '<code>$1</code>');
|
|
1284
|
+
s = s.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');
|
|
1285
|
+
s = s.replace(/\\*(.+?)\\*/g, '<em>$1</em>');
|
|
1286
|
+
s = s.replace(/\\n/g, '<br>');
|
|
1287
|
+
return s;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function addChatMsg(text, cls) {
|
|
1291
|
+
clearEmptyState();
|
|
1292
|
+
var d = document.createElement('div');
|
|
1293
|
+
d.className = 'chat-msg ' + cls;
|
|
1294
|
+
if (cls.indexOf('bot') !== -1 && cls.indexOf('thinking') === -1) {
|
|
1295
|
+
d.innerHTML = renderMd(text);
|
|
1296
|
+
} else {
|
|
1297
|
+
d.textContent = text;
|
|
1298
|
+
}
|
|
1299
|
+
chatMessages.appendChild(d);
|
|
1300
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
1301
|
+
return d;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function sendChatMessage() {
|
|
1305
|
+
var text = chatInput.value.trim();
|
|
1306
|
+
if (!text || chatBusy) return;
|
|
1307
|
+
chatBusy = true;
|
|
1308
|
+
chatSend.disabled = true;
|
|
1309
|
+
chatInput.value = '';
|
|
1310
|
+
|
|
1311
|
+
// Upload file first if attached
|
|
1312
|
+
if (attachedFile) {
|
|
1313
|
+
var formData = new FormData();
|
|
1314
|
+
formData.append('file', attachedFile);
|
|
1315
|
+
var fname = attachedFile.name;
|
|
1316
|
+
clearAttachedFile();
|
|
1317
|
+
addChatMsg('[Uploading ' + fname + '...]', 'user');
|
|
1318
|
+
fetch('/api/upload', { method: 'POST', body: formData }).then(function(r) { return r.json(); }).then(function(data) {
|
|
1319
|
+
if (data.uploaded) {
|
|
1320
|
+
text = text + ' [Uploaded: ' + fname + ', ' + (data.chunks || 0) + ' chunks indexed]';
|
|
1321
|
+
}
|
|
1322
|
+
doStreamChat(text);
|
|
1323
|
+
}).catch(function() { doStreamChat(text); });
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
addChatMsg(text, 'user');
|
|
1328
|
+
chatHistory.push({ role: 'user', text: text });
|
|
1329
|
+
doStreamChat(text);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function doStreamChat(text) {
|
|
1333
|
+
clearEmptyState();
|
|
1334
|
+
var botMsg = document.createElement('div');
|
|
1335
|
+
botMsg.className = 'chat-msg bot thinking';
|
|
1336
|
+
botMsg.textContent = '';
|
|
1337
|
+
chatMessages.appendChild(botMsg);
|
|
1338
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
1339
|
+
var streamedText = '';
|
|
1340
|
+
|
|
1341
|
+
fetch('/api/chat/stream', {
|
|
1342
|
+
method: 'POST',
|
|
1343
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1344
|
+
body: JSON.stringify({ message: text, threadId: activeThreadId || undefined }),
|
|
1345
|
+
}).then(function(response) {
|
|
1346
|
+
if (!response.ok) throw new Error('Stream request failed');
|
|
1347
|
+
var reader = response.body.getReader();
|
|
1348
|
+
var decoder = new TextDecoder();
|
|
1349
|
+
var buffer = '';
|
|
1350
|
+
|
|
1351
|
+
function processChunk() {
|
|
1352
|
+
return reader.read().then(function(result) {
|
|
1353
|
+
if (result.done) {
|
|
1354
|
+
botMsg.classList.remove('thinking');
|
|
1355
|
+
botMsg.innerHTML = renderMd(streamedText || 'No response.');
|
|
1356
|
+
chatHistory.push({ role: 'bot', text: streamedText });
|
|
1357
|
+
chatBusy = false;
|
|
1358
|
+
chatSend.disabled = false;
|
|
1359
|
+
chatInput.focus();
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
buffer += decoder.decode(result.value, { stream: true });
|
|
1363
|
+
var lines = buffer.split('\\n');
|
|
1364
|
+
buffer = lines.pop() || '';
|
|
1365
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1366
|
+
var line = lines[i].trim();
|
|
1367
|
+
if (line.startsWith('event: ')) {
|
|
1368
|
+
var evtType = line.slice(7);
|
|
1369
|
+
i++;
|
|
1370
|
+
if (i < lines.length && lines[i].trim().startsWith('data: ')) {
|
|
1371
|
+
try {
|
|
1372
|
+
var evtData = JSON.parse(lines[i].trim().slice(6));
|
|
1373
|
+
if (evtType === 'delta' && evtData.text) {
|
|
1374
|
+
streamedText += evtData.text;
|
|
1375
|
+
botMsg.textContent = streamedText;
|
|
1376
|
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
1377
|
+
} else if (evtType === 'tool') {
|
|
1378
|
+
if (evtData.status === 'start') {
|
|
1379
|
+
botMsg.textContent = streamedText + '\\n[Using ' + evtData.name + '...]';
|
|
1380
|
+
}
|
|
1381
|
+
} else if (evtType === 'done') {
|
|
1382
|
+
streamedText = evtData.reply || streamedText;
|
|
1383
|
+
} else if (evtType === 'error') {
|
|
1384
|
+
streamedText += '\\nError: ' + (evtData.error || 'Unknown error');
|
|
1385
|
+
}
|
|
1386
|
+
} catch(e) { console.warn('Failed to parse SSE event', e); }
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return processChunk();
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
return processChunk();
|
|
1394
|
+
}).catch(function(e) {
|
|
1395
|
+
botMsg.classList.remove('thinking');
|
|
1396
|
+
botMsg.textContent = 'Error: ' + e.message;
|
|
1397
|
+
chatBusy = false;
|
|
1398
|
+
chatSend.disabled = false;
|
|
1399
|
+
chatInput.focus();
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
chatSend.addEventListener('click', sendChatMessage);
|
|
1404
|
+
chatInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') sendChatMessage(); });
|
|
1405
|
+
|
|
1406
|
+
// --- File Upload ---
|
|
1407
|
+
function triggerFileUpload() {
|
|
1408
|
+
document.getElementById('file-input').click();
|
|
1409
|
+
}
|
|
1410
|
+
document.getElementById('file-input').addEventListener('change', function(e) {
|
|
1411
|
+
var file = e.target.files[0];
|
|
1412
|
+
if (file) attachFile(file);
|
|
1413
|
+
e.target.value = '';
|
|
1414
|
+
});
|
|
1415
|
+
function attachFile(file) {
|
|
1416
|
+
attachedFile = file;
|
|
1417
|
+
document.getElementById('file-pill-name').textContent = file.name;
|
|
1418
|
+
document.getElementById('file-pill-bar').style.display = 'block';
|
|
1419
|
+
}
|
|
1420
|
+
function clearAttachedFile() {
|
|
1421
|
+
attachedFile = null;
|
|
1422
|
+
document.getElementById('file-pill-bar').style.display = 'none';
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Drag and drop
|
|
1426
|
+
var chatPanel = document.getElementById('panel-agent');
|
|
1427
|
+
var dropOverlay = document.getElementById('drop-overlay');
|
|
1428
|
+
chatPanel.addEventListener('dragover', function(e) { e.preventDefault(); dropOverlay.classList.add('visible'); });
|
|
1429
|
+
chatPanel.addEventListener('dragleave', function(e) { if (e.target === chatPanel || e.target === dropOverlay) dropOverlay.classList.remove('visible'); });
|
|
1430
|
+
chatPanel.addEventListener('drop', function(e) {
|
|
1431
|
+
e.preventDefault();
|
|
1432
|
+
dropOverlay.classList.remove('visible');
|
|
1433
|
+
if (e.dataTransfer.files.length) attachFile(e.dataTransfer.files[0]);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// --- Voice Input ---
|
|
1437
|
+
var micRecording = false;
|
|
1438
|
+
var mediaRecorder = null;
|
|
1439
|
+
function toggleMic() {
|
|
1440
|
+
if (micRecording) { stopMic(); return; }
|
|
1441
|
+
if (!navigator.mediaDevices) { toast('Microphone not available'); return; }
|
|
1442
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
|
|
1443
|
+
micRecording = true;
|
|
1444
|
+
document.getElementById('mic-btn').classList.add('recording');
|
|
1445
|
+
var chunks = [];
|
|
1446
|
+
mediaRecorder = new MediaRecorder(stream);
|
|
1447
|
+
mediaRecorder.ondataavailable = function(e) { chunks.push(e.data); };
|
|
1448
|
+
mediaRecorder.onstop = function() {
|
|
1449
|
+
stream.getTracks().forEach(function(t) { t.stop(); });
|
|
1450
|
+
var blob = new Blob(chunks, { type: 'audio/webm' });
|
|
1451
|
+
sendVoice(blob);
|
|
1452
|
+
};
|
|
1453
|
+
mediaRecorder.start();
|
|
1454
|
+
}).catch(function() { toast('Microphone access denied'); });
|
|
1455
|
+
}
|
|
1456
|
+
function stopMic() {
|
|
1457
|
+
micRecording = false;
|
|
1458
|
+
document.getElementById('mic-btn').classList.remove('recording');
|
|
1459
|
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
|
|
1460
|
+
}
|
|
1461
|
+
function sendVoice(blob) {
|
|
1462
|
+
chatBusy = true;
|
|
1463
|
+
chatSend.disabled = true;
|
|
1464
|
+
addChatMsg('[Voice message]', 'user');
|
|
1465
|
+
var botMsg = addChatMsg('Transcribing...', 'bot thinking');
|
|
1466
|
+
var fd = new FormData();
|
|
1467
|
+
fd.append('audio', blob, 'recording.webm');
|
|
1468
|
+
fd.append('tts', 'false');
|
|
1469
|
+
fetch('/api/chat/voice', { method: 'POST', body: fd }).then(function(r) { return r.json(); }).then(function(data) {
|
|
1470
|
+
botMsg.classList.remove('thinking');
|
|
1471
|
+
if (data.error) { botMsg.textContent = 'Error: ' + data.error; }
|
|
1472
|
+
else {
|
|
1473
|
+
if (data.transcript) chatHistory.push({ role: 'user', text: data.transcript });
|
|
1474
|
+
botMsg.innerHTML = renderMd(data.reply || 'No response.');
|
|
1475
|
+
chatHistory.push({ role: 'bot', text: data.reply });
|
|
1476
|
+
}
|
|
1477
|
+
chatBusy = false;
|
|
1478
|
+
chatSend.disabled = false;
|
|
1479
|
+
}).catch(function(e) {
|
|
1480
|
+
botMsg.classList.remove('thinking');
|
|
1481
|
+
botMsg.textContent = 'Error: ' + e.message;
|
|
1482
|
+
chatBusy = false;
|
|
1483
|
+
chatSend.disabled = false;
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// --- STATUS ---
|
|
1488
|
+
function makeCard(label, value) {
|
|
1489
|
+
var card = document.createElement('div');
|
|
1490
|
+
card.className = 'card';
|
|
1491
|
+
var lbl = document.createElement('div');
|
|
1492
|
+
lbl.className = 'label';
|
|
1493
|
+
lbl.textContent = label;
|
|
1494
|
+
var val = document.createElement('div');
|
|
1495
|
+
val.className = 'value';
|
|
1496
|
+
if (value === 'running') val.classList.add('ok');
|
|
1497
|
+
if (value === 'not running') val.classList.add('warn');
|
|
1498
|
+
val.textContent = value;
|
|
1499
|
+
card.appendChild(lbl);
|
|
1500
|
+
card.appendChild(val);
|
|
1501
|
+
return card;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function loadStatus() {
|
|
1505
|
+
var c = document.getElementById('status-cards');
|
|
1506
|
+
c.replaceChildren();
|
|
1507
|
+
for (var i = 0; i < 3; i++) { var sk = document.createElement('div'); sk.className = 'card skeleton skeleton-card'; c.appendChild(sk); }
|
|
1508
|
+
api('/status').then(function(data) {
|
|
1509
|
+
c.replaceChildren();
|
|
1510
|
+
Object.keys(data).forEach(function(label) {
|
|
1511
|
+
c.appendChild(makeCard(label, String(data[label])));
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// --- ANALYTICS ---
|
|
1517
|
+
function loadAnalytics() {
|
|
1518
|
+
// Summary cards — show skeletons while loading
|
|
1519
|
+
var summaryEl = document.getElementById('analytics-summary');
|
|
1520
|
+
summaryEl.replaceChildren();
|
|
1521
|
+
for (var i = 0; i < 4; i++) { var sk = document.createElement('div'); sk.className = 'card skeleton skeleton-card'; summaryEl.appendChild(sk); }
|
|
1522
|
+
api('/analytics/summary').then(function(data) {
|
|
1523
|
+
var c = document.getElementById('analytics-summary');
|
|
1524
|
+
c.replaceChildren();
|
|
1525
|
+
c.appendChild(makeCard('Total Tokens', (data.totalTokens || 0).toLocaleString()));
|
|
1526
|
+
c.appendChild(makeCard('Estimated Cost', '$' + (data.estimatedCostUsd || 0).toFixed(4)));
|
|
1527
|
+
c.appendChild(makeCard('Avg Response', (data.avgResponseTimeMs || 0) + 'ms'));
|
|
1528
|
+
c.appendChild(makeCard('Sessions', String(data.sessionCount || 0)));
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
// Usage chart
|
|
1532
|
+
api('/analytics/usage-over-time').then(function(data) {
|
|
1533
|
+
var chart = document.getElementById('usage-chart');
|
|
1534
|
+
chart.replaceChildren();
|
|
1535
|
+
var days = data.days || [];
|
|
1536
|
+
if (!days.length) { chart.textContent = 'No data yet'; return; }
|
|
1537
|
+
var maxVal = 1;
|
|
1538
|
+
days.forEach(function(d) { var t = (d.input || 0) + (d.output || 0); if (t > maxVal) maxVal = t; });
|
|
1539
|
+
var yLabel = document.createElement('div');
|
|
1540
|
+
yLabel.className = 'chart-y-label';
|
|
1541
|
+
yLabel.textContent = maxVal.toLocaleString();
|
|
1542
|
+
chart.appendChild(yLabel);
|
|
1543
|
+
days.forEach(function(d) {
|
|
1544
|
+
var total = (d.input || 0) + (d.output || 0);
|
|
1545
|
+
var col = document.createElement('div');
|
|
1546
|
+
col.className = 'bar-col';
|
|
1547
|
+
var bar = document.createElement('div');
|
|
1548
|
+
bar.className = 'bar';
|
|
1549
|
+
bar.style.height = Math.max(2, (total / maxVal) * 100) + 'px';
|
|
1550
|
+
bar.setAttribute('data-tooltip', total.toLocaleString() + ' tokens');
|
|
1551
|
+
var label = document.createElement('div');
|
|
1552
|
+
label.className = 'bar-label';
|
|
1553
|
+
label.textContent = (d.day || '').slice(5);
|
|
1554
|
+
col.appendChild(bar);
|
|
1555
|
+
col.appendChild(label);
|
|
1556
|
+
chart.appendChild(col);
|
|
1557
|
+
});
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
// Tools table
|
|
1561
|
+
api('/analytics/tools').then(function(data) {
|
|
1562
|
+
var body = document.getElementById('tools-body');
|
|
1563
|
+
body.replaceChildren();
|
|
1564
|
+
var tools = data.tools || [];
|
|
1565
|
+
document.getElementById('tool-count').textContent = String(tools.length);
|
|
1566
|
+
tools.forEach(function(t) {
|
|
1567
|
+
var tr = document.createElement('tr');
|
|
1568
|
+
var cells = [t.tool_name, String(t.calls), Math.round(t.avg_ms || 0) + 'ms', String(t.errors || 0)];
|
|
1569
|
+
cells.forEach(function(text) {
|
|
1570
|
+
var td = document.createElement('td');
|
|
1571
|
+
td.textContent = text;
|
|
1572
|
+
tr.appendChild(td);
|
|
1573
|
+
});
|
|
1574
|
+
body.appendChild(tr);
|
|
1575
|
+
});
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
// Sessions table
|
|
1579
|
+
api('/analytics/sessions').then(function(data) {
|
|
1580
|
+
var body = document.getElementById('sessions-body');
|
|
1581
|
+
body.replaceChildren();
|
|
1582
|
+
(data.sessions || []).forEach(function(s) {
|
|
1583
|
+
var tr = document.createElement('tr');
|
|
1584
|
+
var cells = [
|
|
1585
|
+
(s.session_id || '').slice(0, 16) + '...',
|
|
1586
|
+
s.provider + '/' + s.model,
|
|
1587
|
+
((s.input_tokens || 0) + (s.output_tokens || 0)).toLocaleString(),
|
|
1588
|
+
'$' + (s.cost || 0).toFixed(4),
|
|
1589
|
+
(s.last_used || '').replace('T', ' ').slice(0, 16)
|
|
1590
|
+
];
|
|
1591
|
+
cells.forEach(function(text) {
|
|
1592
|
+
var td = document.createElement('td');
|
|
1593
|
+
td.textContent = text;
|
|
1594
|
+
tr.appendChild(td);
|
|
1595
|
+
});
|
|
1596
|
+
body.appendChild(tr);
|
|
1597
|
+
});
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
// System Health — perf snapshots
|
|
1601
|
+
api('/analytics/perf-snapshots').then(function(data) {
|
|
1602
|
+
var container = document.getElementById('perf-health');
|
|
1603
|
+
var chart = document.getElementById('rss-chart');
|
|
1604
|
+
if (!container || !chart) return;
|
|
1605
|
+
container.replaceChildren();
|
|
1606
|
+
chart.replaceChildren();
|
|
1607
|
+
var snaps = data.snapshots || [];
|
|
1608
|
+
if (!snaps.length) {
|
|
1609
|
+
var emptyCard = document.createElement('div');
|
|
1610
|
+
emptyCard.className = 'perf-card';
|
|
1611
|
+
var emptyLabel = document.createElement('div');
|
|
1612
|
+
emptyLabel.className = 'perf-label';
|
|
1613
|
+
emptyLabel.textContent = 'No Data';
|
|
1614
|
+
var emptyVal = document.createElement('div');
|
|
1615
|
+
emptyVal.className = 'perf-value';
|
|
1616
|
+
emptyVal.style.cssText = 'font-size:14px;color:var(--text-muted);';
|
|
1617
|
+
emptyVal.textContent = 'Performance data will appear after the first heartbeat.';
|
|
1618
|
+
emptyCard.appendChild(emptyLabel);
|
|
1619
|
+
emptyCard.appendChild(emptyVal);
|
|
1620
|
+
container.appendChild(emptyCard);
|
|
1621
|
+
chart.textContent = 'No data yet';
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
var latest = snaps[snaps.length - 1];
|
|
1625
|
+
var cardData = [
|
|
1626
|
+
{ label: 'RSS Memory', value: (latest.rss_mb || 0).toFixed(1) + ' MB' },
|
|
1627
|
+
{ label: 'Heap Memory', value: (latest.heap_mb || 0).toFixed(1) + ' MB' },
|
|
1628
|
+
{ label: 'Database Size', value: (latest.db_size_mb || 0).toFixed(1) + ' MB' },
|
|
1629
|
+
];
|
|
1630
|
+
cardData.forEach(function(c) {
|
|
1631
|
+
var card = document.createElement('div');
|
|
1632
|
+
card.className = 'perf-card';
|
|
1633
|
+
var lbl = document.createElement('div');
|
|
1634
|
+
lbl.className = 'perf-label';
|
|
1635
|
+
lbl.textContent = c.label;
|
|
1636
|
+
var val = document.createElement('div');
|
|
1637
|
+
val.className = 'perf-value';
|
|
1638
|
+
val.textContent = c.value;
|
|
1639
|
+
card.appendChild(lbl);
|
|
1640
|
+
card.appendChild(val);
|
|
1641
|
+
container.appendChild(card);
|
|
1642
|
+
});
|
|
1643
|
+
// RSS chart
|
|
1644
|
+
var maxRss = 1;
|
|
1645
|
+
snaps.forEach(function(s) { if ((s.rss_mb || 0) > maxRss) maxRss = s.rss_mb; });
|
|
1646
|
+
snaps.forEach(function(s) {
|
|
1647
|
+
var col = document.createElement('div');
|
|
1648
|
+
col.className = 'bar-col';
|
|
1649
|
+
var bar = document.createElement('div');
|
|
1650
|
+
bar.className = 'bar';
|
|
1651
|
+
bar.style.height = Math.max(2, ((s.rss_mb || 0) / maxRss) * 100) + 'px';
|
|
1652
|
+
bar.setAttribute('data-tooltip', (s.rss_mb || 0).toFixed(1) + ' MB');
|
|
1653
|
+
var label = document.createElement('div');
|
|
1654
|
+
label.className = 'bar-label';
|
|
1655
|
+
label.textContent = (s.created_at || '').slice(5, 10);
|
|
1656
|
+
col.appendChild(bar);
|
|
1657
|
+
col.appendChild(label);
|
|
1658
|
+
chart.appendChild(col);
|
|
1659
|
+
});
|
|
1660
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
1661
|
+
|
|
1662
|
+
// Cost breakdown
|
|
1663
|
+
api('/analytics/cost-breakdown').then(function(data) {
|
|
1664
|
+
var body = document.getElementById('cost-body');
|
|
1665
|
+
if (!body) return;
|
|
1666
|
+
body.replaceChildren();
|
|
1667
|
+
var rows = data.breakdown || [];
|
|
1668
|
+
if (!rows.length) {
|
|
1669
|
+
var emptyTr = document.createElement('tr');
|
|
1670
|
+
var emptyTd = document.createElement('td');
|
|
1671
|
+
emptyTd.setAttribute('colspan', '5');
|
|
1672
|
+
emptyTd.style.cssText = 'text-align:center;color:var(--text-faint);';
|
|
1673
|
+
emptyTd.textContent = 'No usage data yet';
|
|
1674
|
+
emptyTr.appendChild(emptyTd);
|
|
1675
|
+
body.appendChild(emptyTr);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
var maxCost = 0.001;
|
|
1679
|
+
rows.forEach(function(r) { if ((r.total_cost || 0) > maxCost) maxCost = r.total_cost; });
|
|
1680
|
+
rows.forEach(function(r) {
|
|
1681
|
+
var tr = document.createElement('tr');
|
|
1682
|
+
var pct = Math.round(((r.total_cost || 0) / maxCost) * 100);
|
|
1683
|
+
// Provider cell
|
|
1684
|
+
var td1 = document.createElement('td');
|
|
1685
|
+
td1.textContent = r.provider || '?';
|
|
1686
|
+
tr.appendChild(td1);
|
|
1687
|
+
// Model cell
|
|
1688
|
+
var td2 = document.createElement('td');
|
|
1689
|
+
td2.textContent = r.model || '?';
|
|
1690
|
+
tr.appendChild(td2);
|
|
1691
|
+
// Tokens cell
|
|
1692
|
+
var td3 = document.createElement('td');
|
|
1693
|
+
td3.textContent = (r.total_tokens || 0).toLocaleString();
|
|
1694
|
+
tr.appendChild(td3);
|
|
1695
|
+
// Cost cell
|
|
1696
|
+
var td4 = document.createElement('td');
|
|
1697
|
+
td4.textContent = '$' + (r.total_cost || 0).toFixed(4);
|
|
1698
|
+
tr.appendChild(td4);
|
|
1699
|
+
// Share bar cell
|
|
1700
|
+
var td5 = document.createElement('td');
|
|
1701
|
+
var barWrap = document.createElement('div');
|
|
1702
|
+
barWrap.className = 'cost-bar-wrap';
|
|
1703
|
+
var barDiv = document.createElement('div');
|
|
1704
|
+
barDiv.className = 'cost-bar';
|
|
1705
|
+
barDiv.style.width = pct + '%';
|
|
1706
|
+
var pctSpan = document.createElement('span');
|
|
1707
|
+
pctSpan.className = 'cost-pct';
|
|
1708
|
+
pctSpan.textContent = (r.requests || 0) + ' req';
|
|
1709
|
+
barWrap.appendChild(barDiv);
|
|
1710
|
+
barWrap.appendChild(pctSpan);
|
|
1711
|
+
td5.appendChild(barWrap);
|
|
1712
|
+
tr.appendChild(td5);
|
|
1713
|
+
body.appendChild(tr);
|
|
1714
|
+
});
|
|
1715
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
1716
|
+
|
|
1717
|
+
// Response time trend
|
|
1718
|
+
api('/analytics/response-time-trend').then(function(data) {
|
|
1719
|
+
var chart = document.getElementById('response-chart');
|
|
1720
|
+
if (!chart) return;
|
|
1721
|
+
chart.replaceChildren();
|
|
1722
|
+
var trend = data.trend || [];
|
|
1723
|
+
if (!trend.length) { chart.textContent = 'No data yet'; return; }
|
|
1724
|
+
var maxMs = 1;
|
|
1725
|
+
trend.forEach(function(t) { if ((t.avg_ms || 0) > maxMs) maxMs = t.avg_ms; });
|
|
1726
|
+
trend.forEach(function(t) {
|
|
1727
|
+
var col = document.createElement('div');
|
|
1728
|
+
col.className = 'bar-col';
|
|
1729
|
+
var bar = document.createElement('div');
|
|
1730
|
+
bar.className = 'bar';
|
|
1731
|
+
bar.style.height = Math.max(2, ((t.avg_ms || 0) / maxMs) * 100) + 'px';
|
|
1732
|
+
bar.setAttribute('data-tooltip', Math.round(t.avg_ms || 0) + 'ms avg (' + Math.round(t.min_ms || 0) + '-' + Math.round(t.max_ms || 0) + 'ms)');
|
|
1733
|
+
var label = document.createElement('div');
|
|
1734
|
+
label.className = 'bar-label';
|
|
1735
|
+
label.textContent = (t.day || '').slice(5);
|
|
1736
|
+
col.appendChild(bar);
|
|
1737
|
+
col.appendChild(label);
|
|
1738
|
+
chart.appendChild(col);
|
|
1739
|
+
});
|
|
1740
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
1741
|
+
|
|
1742
|
+
// Top models
|
|
1743
|
+
api('/analytics/top-models').then(function(data) {
|
|
1744
|
+
var body = document.getElementById('top-models-body');
|
|
1745
|
+
if (!body) return;
|
|
1746
|
+
body.replaceChildren();
|
|
1747
|
+
var models = data.models || [];
|
|
1748
|
+
if (!models.length) {
|
|
1749
|
+
var emptyTr = document.createElement('tr');
|
|
1750
|
+
var emptyTd = document.createElement('td');
|
|
1751
|
+
emptyTd.setAttribute('colspan', '5');
|
|
1752
|
+
emptyTd.style.cssText = 'text-align:center;color:var(--text-faint);';
|
|
1753
|
+
emptyTd.textContent = 'No usage data yet';
|
|
1754
|
+
emptyTr.appendChild(emptyTd);
|
|
1755
|
+
body.appendChild(emptyTr);
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
models.forEach(function(m) {
|
|
1759
|
+
var tr = document.createElement('tr');
|
|
1760
|
+
var cells = [
|
|
1761
|
+
(m.provider || '') + '/' + (m.model || ''),
|
|
1762
|
+
String(m.requests || 0),
|
|
1763
|
+
(m.total_tokens || 0).toLocaleString(),
|
|
1764
|
+
'$' + (m.total_cost || 0).toFixed(4),
|
|
1765
|
+
Math.round(m.avg_response_ms || 0) + 'ms'
|
|
1766
|
+
];
|
|
1767
|
+
cells.forEach(function(text) {
|
|
1768
|
+
var td = document.createElement('td');
|
|
1769
|
+
td.textContent = text;
|
|
1770
|
+
tr.appendChild(td);
|
|
1771
|
+
});
|
|
1772
|
+
body.appendChild(tr);
|
|
1773
|
+
});
|
|
1774
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// --- SYSTEM PROMPT ---
|
|
1778
|
+
function loadSystem() {
|
|
1779
|
+
api('/system').then(function(data) {
|
|
1780
|
+
document.getElementById('system-editor').value = data.content || '';
|
|
1781
|
+
document.getElementById('system-status').textContent = 'Loaded';
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
function saveSystem() {
|
|
1785
|
+
var content = document.getElementById('system-editor').value;
|
|
1786
|
+
api('/system', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({content: content}) }).then(function() {
|
|
1787
|
+
document.getElementById('system-status').textContent = 'Saved';
|
|
1788
|
+
toast('System prompt saved');
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// --- MEMORY ---
|
|
1793
|
+
var memoryRecentLoaded = false;
|
|
1794
|
+
|
|
1795
|
+
function loadMemory() {
|
|
1796
|
+
api('/memory').then(function(data) {
|
|
1797
|
+
document.getElementById('memory-editor').value = data.content || '';
|
|
1798
|
+
document.getElementById('memory-status').textContent = 'Loaded';
|
|
1799
|
+
});
|
|
1800
|
+
if (!memoryRecentLoaded) {
|
|
1801
|
+
memoryRecentLoaded = true;
|
|
1802
|
+
loadRecentMemories();
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
function saveMemory() {
|
|
1806
|
+
var content = document.getElementById('memory-editor').value;
|
|
1807
|
+
api('/memory', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({content: content}) }).then(function() {
|
|
1808
|
+
document.getElementById('memory-status').textContent = 'Saved';
|
|
1809
|
+
toast('Memory saved');
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function renderMemoryItems(results, container) {
|
|
1814
|
+
container.replaceChildren();
|
|
1815
|
+
if (!results || !results.length) {
|
|
1816
|
+
var p = document.createElement('p');
|
|
1817
|
+
p.className = 'empty-state';
|
|
1818
|
+
p.style.padding = '20px 0';
|
|
1819
|
+
p.textContent = 'No memories found.';
|
|
1820
|
+
container.appendChild(p);
|
|
1821
|
+
document.getElementById('memory-count').textContent = '0';
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
document.getElementById('memory-count').textContent = String(results.length);
|
|
1825
|
+
results.forEach(function(r) {
|
|
1826
|
+
var item = document.createElement('div');
|
|
1827
|
+
item.className = 'memory-item';
|
|
1828
|
+
var src = document.createElement('div');
|
|
1829
|
+
src.className = 'source';
|
|
1830
|
+
src.textContent = r.source || '';
|
|
1831
|
+
var cnt = document.createElement('div');
|
|
1832
|
+
cnt.className = 'content';
|
|
1833
|
+
cnt.textContent = r.content;
|
|
1834
|
+
item.appendChild(src);
|
|
1835
|
+
item.appendChild(cnt);
|
|
1836
|
+
container.appendChild(item);
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function loadRecentMemories() {
|
|
1841
|
+
api('/memory/recent').then(function(data) {
|
|
1842
|
+
renderMemoryItems(data.results, document.getElementById('memory-results'));
|
|
1843
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function searchMemories() {
|
|
1847
|
+
var query = document.getElementById('memory-search').value.trim();
|
|
1848
|
+
if (!query) {
|
|
1849
|
+
memoryRecentLoaded = false;
|
|
1850
|
+
loadRecentMemories();
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
api('/memory/search?q=' + encodeURIComponent(query)).then(function(data) {
|
|
1854
|
+
renderMemoryItems(data.results, document.getElementById('memory-results'));
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
document.getElementById('memory-search').addEventListener('keydown', function(e) {
|
|
1859
|
+
if (e.key === 'Enter') searchMemories();
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
// --- SKILLS ---
|
|
1863
|
+
function loadSkills() {
|
|
1864
|
+
api('/skills').then(function(data) {
|
|
1865
|
+
var body = document.getElementById('skills-body');
|
|
1866
|
+
var empty = document.getElementById('skills-empty');
|
|
1867
|
+
body.replaceChildren();
|
|
1868
|
+
if (!data.skills || !data.skills.length) { empty.style.display = 'block'; return; }
|
|
1869
|
+
empty.style.display = 'none';
|
|
1870
|
+
data.skills.forEach(function(s) {
|
|
1871
|
+
var tr = document.createElement('tr');
|
|
1872
|
+
var nameCell = document.createElement('td');
|
|
1873
|
+
var nameStrong = document.createElement('strong');
|
|
1874
|
+
nameStrong.textContent = s.name;
|
|
1875
|
+
nameCell.appendChild(nameStrong);
|
|
1876
|
+
var descCell = document.createElement('td');
|
|
1877
|
+
descCell.textContent = s.description || '';
|
|
1878
|
+
var statusCell = document.createElement('td');
|
|
1879
|
+
var dot = document.createElement('span');
|
|
1880
|
+
dot.className = 'status-dot ' + (s.status === 'ok' ? 'ok' : 'error');
|
|
1881
|
+
statusCell.appendChild(dot);
|
|
1882
|
+
statusCell.appendChild(document.createTextNode(s.status));
|
|
1883
|
+
tr.appendChild(nameCell);
|
|
1884
|
+
tr.appendChild(descCell);
|
|
1885
|
+
tr.appendChild(statusCell);
|
|
1886
|
+
body.appendChild(tr);
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
// --- REGISTRY ---
|
|
1892
|
+
function searchRegistry() {
|
|
1893
|
+
var q = document.getElementById('registry-search').value.trim();
|
|
1894
|
+
var container = document.getElementById('registry-results');
|
|
1895
|
+
container.replaceChildren();
|
|
1896
|
+
var loading = document.createElement('p');
|
|
1897
|
+
loading.className = 'empty-state';
|
|
1898
|
+
loading.textContent = 'Searching...';
|
|
1899
|
+
container.appendChild(loading);
|
|
1900
|
+
|
|
1901
|
+
api('/registry/search?q=' + encodeURIComponent(q)).then(function(data) {
|
|
1902
|
+
container.replaceChildren();
|
|
1903
|
+
var results = data.results || [];
|
|
1904
|
+
if (!results.length) {
|
|
1905
|
+
var p = document.createElement('p');
|
|
1906
|
+
p.className = 'empty-state';
|
|
1907
|
+
p.textContent = q ? 'No skills found for "' + q + '".' : 'No skills available.';
|
|
1908
|
+
container.appendChild(p);
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
results.forEach(function(r) {
|
|
1912
|
+
var item = document.createElement('div');
|
|
1913
|
+
item.className = 'registry-item';
|
|
1914
|
+
var info = document.createElement('div');
|
|
1915
|
+
var name = document.createElement('div');
|
|
1916
|
+
name.className = 'ri-name';
|
|
1917
|
+
name.textContent = r.name;
|
|
1918
|
+
var desc = document.createElement('div');
|
|
1919
|
+
desc.className = 'ri-desc';
|
|
1920
|
+
desc.textContent = r.description || '';
|
|
1921
|
+
info.appendChild(name);
|
|
1922
|
+
info.appendChild(desc);
|
|
1923
|
+
var btn = document.createElement('button');
|
|
1924
|
+
btn.className = 'btn btn-sm btn-primary';
|
|
1925
|
+
btn.textContent = 'Install';
|
|
1926
|
+
btn.onclick = function() { installRegistrySkill(r.name, btn); };
|
|
1927
|
+
item.appendChild(info);
|
|
1928
|
+
item.appendChild(btn);
|
|
1929
|
+
container.appendChild(item);
|
|
1930
|
+
});
|
|
1931
|
+
}).catch(function(e) {
|
|
1932
|
+
container.replaceChildren();
|
|
1933
|
+
var p = document.createElement('p');
|
|
1934
|
+
p.className = 'empty-state';
|
|
1935
|
+
p.textContent = 'Error: ' + e.message;
|
|
1936
|
+
container.appendChild(p);
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
document.getElementById('registry-search').addEventListener('keydown', function(e) {
|
|
1941
|
+
if (e.key === 'Enter') searchRegistry();
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
function installRegistrySkill(name, btn) {
|
|
1945
|
+
btn.disabled = true;
|
|
1946
|
+
btn.textContent = 'Installing...';
|
|
1947
|
+
api('/registry/install', {
|
|
1948
|
+
method: 'POST',
|
|
1949
|
+
headers: {'Content-Type':'application/json'},
|
|
1950
|
+
body: JSON.stringify({ name: name })
|
|
1951
|
+
}).then(function(data) {
|
|
1952
|
+
if (data.ok) {
|
|
1953
|
+
btn.textContent = 'Installed';
|
|
1954
|
+
btn.className = 'btn btn-sm btn-ghost';
|
|
1955
|
+
toast(name + ' installed');
|
|
1956
|
+
} else {
|
|
1957
|
+
btn.textContent = 'Failed';
|
|
1958
|
+
btn.disabled = false;
|
|
1959
|
+
}
|
|
1960
|
+
}).catch(function() {
|
|
1961
|
+
btn.textContent = 'Error';
|
|
1962
|
+
btn.disabled = false;
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// --- RECIPES ---
|
|
1967
|
+
function loadRecipes() {
|
|
1968
|
+
api('/recipes').then(function(data) {
|
|
1969
|
+
var grid = document.getElementById('recipes-grid');
|
|
1970
|
+
var count = document.getElementById('recipe-count');
|
|
1971
|
+
if (!grid || !data.recipes) return;
|
|
1972
|
+
|
|
1973
|
+
count.textContent = data.recipes.length;
|
|
1974
|
+
|
|
1975
|
+
grid.innerHTML = data.recipes.map(function(r) {
|
|
1976
|
+
var safeId = esc(r.id);
|
|
1977
|
+
var requiresHtml = r.requires && r.requires.length > 0
|
|
1978
|
+
? '<div class="recipe-card-requires">Requires: ' + esc(r.requires.join(', ')) + '</div>'
|
|
1979
|
+
: '';
|
|
1980
|
+
var btnHtml = r.installed
|
|
1981
|
+
? '<button class="btn installed" onclick="uninstallRecipe('' + safeId + '')">Installed \u2713</button>'
|
|
1982
|
+
: '<button class="btn btn-primary" onclick="installRecipe('' + safeId + '')">Activate</button>';
|
|
1983
|
+
|
|
1984
|
+
return '<div class="recipe-card">' +
|
|
1985
|
+
'<div class="recipe-card-header">' +
|
|
1986
|
+
'<span class="recipe-card-icon">' + esc(r.icon) + '</span>' +
|
|
1987
|
+
'<span class="recipe-card-title">' + esc(r.name) + '</span>' +
|
|
1988
|
+
'</div>' +
|
|
1989
|
+
'<div class="recipe-card-desc">' + esc(r.description) + '</div>' +
|
|
1990
|
+
'<div class="recipe-card-meta">' +
|
|
1991
|
+
'<span class="recipe-card-schedule">' + esc(r.scheduleHuman) + '</span>' +
|
|
1992
|
+
requiresHtml +
|
|
1993
|
+
btnHtml +
|
|
1994
|
+
'</div></div>';
|
|
1995
|
+
}).join('');
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function installRecipe(id) {
|
|
2000
|
+
api('/recipes/install', {
|
|
2001
|
+
method: 'POST',
|
|
2002
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2003
|
+
body: JSON.stringify({ id: id })
|
|
2004
|
+
}).then(function(r) {
|
|
2005
|
+
if (r.ok) {
|
|
2006
|
+
toast('Recipe activated: ' + (r.name || id));
|
|
2007
|
+
loadRecipes();
|
|
2008
|
+
} else {
|
|
2009
|
+
toast(r.error || 'Failed to install');
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function uninstallRecipe(id) {
|
|
2015
|
+
api('/recipes/uninstall', {
|
|
2016
|
+
method: 'POST',
|
|
2017
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2018
|
+
body: JSON.stringify({ id: id })
|
|
2019
|
+
}).then(function(r) {
|
|
2020
|
+
if (r.ok) {
|
|
2021
|
+
toast('Recipe deactivated');
|
|
2022
|
+
loadRecipes();
|
|
2023
|
+
} else {
|
|
2024
|
+
toast(r.error || 'Failed to uninstall');
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
// --- WORKFLOWS ---
|
|
2030
|
+
function loadWorkflows() {
|
|
2031
|
+
loadRecipes();
|
|
2032
|
+
api('/workflows').then(function(data) {
|
|
2033
|
+
var container = document.getElementById('workflows-list');
|
|
2034
|
+
var empty = document.getElementById('workflows-empty');
|
|
2035
|
+
container.replaceChildren();
|
|
2036
|
+
var wfs = data.workflows || [];
|
|
2037
|
+
if (!wfs.length) { empty.style.display = 'block'; return; }
|
|
2038
|
+
empty.style.display = 'none';
|
|
2039
|
+
wfs.forEach(function(w) {
|
|
2040
|
+
var card = document.createElement('div');
|
|
2041
|
+
card.className = 'card';
|
|
2042
|
+
var name = document.createElement('div');
|
|
2043
|
+
name.className = 'value';
|
|
2044
|
+
name.style.fontSize = '16px';
|
|
2045
|
+
name.textContent = w.name;
|
|
2046
|
+
var desc = document.createElement('div');
|
|
2047
|
+
desc.style.cssText = 'font-size:13px;color:var(--text-muted);margin-top:4px;';
|
|
2048
|
+
desc.textContent = w.description || '';
|
|
2049
|
+
var meta = document.createElement('div');
|
|
2050
|
+
meta.style.cssText = 'font-size:11px;color:var(--text-faint);margin-top:8px;';
|
|
2051
|
+
meta.textContent = (w.agents || []).length + ' agents, ' + (w.steps || 0) + ' steps';
|
|
2052
|
+
card.appendChild(name);
|
|
2053
|
+
card.appendChild(desc);
|
|
2054
|
+
card.appendChild(meta);
|
|
2055
|
+
container.appendChild(card);
|
|
2056
|
+
});
|
|
2057
|
+
document.getElementById('workflows-status').textContent = wfs.length + ' workflows';
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// --- CRON ---
|
|
2062
|
+
function cronToHuman(expr) {
|
|
2063
|
+
var p = expr.split(' ');
|
|
2064
|
+
if (p.length < 5) return expr;
|
|
2065
|
+
var min = p[0], hour = p[1], dom = p[2], mon = p[3], dow = p[4];
|
|
2066
|
+
var days = { '0':'Sun','1':'Mon','2':'Tue','3':'Wed','4':'Thu','5':'Fri','6':'Sat' };
|
|
2067
|
+
var time = '';
|
|
2068
|
+
if (hour !== '*' && min !== '*') {
|
|
2069
|
+
var h = parseInt(hour, 10); var m = parseInt(min, 10);
|
|
2070
|
+
if (isNaN(h) || isNaN(m)) return expr;
|
|
2071
|
+
var ampm = h >= 12 ? 'pm' : 'am';
|
|
2072
|
+
h = h > 12 ? h - 12 : (h === 0 ? 12 : h);
|
|
2073
|
+
time = h + (m > 0 ? ':' + String(m).padStart(2, '0') : '') + ampm;
|
|
2074
|
+
}
|
|
2075
|
+
if (dow === '1-5' && dom === '*') return time ? 'Weekdays at ' + time : 'Every weekday';
|
|
2076
|
+
if (dow === '0,6' && dom === '*') return time ? 'Weekends at ' + time : 'Every weekend';
|
|
2077
|
+
if (dow !== '*' && dom === '*') {
|
|
2078
|
+
var dayNames = dow.split(',').map(function(d) { return days[d] || d; }).join(', ');
|
|
2079
|
+
return time ? dayNames + ' at ' + time : 'Every ' + dayNames;
|
|
2080
|
+
}
|
|
2081
|
+
if (dow === '*' && dom === '*' && mon === '*') {
|
|
2082
|
+
if (min.startsWith('*/')) return 'Every ' + min.slice(2) + ' min';
|
|
2083
|
+
if (hour.startsWith('*/')) return 'Every ' + hour.slice(2) + ' hrs';
|
|
2084
|
+
if (hour === '*') return 'Every minute';
|
|
2085
|
+
return time ? 'Daily at ' + time : 'Daily';
|
|
2086
|
+
}
|
|
2087
|
+
if (dom !== '*' && mon === '*') return time ? 'Monthly on ' + dom + ' at ' + time : 'Monthly on day ' + dom;
|
|
2088
|
+
return expr;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function loadCron() {
|
|
2092
|
+
api('/cron').then(function(data) {
|
|
2093
|
+
var body = document.getElementById('cron-body');
|
|
2094
|
+
var empty = document.getElementById('cron-empty');
|
|
2095
|
+
body.replaceChildren();
|
|
2096
|
+
if (!data.jobs || !data.jobs.length) { empty.style.display = 'block'; return; }
|
|
2097
|
+
empty.style.display = 'none';
|
|
2098
|
+
data.jobs.forEach(function(j) {
|
|
2099
|
+
var tr = document.createElement('tr');
|
|
2100
|
+
var scheduleTd = document.createElement('td');
|
|
2101
|
+
var humanLabel = document.createTextNode(cronToHuman(j.schedule));
|
|
2102
|
+
var rawSpan = document.createElement('span');
|
|
2103
|
+
rawSpan.style.cssText = 'font-size:10px;color:var(--text-muted);';
|
|
2104
|
+
rawSpan.textContent = j.schedule;
|
|
2105
|
+
scheduleTd.appendChild(humanLabel);
|
|
2106
|
+
scheduleTd.appendChild(document.createElement('br'));
|
|
2107
|
+
scheduleTd.appendChild(rawSpan);
|
|
2108
|
+
var cells = [j.name, null, j.task, j.enabled ? 'Yes' : 'No', j.last_run || 'Never'];
|
|
2109
|
+
cells.forEach(function(text, i) {
|
|
2110
|
+
if (i === 1) { tr.appendChild(scheduleTd); return; }
|
|
2111
|
+
var td = document.createElement('td');
|
|
2112
|
+
td.textContent = text;
|
|
2113
|
+
tr.appendChild(td);
|
|
2114
|
+
});
|
|
2115
|
+
body.appendChild(tr);
|
|
2116
|
+
});
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// --- LOGS ---
|
|
2121
|
+
function loadLogs() {
|
|
2122
|
+
api('/logs').then(function(data) {
|
|
2123
|
+
document.getElementById('log-content').textContent = data.content || 'No logs.';
|
|
2124
|
+
document.getElementById('logs-status').textContent = 'Last 100 lines';
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// --- BUDGET ---
|
|
2129
|
+
function loadBudget() {
|
|
2130
|
+
api('/budget').then(function(data) {
|
|
2131
|
+
var cards = document.getElementById('budget-summary-cards');
|
|
2132
|
+
if (!cards) return;
|
|
2133
|
+
|
|
2134
|
+
var dailyPct = data.daily_limit_usd ? Math.min(data.today_spend_usd / data.daily_limit_usd, 1) : 0;
|
|
2135
|
+
var monthPct = data.monthly_limit_usd ? Math.min(data.month_spend_usd / data.monthly_limit_usd, 1) : 0;
|
|
2136
|
+
|
|
2137
|
+
function barClass(pct, hasLimit) {
|
|
2138
|
+
if (!hasLimit) return 'ok';
|
|
2139
|
+
if (pct >= 1) return 'danger';
|
|
2140
|
+
if (pct >= (data.alert_threshold || 0.8)) return 'warn';
|
|
2141
|
+
return 'ok';
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
cards.innerHTML = '<div class="card">' +
|
|
2145
|
+
'<div class="budget-card-label">Today\\'s Spend</div>' +
|
|
2146
|
+
'<div class="budget-card-value">$' + data.today_spend_usd.toFixed(2) + '</div>' +
|
|
2147
|
+
'<div class="budget-card-sub">' + (data.daily_limit_usd ? 'Limit: $' + data.daily_limit_usd.toFixed(2) : 'No daily limit') + '</div>' +
|
|
2148
|
+
(data.daily_limit_usd ? '<div class="budget-bar"><div class="budget-bar-fill ' + barClass(dailyPct, true) + '" style="width:' + Math.max(0, Math.min(100, dailyPct * 100)) + '%"></div></div>' : '') +
|
|
2149
|
+
'</div>' +
|
|
2150
|
+
'<div class="card">' +
|
|
2151
|
+
'<div class="budget-card-label">This Month</div>' +
|
|
2152
|
+
'<div class="budget-card-value">$' + data.month_spend_usd.toFixed(2) + '</div>' +
|
|
2153
|
+
'<div class="budget-card-sub">' + (data.monthly_limit_usd ? 'Limit: $' + data.monthly_limit_usd.toFixed(2) : 'No monthly limit') + '</div>' +
|
|
2154
|
+
(data.monthly_limit_usd ? '<div class="budget-bar"><div class="budget-bar-fill ' + barClass(monthPct, true) + '" style="width:' + Math.max(0, Math.min(100, monthPct * 100)) + '%"></div></div>' : '') +
|
|
2155
|
+
'</div>' +
|
|
2156
|
+
'<div class="card">' +
|
|
2157
|
+
'<div class="budget-card-label">Status</div>' +
|
|
2158
|
+
'<div class="budget-card-value" style="font-size:20px;">' + (data.paused ? '<span style="color:var(--red);">Paused</span>' : '<span style="color:var(--green);">Active</span>') + '</div>' +
|
|
2159
|
+
'<div class="budget-card-sub">' + (data.paused ? 'Budget limit reached' : 'Within budget') + '</div>' +
|
|
2160
|
+
'</div>';
|
|
2161
|
+
|
|
2162
|
+
// Fill in form values
|
|
2163
|
+
if (data.daily_limit_usd) document.getElementById('budget-daily').value = data.daily_limit_usd;
|
|
2164
|
+
if (data.monthly_limit_usd) document.getElementById('budget-monthly').value = data.monthly_limit_usd;
|
|
2165
|
+
if (data.alert_threshold) document.getElementById('budget-alert').value = String(data.alert_threshold);
|
|
2166
|
+
|
|
2167
|
+
// Render chart
|
|
2168
|
+
var chart = document.getElementById('budget-chart');
|
|
2169
|
+
if (chart && data.daily_breakdown && data.daily_breakdown.length > 0) {
|
|
2170
|
+
var maxCost = Math.max.apply(null, data.daily_breakdown.map(function(d) { return d.cost; }));
|
|
2171
|
+
if (maxCost === 0) maxCost = 1;
|
|
2172
|
+
chart.innerHTML = data.daily_breakdown.map(function(d) {
|
|
2173
|
+
var pct = (d.cost / maxCost) * 100;
|
|
2174
|
+
var dayLabel = d.day.slice(5); // MM-DD
|
|
2175
|
+
return '<div class="bar-col"><div class="bar" style="height:' + Math.max(pct, 2) + '%;background:var(--gradient);border-radius:4px 4px 0 0;max-width:60px;" data-tooltip="$' + d.cost.toFixed(4) + '"></div><div class="bar-label">' + dayLabel + '</div></div>';
|
|
2176
|
+
}).join('');
|
|
2177
|
+
} else if (chart) {
|
|
2178
|
+
chart.innerHTML = '<div style="color:var(--text-muted);text-align:center;padding:20px;">No spending data yet</div>';
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
function saveBudget() {
|
|
2184
|
+
var daily = parseFloat(document.getElementById('budget-daily').value) || null;
|
|
2185
|
+
var monthly = parseFloat(document.getElementById('budget-monthly').value) || null;
|
|
2186
|
+
var threshold = parseFloat(document.getElementById('budget-alert').value) || 0.8;
|
|
2187
|
+
|
|
2188
|
+
api('/budget', {
|
|
2189
|
+
method: 'PUT',
|
|
2190
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2191
|
+
body: JSON.stringify({ daily_limit_usd: daily, monthly_limit_usd: monthly, alert_threshold: threshold })
|
|
2192
|
+
}).then(function(r) {
|
|
2193
|
+
var s = document.getElementById('budget-status');
|
|
2194
|
+
if (r.ok) {
|
|
2195
|
+
s.textContent = 'Budget saved';
|
|
2196
|
+
s.style.color = 'var(--green)';
|
|
2197
|
+
loadBudget();
|
|
2198
|
+
toast('Budget limits saved');
|
|
2199
|
+
} else {
|
|
2200
|
+
s.textContent = r.error || 'Failed';
|
|
2201
|
+
s.style.color = 'var(--red)';
|
|
2202
|
+
}
|
|
2203
|
+
setTimeout(function() { s.textContent = ''; }, 3000);
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function clearBudget() {
|
|
2208
|
+
api('/budget', {
|
|
2209
|
+
method: 'PUT',
|
|
2210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2211
|
+
body: JSON.stringify({ daily_limit_usd: null, monthly_limit_usd: null })
|
|
2212
|
+
}).then(function(r) {
|
|
2213
|
+
document.getElementById('budget-daily').value = '';
|
|
2214
|
+
document.getElementById('budget-monthly').value = '';
|
|
2215
|
+
loadBudget();
|
|
2216
|
+
toast('Budget limits removed');
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// --- PRIVACY ---
|
|
2221
|
+
var apiLogOffset = 0;
|
|
2222
|
+
var toolLogOffset = 0;
|
|
2223
|
+
|
|
2224
|
+
function loadPrivacy() {
|
|
2225
|
+
apiLogOffset = 0;
|
|
2226
|
+
toolLogOffset = 0;
|
|
2227
|
+
|
|
2228
|
+
api('/privacy/summary').then(function(data) {
|
|
2229
|
+
var cards = document.getElementById('privacy-summary-cards');
|
|
2230
|
+
if (!cards) return;
|
|
2231
|
+
|
|
2232
|
+
cards.innerHTML =
|
|
2233
|
+
'<div class="card"><div class="label">Memories Stored</div><div class="value">' + (data.memoryCount || 0) + '</div></div>' +
|
|
2234
|
+
'<div class="card"><div class="label">Messages</div><div class="value">' + (data.messageCount || 0) + '</div></div>' +
|
|
2235
|
+
'<div class="card"><div class="label">API Calls Made</div><div class="value">' + (data.apiCallCount || 0) + '</div></div>' +
|
|
2236
|
+
'<div class="card"><div class="label">Tokens Sent to Providers</div><div class="value">' + formatNum(data.totalTokensSent || 0) + '</div></div>' +
|
|
2237
|
+
'<div class="card"><div class="label">Tool Executions</div><div class="value">' + (data.toolCallCount || 0) + '</div></div>' +
|
|
2238
|
+
'<div class="card"><div class="label">Stored Secrets</div><div class="value">' + (data.secretCount || 0) + '</div></div>';
|
|
2239
|
+
|
|
2240
|
+
// Provider breakdown
|
|
2241
|
+
var providers = document.getElementById('privacy-providers');
|
|
2242
|
+
if (providers && data.providerBreakdown) {
|
|
2243
|
+
providers.innerHTML = data.providerBreakdown.map(function(p) {
|
|
2244
|
+
return '<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px 16px;font-size:12px;">' +
|
|
2245
|
+
'<div style="font-weight:600;color:var(--text);">' + esc(p.provider) + '</div>' +
|
|
2246
|
+
'<div style="color:var(--text-muted);">' + parseInt(p.calls || 0) + ' calls · ' + formatNum(p.tokens_sent || 0) + ' tokens sent</div>' +
|
|
2247
|
+
'</div>';
|
|
2248
|
+
}).join('');
|
|
2249
|
+
}
|
|
2250
|
+
});
|
|
2251
|
+
|
|
2252
|
+
loadApiLog();
|
|
2253
|
+
loadToolLog();
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function formatNum(n) {
|
|
2257
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
2258
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
2259
|
+
return String(n);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
function loadApiLog() {
|
|
2263
|
+
api('/privacy/api-log?limit=20&offset=' + apiLogOffset).then(function(data) {
|
|
2264
|
+
var body = document.getElementById('api-log-body');
|
|
2265
|
+
var more = document.getElementById('api-log-more');
|
|
2266
|
+
if (!body) return;
|
|
2267
|
+
|
|
2268
|
+
if (apiLogOffset === 0) body.innerHTML = '';
|
|
2269
|
+
|
|
2270
|
+
body.innerHTML += data.rows.map(function(r) {
|
|
2271
|
+
var time = r.created_at ? new Date(r.created_at + 'Z').toLocaleString() : '\u2014';
|
|
2272
|
+
return '<tr>' +
|
|
2273
|
+
'<td>' + esc(time) + '</td>' +
|
|
2274
|
+
'<td>' + esc(r.provider || '\u2014') + '</td>' +
|
|
2275
|
+
'<td style="font-size:11px;">' + esc(r.model || '\u2014') + '</td>' +
|
|
2276
|
+
'<td>' + parseInt(r.input_tokens || 0) + '</td>' +
|
|
2277
|
+
'<td>' + parseInt(r.output_tokens || 0) + '</td>' +
|
|
2278
|
+
'<td>' + (r.cost_usd ? '$' + parseFloat(r.cost_usd).toFixed(4) : '\u2014') + '</td>' +
|
|
2279
|
+
'</tr>';
|
|
2280
|
+
}).join('');
|
|
2281
|
+
|
|
2282
|
+
if (more) more.style.display = (apiLogOffset + 20 < data.total) ? '' : 'none';
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
function loadMoreApiLog() {
|
|
2287
|
+
apiLogOffset += 20;
|
|
2288
|
+
loadApiLog();
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
function loadToolLog() {
|
|
2292
|
+
api('/privacy/tool-log?limit=20&offset=' + toolLogOffset).then(function(data) {
|
|
2293
|
+
var body = document.getElementById('tool-log-body');
|
|
2294
|
+
var more = document.getElementById('tool-log-more');
|
|
2295
|
+
if (!body) return;
|
|
2296
|
+
|
|
2297
|
+
if (toolLogOffset === 0) body.innerHTML = '';
|
|
2298
|
+
|
|
2299
|
+
body.innerHTML += data.rows.map(function(r) {
|
|
2300
|
+
var time = r.created_at ? new Date(r.created_at + 'Z').toLocaleString() : '\u2014';
|
|
2301
|
+
var status = r.success ? '<span style="color:var(--green);">OK</span>' : '<span style="color:var(--red);">Error</span>';
|
|
2302
|
+
return '<tr>' +
|
|
2303
|
+
'<td>' + esc(time) + '</td>' +
|
|
2304
|
+
'<td>' + esc(r.tool_name || '\u2014') + '</td>' +
|
|
2305
|
+
'<td>' + (r.duration_ms ? parseInt(r.duration_ms) + 'ms' : '\u2014') + '</td>' +
|
|
2306
|
+
'<td>' + status + '</td>' +
|
|
2307
|
+
'</tr>';
|
|
2308
|
+
}).join('');
|
|
2309
|
+
|
|
2310
|
+
if (more) more.style.display = (toolLogOffset + 20 < data.total) ? '' : 'none';
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function loadMoreToolLog() {
|
|
2315
|
+
toolLogOffset += 20;
|
|
2316
|
+
loadToolLog();
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function wipeData(type) {
|
|
2320
|
+
var msg = {
|
|
2321
|
+
memories: 'Delete ALL stored memories? This cannot be undone.',
|
|
2322
|
+
messages: 'Delete ALL conversation messages? This cannot be undone.',
|
|
2323
|
+
usage: 'Delete ALL API usage logs and tool metrics? This cannot be undone.',
|
|
2324
|
+
all: 'DELETE EVERYTHING? All memories, messages, usage data, and secrets will be permanently removed. This cannot be undone.'
|
|
2325
|
+
};
|
|
2326
|
+
|
|
2327
|
+
if (!confirm(msg[type] || 'Are you sure?')) return;
|
|
2328
|
+
|
|
2329
|
+
var confirmToken = type === 'all' ? 'DELETE_ALL' : 'DELETE';
|
|
2330
|
+
var endpoint = '/privacy/wipe-' + type;
|
|
2331
|
+
api(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: confirmToken }) }).then(function(r) {
|
|
2332
|
+
var s = document.getElementById('wipe-status');
|
|
2333
|
+
if (r.ok) {
|
|
2334
|
+
s.textContent = r.message || 'Data deleted';
|
|
2335
|
+
s.style.color = 'var(--green)';
|
|
2336
|
+
loadPrivacy();
|
|
2337
|
+
toast('Data deleted successfully');
|
|
2338
|
+
} else {
|
|
2339
|
+
s.textContent = r.error || 'Failed';
|
|
2340
|
+
s.style.color = 'var(--red)';
|
|
2341
|
+
}
|
|
2342
|
+
setTimeout(function() { s.textContent = ''; }, 3000);
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// --- SETTINGS ---
|
|
2347
|
+
var settingsProviders = [];
|
|
2348
|
+
|
|
2349
|
+
function loadSettings() {
|
|
2350
|
+
api('/config').then(function(data) {
|
|
2351
|
+
settingsProviders = data.providers || [];
|
|
2352
|
+
var sel = document.getElementById('settings-provider');
|
|
2353
|
+
sel.replaceChildren();
|
|
2354
|
+
settingsProviders.forEach(function(p) {
|
|
2355
|
+
var opt = document.createElement('option');
|
|
2356
|
+
opt.value = p.name;
|
|
2357
|
+
opt.textContent = p.name;
|
|
2358
|
+
if (p.name === data.activeProvider) opt.selected = true;
|
|
2359
|
+
sel.appendChild(opt);
|
|
2360
|
+
});
|
|
2361
|
+
document.getElementById('settings-model').value = data.model || '';
|
|
2362
|
+
document.getElementById('settings-status').textContent = '';
|
|
2363
|
+
});
|
|
2364
|
+
api('/settings/heartbeat').then(function(data) {
|
|
2365
|
+
document.getElementById('settings-heartbeat').value = data.minutes || 30;
|
|
2366
|
+
document.getElementById('heartbeat-status').textContent = '';
|
|
2367
|
+
});
|
|
2368
|
+
loadChannelStatus();
|
|
2369
|
+
loadDbStats();
|
|
2370
|
+
loadSecrets();
|
|
2371
|
+
loadSmartRouting();
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function onProviderChange() {
|
|
2375
|
+
var sel = document.getElementById('settings-provider');
|
|
2376
|
+
var provider = sel.value;
|
|
2377
|
+
var match = settingsProviders.find(function(p) { return p.name === provider; });
|
|
2378
|
+
if (match) {
|
|
2379
|
+
document.getElementById('settings-model').value = match.model || '';
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
function saveModelConfig() {
|
|
2384
|
+
var provider = document.getElementById('settings-provider').value;
|
|
2385
|
+
var model = document.getElementById('settings-model').value.trim();
|
|
2386
|
+
if (!provider) return;
|
|
2387
|
+
if (!model) {
|
|
2388
|
+
document.getElementById('settings-status').textContent = 'Model is required';
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
api('/config/model', {
|
|
2392
|
+
method: 'PUT',
|
|
2393
|
+
headers: {'Content-Type':'application/json'},
|
|
2394
|
+
body: JSON.stringify({ provider: provider, model: model })
|
|
2395
|
+
}).then(function(data) {
|
|
2396
|
+
if (data.ok) {
|
|
2397
|
+
document.getElementById('settings-status').textContent = 'Saved \u2014 restart Zubo to apply';
|
|
2398
|
+
toast('Model updated');
|
|
2399
|
+
loadSettings();
|
|
2400
|
+
} else {
|
|
2401
|
+
document.getElementById('settings-status').textContent = data.error || 'Error';
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
function testLlm() {
|
|
2407
|
+
document.getElementById('settings-status').textContent = 'Testing...';
|
|
2408
|
+
api('/test-llm', { method: 'POST' }).then(function(data) {
|
|
2409
|
+
if (data.ok) {
|
|
2410
|
+
document.getElementById('settings-status').textContent = 'Connected (' + data.model + ')';
|
|
2411
|
+
toast('LLM connection OK');
|
|
2412
|
+
} else {
|
|
2413
|
+
document.getElementById('settings-status').textContent = 'Failed: ' + (data.error || 'Unknown');
|
|
2414
|
+
}
|
|
2415
|
+
}).catch(function(e) {
|
|
2416
|
+
document.getElementById('settings-status').textContent = 'Error: ' + e.message;
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
function saveHeartbeat() {
|
|
2421
|
+
var mins = parseInt(document.getElementById('settings-heartbeat').value, 10);
|
|
2422
|
+
if (!mins || mins < 1 || mins > 1440) {
|
|
2423
|
+
document.getElementById('heartbeat-status').textContent = 'Must be 1\u20131440 minutes';
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
api('/settings/heartbeat', {
|
|
2427
|
+
method: 'PUT',
|
|
2428
|
+
headers: {'Content-Type':'application/json'},
|
|
2429
|
+
body: JSON.stringify({ minutes: mins })
|
|
2430
|
+
}).then(function(data) {
|
|
2431
|
+
if (data.ok) {
|
|
2432
|
+
document.getElementById('heartbeat-status').textContent = 'Applied immediately';
|
|
2433
|
+
toast('Heartbeat updated to ' + data.minutes + ' min');
|
|
2434
|
+
} else {
|
|
2435
|
+
document.getElementById('heartbeat-status').textContent = data.error || 'Error';
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// --- Smart Routing ---
|
|
2441
|
+
function loadSmartRouting() {
|
|
2442
|
+
api('/smart-routing').then(function(data) {
|
|
2443
|
+
document.getElementById('sr-enabled').value = data.enabled ? 'true' : 'false';
|
|
2444
|
+
var fpSel = document.getElementById('sr-fast-provider');
|
|
2445
|
+
fpSel.replaceChildren();
|
|
2446
|
+
var emptyOpt = document.createElement('option');
|
|
2447
|
+
emptyOpt.value = '';
|
|
2448
|
+
emptyOpt.textContent = '-- Select --';
|
|
2449
|
+
fpSel.appendChild(emptyOpt);
|
|
2450
|
+
settingsProviders.forEach(function(p) {
|
|
2451
|
+
var opt = document.createElement('option');
|
|
2452
|
+
opt.value = p.name;
|
|
2453
|
+
opt.textContent = p.name;
|
|
2454
|
+
if (p.name === data.fastProvider) opt.selected = true;
|
|
2455
|
+
fpSel.appendChild(opt);
|
|
2456
|
+
});
|
|
2457
|
+
document.getElementById('sr-fast-model').value = data.fastModel || '';
|
|
2458
|
+
document.getElementById('sr-status').textContent = '';
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function saveSmartRouting() {
|
|
2463
|
+
var enabled = document.getElementById('sr-enabled').value === 'true';
|
|
2464
|
+
var fastProvider = document.getElementById('sr-fast-provider').value;
|
|
2465
|
+
var fastModel = document.getElementById('sr-fast-model').value.trim();
|
|
2466
|
+
if (enabled && !fastProvider) {
|
|
2467
|
+
document.getElementById('sr-status').textContent = 'Select a fast provider';
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
api('/smart-routing', {
|
|
2471
|
+
method: 'PUT',
|
|
2472
|
+
headers: {'Content-Type':'application/json'},
|
|
2473
|
+
body: JSON.stringify({ enabled: enabled, fastProvider: fastProvider, fastModel: fastModel })
|
|
2474
|
+
}).then(function(data) {
|
|
2475
|
+
if (data.ok) {
|
|
2476
|
+
document.getElementById('sr-status').textContent = 'Saved \u2014 restart Zubo to apply';
|
|
2477
|
+
toast('Smart routing ' + (enabled ? 'enabled' : 'disabled'));
|
|
2478
|
+
} else {
|
|
2479
|
+
document.getElementById('sr-status').textContent = data.error || 'Error';
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// --- Channel Status ---
|
|
2485
|
+
var channelLabels = { webchat: 'Web Chat', telegram: 'Telegram', discord: 'Discord', slack: 'Slack', whatsapp: 'WhatsApp', signal: 'Signal' };
|
|
2486
|
+
|
|
2487
|
+
function loadChannelStatus() {
|
|
2488
|
+
api('/channel-status').then(function(data) {
|
|
2489
|
+
var list = document.getElementById('channel-status-list');
|
|
2490
|
+
list.replaceChildren();
|
|
2491
|
+
var channels = data.channels || {};
|
|
2492
|
+
var connCount = 0;
|
|
2493
|
+
Object.keys(channels).forEach(function(name) {
|
|
2494
|
+
var ch = channels[name];
|
|
2495
|
+
if (ch.enabled) connCount++;
|
|
2496
|
+
var row = document.createElement('div');
|
|
2497
|
+
row.style.cssText = 'display:flex;align-items:center;gap:10px;padding:8px 12px;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;';
|
|
2498
|
+
var dot = document.createElement('span');
|
|
2499
|
+
dot.className = 'status-dot ' + (ch.enabled ? 'ok' : '');
|
|
2500
|
+
if (!ch.enabled) dot.style.background = 'var(--text-faint)';
|
|
2501
|
+
var label = document.createElement('span');
|
|
2502
|
+
label.style.cssText = 'font-size:13px;font-weight:500;color:var(--text);flex:1;';
|
|
2503
|
+
label.textContent = channelLabels[name] || name;
|
|
2504
|
+
var status = document.createElement('span');
|
|
2505
|
+
status.style.cssText = 'font-size:11px;color:' + (ch.enabled ? 'var(--green)' : 'var(--text-faint)') + ';';
|
|
2506
|
+
status.textContent = ch.enabled ? 'Connected' : (ch.configured ? 'Disabled' : 'Not configured');
|
|
2507
|
+
row.appendChild(dot);
|
|
2508
|
+
row.appendChild(label);
|
|
2509
|
+
row.appendChild(status);
|
|
2510
|
+
list.appendChild(row);
|
|
2511
|
+
});
|
|
2512
|
+
document.getElementById('channel-count-badge').textContent = connCount + ' active';
|
|
2513
|
+
document.getElementById('sidebar-conn-badge').textContent = connCount + ' channels';
|
|
2514
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// --- DATA EXPORT/IMPORT ---
|
|
2518
|
+
function loadDbStats() {
|
|
2519
|
+
api('/db-stats').then(function(data) {
|
|
2520
|
+
var el = document.getElementById('db-stats');
|
|
2521
|
+
var tables = data.tables || {};
|
|
2522
|
+
var totalRows = 0;
|
|
2523
|
+
Object.keys(tables).forEach(function(k) { totalRows += tables[k]; });
|
|
2524
|
+
var sizeMb = ((data.sizeBytes || 0) / 1024 / 1024).toFixed(2);
|
|
2525
|
+
el.textContent = 'DB size: ' + sizeMb + ' MB, ' + totalRows + ' total rows';
|
|
2526
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
function exportJson() {
|
|
2530
|
+
document.getElementById('data-status').textContent = 'Exporting...';
|
|
2531
|
+
fetch('/api/dashboard/export', { method: 'POST' }).then(function(r) {
|
|
2532
|
+
if (!r.ok) throw new Error('Export failed');
|
|
2533
|
+
return r.blob();
|
|
2534
|
+
}).then(function(blob) {
|
|
2535
|
+
var a = document.createElement('a');
|
|
2536
|
+
a.href = URL.createObjectURL(blob);
|
|
2537
|
+
a.download = 'zubo-export.json';
|
|
2538
|
+
a.click();
|
|
2539
|
+
URL.revokeObjectURL(a.href);
|
|
2540
|
+
document.getElementById('data-status').textContent = 'Export downloaded';
|
|
2541
|
+
toast('Export complete');
|
|
2542
|
+
}).catch(function(e) {
|
|
2543
|
+
document.getElementById('data-status').textContent = 'Error: ' + e.message;
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function backupDb() {
|
|
2548
|
+
document.getElementById('data-status').textContent = 'Backing up...';
|
|
2549
|
+
api('/backup', { method: 'POST' }).then(function(data) {
|
|
2550
|
+
if (data.ok) {
|
|
2551
|
+
document.getElementById('data-status').textContent = 'Backup saved: ' + data.path;
|
|
2552
|
+
toast('SQLite backup created');
|
|
2553
|
+
} else {
|
|
2554
|
+
document.getElementById('data-status').textContent = 'Error: ' + (data.error || 'Unknown');
|
|
2555
|
+
}
|
|
2556
|
+
}).catch(function(e) {
|
|
2557
|
+
document.getElementById('data-status').textContent = 'Error: ' + e.message;
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
function importJson(event) {
|
|
2562
|
+
var file = event.target.files[0];
|
|
2563
|
+
if (!file) return;
|
|
2564
|
+
event.target.value = '';
|
|
2565
|
+
document.getElementById('data-status').textContent = 'Importing...';
|
|
2566
|
+
var reader = new FileReader();
|
|
2567
|
+
reader.onload = function() {
|
|
2568
|
+
fetch('/api/dashboard/import', { method: 'POST', body: reader.result }).then(function(r) { return r.json(); }).then(function(data) {
|
|
2569
|
+
if (data.ok) {
|
|
2570
|
+
document.getElementById('data-status').textContent = 'Imported ' + data.imported + ' rows (' + data.skipped + ' skipped)';
|
|
2571
|
+
toast('Import complete');
|
|
2572
|
+
loadDbStats();
|
|
2573
|
+
} else {
|
|
2574
|
+
document.getElementById('data-status').textContent = 'Error: ' + (data.error || 'Unknown');
|
|
2575
|
+
}
|
|
2576
|
+
}).catch(function(e) {
|
|
2577
|
+
document.getElementById('data-status').textContent = 'Error: ' + e.message;
|
|
2578
|
+
});
|
|
2579
|
+
};
|
|
2580
|
+
reader.readAsText(file);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// --- SECRETS ---
|
|
2584
|
+
function loadSecrets() {
|
|
2585
|
+
api('/secrets').then(function(data) {
|
|
2586
|
+
var list = document.getElementById('secrets-list');
|
|
2587
|
+
var empty = document.getElementById('secrets-empty');
|
|
2588
|
+
list.replaceChildren();
|
|
2589
|
+
var secrets = data.secrets || [];
|
|
2590
|
+
if (secrets.length === 0) {
|
|
2591
|
+
empty.style.display = '';
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
empty.style.display = 'none';
|
|
2595
|
+
secrets.forEach(function(s) {
|
|
2596
|
+
var isConfig = s.source === 'config';
|
|
2597
|
+
var row = document.createElement('div');
|
|
2598
|
+
row.style.cssText = 'display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--bg-surface);border:1px solid var(--border);border-radius:8px;';
|
|
2599
|
+
|
|
2600
|
+
var nameEl = document.createElement('span');
|
|
2601
|
+
nameEl.style.cssText = 'font-family:var(--mono);font-size:13px;font-weight:500;color:var(--text);min-width:140px;';
|
|
2602
|
+
nameEl.textContent = s.name;
|
|
2603
|
+
|
|
2604
|
+
var serviceEl = document.createElement('span');
|
|
2605
|
+
serviceEl.style.cssText = 'font-size:11px;color:var(--text-muted);min-width:80px;';
|
|
2606
|
+
if (isConfig) {
|
|
2607
|
+
var badge = document.createElement('span');
|
|
2608
|
+
badge.style.cssText = 'display:inline-block;padding:2px 7px;border-radius:4px;font-size:10px;font-weight:600;background:rgba(124,58,237,0.15);color:var(--accent);';
|
|
2609
|
+
badge.textContent = 'config.json';
|
|
2610
|
+
serviceEl.textContent = '';
|
|
2611
|
+
serviceEl.appendChild(badge);
|
|
2612
|
+
} else {
|
|
2613
|
+
serviceEl.textContent = s.service || '';
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
var valueEl = document.createElement('span');
|
|
2617
|
+
valueEl.style.cssText = 'font-family:var(--mono);font-size:12px;color:var(--text-secondary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
|
|
2618
|
+
valueEl.textContent = '\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022';
|
|
2619
|
+
valueEl.dataset.secretName = s.name;
|
|
2620
|
+
valueEl.dataset.revealed = 'false';
|
|
2621
|
+
|
|
2622
|
+
var revealBtn = document.createElement('button');
|
|
2623
|
+
revealBtn.className = 'btn btn-ghost';
|
|
2624
|
+
revealBtn.style.cssText = 'font-size:11px;padding:4px 10px;';
|
|
2625
|
+
revealBtn.textContent = 'Reveal';
|
|
2626
|
+
revealBtn.onclick = function() {
|
|
2627
|
+
if (valueEl.dataset.revealed === 'true') {
|
|
2628
|
+
valueEl.textContent = '\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022\\u2022';
|
|
2629
|
+
valueEl.dataset.revealed = 'false';
|
|
2630
|
+
revealBtn.textContent = 'Reveal';
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
revealBtn.textContent = 'Loading...';
|
|
2634
|
+
api('/secrets/' + encodeURIComponent(s.name)).then(function(d) {
|
|
2635
|
+
if (d.value !== undefined) {
|
|
2636
|
+
valueEl.textContent = d.value;
|
|
2637
|
+
valueEl.dataset.revealed = 'true';
|
|
2638
|
+
revealBtn.textContent = 'Hide';
|
|
2639
|
+
} else {
|
|
2640
|
+
revealBtn.textContent = 'Error';
|
|
2641
|
+
}
|
|
2642
|
+
}).catch(function() { revealBtn.textContent = 'Error'; });
|
|
2643
|
+
};
|
|
2644
|
+
|
|
2645
|
+
row.appendChild(nameEl);
|
|
2646
|
+
row.appendChild(serviceEl);
|
|
2647
|
+
row.appendChild(valueEl);
|
|
2648
|
+
row.appendChild(revealBtn);
|
|
2649
|
+
|
|
2650
|
+
if (!isConfig) {
|
|
2651
|
+
var editBtn = document.createElement('button');
|
|
2652
|
+
editBtn.className = 'btn btn-ghost';
|
|
2653
|
+
editBtn.style.cssText = 'font-size:11px;padding:4px 10px;';
|
|
2654
|
+
editBtn.textContent = 'Edit';
|
|
2655
|
+
editBtn.onclick = function() { editSecret(s.name, s.service); };
|
|
2656
|
+
|
|
2657
|
+
var delBtn = document.createElement('button');
|
|
2658
|
+
delBtn.className = 'btn btn-ghost';
|
|
2659
|
+
delBtn.style.cssText = 'font-size:11px;padding:4px 10px;color:var(--red);';
|
|
2660
|
+
delBtn.textContent = 'Delete';
|
|
2661
|
+
delBtn.onclick = function() { deleteSecretUI(s.name); };
|
|
2662
|
+
|
|
2663
|
+
row.appendChild(editBtn);
|
|
2664
|
+
row.appendChild(delBtn);
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
list.appendChild(row);
|
|
2668
|
+
});
|
|
2669
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
function showAddSecretForm() {
|
|
2673
|
+
document.getElementById('secret-add-form').style.display = '';
|
|
2674
|
+
document.getElementById('secret-name-input').value = '';
|
|
2675
|
+
document.getElementById('secret-value-input').value = '';
|
|
2676
|
+
document.getElementById('secret-service-input').value = '';
|
|
2677
|
+
document.getElementById('secret-name-input').disabled = false;
|
|
2678
|
+
document.getElementById('secret-name-input').focus();
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function hideAddSecretForm() {
|
|
2682
|
+
document.getElementById('secret-add-form').style.display = 'none';
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
function editSecret(name, service) {
|
|
2686
|
+
document.getElementById('secret-add-form').style.display = '';
|
|
2687
|
+
document.getElementById('secret-name-input').value = name;
|
|
2688
|
+
document.getElementById('secret-name-input').disabled = true;
|
|
2689
|
+
document.getElementById('secret-value-input').value = '';
|
|
2690
|
+
document.getElementById('secret-value-input').placeholder = 'Enter new value';
|
|
2691
|
+
document.getElementById('secret-service-input').value = service || '';
|
|
2692
|
+
document.getElementById('secret-value-input').focus();
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
function saveSecret() {
|
|
2696
|
+
var name = document.getElementById('secret-name-input').value.trim();
|
|
2697
|
+
var value = document.getElementById('secret-value-input').value;
|
|
2698
|
+
var service = document.getElementById('secret-service-input').value.trim();
|
|
2699
|
+
if (!name || !/^[a-z0-9_]+$/.test(name)) { toast('Name must be lowercase with underscores only'); return; }
|
|
2700
|
+
if (!value) { toast('Value is required'); return; }
|
|
2701
|
+
api('/secrets', {
|
|
2702
|
+
method: 'POST',
|
|
2703
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2704
|
+
body: JSON.stringify({ name: name, value: value, service: service || undefined })
|
|
2705
|
+
}).then(function(data) {
|
|
2706
|
+
if (data.ok) {
|
|
2707
|
+
toast('Secret saved');
|
|
2708
|
+
hideAddSecretForm();
|
|
2709
|
+
loadSecrets();
|
|
2710
|
+
} else {
|
|
2711
|
+
toast(data.error || 'Error saving secret');
|
|
2712
|
+
}
|
|
2713
|
+
}).catch(function(e) { toast('Error: ' + e.message); });
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function deleteSecretUI(name) {
|
|
2717
|
+
if (!confirm('Delete secret "' + name + '"? This cannot be undone.')) return;
|
|
2718
|
+
fetch('/api/dashboard/secrets/' + encodeURIComponent(name), { method: 'DELETE' })
|
|
2719
|
+
.then(function(r) { return r.json(); })
|
|
2720
|
+
.then(function(data) {
|
|
2721
|
+
if (data.deleted) {
|
|
2722
|
+
toast('Secret deleted');
|
|
2723
|
+
loadSecrets();
|
|
2724
|
+
} else {
|
|
2725
|
+
toast('Secret not found');
|
|
2726
|
+
}
|
|
2727
|
+
}).catch(function(e) { toast('Error: ' + e.message); });
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// --- ONBOARDING ---
|
|
2731
|
+
var onboardingStep = 0;
|
|
2732
|
+
var onboardingSteps = [
|
|
2733
|
+
{ title: 'Welcome to Zubo', body: 'Your personal AI agent that remembers you, runs tasks, and connects to your favorite services. Let\\'s get you set up.', btn: 'Get Started' },
|
|
2734
|
+
{ title: 'Set Your Agent\\'s Name', body: 'Give your Zubo agent a personality. Edit the system prompt to customize how it talks and what it knows about you.', btn: 'Next' },
|
|
2735
|
+
{ title: 'Connect a Channel', body: 'Zubo works via web chat by default. You can also connect Telegram, Discord, Slack, WhatsApp, or Signal in Settings.', btn: 'Next' },
|
|
2736
|
+
{ title: 'You\\'re All Set!', body: 'Start chatting with Zubo. It remembers your conversations, can learn new skills, and runs scheduled tasks for you.', btn: 'Start Chatting' },
|
|
2737
|
+
];
|
|
2738
|
+
|
|
2739
|
+
function checkOnboarding() {
|
|
2740
|
+
api('/onboarding').then(function(data) {
|
|
2741
|
+
if (data.completed) return;
|
|
2742
|
+
onboardingStep = data.step || 0;
|
|
2743
|
+
showOnboardingStep();
|
|
2744
|
+
document.getElementById('onboarding-modal').classList.add('visible');
|
|
2745
|
+
}).catch(function(err) { console.warn('Dashboard API request failed', err); });
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
function showOnboardingStep() {
|
|
2749
|
+
var step = onboardingSteps[onboardingStep];
|
|
2750
|
+
if (!step) return;
|
|
2751
|
+
var content = document.getElementById('onboarding-content');
|
|
2752
|
+
content.replaceChildren();
|
|
2753
|
+
var h = document.createElement('h2');
|
|
2754
|
+
h.textContent = step.title;
|
|
2755
|
+
var p = document.createElement('p');
|
|
2756
|
+
p.textContent = step.body;
|
|
2757
|
+
content.appendChild(h);
|
|
2758
|
+
content.appendChild(p);
|
|
2759
|
+
document.getElementById('onboarding-next').textContent = step.btn;
|
|
2760
|
+
// Update dots
|
|
2761
|
+
var dots = document.querySelectorAll('#onboarding-steps .step-dot');
|
|
2762
|
+
dots.forEach(function(d, i) {
|
|
2763
|
+
d.className = 'step-dot';
|
|
2764
|
+
if (i < onboardingStep) d.classList.add('done');
|
|
2765
|
+
if (i === onboardingStep) d.classList.add('active');
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
function nextOnboardingStep() {
|
|
2770
|
+
onboardingStep++;
|
|
2771
|
+
if (onboardingStep >= onboardingSteps.length) {
|
|
2772
|
+
skipOnboarding();
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
showOnboardingStep();
|
|
2776
|
+
api('/onboarding', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ completed: false, step: onboardingStep }) });
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
function skipOnboarding() {
|
|
2780
|
+
document.getElementById('onboarding-modal').classList.remove('visible');
|
|
2781
|
+
api('/onboarding', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ completed: true, step: onboardingSteps.length }) });
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// --- MOBILE MENU ---
|
|
2785
|
+
function toggleMobileMenu() {
|
|
2786
|
+
document.getElementById('sidebar').classList.toggle('open');
|
|
2787
|
+
document.getElementById('mobile-overlay').classList.toggle('visible');
|
|
2788
|
+
}
|
|
2789
|
+
function closeMobileMenu() {
|
|
2790
|
+
document.getElementById('sidebar').classList.remove('open');
|
|
2791
|
+
document.getElementById('mobile-overlay').classList.remove('visible');
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// --- COMMAND PALETTE ---
|
|
2795
|
+
// Bind cmd-input listener once (not on every toggle)
|
|
2796
|
+
(function() {
|
|
2797
|
+
var input = document.getElementById('cmd-input');
|
|
2798
|
+
if (input) input.addEventListener('input', function() { renderCmdResults(input.value); });
|
|
2799
|
+
})();
|
|
2800
|
+
|
|
2801
|
+
function toggleCommandPalette() {
|
|
2802
|
+
var pal = document.getElementById('cmd-palette');
|
|
2803
|
+
if (pal.classList.contains('visible')) { closeCommandPalette(); return; }
|
|
2804
|
+
pal.classList.add('visible');
|
|
2805
|
+
var input = document.getElementById('cmd-input');
|
|
2806
|
+
input.value = '';
|
|
2807
|
+
input.focus();
|
|
2808
|
+
renderCmdResults('');
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function closeCommandPalette() {
|
|
2812
|
+
document.getElementById('cmd-palette').classList.remove('visible');
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function renderCmdResults(query) {
|
|
2816
|
+
var container = document.getElementById('cmd-results');
|
|
2817
|
+
container.replaceChildren();
|
|
2818
|
+
var items = panelNames.map(function(name) {
|
|
2819
|
+
return { name: name, title: panelTitles[name] || name };
|
|
2820
|
+
});
|
|
2821
|
+
if (query) {
|
|
2822
|
+
items = items.filter(function(item) {
|
|
2823
|
+
return item.title.toLowerCase().includes(query.toLowerCase());
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
items.forEach(function(item) {
|
|
2827
|
+
var div = document.createElement('div');
|
|
2828
|
+
div.className = 'cmd-result';
|
|
2829
|
+
div.textContent = item.title;
|
|
2830
|
+
div.onclick = function() { showPanel(item.name); closeCommandPalette(); };
|
|
2831
|
+
container.appendChild(div);
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// --- KEYBOARD SHORTCUTS ---
|
|
2836
|
+
document.addEventListener('keydown', function(e) {
|
|
2837
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
2838
|
+
e.preventDefault();
|
|
2839
|
+
toggleCommandPalette();
|
|
2840
|
+
}
|
|
2841
|
+
if (e.key === '/' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName)) {
|
|
2842
|
+
e.preventDefault();
|
|
2843
|
+
showPanel('agent');
|
|
2844
|
+
document.getElementById('chat-input').focus();
|
|
2845
|
+
}
|
|
2846
|
+
if (e.key === 'Escape') {
|
|
2847
|
+
closeCommandPalette();
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
|
|
2851
|
+
// --- CONVERSATION THREADS ---
|
|
2852
|
+
var activeThreadId = null;
|
|
2853
|
+
|
|
2854
|
+
function loadThreads() {
|
|
2855
|
+
api('/threads').then(function(data) {
|
|
2856
|
+
var list = document.getElementById('thread-list');
|
|
2857
|
+
list.replaceChildren();
|
|
2858
|
+
var threads = data.threads || [];
|
|
2859
|
+
threads.forEach(function(t) {
|
|
2860
|
+
var item = document.createElement('div');
|
|
2861
|
+
item.className = 'thread-item' + (t.id === activeThreadId ? ' active' : '');
|
|
2862
|
+
item.textContent = t.title;
|
|
2863
|
+
item.onclick = function() { switchThread(t.id, t.title); };
|
|
2864
|
+
var del = document.createElement('button');
|
|
2865
|
+
del.className = 'thread-delete';
|
|
2866
|
+
del.textContent = '\\u00d7';
|
|
2867
|
+
del.onclick = function(e) { e.stopPropagation(); deleteThread(t.id); };
|
|
2868
|
+
item.appendChild(del);
|
|
2869
|
+
list.appendChild(item);
|
|
2870
|
+
});
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
function createThread() {
|
|
2875
|
+
api('/threads', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({}) }).then(function(data) {
|
|
2876
|
+
activeThreadId = data.id;
|
|
2877
|
+
loadThreads();
|
|
2878
|
+
clearChatMessages();
|
|
2879
|
+
toast('New conversation started');
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function switchThread(id, title) {
|
|
2884
|
+
activeThreadId = id;
|
|
2885
|
+
loadThreads();
|
|
2886
|
+
api('/threads/' + id + '/messages').then(function(data) {
|
|
2887
|
+
var msgs = data.messages || [];
|
|
2888
|
+
clearChatMessages();
|
|
2889
|
+
if (msgs.length === 0) return;
|
|
2890
|
+
msgs.forEach(function(m) {
|
|
2891
|
+
var text = Array.isArray(m.content)
|
|
2892
|
+
? m.content.filter(function(b) { return b.type === 'text'; }).map(function(b) { return b.text; }).join('\\n')
|
|
2893
|
+
: String(m.content || '');
|
|
2894
|
+
if (text) addChatMsg(text, m.role === 'user' ? 'user' : 'bot');
|
|
2895
|
+
});
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
function deleteThread(id) {
|
|
2900
|
+
api('/threads/' + id, { method: 'DELETE' }).then(function() {
|
|
2901
|
+
if (activeThreadId === id) {
|
|
2902
|
+
activeThreadId = null;
|
|
2903
|
+
clearChatMessages();
|
|
2904
|
+
}
|
|
2905
|
+
loadThreads();
|
|
2906
|
+
toast('Conversation deleted');
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
function clearChatMessages() {
|
|
2911
|
+
var container = document.getElementById('chat-messages');
|
|
2912
|
+
container.replaceChildren();
|
|
2913
|
+
// Rebuild the static welcome state using DOM methods
|
|
2914
|
+
var welcome = document.createElement('div');
|
|
2915
|
+
welcome.className = 'chat-empty chat-welcome';
|
|
2916
|
+
var icon = document.createElement('div');
|
|
2917
|
+
icon.className = 'chat-welcome-icon';
|
|
2918
|
+
icon.textContent = '';
|
|
2919
|
+
var heading = document.createElement('h3');
|
|
2920
|
+
heading.className = 'gradient-text';
|
|
2921
|
+
heading.textContent = 'What can I help you with?';
|
|
2922
|
+
var subtext = document.createElement('div');
|
|
2923
|
+
subtext.className = 'chat-empty-text';
|
|
2924
|
+
subtext.textContent = 'Ask me anything, or try a suggestion below';
|
|
2925
|
+
var chips = document.createElement('div');
|
|
2926
|
+
chips.className = 'suggestion-chips';
|
|
2927
|
+
['Summarize my day','Check the weather','What can you do?','Set a reminder'].forEach(function(label) {
|
|
2928
|
+
var chip = document.createElement('button');
|
|
2929
|
+
chip.className = 'suggestion-chip';
|
|
2930
|
+
chip.textContent = label;
|
|
2931
|
+
chip.onclick = function() { useSuggestion(chip); };
|
|
2932
|
+
chips.appendChild(chip);
|
|
2933
|
+
});
|
|
2934
|
+
welcome.appendChild(icon);
|
|
2935
|
+
welcome.appendChild(heading);
|
|
2936
|
+
welcome.appendChild(subtext);
|
|
2937
|
+
welcome.appendChild(chips);
|
|
2938
|
+
container.appendChild(welcome);
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
// --- EXPORT THREAD ---
|
|
2942
|
+
function exportThread() {
|
|
2943
|
+
var id = activeThreadId;
|
|
2944
|
+
if (!id) { toast('No conversation to export'); return; }
|
|
2945
|
+
fetch('/api/dashboard/threads/' + id + '/export').then(function(r) {
|
|
2946
|
+
if (!r.ok) throw new Error('Export failed');
|
|
2947
|
+
return r.blob();
|
|
2948
|
+
}).then(function(blob) {
|
|
2949
|
+
var a = document.createElement('a');
|
|
2950
|
+
a.href = URL.createObjectURL(blob);
|
|
2951
|
+
a.download = 'conversation.md';
|
|
2952
|
+
a.click();
|
|
2953
|
+
URL.revokeObjectURL(a.href);
|
|
2954
|
+
toast('Conversation exported');
|
|
2955
|
+
}).catch(function(e) { toast('Export failed: ' + e.message); });
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// Auto-refresh channel status every 30s
|
|
2959
|
+
setInterval(function() {
|
|
2960
|
+
if (document.getElementById('panel-settings').classList.contains('active')) loadChannelStatus();
|
|
2961
|
+
}, 30000);
|
|
2962
|
+
|
|
2963
|
+
// Init
|
|
2964
|
+
routeFromHash();
|
|
2965
|
+
checkOnboarding();
|
|
2966
|
+
loadThreads();
|
|
2967
|
+
</script>
|
|
2968
|
+
</body>
|
|
2969
|
+
</html>`;
|