opc-agent 2.1.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +603 -545
- package/dist/channels/voice.d.ts +59 -0
- package/dist/channels/voice.js +351 -1
- package/dist/cli.js +172 -1
- package/dist/core/agent.d.ts +4 -0
- package/dist/core/agent.js +35 -0
- package/dist/core/collaboration.d.ts +89 -0
- package/dist/core/collaboration.js +201 -0
- package/dist/deploy/index.d.ts +40 -0
- package/dist/deploy/index.js +261 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +47 -3
- package/dist/mcp/servers/calculator-mcp.d.ts +3 -0
- package/dist/mcp/servers/calculator-mcp.js +65 -0
- package/dist/mcp/servers/crypto-mcp.d.ts +3 -0
- package/dist/mcp/servers/crypto-mcp.js +108 -0
- package/dist/mcp/servers/database-mcp.d.ts +3 -0
- package/dist/mcp/servers/database-mcp.js +73 -0
- package/dist/mcp/servers/datetime-mcp.d.ts +3 -0
- package/dist/mcp/servers/datetime-mcp.js +71 -0
- package/dist/mcp/servers/filesystem.d.ts +3 -0
- package/dist/mcp/servers/filesystem.js +101 -0
- package/dist/mcp/servers/github-mcp.d.ts +3 -0
- package/dist/mcp/servers/github-mcp.js +60 -0
- package/dist/mcp/servers/index.d.ts +21 -0
- package/dist/mcp/servers/index.js +50 -0
- package/dist/mcp/servers/json-mcp.d.ts +3 -0
- package/dist/mcp/servers/json-mcp.js +126 -0
- package/dist/mcp/servers/memory-mcp.d.ts +3 -0
- package/dist/mcp/servers/memory-mcp.js +60 -0
- package/dist/mcp/servers/regex-mcp.d.ts +3 -0
- package/dist/mcp/servers/regex-mcp.js +56 -0
- package/dist/mcp/servers/web-mcp.d.ts +3 -0
- package/dist/mcp/servers/web-mcp.js +51 -0
- package/dist/schema/oad.d.ts +292 -12
- package/dist/schema/oad.js +12 -1
- package/dist/security/guardrails.d.ts +50 -0
- package/dist/security/guardrails.js +197 -0
- package/dist/studio/server.d.ts +31 -1
- package/dist/studio/server.js +154 -3
- package/dist/studio-ui/index.html +1278 -662
- package/dist/tools/integrations/calendar.d.ts +3 -0
- package/dist/tools/integrations/calendar.js +73 -0
- package/dist/tools/integrations/code-exec.d.ts +3 -0
- package/dist/tools/integrations/code-exec.js +42 -0
- package/dist/tools/integrations/csv-analyzer.d.ts +3 -0
- package/dist/tools/integrations/csv-analyzer.js +142 -0
- package/dist/tools/integrations/database.d.ts +3 -0
- package/dist/tools/integrations/database.js +44 -0
- package/dist/tools/integrations/email-send.d.ts +3 -0
- package/dist/tools/integrations/email-send.js +104 -0
- package/dist/tools/integrations/git-tool.d.ts +3 -0
- package/dist/tools/integrations/git-tool.js +49 -0
- package/dist/tools/integrations/github-tool.d.ts +3 -0
- package/dist/tools/integrations/github-tool.js +77 -0
- package/dist/tools/integrations/image-gen.d.ts +3 -0
- package/dist/tools/integrations/image-gen.js +58 -0
- package/dist/tools/integrations/index.d.ts +30 -0
- package/dist/tools/integrations/index.js +107 -0
- package/dist/tools/integrations/jira.d.ts +3 -0
- package/dist/tools/integrations/jira.js +85 -0
- package/dist/tools/integrations/notion.d.ts +3 -0
- package/dist/tools/integrations/notion.js +71 -0
- package/dist/tools/integrations/npm-tool.d.ts +3 -0
- package/dist/tools/integrations/npm-tool.js +49 -0
- package/dist/tools/integrations/pdf-reader.d.ts +3 -0
- package/dist/tools/integrations/pdf-reader.js +91 -0
- package/dist/tools/integrations/slack.d.ts +3 -0
- package/dist/tools/integrations/slack.js +67 -0
- package/dist/tools/integrations/summarizer.d.ts +3 -0
- package/dist/tools/integrations/summarizer.js +49 -0
- package/dist/tools/integrations/translator.d.ts +3 -0
- package/dist/tools/integrations/translator.js +48 -0
- package/dist/tools/integrations/trello.d.ts +3 -0
- package/dist/tools/integrations/trello.js +60 -0
- package/dist/tools/integrations/vector-search.d.ts +3 -0
- package/dist/tools/integrations/vector-search.js +44 -0
- package/dist/tools/integrations/web-scraper.d.ts +3 -0
- package/dist/tools/integrations/web-scraper.js +48 -0
- package/dist/tools/integrations/web-search.d.ts +3 -0
- package/dist/tools/integrations/web-search.js +60 -0
- package/dist/tools/integrations/webhook.d.ts +3 -0
- package/dist/tools/integrations/webhook.js +39 -0
- package/dist/ui/components.d.ts +10 -0
- package/dist/ui/components.js +123 -0
- package/package.json +3 -3
- package/src/channels/voice.ts +365 -0
- package/src/cli.ts +176 -2
- package/src/core/agent.ts +38 -0
- package/src/core/collaboration.ts +275 -0
- package/src/deploy/index.ts +255 -0
- package/src/index.ts +21 -1
- package/src/mcp/servers/calculator-mcp.ts +65 -0
- package/src/mcp/servers/crypto-mcp.ts +73 -0
- package/src/mcp/servers/database-mcp.ts +72 -0
- package/src/mcp/servers/datetime-mcp.ts +69 -0
- package/src/mcp/servers/filesystem.ts +66 -0
- package/src/mcp/servers/github-mcp.ts +58 -0
- package/src/mcp/servers/index.ts +63 -0
- package/src/mcp/servers/json-mcp.ts +102 -0
- package/src/mcp/servers/memory-mcp.ts +56 -0
- package/src/mcp/servers/regex-mcp.ts +53 -0
- package/src/mcp/servers/web-mcp.ts +49 -0
- package/src/schema/oad.ts +13 -0
- package/src/security/guardrails.ts +248 -0
- package/src/studio/server.ts +166 -4
- package/src/studio-ui/index.html +1278 -662
- package/src/tools/integrations/calendar.ts +73 -0
- package/src/tools/integrations/code-exec.ts +39 -0
- package/src/tools/integrations/csv-analyzer.ts +92 -0
- package/src/tools/integrations/database.ts +44 -0
- package/src/tools/integrations/email-send.ts +76 -0
- package/src/tools/integrations/git-tool.ts +42 -0
- package/src/tools/integrations/github-tool.ts +76 -0
- package/src/tools/integrations/image-gen.ts +56 -0
- package/src/tools/integrations/index.ts +92 -0
- package/src/tools/integrations/jira.ts +83 -0
- package/src/tools/integrations/notion.ts +71 -0
- package/src/tools/integrations/npm-tool.ts +48 -0
- package/src/tools/integrations/pdf-reader.ts +58 -0
- package/src/tools/integrations/slack.ts +65 -0
- package/src/tools/integrations/summarizer.ts +49 -0
- package/src/tools/integrations/translator.ts +48 -0
- package/src/tools/integrations/trello.ts +60 -0
- package/src/tools/integrations/vector-search.ts +42 -0
- package/src/tools/integrations/web-scraper.ts +47 -0
- package/src/tools/integrations/web-search.ts +58 -0
- package/src/tools/integrations/webhook.ts +38 -0
- package/src/ui/components.ts +127 -0
- package/tests/brain-seed-extended.test.ts +490 -0
- package/tests/collaboration.test.ts +319 -0
- package/tests/deploy-and-dag.test.ts +196 -0
- package/tests/guardrails.test.ts +177 -0
- package/tests/integrations.test.ts +249 -0
- package/tests/mcp-servers.test.ts +260 -0
- package/tests/voice-enhanced.test.ts +169 -0
- package/dist/dtv/data.d.ts +0 -18
- package/dist/dtv/data.js +0 -25
- package/dist/dtv/trust.d.ts +0 -19
- package/dist/dtv/trust.js +0 -40
- package/dist/dtv/value.d.ts +0 -23
- package/dist/dtv/value.js +0 -38
- package/dist/marketplace/index.d.ts +0 -34
- package/dist/marketplace/index.js +0 -202
package/src/studio-ui/index.html
CHANGED
|
@@ -1,662 +1,1278 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>OPC Studio</title>
|
|
7
|
-
<style>
|
|
8
|
-
/* === Global === */
|
|
9
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
-
:root {
|
|
11
|
-
--bg: #0a0a0a;
|
|
12
|
-
--bg-card: #141414;
|
|
13
|
-
--bg-hover: #1a1a1a;
|
|
14
|
-
--border: #262626;
|
|
15
|
-
--text: #e5e5e5;
|
|
16
|
-
--text-muted: #737373;
|
|
17
|
-
--accent: #3b82f6;
|
|
18
|
-
--accent-hover: #2563eb;
|
|
19
|
-
--green: #22c55e;
|
|
20
|
-
--red: #ef4444;
|
|
21
|
-
--yellow: #eab308;
|
|
22
|
-
--purple: #a855f7;
|
|
23
|
-
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
24
|
-
--mono: 'SF Mono', 'Fira Code', monospace;
|
|
25
|
-
--radius: 8px;
|
|
26
|
-
}
|
|
27
|
-
body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
28
|
-
|
|
29
|
-
/* === Layout === */
|
|
30
|
-
.app { display: flex; min-height: 100vh; }
|
|
31
|
-
|
|
32
|
-
/* Sidebar */
|
|
33
|
-
.sidebar {
|
|
34
|
-
width: 240px; background: var(--bg-card); border-right: 1px solid var(--border);
|
|
35
|
-
padding: 16px; display: flex; flex-direction: column; position: fixed; height: 100vh;
|
|
36
|
-
}
|
|
37
|
-
.sidebar-logo { font-size: 18px; font-weight: 700; padding: 8px 12px; margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
|
|
38
|
-
.sidebar-logo span { color: var(--accent); }
|
|
39
|
-
.sidebar-nav { flex: 1; }
|
|
40
|
-
.nav-item {
|
|
41
|
-
display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius);
|
|
42
|
-
cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-size: 14px; margin-bottom: 2px;
|
|
43
|
-
}
|
|
44
|
-
.nav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
45
|
-
.nav-item.active { background: var(--bg-hover); color: var(--text); font-weight: 500; }
|
|
46
|
-
.nav-item .icon { width: 18px; text-align: center; }
|
|
47
|
-
|
|
48
|
-
/* Main content */
|
|
49
|
-
.main { flex: 1; margin-left: 240px; padding: 32px; max-width: 1200px; }
|
|
50
|
-
|
|
51
|
-
/* Header */
|
|
52
|
-
.page-header { margin-bottom: 32px; }
|
|
53
|
-
.page-title { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
|
|
54
|
-
.page-subtitle { color: var(--text-muted); font-size: 14px; }
|
|
55
|
-
|
|
56
|
-
/* Cards */
|
|
57
|
-
.card {
|
|
58
|
-
background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
59
|
-
padding: 20px; margin-bottom: 16px;
|
|
60
|
-
}
|
|
61
|
-
.card-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
|
62
|
-
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
63
|
-
|
|
64
|
-
/* Stats */
|
|
65
|
-
.stat { text-align: center; padding: 16px; }
|
|
66
|
-
.stat-value { font-size: 32px; font-weight: 700; color: var(--accent); }
|
|
67
|
-
.stat-label { font-size: 12px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
68
|
-
|
|
69
|
-
/* Status badge */
|
|
70
|
-
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; }
|
|
71
|
-
.badge-green { background: rgba(34,197,94,0.1); color: var(--green); }
|
|
72
|
-
.badge-red { background: rgba(239,68,68,0.1); color: var(--red); }
|
|
73
|
-
.badge-yellow { background: rgba(234,179,8,0.1); color: var(--yellow); }
|
|
74
|
-
.badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
75
|
-
|
|
76
|
-
/* Table */
|
|
77
|
-
.table { width: 100%; border-collapse: collapse; }
|
|
78
|
-
.table th { text-align: left; padding: 10px 12px; font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
|
|
79
|
-
.table td { padding: 10px 12px; font-size: 14px; border-bottom: 1px solid var(--border); }
|
|
80
|
-
.table tr:hover { background: var(--bg-hover); }
|
|
81
|
-
|
|
82
|
-
/* Chat */
|
|
83
|
-
.chat-container { height: 500px; display: flex; flex-direction: column; }
|
|
84
|
-
.chat-messages { flex: 1; overflow-y: auto; padding: 16px; }
|
|
85
|
-
.chat-input-row { display: flex; gap: 8px; padding: 16px; border-top: 1px solid var(--border); }
|
|
86
|
-
.chat-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; color: var(--text); font-size: 14px; outline: none; }
|
|
87
|
-
.chat-input:focus { border-color: var(--accent); }
|
|
88
|
-
.chat-send { background: var(--accent); color: white; border: none; border-radius: var(--radius); padding: 10px 20px; font-weight: 500; cursor: pointer; }
|
|
89
|
-
.chat-send:hover { background: var(--accent-hover); }
|
|
90
|
-
.message { margin-bottom: 16px; }
|
|
91
|
-
.message-user { text-align: right; }
|
|
92
|
-
.message-user .bubble { background: var(--accent); color: white; display: inline-block; padding: 10px 14px; border-radius: 14px 14px 4px 14px; max-width: 70%; text-align: left; }
|
|
93
|
-
.message-agent .bubble { background: var(--bg-hover); display: inline-block; padding: 10px 14px; border-radius: 14px 14px 14px 4px; max-width: 70%; }
|
|
94
|
-
.message-label { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
|
|
95
|
-
|
|
96
|
-
/* Config editor */
|
|
97
|
-
.editor { width: 100%; min-height: 400px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; color: var(--text); font-family: var(--mono); font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
|
|
98
|
-
.editor:focus { border-color: var(--accent); }
|
|
99
|
-
|
|
100
|
-
/* Button */
|
|
101
|
-
.btn { padding: 8px 16px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); transition: all 0.15s; }
|
|
102
|
-
.btn:hover { background: var(--bg-hover); }
|
|
103
|
-
.btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
|
|
104
|
-
.btn-primary:hover { background: var(--accent-hover); }
|
|
105
|
-
|
|
106
|
-
/* Doctor checks */
|
|
107
|
-
.check-item { display: flex; align-items: center; gap: 10px; padding: 8px 0; }
|
|
108
|
-
.check-icon { font-size: 16px; }
|
|
109
|
-
.check-name { font-weight: 500; min-width: 180px; }
|
|
110
|
-
.check-detail { color: var(--text-muted); font-size: 13px; }
|
|
111
|
-
.check-fix { color: var(--yellow); font-size: 12px; font-style: italic; }
|
|
112
|
-
|
|
113
|
-
/* Memory list */
|
|
114
|
-
.memory-item { padding: 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
115
|
-
.memory-item:hover { background: var(--bg-hover); }
|
|
116
|
-
.memory-slug { font-weight: 500; font-family: var(--mono); font-size: 13px; }
|
|
117
|
-
.memory-preview { color: var(--text-muted); font-size: 13px; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
118
|
-
.memory-meta { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
119
|
-
|
|
120
|
-
/* Search */
|
|
121
|
-
.search-bar { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
122
|
-
.search-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 14px; color: var(--text); font-size: 14px; outline: none; }
|
|
123
|
-
|
|
124
|
-
/* Page sections (hidden by default) */
|
|
125
|
-
.page { display: none; }
|
|
126
|
-
.page.active { display: block; }
|
|
127
|
-
|
|
128
|
-
/* Scrollbar */
|
|
129
|
-
::-webkit-scrollbar { width: 6px; }
|
|
130
|
-
::-webkit-scrollbar-track { background: transparent; }
|
|
131
|
-
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
132
|
-
|
|
133
|
-
/* Loading */
|
|
134
|
-
.loading { color: var(--text-muted); text-align: center; padding: 40px; }
|
|
135
|
-
.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
136
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
137
|
-
</style>
|
|
138
|
-
</head>
|
|
139
|
-
<body>
|
|
140
|
-
<div class="app">
|
|
141
|
-
<!-- Sidebar -->
|
|
142
|
-
<nav class="sidebar">
|
|
143
|
-
<div class="sidebar-logo">⚡ <span>OPC</span> Studio</div>
|
|
144
|
-
<div class="sidebar-nav">
|
|
145
|
-
<div class="nav-item active" data-page="dashboard"><span class="icon">📊</span> Dashboard</div>
|
|
146
|
-
<div class="nav-item" data-page="chat"><span class="icon">💬</span> Chat</div>
|
|
147
|
-
<div class="nav-item" data-page="config"><span class="icon">⚙️</span> Config</div>
|
|
148
|
-
<div class="nav-item" data-page="memory"><span class="icon">🧠</span> Memory</div>
|
|
149
|
-
<div class="nav-item" data-page="skills"><span class="icon">🛠</span> Skills</div>
|
|
150
|
-
<div class="nav-item" data-page="tools"><span class="icon">🔧</span> Tools</div>
|
|
151
|
-
<div class="nav-item" data-page="channels"><span class="icon">📡</span> Channels</div>
|
|
152
|
-
<div class="nav-item" data-page="workflows"><span class="icon">🔀</span> Workflows</div>
|
|
153
|
-
<div class="nav-item" data-page="jobs"><span class="icon">⏰</span> Jobs</div>
|
|
154
|
-
<div class="nav-item" data-page="plugins"><span class="icon">🔌</span> Plugins</div>
|
|
155
|
-
<div class="nav-item" data-page="protocols"><span class="icon">📡</span> Protocols</div>
|
|
156
|
-
<div class="nav-item" data-page="doctor"><span class="icon">🩺</span> Doctor</div>
|
|
157
|
-
<div class="nav-item" data-page="evals"><span class="icon">🧪</span> Evals</div>
|
|
158
|
-
<div class="nav-item" data-page="telemetry"><span class="icon">📈</span> Telemetry</div>
|
|
159
|
-
<div class="nav-item" data-page="logs"><span class="icon">📜</span> Logs</div>
|
|
160
|
-
<div
|
|
161
|
-
<div
|
|
162
|
-
<div class="nav-item" data-page="
|
|
163
|
-
<div class="nav-item" data-page="
|
|
164
|
-
<div class="nav-item" data-page="
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<div class="page-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<div class="card stat"><div class="stat-value" id="agent-
|
|
180
|
-
<div class="card stat"><div class="stat-value" id="agent-
|
|
181
|
-
<div class="card stat"><div class="stat-value" id="agent-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<div
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<div
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
<div class="page-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<div class="page-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<div class="page-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
<
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<div class="page-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<div class="page-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<div class="page-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<div
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<div
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
<div class="
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
<div class="
|
|
332
|
-
<div class="
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
<div class="
|
|
340
|
-
<
|
|
341
|
-
<div
|
|
342
|
-
</div>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
<!--
|
|
369
|
-
<div class="page" id="page-
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
document.
|
|
465
|
-
document.
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
document.getElementById('
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
//
|
|
474
|
-
async function
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
function
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>OPC Studio</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* === Global === */
|
|
9
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0a0a0a;
|
|
12
|
+
--bg-card: #141414;
|
|
13
|
+
--bg-hover: #1a1a1a;
|
|
14
|
+
--border: #262626;
|
|
15
|
+
--text: #e5e5e5;
|
|
16
|
+
--text-muted: #737373;
|
|
17
|
+
--accent: #3b82f6;
|
|
18
|
+
--accent-hover: #2563eb;
|
|
19
|
+
--green: #22c55e;
|
|
20
|
+
--red: #ef4444;
|
|
21
|
+
--yellow: #eab308;
|
|
22
|
+
--purple: #a855f7;
|
|
23
|
+
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
24
|
+
--mono: 'SF Mono', 'Fira Code', monospace;
|
|
25
|
+
--radius: 8px;
|
|
26
|
+
}
|
|
27
|
+
body { font-family: var(--font); background: var(--bg); color: var(--text); min-height: 100vh; }
|
|
28
|
+
|
|
29
|
+
/* === Layout === */
|
|
30
|
+
.app { display: flex; min-height: 100vh; }
|
|
31
|
+
|
|
32
|
+
/* Sidebar */
|
|
33
|
+
.sidebar {
|
|
34
|
+
width: 240px; background: var(--bg-card); border-right: 1px solid var(--border);
|
|
35
|
+
padding: 16px; display: flex; flex-direction: column; position: fixed; height: 100vh;
|
|
36
|
+
}
|
|
37
|
+
.sidebar-logo { font-size: 18px; font-weight: 700; padding: 8px 12px; margin-bottom: 24px; display: flex; align-items: center; gap: 8px; }
|
|
38
|
+
.sidebar-logo span { color: var(--accent); }
|
|
39
|
+
.sidebar-nav { flex: 1; }
|
|
40
|
+
.nav-item {
|
|
41
|
+
display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: var(--radius);
|
|
42
|
+
cursor: pointer; color: var(--text-muted); transition: all 0.15s; font-size: 14px; margin-bottom: 2px;
|
|
43
|
+
}
|
|
44
|
+
.nav-item:hover { background: var(--bg-hover); color: var(--text); }
|
|
45
|
+
.nav-item.active { background: var(--bg-hover); color: var(--text); font-weight: 500; }
|
|
46
|
+
.nav-item .icon { width: 18px; text-align: center; }
|
|
47
|
+
|
|
48
|
+
/* Main content */
|
|
49
|
+
.main { flex: 1; margin-left: 240px; padding: 32px; max-width: 1200px; }
|
|
50
|
+
|
|
51
|
+
/* Header */
|
|
52
|
+
.page-header { margin-bottom: 32px; }
|
|
53
|
+
.page-title { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
|
|
54
|
+
.page-subtitle { color: var(--text-muted); font-size: 14px; }
|
|
55
|
+
|
|
56
|
+
/* Cards */
|
|
57
|
+
.card {
|
|
58
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
59
|
+
padding: 20px; margin-bottom: 16px;
|
|
60
|
+
}
|
|
61
|
+
.card-title { font-size: 14px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
|
62
|
+
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
63
|
+
|
|
64
|
+
/* Stats */
|
|
65
|
+
.stat { text-align: center; padding: 16px; }
|
|
66
|
+
.stat-value { font-size: 32px; font-weight: 700; color: var(--accent); }
|
|
67
|
+
.stat-label { font-size: 12px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
68
|
+
|
|
69
|
+
/* Status badge */
|
|
70
|
+
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 500; }
|
|
71
|
+
.badge-green { background: rgba(34,197,94,0.1); color: var(--green); }
|
|
72
|
+
.badge-red { background: rgba(239,68,68,0.1); color: var(--red); }
|
|
73
|
+
.badge-yellow { background: rgba(234,179,8,0.1); color: var(--yellow); }
|
|
74
|
+
.badge-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
75
|
+
|
|
76
|
+
/* Table */
|
|
77
|
+
.table { width: 100%; border-collapse: collapse; }
|
|
78
|
+
.table th { text-align: left; padding: 10px 12px; font-size: 12px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
|
|
79
|
+
.table td { padding: 10px 12px; font-size: 14px; border-bottom: 1px solid var(--border); }
|
|
80
|
+
.table tr:hover { background: var(--bg-hover); }
|
|
81
|
+
|
|
82
|
+
/* Chat */
|
|
83
|
+
.chat-container { height: 500px; display: flex; flex-direction: column; }
|
|
84
|
+
.chat-messages { flex: 1; overflow-y: auto; padding: 16px; }
|
|
85
|
+
.chat-input-row { display: flex; gap: 8px; padding: 16px; border-top: 1px solid var(--border); }
|
|
86
|
+
.chat-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; color: var(--text); font-size: 14px; outline: none; }
|
|
87
|
+
.chat-input:focus { border-color: var(--accent); }
|
|
88
|
+
.chat-send { background: var(--accent); color: white; border: none; border-radius: var(--radius); padding: 10px 20px; font-weight: 500; cursor: pointer; }
|
|
89
|
+
.chat-send:hover { background: var(--accent-hover); }
|
|
90
|
+
.message { margin-bottom: 16px; }
|
|
91
|
+
.message-user { text-align: right; }
|
|
92
|
+
.message-user .bubble { background: var(--accent); color: white; display: inline-block; padding: 10px 14px; border-radius: 14px 14px 4px 14px; max-width: 70%; text-align: left; }
|
|
93
|
+
.message-agent .bubble { background: var(--bg-hover); display: inline-block; padding: 10px 14px; border-radius: 14px 14px 14px 4px; max-width: 70%; }
|
|
94
|
+
.message-label { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
|
|
95
|
+
|
|
96
|
+
/* Config editor */
|
|
97
|
+
.editor { width: 100%; min-height: 400px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; color: var(--text); font-family: var(--mono); font-size: 13px; line-height: 1.6; resize: vertical; outline: none; }
|
|
98
|
+
.editor:focus { border-color: var(--accent); }
|
|
99
|
+
|
|
100
|
+
/* Button */
|
|
101
|
+
.btn { padding: 8px 16px; border-radius: var(--radius); font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--border); background: var(--bg-card); color: var(--text); transition: all 0.15s; }
|
|
102
|
+
.btn:hover { background: var(--bg-hover); }
|
|
103
|
+
.btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
|
|
104
|
+
.btn-primary:hover { background: var(--accent-hover); }
|
|
105
|
+
|
|
106
|
+
/* Doctor checks */
|
|
107
|
+
.check-item { display: flex; align-items: center; gap: 10px; padding: 8px 0; }
|
|
108
|
+
.check-icon { font-size: 16px; }
|
|
109
|
+
.check-name { font-weight: 500; min-width: 180px; }
|
|
110
|
+
.check-detail { color: var(--text-muted); font-size: 13px; }
|
|
111
|
+
.check-fix { color: var(--yellow); font-size: 12px; font-style: italic; }
|
|
112
|
+
|
|
113
|
+
/* Memory list */
|
|
114
|
+
.memory-item { padding: 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
|
115
|
+
.memory-item:hover { background: var(--bg-hover); }
|
|
116
|
+
.memory-slug { font-weight: 500; font-family: var(--mono); font-size: 13px; }
|
|
117
|
+
.memory-preview { color: var(--text-muted); font-size: 13px; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
118
|
+
.memory-meta { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
|
|
119
|
+
|
|
120
|
+
/* Search */
|
|
121
|
+
.search-bar { display: flex; gap: 8px; margin-bottom: 16px; }
|
|
122
|
+
.search-input { flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 14px; color: var(--text); font-size: 14px; outline: none; }
|
|
123
|
+
|
|
124
|
+
/* Page sections (hidden by default) */
|
|
125
|
+
.page { display: none; }
|
|
126
|
+
.page.active { display: block; }
|
|
127
|
+
|
|
128
|
+
/* Scrollbar */
|
|
129
|
+
::-webkit-scrollbar { width: 6px; }
|
|
130
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
131
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
132
|
+
|
|
133
|
+
/* Loading */
|
|
134
|
+
.loading { color: var(--text-muted); text-align: center; padding: 40px; }
|
|
135
|
+
.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; }
|
|
136
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
137
|
+
</style>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<div class="app">
|
|
141
|
+
<!-- Sidebar -->
|
|
142
|
+
<nav class="sidebar">
|
|
143
|
+
<div class="sidebar-logo">⚡ <span>OPC</span> Studio</div>
|
|
144
|
+
<div class="sidebar-nav">
|
|
145
|
+
<div class="nav-item active" data-page="dashboard"><span class="icon">📊</span> Dashboard</div>
|
|
146
|
+
<div class="nav-item" data-page="chat"><span class="icon">💬</span> Chat</div>
|
|
147
|
+
<div class="nav-item" data-page="config"><span class="icon">⚙️</span> Config</div>
|
|
148
|
+
<div class="nav-item" data-page="memory"><span class="icon">🧠</span> Memory</div>
|
|
149
|
+
<div class="nav-item" data-page="skills"><span class="icon">🛠</span> Skills</div>
|
|
150
|
+
<div class="nav-item" data-page="tools"><span class="icon">🔧</span> Tools</div>
|
|
151
|
+
<div class="nav-item" data-page="channels"><span class="icon">📡</span> Channels</div>
|
|
152
|
+
<div class="nav-item" data-page="workflows"><span class="icon">🔀</span> Workflows</div>
|
|
153
|
+
<div class="nav-item" data-page="jobs"><span class="icon">⏰</span> Jobs</div>
|
|
154
|
+
<div class="nav-item" data-page="plugins"><span class="icon">🔌</span> Plugins</div>
|
|
155
|
+
<div class="nav-item" data-page="protocols"><span class="icon">📡</span> Protocols</div>
|
|
156
|
+
<div class="nav-item" data-page="doctor"><span class="icon">🩺</span> Doctor</div>
|
|
157
|
+
<div class="nav-item" data-page="evals"><span class="icon">🧪</span> Evals</div>
|
|
158
|
+
<div class="nav-item" data-page="telemetry"><span class="icon">📈</span> Telemetry</div>
|
|
159
|
+
<div class="nav-item" data-page="logs"><span class="icon">📜</span> Logs</div>
|
|
160
|
+
<div class="nav-item" data-page="playground"><span class="icon">🎮</span> Playground</div>
|
|
161
|
+
<div style="padding: 8px 12px; margin-top: 16px; font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px;">Modules</div>
|
|
162
|
+
<div class="nav-item" data-page="modules"><span class="icon">🔌</span> Modules</div>
|
|
163
|
+
<div class="nav-item" data-page="brain-module"><span class="icon">🧠</span> DeepBrain</div>
|
|
164
|
+
<div class="nav-item" data-page="kits-module"><span class="icon">📊</span> AgentKits</div>
|
|
165
|
+
<div class="nav-item" data-page="workstation-module"><span class="icon">👤</span> Workstation</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div style="padding: 8px 12px; font-size: 11px; color: var(--text-muted);">OPC Agent v2.1</div>
|
|
168
|
+
</nav>
|
|
169
|
+
|
|
170
|
+
<!-- Main Content -->
|
|
171
|
+
<main class="main">
|
|
172
|
+
<!-- Dashboard -->
|
|
173
|
+
<div class="page active" id="page-dashboard">
|
|
174
|
+
<div class="page-header">
|
|
175
|
+
<div class="page-title">Dashboard</div>
|
|
176
|
+
<div class="page-subtitle">Agent overview and health status</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="card-grid">
|
|
179
|
+
<div class="card stat"><div class="stat-value" id="agent-name">—</div><div class="stat-label">Agent Name</div></div>
|
|
180
|
+
<div class="card stat"><div class="stat-value" id="agent-model">—</div><div class="stat-label">Model</div></div>
|
|
181
|
+
<div class="card stat"><div class="stat-value" id="agent-channels">—</div><div class="stat-label">Channels</div></div>
|
|
182
|
+
<div class="card stat"><div class="stat-value" id="agent-skills">—</div><div class="stat-label">Skills</div></div>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="card">
|
|
185
|
+
<div class="card-title">🟢 Agent Status</div>
|
|
186
|
+
<div id="agent-status-detail">Loading...</div>
|
|
187
|
+
</div>
|
|
188
|
+
<div class="card">
|
|
189
|
+
<div class="card-title">🧠 Memory Stats</div>
|
|
190
|
+
<div id="memory-stats">Loading...</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<!-- Chat -->
|
|
195
|
+
<div class="page" id="page-chat">
|
|
196
|
+
<div class="page-header">
|
|
197
|
+
<div class="page-title">Chat</div>
|
|
198
|
+
<div class="page-subtitle">Test your agent in real-time</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="card chat-container">
|
|
201
|
+
<div class="chat-messages" id="chat-messages">
|
|
202
|
+
<div class="message message-agent"><div class="message-label">Agent</div><div class="bubble">Hi! I'm ready to chat. Type a message below.</div></div>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="chat-input-row">
|
|
205
|
+
<input class="chat-input" id="chat-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendChat()">
|
|
206
|
+
<button class="chat-send" onclick="sendChat()">Send</button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<!-- Config -->
|
|
212
|
+
<div class="page" id="page-config">
|
|
213
|
+
<div class="page-header">
|
|
214
|
+
<div class="page-title">Configuration</div>
|
|
215
|
+
<div class="page-subtitle">Edit agent.yaml</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="card">
|
|
218
|
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
|
219
|
+
<div class="card-title" style="margin:0">agent.yaml</div>
|
|
220
|
+
<button class="btn btn-primary" onclick="saveConfig()">Save</button>
|
|
221
|
+
</div>
|
|
222
|
+
<textarea class="editor" id="config-editor">Loading...</textarea>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Memory -->
|
|
227
|
+
<div class="page" id="page-memory">
|
|
228
|
+
<div class="page-header">
|
|
229
|
+
<div class="page-title">Memory</div>
|
|
230
|
+
<div class="page-subtitle">DeepBrain knowledge pages</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="search-bar">
|
|
233
|
+
<input class="search-input" id="memory-search" placeholder="Search memories..." onkeydown="if(event.key==='Enter')searchMemory()">
|
|
234
|
+
<button class="btn" onclick="searchMemory()">Search</button>
|
|
235
|
+
</div>
|
|
236
|
+
<div class="card" id="memory-list">Loading...</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<!-- Skills -->
|
|
240
|
+
<div class="page" id="page-skills">
|
|
241
|
+
<div class="page-header">
|
|
242
|
+
<div class="page-title">Skills</div>
|
|
243
|
+
<div class="page-subtitle">Agent capabilities</div>
|
|
244
|
+
</div>
|
|
245
|
+
<div class="card" id="skills-list">Loading...</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<!-- Tools -->
|
|
249
|
+
<div class="page" id="page-tools">
|
|
250
|
+
<div class="page-header">
|
|
251
|
+
<div class="page-title">Tools</div>
|
|
252
|
+
<div class="page-subtitle">Built-in and MCP tools</div>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="card" id="tools-list">Loading...</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<!-- Channels -->
|
|
258
|
+
<div class="page" id="page-channels">
|
|
259
|
+
<div class="page-header">
|
|
260
|
+
<div class="page-title">Channels</div>
|
|
261
|
+
<div class="page-subtitle">Communication endpoints</div>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="card" id="channels-list">Loading...</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<!-- Workflows — Visual DAG Editor -->
|
|
267
|
+
<div class="page" id="page-workflows">
|
|
268
|
+
<div class="page-header" style="display:flex;justify-content:space-between;align-items:flex-start">
|
|
269
|
+
<div>
|
|
270
|
+
<div class="page-title">Workflows</div>
|
|
271
|
+
<div class="page-subtitle">Visual DAG workflow editor</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
274
|
+
<select id="wf-list-select" style="padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px">
|
|
275
|
+
<option value="">— New Workflow —</option>
|
|
276
|
+
</select>
|
|
277
|
+
<button class="btn" onclick="dagEditor.loadSelected()">Load</button>
|
|
278
|
+
<button class="btn btn-primary" onclick="dagEditor.save()">💾 Save</button>
|
|
279
|
+
<button class="btn" onclick="dagEditor.exportJSON()">📤 Export</button>
|
|
280
|
+
<button class="btn" onclick="dagEditor.importJSON()">📥 Import</button>
|
|
281
|
+
<button class="btn" style="background:var(--green);border-color:var(--green);color:#000" onclick="dagEditor.run()">▶ Run</button>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<div style="display:flex;gap:12px;margin-bottom:12px">
|
|
285
|
+
<div class="card" style="width:180px;padding:12px;flex-shrink:0">
|
|
286
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">Node Palette</div>
|
|
287
|
+
<div class="dag-palette-item" draggable="true" data-type="input" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'input')">📥 Input</div>
|
|
288
|
+
<div class="dag-palette-item" draggable="true" data-type="agent" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'agent')">🤖 Agent</div>
|
|
289
|
+
<div class="dag-palette-item" draggable="true" data-type="tool" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'tool')">🔧 Tool</div>
|
|
290
|
+
<div class="dag-palette-item" draggable="true" data-type="condition" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'condition')">❓ Condition</div>
|
|
291
|
+
<div class="dag-palette-item" draggable="true" data-type="loop" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'loop')">🔁 Loop</div>
|
|
292
|
+
<div class="dag-palette-item" draggable="true" data-type="parallel" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'parallel')">⚡ Parallel</div>
|
|
293
|
+
<div class="dag-palette-item" draggable="true" data-type="output" style="cursor:grab;padding:6px 10px;margin:4px 0;border-radius:6px;border:1px solid var(--border);font-size:13px;background:var(--bg)" ondragstart="dagEditor.onPaletteDrag(event,'output')">📤 Output</div>
|
|
294
|
+
<hr style="border-color:var(--border);margin:12px 0">
|
|
295
|
+
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">Actions</div>
|
|
296
|
+
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.undo()">↩ Undo</button>
|
|
297
|
+
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.redo()">↪ Redo</button>
|
|
298
|
+
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.deleteSelected()">🗑 Delete</button>
|
|
299
|
+
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.zoomIn()">🔍+ Zoom In</button>
|
|
300
|
+
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.zoomOut()">🔍- Zoom Out</button>
|
|
301
|
+
<button class="btn" style="width:100%;margin:3px 0;font-size:12px" onclick="dagEditor.fitView()">⊞ Fit</button>
|
|
302
|
+
</div>
|
|
303
|
+
<div style="flex:1;position:relative">
|
|
304
|
+
<canvas id="dag-canvas" style="width:100%;height:600px;border-radius:8px;border:1px solid var(--border);background:#0d0d0d;cursor:default"></canvas>
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
<!-- Node properties panel -->
|
|
308
|
+
<div class="card" id="dag-props" style="display:none">
|
|
309
|
+
<div class="card-title">Node Properties</div>
|
|
310
|
+
<div id="dag-props-content"></div>
|
|
311
|
+
</div>
|
|
312
|
+
<!-- Run output -->
|
|
313
|
+
<div class="card" id="dag-run-output" style="display:none">
|
|
314
|
+
<div class="card-title">▶ Execution Results</div>
|
|
315
|
+
<pre id="dag-run-results" style="font-family:var(--mono);font-size:12px;max-height:300px;overflow:auto;color:var(--text-muted)"></pre>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- Jobs -->
|
|
320
|
+
<div class="page" id="page-jobs">
|
|
321
|
+
<div class="page-header">
|
|
322
|
+
<div class="page-title">Scheduled Jobs</div>
|
|
323
|
+
<div class="page-subtitle">Cron tasks</div>
|
|
324
|
+
</div>
|
|
325
|
+
<div class="card" id="jobs-list">Loading...</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<!-- Plugins -->
|
|
329
|
+
<div class="page" id="page-plugins">
|
|
330
|
+
<div class="page-header">
|
|
331
|
+
<div class="page-title">Plugins</div>
|
|
332
|
+
<div class="page-subtitle">Middleware and extensions</div>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="card" id="plugins-list">Loading...</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<!-- Doctor -->
|
|
338
|
+
<div class="page" id="page-doctor">
|
|
339
|
+
<div class="page-header">
|
|
340
|
+
<div class="page-title">Doctor</div>
|
|
341
|
+
<div class="page-subtitle">Environment health check</div>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="card" id="doctor-results">
|
|
344
|
+
<button class="btn btn-primary" onclick="runDoctor()">Run Diagnostic</button>
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<!-- Logs -->
|
|
349
|
+
<div class="page" id="page-evals">
|
|
350
|
+
<div class="page-header">
|
|
351
|
+
<div class="page-title">🧪 Evals</div>
|
|
352
|
+
<div class="page-subtitle">Agent quality evaluation</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div class="card" id="eval-panel">
|
|
355
|
+
<div style="margin-bottom:16px">
|
|
356
|
+
<label>Suite: </label>
|
|
357
|
+
<select id="eval-suite-select" style="padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text)">
|
|
358
|
+
<option value="basic">basic</option>
|
|
359
|
+
<option value="safety">safety</option>
|
|
360
|
+
<option value="memory">memory</option>
|
|
361
|
+
</select>
|
|
362
|
+
<button class="btn btn-primary" onclick="runEval()" style="margin-left:8px">Run Suite</button>
|
|
363
|
+
</div>
|
|
364
|
+
<div id="eval-results"></div>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
<!-- Telemetry -->
|
|
369
|
+
<div class="page" id="page-telemetry">
|
|
370
|
+
<div class="page-header">
|
|
371
|
+
<div class="page-title">Telemetry</div>
|
|
372
|
+
<div class="page-subtitle">OTel-compatible tracing & metrics</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="card-grid" style="grid-template-columns: repeat(4,1fr); margin-bottom:16px">
|
|
375
|
+
<div class="card" id="tel-total-spans"><div style="font-size:11px;color:var(--text-muted)">Total Spans</div><div style="font-size:24px;font-weight:700" id="tel-stat-spans">—</div></div>
|
|
376
|
+
<div class="card" id="tel-total-traces"><div style="font-size:11px;color:var(--text-muted)">Total Traces</div><div style="font-size:24px;font-weight:700" id="tel-stat-traces">—</div></div>
|
|
377
|
+
<div class="card" id="tel-error-rate"><div style="font-size:11px;color:var(--text-muted)">Error Rate</div><div style="font-size:24px;font-weight:700" id="tel-stat-errors">—</div></div>
|
|
378
|
+
<div class="card" id="tel-p95"><div style="font-size:11px;color:var(--text-muted)">P95 Latency</div><div style="font-size:24px;font-weight:700" id="tel-stat-p95">—</div></div>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="card" style="margin-bottom:16px">
|
|
381
|
+
<h3 style="margin:0 0 12px">Recent Traces</h3>
|
|
382
|
+
<div id="tel-traces-list" style="font-family:var(--mono);font-size:12px">Loading...</div>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="card">
|
|
385
|
+
<h3 style="margin:0 0 12px">Trace Waterfall</h3>
|
|
386
|
+
<div id="tel-waterfall" style="font-family:var(--mono);font-size:12px;min-height:100px;color:var(--text-muted)">Click a trace above to view spans</div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<!-- Logs -->
|
|
391
|
+
<div class="page" id="page-logs">
|
|
392
|
+
<div class="page-header">
|
|
393
|
+
<div class="page-title">Logs</div>
|
|
394
|
+
<div class="page-subtitle">Recent agent activity</div>
|
|
395
|
+
</div>
|
|
396
|
+
<div class="card"><pre id="logs-content" style="font-family:var(--mono);font-size:12px;max-height:600px;overflow:auto;color:var(--text-muted)">Loading...</pre></div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<!-- Modules Status -->
|
|
400
|
+
<div class="page" id="page-modules">
|
|
401
|
+
<div class="page-header">
|
|
402
|
+
<div class="page-title">Modules</div>
|
|
403
|
+
<div class="page-subtitle">Sub-module status and health</div>
|
|
404
|
+
</div>
|
|
405
|
+
<div id="modules-grid" class="card-grid">Loading...</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<!-- DeepBrain Module -->
|
|
409
|
+
<div class="page" id="page-brain-module">
|
|
410
|
+
<iframe src="/brain/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<!-- AgentKits Module -->
|
|
414
|
+
<div class="page" id="page-kits-module">
|
|
415
|
+
<iframe src="/kits/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
<!-- Workstation Module -->
|
|
419
|
+
<div class="page" id="page-workstation-module">
|
|
420
|
+
<iframe src="/workstation/" style="width:100%;height:calc(100vh - 32px);border:none;border-radius:8px;"></iframe>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
<!-- Playground Page -->
|
|
424
|
+
<div class="page" id="page-playground">
|
|
425
|
+
<div class="page-header">
|
|
426
|
+
<h1 class="page-title">🎮 Playground</h1>
|
|
427
|
+
<p class="page-subtitle">Interactive chat with model selection, system prompts, and streaming</p>
|
|
428
|
+
</div>
|
|
429
|
+
<div style="display:flex;gap:16px;margin-bottom:16px;flex-wrap:wrap;align-items:flex-end;">
|
|
430
|
+
<div style="flex:1;min-width:150px;">
|
|
431
|
+
<label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px;">Model</label>
|
|
432
|
+
<select id="pg-model" style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;">
|
|
433
|
+
<option>gpt-4o</option><option>gpt-4o-mini</option><option>claude-sonnet-4</option><option>claude-haiku</option><option>gemini-2.0-flash</option><option>deepseek-v3</option>
|
|
434
|
+
</select>
|
|
435
|
+
</div>
|
|
436
|
+
<div style="flex:0 0 160px;">
|
|
437
|
+
<label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px;">Temperature: <span id="pg-temp-val">0.7</span></label>
|
|
438
|
+
<input type="range" id="pg-temp" min="0" max="2" step="0.1" value="0.7" style="width:100%;" oninput="document.getElementById('pg-temp-val').textContent=this.value">
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
<div style="margin-bottom:16px;">
|
|
442
|
+
<label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:4px;">System Prompt</label>
|
|
443
|
+
<textarea id="pg-system" rows="2" placeholder="You are a helpful assistant..." style="width:100%;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;resize:vertical;font-family:inherit;"></textarea>
|
|
444
|
+
</div>
|
|
445
|
+
<div id="pg-messages" style="flex:1;min-height:300px;max-height:50vh;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px;background:var(--bg-card);display:flex;flex-direction:column;gap:10px;">
|
|
446
|
+
<div style="color:var(--text-muted);font-size:13px;text-align:center;padding:40px;">Send a message to start chatting</div>
|
|
447
|
+
</div>
|
|
448
|
+
<div style="display:flex;gap:8px;">
|
|
449
|
+
<textarea id="pg-input" rows="2" placeholder="Type your message..." style="flex:1;padding:10px;border-radius:8px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:14px;resize:none;font-family:inherit;" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();pgSend()}"></textarea>
|
|
450
|
+
<button onclick="pgSend()" id="pg-send-btn" style="padding:10px 24px;border:none;border-radius:8px;background:var(--accent);color:#000;font-weight:600;cursor:pointer;font-size:14px;">Send</button>
|
|
451
|
+
<button onclick="pgClear()" style="padding:10px 16px;border:1px solid var(--border);border-radius:8px;background:transparent;color:var(--text-muted);cursor:pointer;font-size:13px;">Clear</button>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
</main>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<script>
|
|
459
|
+
const API = ''; // same origin
|
|
460
|
+
|
|
461
|
+
// Navigation
|
|
462
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
463
|
+
item.addEventListener('click', () => {
|
|
464
|
+
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
465
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
466
|
+
item.classList.add('active');
|
|
467
|
+
const page = item.dataset.page;
|
|
468
|
+
document.getElementById('page-' + page).classList.add('active');
|
|
469
|
+
loadPage(page);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Page loaders
|
|
474
|
+
async function loadPage(page) {
|
|
475
|
+
switch (page) {
|
|
476
|
+
case 'dashboard': loadDashboard(); break;
|
|
477
|
+
case 'config': loadConfig(); break;
|
|
478
|
+
case 'memory': loadMemory(); break;
|
|
479
|
+
case 'skills': loadSkills(); break;
|
|
480
|
+
case 'tools': loadTools(); break;
|
|
481
|
+
case 'channels': loadChannels(); break;
|
|
482
|
+
case 'workflows': loadWorkflows(); break;
|
|
483
|
+
case 'jobs': loadJobs(); break;
|
|
484
|
+
case 'plugins': loadPlugins(); break;
|
|
485
|
+
case 'protocols': loadProtocols(); break;
|
|
486
|
+
case 'logs': loadLogs(); break;
|
|
487
|
+
case 'modules': loadModules(); break;
|
|
488
|
+
case 'telemetry': loadTelemetry(); break;
|
|
489
|
+
case 'evals': break; // static page, run via button
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function api(path) {
|
|
494
|
+
const r = await fetch(API + '/api/' + path);
|
|
495
|
+
return r.json();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function apiPost(path, body) {
|
|
499
|
+
const r = await fetch(API + '/api/' + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
500
|
+
return r.json();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function apiPut(path, body) {
|
|
504
|
+
const r = await fetch(API + '/api/' + path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
505
|
+
return r.json();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Dashboard
|
|
509
|
+
async function loadDashboard() {
|
|
510
|
+
try {
|
|
511
|
+
const info = await api('agent/info');
|
|
512
|
+
document.getElementById('agent-name').textContent = info.name || '—';
|
|
513
|
+
document.getElementById('agent-model').textContent = info.model || '—';
|
|
514
|
+
document.getElementById('agent-channels').textContent = info.channels?.length || 0;
|
|
515
|
+
document.getElementById('agent-skills').textContent = info.skills?.length || 0;
|
|
516
|
+
document.getElementById('agent-status-detail').innerHTML =
|
|
517
|
+
`<span class="badge badge-green"><span class="badge-dot"></span> ${info.status || 'unknown'}</span>` +
|
|
518
|
+
` Provider: ${info.provider || '—'} Version: ${info.version || '—'}`;
|
|
519
|
+
|
|
520
|
+
const stats = await api('memory/stats');
|
|
521
|
+
document.getElementById('memory-stats').innerHTML =
|
|
522
|
+
`Pages: <strong>${stats.pages || 0}</strong> Chunks: <strong>${stats.chunks || 0}</strong>`;
|
|
523
|
+
} catch (e) {
|
|
524
|
+
document.getElementById('agent-status-detail').textContent = 'Failed to connect to agent';
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Chat
|
|
529
|
+
async function sendChat() {
|
|
530
|
+
const input = document.getElementById('chat-input');
|
|
531
|
+
const msg = input.value.trim();
|
|
532
|
+
if (!msg) return;
|
|
533
|
+
input.value = '';
|
|
534
|
+
|
|
535
|
+
const messages = document.getElementById('chat-messages');
|
|
536
|
+
messages.innerHTML += `<div class="message message-user"><div class="message-label">You</div><div class="bubble">${escapeHtml(msg)}</div></div>`;
|
|
537
|
+
messages.innerHTML += `<div class="message message-agent" id="pending"><div class="message-label">Agent</div><div class="bubble"><span class="spinner"></span> Thinking...</div></div>`;
|
|
538
|
+
messages.scrollTop = messages.scrollHeight;
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const result = await apiPost('agent/chat', { message: msg, sessionId: 'studio' });
|
|
542
|
+
document.getElementById('pending').innerHTML = `<div class="message-label">Agent</div><div class="bubble">${escapeHtml(result.response || 'No response')}</div>`;
|
|
543
|
+
document.getElementById('pending').id = '';
|
|
544
|
+
} catch (e) {
|
|
545
|
+
document.getElementById('pending').innerHTML = `<div class="message-label">Agent</div><div class="bubble" style="color:var(--red)">Error: ${e.message}</div>`;
|
|
546
|
+
document.getElementById('pending').id = '';
|
|
547
|
+
}
|
|
548
|
+
messages.scrollTop = messages.scrollHeight;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Config
|
|
552
|
+
async function loadConfig() {
|
|
553
|
+
const data = await api('agent/config');
|
|
554
|
+
document.getElementById('config-editor').value = data.content || '# No config found';
|
|
555
|
+
}
|
|
556
|
+
async function saveConfig() {
|
|
557
|
+
const content = document.getElementById('config-editor').value;
|
|
558
|
+
await apiPut('agent/config', { content });
|
|
559
|
+
alert('Config saved!');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Memory
|
|
563
|
+
async function loadMemory() {
|
|
564
|
+
const data = await api('memory/list');
|
|
565
|
+
const el = document.getElementById('memory-list');
|
|
566
|
+
if (!data.pages?.length) { el.innerHTML = '<div class="loading">No memories yet</div>'; return; }
|
|
567
|
+
el.innerHTML = data.pages.map(p => `
|
|
568
|
+
<div class="memory-item">
|
|
569
|
+
<div class="memory-slug">${escapeHtml(p.slug || p.title || '—')}</div>
|
|
570
|
+
<div class="memory-preview">${escapeHtml((p.compiled_truth || p.content || '').slice(0, 100))}</div>
|
|
571
|
+
<div class="memory-meta">Type: ${p.type || '—'} | Tags: ${(p.tags || []).join(', ') || '—'}</div>
|
|
572
|
+
</div>
|
|
573
|
+
`).join('');
|
|
574
|
+
}
|
|
575
|
+
async function searchMemory() {
|
|
576
|
+
const q = document.getElementById('memory-search').value;
|
|
577
|
+
if (!q) return loadMemory();
|
|
578
|
+
const data = await api('memory/search?q=' + encodeURIComponent(q));
|
|
579
|
+
const el = document.getElementById('memory-list');
|
|
580
|
+
if (!data.results?.length) { el.innerHTML = '<div class="loading">No results</div>'; return; }
|
|
581
|
+
el.innerHTML = data.results.map(r => `
|
|
582
|
+
<div class="memory-item">
|
|
583
|
+
<div class="memory-slug">${escapeHtml(r.slug || r.title || '—')}</div>
|
|
584
|
+
<div class="memory-preview">${escapeHtml((r.compiled_truth || r.content || '').slice(0, 100))}</div>
|
|
585
|
+
</div>
|
|
586
|
+
`).join('');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Generic list loaders
|
|
590
|
+
async function loadSkills() {
|
|
591
|
+
const data = await api('skills/list');
|
|
592
|
+
renderList('skills-list', data.skills || [], s => `<strong>${s.name}</strong> — ${s.description || '—'} (used ${s.usageCount || 0} times)`);
|
|
593
|
+
}
|
|
594
|
+
async function loadTools() {
|
|
595
|
+
const data = await api('tools/list');
|
|
596
|
+
renderList('tools-list', data.tools || [], t => `<strong>🔧 ${t.name}</strong> — ${t.description || '—'}`);
|
|
597
|
+
}
|
|
598
|
+
async function loadChannels() {
|
|
599
|
+
const data = await api('channels/list');
|
|
600
|
+
renderList('channels-list', data.channels || [], c => `<span class="badge badge-green"><span class="badge-dot"></span> ${c.type}</span> ${c.port ? 'Port ' + c.port : ''} ${c.mode || ''}`);
|
|
601
|
+
}
|
|
602
|
+
async function loadWorkflows() {
|
|
603
|
+
// Load saved workflows into dropdown
|
|
604
|
+
try {
|
|
605
|
+
const data = await api('workflows');
|
|
606
|
+
const select = document.getElementById('wf-list-select');
|
|
607
|
+
select.innerHTML = '<option value="">— New Workflow —</option>';
|
|
608
|
+
for (const wf of (data.workflows || [])) {
|
|
609
|
+
const opt = document.createElement('option');
|
|
610
|
+
opt.value = wf.id;
|
|
611
|
+
opt.textContent = wf.name || wf.id;
|
|
612
|
+
select.appendChild(opt);
|
|
613
|
+
}
|
|
614
|
+
} catch {}
|
|
615
|
+
dagEditor.initCanvas();
|
|
616
|
+
}
|
|
617
|
+
async function loadJobs() {
|
|
618
|
+
const data = await api('jobs/list');
|
|
619
|
+
renderList('jobs-list', data.jobs || [], j => `<strong>⏰ ${j.name}</strong> — <code>${j.schedule}</code> — ${j.task || '—'}`);
|
|
620
|
+
}
|
|
621
|
+
async function loadPlugins() {
|
|
622
|
+
const data = await api('plugins/list');
|
|
623
|
+
renderList('plugins-list', data.plugins || [], p => `<strong>🔌 ${p.name}</strong> ${p.config ? JSON.stringify(p.config) : ''}`);
|
|
624
|
+
}
|
|
625
|
+
async function loadProtocols() {
|
|
626
|
+
const data = await api('protocols');
|
|
627
|
+
const list = data.protocols || [];
|
|
628
|
+
const content = document.getElementById('content');
|
|
629
|
+
content.innerHTML = `<h2>📡 Protocols</h2><div class="grid">${list.map(p =>
|
|
630
|
+
`<div class="card stat"><div class="stat-value">${p.enabled ? '🟢' : '⚫'} ${p.name}</div><div class="stat-label">${p.description}</div></div>`
|
|
631
|
+
).join('')}</div>`;
|
|
632
|
+
}
|
|
633
|
+
async function loadLogs() {
|
|
634
|
+
const data = await api('logs/recent');
|
|
635
|
+
document.getElementById('logs-content').textContent = (data.lines || []).join('\n') || 'No logs';
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Modules
|
|
639
|
+
async function loadModules() {
|
|
640
|
+
const data = await api('modules');
|
|
641
|
+
const grid = document.getElementById('modules-grid');
|
|
642
|
+
if (data.modules) {
|
|
643
|
+
grid.innerHTML = data.modules.map(m => `
|
|
644
|
+
<div class="card stat" style="cursor:pointer" onclick="document.querySelector('[data-page=${m.path.replace(/\//g,'')}-module]')?.click()">
|
|
645
|
+
<div class="stat-value">${m.icon} ${m.name}</div>
|
|
646
|
+
<div class="stat-label">${m.running ? '🟢 Running on port ' + m.port : '⚫ Not running'}</div>
|
|
647
|
+
</div>`).join('');
|
|
648
|
+
} else {
|
|
649
|
+
grid.innerHTML = '<div class="card">Failed to load module status</div>';
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Telemetry
|
|
654
|
+
async function loadTelemetry() {
|
|
655
|
+
const stats = await api('telemetry/stats');
|
|
656
|
+
if (stats && !stats.error) {
|
|
657
|
+
document.getElementById('tel-stat-spans').textContent = stats.totalSpans;
|
|
658
|
+
document.getElementById('tel-stat-traces').textContent = stats.totalTraces;
|
|
659
|
+
document.getElementById('tel-stat-errors').textContent = (stats.errorRate * 100).toFixed(1) + '%';
|
|
660
|
+
document.getElementById('tel-stat-p95').textContent = stats.p95Latency.toFixed(0) + 'ms';
|
|
661
|
+
}
|
|
662
|
+
const tracesData = await api('telemetry/traces?limit=50');
|
|
663
|
+
const el = document.getElementById('tel-traces-list');
|
|
664
|
+
if (tracesData.traces && tracesData.traces.length > 0) {
|
|
665
|
+
el.innerHTML = '<table style="width:100%;border-collapse:collapse"><tr style="color:var(--text-muted);text-align:left"><th>Trace ID</th><th>Root Span</th><th>Time</th><th>Spans</th><th>Status</th></tr>' +
|
|
666
|
+
tracesData.traces.map(t => {
|
|
667
|
+
const time = new Date(t.startTime).toLocaleTimeString();
|
|
668
|
+
const statusColor = t.status === 'ok' ? '#4ade80' : t.status === 'error' ? '#f87171' : '#9ca3af';
|
|
669
|
+
return `<tr style="cursor:pointer;border-top:1px solid var(--border)" onclick="loadTraceWaterfall('${t.traceId}')"><td style="padding:6px;color:var(--accent)">${t.traceId.slice(0,12)}</td><td>${t.rootSpan}</td><td>${time}</td><td>${t.spanCount}</td><td style="color:${statusColor}">${t.status}</td></tr>`;
|
|
670
|
+
}).join('') + '</table>';
|
|
671
|
+
} else {
|
|
672
|
+
el.innerHTML = '<div style="color:var(--text-muted)">No traces yet. Enable telemetry: spec.telemetry.enabled: true</div>';
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function loadTraceWaterfall(traceId) {
|
|
677
|
+
const data = await api('telemetry/traces?id=' + traceId);
|
|
678
|
+
const el = document.getElementById('tel-waterfall');
|
|
679
|
+
if (!data.spans || data.spans.length === 0) { el.innerHTML = 'No spans found'; return; }
|
|
680
|
+
const spans = data.spans;
|
|
681
|
+
const minTime = Math.min(...spans.map(s => s.startTime));
|
|
682
|
+
const maxTime = Math.max(...spans.map(s => s.endTime || s.startTime));
|
|
683
|
+
const totalDur = maxTime - minTime || 1;
|
|
684
|
+
const depthMap = {};
|
|
685
|
+
spans.forEach(s => { depthMap[s.spanId] = s.parentSpanId && depthMap[s.parentSpanId] !== undefined ? depthMap[s.parentSpanId] + 1 : 0; });
|
|
686
|
+
el.innerHTML = spans.map(s => {
|
|
687
|
+
const left = ((s.startTime - minTime) / totalDur * 70).toFixed(1);
|
|
688
|
+
const width = Math.max(1, ((s.endTime || s.startTime) - s.startTime) / totalDur * 70).toFixed(1);
|
|
689
|
+
const color = s.status === 'ok' ? '#4ade80' : s.status === 'error' ? '#f87171' : '#60a5fa';
|
|
690
|
+
const depth = depthMap[s.spanId] || 0;
|
|
691
|
+
const dur = s.endTime ? (s.endTime - s.startTime) + 'ms' : '—';
|
|
692
|
+
return `<div style="display:flex;align-items:center;margin:2px 0;padding-left:${depth*20}px"><span style="width:180px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${s.name}</span><div style="flex:1;position:relative;height:18px;background:var(--bg-hover);border-radius:3px"><div style="position:absolute;left:${left}%;width:${width}%;height:100%;background:${color};border-radius:3px;opacity:0.8" title="${dur}"></div></div><span style="width:60px;text-align:right;flex-shrink:0;margin-left:8px">${dur}</span></div>`;
|
|
693
|
+
}).join('');
|
|
694
|
+
}
|
|
695
|
+
window.loadTraceWaterfall = loadTraceWaterfall;
|
|
696
|
+
|
|
697
|
+
// Doctor
|
|
698
|
+
async function runDoctor() {
|
|
699
|
+
const el = document.getElementById('doctor-results');
|
|
700
|
+
el.innerHTML = '<div class="loading"><span class="spinner"></span> Running checks...</div>';
|
|
701
|
+
const data = await api('doctor/check');
|
|
702
|
+
if (data.checks) {
|
|
703
|
+
el.innerHTML = data.checks.map(c => `
|
|
704
|
+
<div class="check-item">
|
|
705
|
+
<span class="check-icon">${c.ok ? '✅' : '❌'}</span>
|
|
706
|
+
<span class="check-name">${c.name}</span>
|
|
707
|
+
<span class="check-detail">${c.detail || ''}</span>
|
|
708
|
+
${c.fix ? `<span class="check-fix">→ ${c.fix}</span>` : ''}
|
|
709
|
+
</div>
|
|
710
|
+
`).join('') + `<div style="margin-top:16px"><button class="btn btn-primary" onclick="runDoctor()">Re-run</button></div>`;
|
|
711
|
+
} else {
|
|
712
|
+
el.innerHTML = `<pre>${JSON.stringify(data, null, 2)}</pre><button class="btn btn-primary" onclick="runDoctor()">Re-run</button>`;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Helpers
|
|
717
|
+
async function runEval() {
|
|
718
|
+
const suite = document.getElementById('eval-suite-select').value;
|
|
719
|
+
const el = document.getElementById('eval-results');
|
|
720
|
+
el.innerHTML = '<div class="loading"><span class="spinner"></span> Running eval...</div>';
|
|
721
|
+
try {
|
|
722
|
+
const resp = await fetch('/api/eval/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ suite }) });
|
|
723
|
+
const report = await resp.json();
|
|
724
|
+
let html = `<div style="margin-bottom:12px;font-weight:600">${report.summary || ''}</div>`;
|
|
725
|
+
html += '<table style="width:100%;border-collapse:collapse">';
|
|
726
|
+
html += '<tr style="border-bottom:1px solid var(--border)"><th style="text-align:left;padding:6px">Case</th><th>Status</th><th>Latency</th></tr>';
|
|
727
|
+
for (const r of (report.results || [])) {
|
|
728
|
+
const color = r.passed ? '#4ade80' : '#f87171';
|
|
729
|
+
html += `<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px">${r.caseId}</td><td style="color:${color};text-align:center">${r.passed ? 'PASS' : 'FAIL'}</td><td style="text-align:center">${r.scores?.latency_ms || 0}ms</td></tr>`;
|
|
730
|
+
}
|
|
731
|
+
html += '</table>';
|
|
732
|
+
el.innerHTML = html;
|
|
733
|
+
} catch (e) { el.innerHTML = `<div style="color:#f87171">Error: ${e.message}</div>`; }
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function renderList(id, items, renderFn) {
|
|
737
|
+
const el = document.getElementById(id);
|
|
738
|
+
if (!items.length) { el.innerHTML = '<div class="loading">None configured</div>'; return; }
|
|
739
|
+
el.innerHTML = '<table class="table"><tbody>' +
|
|
740
|
+
items.map(i => `<tr><td>${renderFn(i)}</td></tr>`).join('') +
|
|
741
|
+
'</tbody></table>';
|
|
742
|
+
}
|
|
743
|
+
function escapeHtml(s) {
|
|
744
|
+
return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ═══════════════════════════════════════════════
|
|
748
|
+
// DAG Visual Editor
|
|
749
|
+
// ═══════════════════════════════════════════════
|
|
750
|
+
const dagEditor = (() => {
|
|
751
|
+
let canvas, ctx;
|
|
752
|
+
let nodes = [], edges = [];
|
|
753
|
+
let selected = null, dragging = null, dragOffset = { x: 0, y: 0 };
|
|
754
|
+
let connecting = null; // { nodeId, port:'out', mx, my }
|
|
755
|
+
let pan = { x: 0, y: 0 }, zoom = 1;
|
|
756
|
+
let isPanning = false, panStart = { x: 0, y: 0 };
|
|
757
|
+
let undoStack = [], redoStack = [];
|
|
758
|
+
let workflowId = null, workflowName = 'Untitled';
|
|
759
|
+
let initialized = false;
|
|
760
|
+
const GRID = 20, NODE_W = 160, NODE_H = 60, PORT_R = 7;
|
|
761
|
+
const NODE_COLORS = {
|
|
762
|
+
input: '#22c55e', output: '#ef4444', agent: '#3b82f6',
|
|
763
|
+
tool: '#eab308', condition: '#a855f7', loop: '#f97316', parallel: '#06b6d4'
|
|
764
|
+
};
|
|
765
|
+
const NODE_ICONS = {
|
|
766
|
+
input: '📥', output: '📤', agent: '🤖', tool: '🔧',
|
|
767
|
+
condition: '❓', loop: '🔁', parallel: '⚡'
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
function snap(v) { return Math.round(v / GRID) * GRID; }
|
|
771
|
+
|
|
772
|
+
function initCanvas() {
|
|
773
|
+
canvas = document.getElementById('dag-canvas');
|
|
774
|
+
if (!canvas) return;
|
|
775
|
+
const rect = canvas.parentElement.getBoundingClientRect();
|
|
776
|
+
canvas.width = rect.width * (window.devicePixelRatio || 1);
|
|
777
|
+
canvas.height = 600 * (window.devicePixelRatio || 1);
|
|
778
|
+
canvas.style.width = rect.width + 'px';
|
|
779
|
+
canvas.style.height = '600px';
|
|
780
|
+
ctx = canvas.getContext('2d');
|
|
781
|
+
ctx.scale(window.devicePixelRatio || 1, window.devicePixelRatio || 1);
|
|
782
|
+
|
|
783
|
+
if (!initialized) {
|
|
784
|
+
canvas.addEventListener('mousedown', onMouseDown);
|
|
785
|
+
canvas.addEventListener('mousemove', onMouseMove);
|
|
786
|
+
canvas.addEventListener('mouseup', onMouseUp);
|
|
787
|
+
canvas.addEventListener('dblclick', onDblClick);
|
|
788
|
+
canvas.addEventListener('wheel', onWheel);
|
|
789
|
+
canvas.addEventListener('dragover', e => e.preventDefault());
|
|
790
|
+
canvas.addEventListener('drop', onDrop);
|
|
791
|
+
canvas.addEventListener('contextmenu', e => e.preventDefault());
|
|
792
|
+
document.addEventListener('keydown', onKey);
|
|
793
|
+
initialized = true;
|
|
794
|
+
}
|
|
795
|
+
render();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function toCanvas(e) {
|
|
799
|
+
const r = canvas.getBoundingClientRect();
|
|
800
|
+
return { x: (e.clientX - r.left - pan.x) / zoom, y: (e.clientY - r.top - pan.y) / zoom };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function nodeAt(x, y) {
|
|
804
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
805
|
+
const n = nodes[i];
|
|
806
|
+
if (x >= n.x && x <= n.x + NODE_W && y >= n.y && y <= n.y + NODE_H) return n;
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function portAt(x, y) {
|
|
812
|
+
for (const n of nodes) {
|
|
813
|
+
// Input port (left center)
|
|
814
|
+
if (n.type !== 'input') {
|
|
815
|
+
const px = n.x, py = n.y + NODE_H / 2;
|
|
816
|
+
if (Math.hypot(x - px, y - py) < PORT_R + 4) return { node: n, port: 'in' };
|
|
817
|
+
}
|
|
818
|
+
// Output port (right center)
|
|
819
|
+
if (n.type !== 'output') {
|
|
820
|
+
const px = n.x + NODE_W, py = n.y + NODE_H / 2;
|
|
821
|
+
if (Math.hypot(x - px, y - py) < PORT_R + 4) return { node: n, port: 'out' };
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function pushUndo() {
|
|
828
|
+
undoStack.push(JSON.stringify({ nodes, edges }));
|
|
829
|
+
if (undoStack.length > 50) undoStack.shift();
|
|
830
|
+
redoStack = [];
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function addNode(type, x, y) {
|
|
834
|
+
pushUndo();
|
|
835
|
+
const id = 'n' + Date.now() + Math.random().toString(36).slice(2, 6);
|
|
836
|
+
const node = { id, type, name: type.charAt(0).toUpperCase() + type.slice(1), x: snap(x), y: snap(y), config: {} };
|
|
837
|
+
if (type === 'agent') { node.config = { systemPrompt: '', model: 'gpt-4o' }; }
|
|
838
|
+
if (type === 'tool') { node.config = { toolName: '' }; }
|
|
839
|
+
if (type === 'condition') { node.config = { expression: '' }; }
|
|
840
|
+
if (type === 'loop') { node.config = { maxIterations: 10, condition: '' }; }
|
|
841
|
+
if (type === 'parallel') { node.config = { branches: [] }; }
|
|
842
|
+
nodes.push(node);
|
|
843
|
+
selected = node;
|
|
844
|
+
showProps(node);
|
|
845
|
+
render();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function addEdge(fromId, toId) {
|
|
849
|
+
if (fromId === toId) return;
|
|
850
|
+
if (edges.find(e => e.from === fromId && e.to === toId)) return;
|
|
851
|
+
pushUndo();
|
|
852
|
+
edges.push({ id: 'e' + Date.now(), from: fromId, to: toId, fromPort: 'out', toPort: 'in' });
|
|
853
|
+
render();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ── Rendering ──
|
|
857
|
+
function render() {
|
|
858
|
+
if (!ctx) return;
|
|
859
|
+
const w = canvas.width / (window.devicePixelRatio || 1);
|
|
860
|
+
const h = canvas.height / (window.devicePixelRatio || 1);
|
|
861
|
+
ctx.clearRect(0, 0, w, h);
|
|
862
|
+
ctx.save();
|
|
863
|
+
ctx.translate(pan.x, pan.y);
|
|
864
|
+
ctx.scale(zoom, zoom);
|
|
865
|
+
|
|
866
|
+
// Grid
|
|
867
|
+
ctx.strokeStyle = '#1a1a1a';
|
|
868
|
+
ctx.lineWidth = 0.5;
|
|
869
|
+
const gs = GRID;
|
|
870
|
+
const startX = Math.floor(-pan.x / zoom / gs) * gs - gs;
|
|
871
|
+
const startY = Math.floor(-pan.y / zoom / gs) * gs - gs;
|
|
872
|
+
const endX = startX + w / zoom + gs * 2;
|
|
873
|
+
const endY = startY + h / zoom + gs * 2;
|
|
874
|
+
for (let x = startX; x < endX; x += gs) { ctx.beginPath(); ctx.moveTo(x, startY); ctx.lineTo(x, endY); ctx.stroke(); }
|
|
875
|
+
for (let y = startY; y < endY; y += gs) { ctx.beginPath(); ctx.moveTo(startX, y); ctx.lineTo(endX, y); ctx.stroke(); }
|
|
876
|
+
|
|
877
|
+
// Edges
|
|
878
|
+
for (const e of edges) {
|
|
879
|
+
const from = nodes.find(n => n.id === e.from);
|
|
880
|
+
const to = nodes.find(n => n.id === e.to);
|
|
881
|
+
if (!from || !to) continue;
|
|
882
|
+
const x1 = from.x + NODE_W, y1 = from.y + NODE_H / 2;
|
|
883
|
+
const x2 = to.x, y2 = to.y + NODE_H / 2;
|
|
884
|
+
const cp = Math.abs(x2 - x1) * 0.5 + 40;
|
|
885
|
+
ctx.beginPath();
|
|
886
|
+
ctx.moveTo(x1, y1);
|
|
887
|
+
ctx.bezierCurveTo(x1 + cp, y1, x2 - cp, y2, x2, y2);
|
|
888
|
+
ctx.strokeStyle = '#555';
|
|
889
|
+
ctx.lineWidth = 2;
|
|
890
|
+
ctx.stroke();
|
|
891
|
+
// Arrow
|
|
892
|
+
const angle = Math.atan2(y2 - (y2 - 0.1), x2 - (x2 - cp * 0.1));
|
|
893
|
+
ctx.fillStyle = '#555';
|
|
894
|
+
ctx.beginPath();
|
|
895
|
+
ctx.moveTo(x2, y2);
|
|
896
|
+
ctx.lineTo(x2 - 8, y2 - 4);
|
|
897
|
+
ctx.lineTo(x2 - 8, y2 + 4);
|
|
898
|
+
ctx.fill();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Connecting line (in progress)
|
|
902
|
+
if (connecting) {
|
|
903
|
+
const from = nodes.find(n => n.id === connecting.nodeId);
|
|
904
|
+
if (from) {
|
|
905
|
+
const x1 = from.x + NODE_W, y1 = from.y + NODE_H / 2;
|
|
906
|
+
ctx.beginPath();
|
|
907
|
+
ctx.moveTo(x1, y1);
|
|
908
|
+
const cp = Math.abs(connecting.mx - x1) * 0.5 + 40;
|
|
909
|
+
ctx.bezierCurveTo(x1 + cp, y1, connecting.mx - cp, connecting.my, connecting.mx, connecting.my);
|
|
910
|
+
ctx.strokeStyle = '#3b82f6';
|
|
911
|
+
ctx.lineWidth = 2;
|
|
912
|
+
ctx.setLineDash([6, 3]);
|
|
913
|
+
ctx.stroke();
|
|
914
|
+
ctx.setLineDash([]);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Nodes
|
|
919
|
+
for (const n of nodes) {
|
|
920
|
+
const isSelected = selected && selected.id === n.id;
|
|
921
|
+
const color = NODE_COLORS[n.type] || '#666';
|
|
922
|
+
// Shadow
|
|
923
|
+
ctx.fillStyle = 'rgba(0,0,0,0.3)';
|
|
924
|
+
ctx.beginPath();
|
|
925
|
+
ctx.roundRect(n.x + 2, n.y + 2, NODE_W, NODE_H, 10);
|
|
926
|
+
ctx.fill();
|
|
927
|
+
// Body
|
|
928
|
+
ctx.fillStyle = isSelected ? '#252525' : '#1e1e1e';
|
|
929
|
+
ctx.strokeStyle = isSelected ? color : '#333';
|
|
930
|
+
ctx.lineWidth = isSelected ? 2.5 : 1;
|
|
931
|
+
ctx.beginPath();
|
|
932
|
+
ctx.roundRect(n.x, n.y, NODE_W, NODE_H, 10);
|
|
933
|
+
ctx.fill();
|
|
934
|
+
ctx.stroke();
|
|
935
|
+
// Color bar top
|
|
936
|
+
ctx.fillStyle = color;
|
|
937
|
+
ctx.beginPath();
|
|
938
|
+
ctx.roundRect(n.x, n.y, NODE_W, 4, [10, 10, 0, 0]);
|
|
939
|
+
ctx.fill();
|
|
940
|
+
// Icon + name
|
|
941
|
+
ctx.fillStyle = '#e5e5e5';
|
|
942
|
+
ctx.font = '13px -apple-system, sans-serif';
|
|
943
|
+
ctx.textBaseline = 'middle';
|
|
944
|
+
const icon = NODE_ICONS[n.type] || '⬜';
|
|
945
|
+
ctx.fillText(icon + ' ' + n.name, n.x + 12, n.y + 28);
|
|
946
|
+
// Type label
|
|
947
|
+
ctx.fillStyle = '#737373';
|
|
948
|
+
ctx.font = '10px -apple-system, sans-serif';
|
|
949
|
+
ctx.fillText(n.type, n.x + 12, n.y + 46);
|
|
950
|
+
// Input port
|
|
951
|
+
if (n.type !== 'input') {
|
|
952
|
+
ctx.beginPath();
|
|
953
|
+
ctx.arc(n.x, n.y + NODE_H / 2, PORT_R, 0, Math.PI * 2);
|
|
954
|
+
ctx.fillStyle = '#333';
|
|
955
|
+
ctx.fill();
|
|
956
|
+
ctx.strokeStyle = color;
|
|
957
|
+
ctx.lineWidth = 1.5;
|
|
958
|
+
ctx.stroke();
|
|
959
|
+
}
|
|
960
|
+
// Output port
|
|
961
|
+
if (n.type !== 'output') {
|
|
962
|
+
ctx.beginPath();
|
|
963
|
+
ctx.arc(n.x + NODE_W, n.y + NODE_H / 2, PORT_R, 0, Math.PI * 2);
|
|
964
|
+
ctx.fillStyle = '#333';
|
|
965
|
+
ctx.fill();
|
|
966
|
+
ctx.strokeStyle = color;
|
|
967
|
+
ctx.lineWidth = 1.5;
|
|
968
|
+
ctx.stroke();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
ctx.restore();
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ── Events ──
|
|
975
|
+
function onMouseDown(e) {
|
|
976
|
+
const p = toCanvas(e);
|
|
977
|
+
// Check port first
|
|
978
|
+
const port = portAt(p.x, p.y);
|
|
979
|
+
if (port && port.port === 'out') {
|
|
980
|
+
connecting = { nodeId: port.node.id, mx: p.x, my: p.y };
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const node = nodeAt(p.x, p.y);
|
|
984
|
+
if (node) {
|
|
985
|
+
selected = node;
|
|
986
|
+
dragging = node;
|
|
987
|
+
dragOffset = { x: p.x - node.x, y: p.y - node.y };
|
|
988
|
+
pushUndo();
|
|
989
|
+
showProps(node);
|
|
990
|
+
render();
|
|
991
|
+
} else {
|
|
992
|
+
selected = null;
|
|
993
|
+
hideProps();
|
|
994
|
+
isPanning = true;
|
|
995
|
+
panStart = { x: e.clientX - pan.x, y: e.clientY - pan.y };
|
|
996
|
+
render();
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function onMouseMove(e) {
|
|
1001
|
+
const p = toCanvas(e);
|
|
1002
|
+
if (connecting) {
|
|
1003
|
+
connecting.mx = p.x;
|
|
1004
|
+
connecting.my = p.y;
|
|
1005
|
+
render();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (dragging) {
|
|
1009
|
+
dragging.x = snap(p.x - dragOffset.x);
|
|
1010
|
+
dragging.y = snap(p.y - dragOffset.y);
|
|
1011
|
+
render();
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (isPanning) {
|
|
1015
|
+
pan.x = e.clientX - panStart.x;
|
|
1016
|
+
pan.y = e.clientY - panStart.y;
|
|
1017
|
+
render();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function onMouseUp(e) {
|
|
1022
|
+
if (connecting) {
|
|
1023
|
+
const p = toCanvas(e);
|
|
1024
|
+
const port = portAt(p.x, p.y);
|
|
1025
|
+
if (port && port.port === 'in' && port.node.id !== connecting.nodeId) {
|
|
1026
|
+
addEdge(connecting.nodeId, port.node.id);
|
|
1027
|
+
}
|
|
1028
|
+
connecting = null;
|
|
1029
|
+
render();
|
|
1030
|
+
}
|
|
1031
|
+
dragging = null;
|
|
1032
|
+
isPanning = false;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function onDblClick(e) {
|
|
1036
|
+
const p = toCanvas(e);
|
|
1037
|
+
const node = nodeAt(p.x, p.y);
|
|
1038
|
+
if (node) {
|
|
1039
|
+
const newName = prompt('Node name:', node.name);
|
|
1040
|
+
if (newName !== null) { pushUndo(); node.name = newName; render(); showProps(node); }
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function onWheel(e) {
|
|
1045
|
+
e.preventDefault();
|
|
1046
|
+
const r = canvas.getBoundingClientRect();
|
|
1047
|
+
const mx = e.clientX - r.left, my = e.clientY - r.top;
|
|
1048
|
+
const oldZoom = zoom;
|
|
1049
|
+
zoom *= e.deltaY < 0 ? 1.1 : 0.9;
|
|
1050
|
+
zoom = Math.max(0.2, Math.min(3, zoom));
|
|
1051
|
+
pan.x = mx - (mx - pan.x) * (zoom / oldZoom);
|
|
1052
|
+
pan.y = my - (my - pan.y) * (zoom / oldZoom);
|
|
1053
|
+
render();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function onDrop(e) {
|
|
1057
|
+
e.preventDefault();
|
|
1058
|
+
const type = e.dataTransfer.getData('node-type');
|
|
1059
|
+
if (!type) return;
|
|
1060
|
+
const p = toCanvas(e);
|
|
1061
|
+
addNode(type, p.x - NODE_W / 2, p.y - NODE_H / 2);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function onKey(e) {
|
|
1065
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
1066
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && selected) { deleteSelected(); }
|
|
1067
|
+
if (e.ctrlKey && e.key === 'z') { undo(); }
|
|
1068
|
+
if (e.ctrlKey && e.key === 'y') { redo(); }
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function showProps(node) {
|
|
1072
|
+
const panel = document.getElementById('dag-props');
|
|
1073
|
+
const content = document.getElementById('dag-props-content');
|
|
1074
|
+
panel.style.display = 'block';
|
|
1075
|
+
let html = `<div style="margin-bottom:8px"><strong>${NODE_ICONS[node.type]} ${node.name}</strong> <span style="color:var(--text-muted)">(${node.type})</span></div>`;
|
|
1076
|
+
html += `<label style="font-size:12px;color:var(--text-muted)">Name</label><br><input class="search-input" value="${escapeHtml(node.name)}" onchange="dagEditor.updateProp('${node.id}','name',this.value)" style="margin-bottom:8px;width:300px"><br>`;
|
|
1077
|
+
for (const [key, val] of Object.entries(node.config)) {
|
|
1078
|
+
html += `<label style="font-size:12px;color:var(--text-muted)">${key}</label><br>`;
|
|
1079
|
+
if (typeof val === 'number') {
|
|
1080
|
+
html += `<input class="search-input" type="number" value="${val}" onchange="dagEditor.updateConfig('${node.id}','${key}',Number(this.value))" style="margin-bottom:8px;width:300px"><br>`;
|
|
1081
|
+
} else {
|
|
1082
|
+
html += `<input class="search-input" value="${escapeHtml(String(val))}" onchange="dagEditor.updateConfig('${node.id}','${key}',this.value)" style="margin-bottom:8px;width:300px"><br>`;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
content.innerHTML = html;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function hideProps() {
|
|
1089
|
+
document.getElementById('dag-props').style.display = 'none';
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// ── Public API ──
|
|
1093
|
+
return {
|
|
1094
|
+
initCanvas,
|
|
1095
|
+
onPaletteDrag(e, type) { e.dataTransfer.setData('node-type', type); },
|
|
1096
|
+
deleteSelected() {
|
|
1097
|
+
if (!selected) return;
|
|
1098
|
+
pushUndo();
|
|
1099
|
+
edges = edges.filter(e => e.from !== selected.id && e.to !== selected.id);
|
|
1100
|
+
nodes = nodes.filter(n => n.id !== selected.id);
|
|
1101
|
+
selected = null;
|
|
1102
|
+
hideProps();
|
|
1103
|
+
render();
|
|
1104
|
+
},
|
|
1105
|
+
undo() {
|
|
1106
|
+
if (!undoStack.length) return;
|
|
1107
|
+
redoStack.push(JSON.stringify({ nodes, edges }));
|
|
1108
|
+
const state = JSON.parse(undoStack.pop());
|
|
1109
|
+
nodes = state.nodes; edges = state.edges; selected = null; hideProps(); render();
|
|
1110
|
+
},
|
|
1111
|
+
redo() {
|
|
1112
|
+
if (!redoStack.length) return;
|
|
1113
|
+
undoStack.push(JSON.stringify({ nodes, edges }));
|
|
1114
|
+
const state = JSON.parse(redoStack.pop());
|
|
1115
|
+
nodes = state.nodes; edges = state.edges; selected = null; hideProps(); render();
|
|
1116
|
+
},
|
|
1117
|
+
zoomIn() { zoom = Math.min(3, zoom * 1.2); render(); },
|
|
1118
|
+
zoomOut() { zoom = Math.max(0.2, zoom * 0.8); render(); },
|
|
1119
|
+
fitView() {
|
|
1120
|
+
if (!nodes.length) { pan = { x: 50, y: 50 }; zoom = 1; render(); return; }
|
|
1121
|
+
const minX = Math.min(...nodes.map(n => n.x));
|
|
1122
|
+
const minY = Math.min(...nodes.map(n => n.y));
|
|
1123
|
+
const maxX = Math.max(...nodes.map(n => n.x + NODE_W));
|
|
1124
|
+
const maxY = Math.max(...nodes.map(n => n.y + NODE_H));
|
|
1125
|
+
const cw = canvas.width / (window.devicePixelRatio || 1);
|
|
1126
|
+
const ch = canvas.height / (window.devicePixelRatio || 1);
|
|
1127
|
+
zoom = Math.min(cw / (maxX - minX + 100), ch / (maxY - minY + 100), 2);
|
|
1128
|
+
pan.x = (cw - (maxX + minX) * zoom) / 2;
|
|
1129
|
+
pan.y = (ch - (maxY + minY) * zoom) / 2;
|
|
1130
|
+
render();
|
|
1131
|
+
},
|
|
1132
|
+
updateProp(nodeId, key, value) {
|
|
1133
|
+
const n = nodes.find(n => n.id === nodeId);
|
|
1134
|
+
if (n) { pushUndo(); n[key] = value; render(); }
|
|
1135
|
+
},
|
|
1136
|
+
updateConfig(nodeId, key, value) {
|
|
1137
|
+
const n = nodes.find(n => n.id === nodeId);
|
|
1138
|
+
if (n) { pushUndo(); n.config[key] = value; }
|
|
1139
|
+
},
|
|
1140
|
+
async save() {
|
|
1141
|
+
const name = prompt('Workflow name:', workflowName);
|
|
1142
|
+
if (!name) return;
|
|
1143
|
+
workflowName = name;
|
|
1144
|
+
const wf = { id: workflowId || undefined, name, nodes, edges };
|
|
1145
|
+
try {
|
|
1146
|
+
const result = await apiPost('workflows', wf);
|
|
1147
|
+
workflowId = result.id;
|
|
1148
|
+
alert('Saved: ' + result.id);
|
|
1149
|
+
loadWorkflows();
|
|
1150
|
+
} catch (e) { alert('Save failed: ' + e.message); }
|
|
1151
|
+
},
|
|
1152
|
+
async loadSelected() {
|
|
1153
|
+
const id = document.getElementById('wf-list-select').value;
|
|
1154
|
+
if (!id) { nodes = []; edges = []; workflowId = null; workflowName = 'Untitled'; selected = null; hideProps(); render(); return; }
|
|
1155
|
+
try {
|
|
1156
|
+
const wf = await api('workflows/' + id);
|
|
1157
|
+
if (wf.error) { alert(wf.error); return; }
|
|
1158
|
+
nodes = wf.nodes || []; edges = wf.edges || [];
|
|
1159
|
+
workflowId = wf.id; workflowName = wf.name;
|
|
1160
|
+
selected = null; hideProps(); undoStack = []; redoStack = [];
|
|
1161
|
+
render();
|
|
1162
|
+
} catch (e) { alert('Load failed: ' + e.message); }
|
|
1163
|
+
},
|
|
1164
|
+
exportJSON() {
|
|
1165
|
+
const json = JSON.stringify({ name: workflowName, nodes, edges }, null, 2);
|
|
1166
|
+
const blob = new Blob([json], { type: 'application/json' });
|
|
1167
|
+
const a = document.createElement('a');
|
|
1168
|
+
a.href = URL.createObjectURL(blob);
|
|
1169
|
+
a.download = (workflowName || 'workflow') + '.json';
|
|
1170
|
+
a.click();
|
|
1171
|
+
},
|
|
1172
|
+
importJSON() {
|
|
1173
|
+
const input = document.createElement('input');
|
|
1174
|
+
input.type = 'file';
|
|
1175
|
+
input.accept = '.json';
|
|
1176
|
+
input.onchange = async (e) => {
|
|
1177
|
+
const file = e.target.files[0];
|
|
1178
|
+
if (!file) return;
|
|
1179
|
+
const text = await file.text();
|
|
1180
|
+
try {
|
|
1181
|
+
const wf = JSON.parse(text);
|
|
1182
|
+
pushUndo();
|
|
1183
|
+
nodes = wf.nodes || []; edges = wf.edges || [];
|
|
1184
|
+
workflowName = wf.name || 'Imported';
|
|
1185
|
+
workflowId = null; selected = null; hideProps();
|
|
1186
|
+
render();
|
|
1187
|
+
} catch { alert('Invalid JSON'); }
|
|
1188
|
+
};
|
|
1189
|
+
input.click();
|
|
1190
|
+
},
|
|
1191
|
+
async run() {
|
|
1192
|
+
if (!workflowId) { alert('Save workflow first'); return; }
|
|
1193
|
+
const outEl = document.getElementById('dag-run-output');
|
|
1194
|
+
const resEl = document.getElementById('dag-run-results');
|
|
1195
|
+
outEl.style.display = 'block';
|
|
1196
|
+
resEl.textContent = 'Running...';
|
|
1197
|
+
try {
|
|
1198
|
+
const result = await apiPost('workflows/' + workflowId + '/run', {});
|
|
1199
|
+
resEl.textContent = JSON.stringify(result, null, 2);
|
|
1200
|
+
} catch (e) { resEl.textContent = 'Error: ' + e.message; }
|
|
1201
|
+
},
|
|
1202
|
+
// For serialization tests
|
|
1203
|
+
serialize() { return { name: workflowName, nodes, edges }; },
|
|
1204
|
+
deserialize(wf) { nodes = wf.nodes || []; edges = wf.edges || []; workflowName = wf.name || ''; render(); },
|
|
1205
|
+
getNodes() { return nodes; },
|
|
1206
|
+
getEdges() { return edges; },
|
|
1207
|
+
};
|
|
1208
|
+
})();
|
|
1209
|
+
|
|
1210
|
+
// Initial load
|
|
1211
|
+
loadDashboard();
|
|
1212
|
+
|
|
1213
|
+
// ─── Playground ───────────────────────────────────
|
|
1214
|
+
const pgMessages = [];
|
|
1215
|
+
function pgAddMsg(role, text) {
|
|
1216
|
+
const el = document.createElement('div');
|
|
1217
|
+
el.style.cssText = role === 'user'
|
|
1218
|
+
? 'align-self:flex-end;background:var(--accent);color:#000;padding:8px 12px;border-radius:12px 12px 4px 12px;max-width:75%;font-size:14px;white-space:pre-wrap;'
|
|
1219
|
+
: 'align-self:flex-start;background:var(--bg-hover);padding:8px 12px;border-radius:12px 12px 12px 4px;max-width:75%;font-size:14px;white-space:pre-wrap;';
|
|
1220
|
+
el.textContent = text;
|
|
1221
|
+
const container = document.getElementById('pg-messages');
|
|
1222
|
+
// Remove placeholder
|
|
1223
|
+
if (pgMessages.length === 0) container.innerHTML = '';
|
|
1224
|
+
container.appendChild(el);
|
|
1225
|
+
container.scrollTop = container.scrollHeight;
|
|
1226
|
+
return el;
|
|
1227
|
+
}
|
|
1228
|
+
async function pgSend() {
|
|
1229
|
+
const input = document.getElementById('pg-input');
|
|
1230
|
+
const text = input.value.trim();
|
|
1231
|
+
if (!text) return;
|
|
1232
|
+
input.value = '';
|
|
1233
|
+
document.getElementById('pg-send-btn').disabled = true;
|
|
1234
|
+
pgMessages.push({ role: 'user', content: text });
|
|
1235
|
+
pgAddMsg('user', text);
|
|
1236
|
+
const el = pgAddMsg('assistant', '');
|
|
1237
|
+
let full = '';
|
|
1238
|
+
try {
|
|
1239
|
+
const res = await fetch(API + '/api/playground/chat', {
|
|
1240
|
+
method: 'POST',
|
|
1241
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1242
|
+
body: JSON.stringify({
|
|
1243
|
+
messages: pgMessages,
|
|
1244
|
+
model: document.getElementById('pg-model').value,
|
|
1245
|
+
temperature: parseFloat(document.getElementById('pg-temp').value),
|
|
1246
|
+
systemPrompt: document.getElementById('pg-system').value || undefined,
|
|
1247
|
+
}),
|
|
1248
|
+
});
|
|
1249
|
+
const reader = res.body.getReader();
|
|
1250
|
+
const decoder = new TextDecoder();
|
|
1251
|
+
let buf = '';
|
|
1252
|
+
while (true) {
|
|
1253
|
+
const { done, value } = await reader.read();
|
|
1254
|
+
if (done) break;
|
|
1255
|
+
buf += decoder.decode(value, { stream: true });
|
|
1256
|
+
const lines = buf.split('\n');
|
|
1257
|
+
buf = lines.pop() || '';
|
|
1258
|
+
for (const line of lines) {
|
|
1259
|
+
if (line.startsWith('data: ')) {
|
|
1260
|
+
const d = line.slice(6);
|
|
1261
|
+
if (d === '[DONE]') break;
|
|
1262
|
+
try { const j = JSON.parse(d); full += j.content || ''; el.textContent = full; } catch {}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
document.getElementById('pg-messages').scrollTop = document.getElementById('pg-messages').scrollHeight;
|
|
1266
|
+
}
|
|
1267
|
+
} catch (e) { full = 'Error: ' + e.message; el.textContent = full; }
|
|
1268
|
+
pgMessages.push({ role: 'assistant', content: full });
|
|
1269
|
+
document.getElementById('pg-send-btn').disabled = false;
|
|
1270
|
+
input.focus();
|
|
1271
|
+
}
|
|
1272
|
+
function pgClear() {
|
|
1273
|
+
pgMessages.length = 0;
|
|
1274
|
+
document.getElementById('pg-messages').innerHTML = '<div style="color:var(--text-muted);font-size:13px;text-align:center;padding:40px;">Send a message to start chatting</div>';
|
|
1275
|
+
}
|
|
1276
|
+
</script>
|
|
1277
|
+
</body>
|
|
1278
|
+
</html>
|