zubo 0.1.19 → 0.1.21
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 +2 -2
- package/package.json +1 -1
- package/site/docs/agents.html +2 -2
- package/site/docs/api.html +2 -2
- package/site/docs/cli.html +7 -2
- package/site/docs/config.html +92 -0
- package/site/docs/index.html +8 -6
- package/site/docs/integrations.html +3 -3
- package/site/docs/marketplace.html +9 -9
- package/site/docs/security.html +4 -4
- package/site/docs/skills.html +1 -1
- package/site/docs/webhooks.html +17 -0
- package/site/index.html +4 -4
- package/site/install.sh +11 -5
- package/src/agent/compaction.ts +20 -4
- package/src/agent/history.ts +7 -2
- package/src/agent/loop.ts +50 -18
- package/src/agent/prompts.ts +2 -0
- package/src/agent/session.ts +69 -2
- package/src/agent/summarizer.ts +223 -0
- package/src/channels/dashboard.html.ts +98 -56
- package/src/channels/telegram.ts +10 -1
- package/src/channels/webchat.ts +40 -8
- package/src/llm/claude-code.ts +1 -2
- package/src/llm/codex.ts +3 -3
- package/src/llm/factory.ts +81 -2
- package/src/llm/failover.ts +59 -4
- package/src/llm/smart-router.ts +14 -6
- package/src/memory/knowledge-graph.ts +1 -1
- package/src/memory/vector-index.ts +1 -1
- package/src/scheduler/visual-workflows.ts +1 -1
- package/src/setup-web.html.ts +1371 -0
- package/src/setup-web.ts +165 -0
- package/src/setup.ts +266 -15
- package/src/start.ts +12 -2
- package/src/tools/builtin/config-update.ts +18 -1
- package/src/tools/executor.ts +2 -2
- package/src/tools/mcp-registry.ts +12 -6
- package/src/tools/permissions.ts +2 -2
|
@@ -0,0 +1,1371 @@
|
|
|
1
|
+
// Setup wizard HTML template — local-only, served on 127.0.0.1 during `zubo setup`
|
|
2
|
+
// All innerHTML usage is with hardcoded strings and user's own form input (no external/untrusted data)
|
|
3
|
+
export const SETUP_WIZARD_HTML = `<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
8
|
+
<title>Zubo Setup</title>
|
|
9
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%237c3aed'/><path d='M50 15C52 37 63 48 85 50C63 52 52 63 50 85C48 63 37 52 15 50C37 48 48 37 50 15Z' fill='white'/></svg>">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
11
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
12
|
+
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,700;12..96,800&family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
13
|
+
<style>
|
|
14
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
--bg: #060608;
|
|
18
|
+
--bg-raised: #0e0e12;
|
|
19
|
+
--bg-surface: #111116;
|
|
20
|
+
--bg-hover: #18181f;
|
|
21
|
+
--border: #1e1e26;
|
|
22
|
+
--border-subtle: #18181f;
|
|
23
|
+
--text: #f0f0f5;
|
|
24
|
+
--text-secondary: #9595a8;
|
|
25
|
+
--text-muted: #5f5f73;
|
|
26
|
+
--accent: #7c3aed;
|
|
27
|
+
--accent-hover: #6d28d9;
|
|
28
|
+
--accent-bg: rgba(124,58,237,0.08);
|
|
29
|
+
--accent-border: rgba(124,58,237,0.2);
|
|
30
|
+
--green: #10b981;
|
|
31
|
+
--green-bg: rgba(16,185,129,0.1);
|
|
32
|
+
--red: #ef4444;
|
|
33
|
+
--red-bg: rgba(239,68,68,0.1);
|
|
34
|
+
--yellow: #f59e0b;
|
|
35
|
+
--fuchsia: #d946ef;
|
|
36
|
+
--gradient: linear-gradient(135deg, #7c3aed, #d946ef);
|
|
37
|
+
--radius: 10px;
|
|
38
|
+
--radius-lg: 14px;
|
|
39
|
+
--font: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
40
|
+
--display: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
41
|
+
--mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
body {
|
|
45
|
+
font-family: var(--font);
|
|
46
|
+
background: var(--bg);
|
|
47
|
+
color: var(--text);
|
|
48
|
+
min-height: 100vh;
|
|
49
|
+
display: flex;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
align-items: flex-start;
|
|
52
|
+
padding: 40px 20px;
|
|
53
|
+
-webkit-font-smoothing: antialiased;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.wizard {
|
|
57
|
+
width: 100%;
|
|
58
|
+
max-width: 720px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Header */
|
|
62
|
+
.header {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 10px;
|
|
66
|
+
margin-bottom: 32px;
|
|
67
|
+
}
|
|
68
|
+
.header svg { width: 28px; height: 28px; flex-shrink: 0; }
|
|
69
|
+
.header h1 {
|
|
70
|
+
font-family: var(--display);
|
|
71
|
+
font-size: 20px;
|
|
72
|
+
font-weight: 700;
|
|
73
|
+
letter-spacing: -0.3px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Stepper */
|
|
77
|
+
.stepper {
|
|
78
|
+
display: flex;
|
|
79
|
+
gap: 4px;
|
|
80
|
+
margin-bottom: 32px;
|
|
81
|
+
padding: 6px;
|
|
82
|
+
background: var(--bg-raised);
|
|
83
|
+
border-radius: var(--radius);
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
}
|
|
86
|
+
.stepper-item {
|
|
87
|
+
flex: 1;
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 8px;
|
|
91
|
+
padding: 10px 14px;
|
|
92
|
+
border-radius: 8px;
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
font-weight: 500;
|
|
95
|
+
color: var(--text-muted);
|
|
96
|
+
transition: all 300ms ease;
|
|
97
|
+
cursor: default;
|
|
98
|
+
}
|
|
99
|
+
.stepper-item.active {
|
|
100
|
+
background: var(--bg-surface);
|
|
101
|
+
color: var(--text);
|
|
102
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
103
|
+
}
|
|
104
|
+
.stepper-item.done { color: var(--green); }
|
|
105
|
+
.stepper-dot {
|
|
106
|
+
width: 20px;
|
|
107
|
+
height: 20px;
|
|
108
|
+
border-radius: 50%;
|
|
109
|
+
border: 2px solid var(--border);
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
font-size: 10px;
|
|
114
|
+
font-weight: 700;
|
|
115
|
+
flex-shrink: 0;
|
|
116
|
+
transition: all 300ms ease;
|
|
117
|
+
}
|
|
118
|
+
.stepper-item.active .stepper-dot {
|
|
119
|
+
border-color: var(--accent);
|
|
120
|
+
background: var(--accent);
|
|
121
|
+
color: white;
|
|
122
|
+
}
|
|
123
|
+
.stepper-item.done .stepper-dot {
|
|
124
|
+
border-color: var(--green);
|
|
125
|
+
background: var(--green);
|
|
126
|
+
color: white;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Step panel */
|
|
130
|
+
.step-panel {
|
|
131
|
+
background: var(--bg-raised);
|
|
132
|
+
border: 1px solid var(--border);
|
|
133
|
+
border-radius: var(--radius-lg);
|
|
134
|
+
padding: 32px;
|
|
135
|
+
min-height: 340px;
|
|
136
|
+
position: relative;
|
|
137
|
+
}
|
|
138
|
+
.step-content {
|
|
139
|
+
transition: opacity 300ms ease, transform 300ms ease;
|
|
140
|
+
}
|
|
141
|
+
.step-content.entering {
|
|
142
|
+
opacity: 0;
|
|
143
|
+
transform: translateY(12px);
|
|
144
|
+
}
|
|
145
|
+
.step-title {
|
|
146
|
+
font-family: var(--display);
|
|
147
|
+
font-size: 18px;
|
|
148
|
+
font-weight: 700;
|
|
149
|
+
margin-bottom: 6px;
|
|
150
|
+
}
|
|
151
|
+
.step-desc {
|
|
152
|
+
font-size: 13px;
|
|
153
|
+
color: var(--text-secondary);
|
|
154
|
+
margin-bottom: 24px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Navigation */
|
|
158
|
+
.nav-bar {
|
|
159
|
+
display: flex;
|
|
160
|
+
justify-content: space-between;
|
|
161
|
+
margin-top: 24px;
|
|
162
|
+
}
|
|
163
|
+
.btn {
|
|
164
|
+
font-family: var(--font);
|
|
165
|
+
font-size: 13px;
|
|
166
|
+
font-weight: 600;
|
|
167
|
+
padding: 10px 24px;
|
|
168
|
+
border-radius: var(--radius);
|
|
169
|
+
border: 1px solid var(--border);
|
|
170
|
+
background: var(--bg-surface);
|
|
171
|
+
color: var(--text);
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
transition: all 200ms ease;
|
|
174
|
+
}
|
|
175
|
+
.btn:hover { background: var(--bg-hover); }
|
|
176
|
+
.btn-primary {
|
|
177
|
+
background: var(--accent);
|
|
178
|
+
border-color: var(--accent);
|
|
179
|
+
color: white;
|
|
180
|
+
}
|
|
181
|
+
.btn-primary:hover { background: var(--accent-hover); }
|
|
182
|
+
.btn-primary:disabled {
|
|
183
|
+
opacity: 0.4;
|
|
184
|
+
cursor: not-allowed;
|
|
185
|
+
}
|
|
186
|
+
.btn-ghost {
|
|
187
|
+
background: transparent;
|
|
188
|
+
border-color: transparent;
|
|
189
|
+
color: var(--text-secondary);
|
|
190
|
+
}
|
|
191
|
+
.btn-ghost:hover { color: var(--text); background: var(--bg-hover); }
|
|
192
|
+
|
|
193
|
+
/* Provider grid */
|
|
194
|
+
.provider-grid {
|
|
195
|
+
display: grid;
|
|
196
|
+
grid-template-columns: repeat(3, 1fr);
|
|
197
|
+
gap: 8px;
|
|
198
|
+
margin-bottom: 20px;
|
|
199
|
+
}
|
|
200
|
+
.provider-card {
|
|
201
|
+
padding: 14px;
|
|
202
|
+
background: var(--bg-surface);
|
|
203
|
+
border: 1px solid var(--border);
|
|
204
|
+
border-radius: var(--radius);
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
transition: all 200ms ease;
|
|
207
|
+
text-align: center;
|
|
208
|
+
font-size: 13px;
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
}
|
|
211
|
+
.provider-card:hover {
|
|
212
|
+
border-color: var(--accent-border);
|
|
213
|
+
background: var(--accent-bg);
|
|
214
|
+
}
|
|
215
|
+
.provider-card.selected {
|
|
216
|
+
border-color: var(--accent);
|
|
217
|
+
background: var(--accent-bg);
|
|
218
|
+
box-shadow: 0 0 0 1px var(--accent);
|
|
219
|
+
}
|
|
220
|
+
.provider-card .provider-icon {
|
|
221
|
+
font-size: 22px;
|
|
222
|
+
margin-bottom: 6px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Provider form */
|
|
226
|
+
.provider-form {
|
|
227
|
+
background: var(--bg-surface);
|
|
228
|
+
border: 1px solid var(--border);
|
|
229
|
+
border-radius: var(--radius);
|
|
230
|
+
padding: 20px;
|
|
231
|
+
margin-top: 16px;
|
|
232
|
+
animation: slideDown 200ms ease;
|
|
233
|
+
}
|
|
234
|
+
@keyframes slideDown {
|
|
235
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
236
|
+
to { opacity: 1; transform: translateY(0); }
|
|
237
|
+
}
|
|
238
|
+
.form-group { margin-bottom: 14px; }
|
|
239
|
+
.form-group:last-child { margin-bottom: 0; }
|
|
240
|
+
.form-label {
|
|
241
|
+
display: block;
|
|
242
|
+
font-size: 12px;
|
|
243
|
+
font-weight: 600;
|
|
244
|
+
color: var(--text-secondary);
|
|
245
|
+
margin-bottom: 6px;
|
|
246
|
+
text-transform: uppercase;
|
|
247
|
+
letter-spacing: 0.5px;
|
|
248
|
+
}
|
|
249
|
+
.form-input {
|
|
250
|
+
width: 100%;
|
|
251
|
+
padding: 10px 12px;
|
|
252
|
+
background: var(--bg);
|
|
253
|
+
border: 1px solid var(--border);
|
|
254
|
+
border-radius: 8px;
|
|
255
|
+
color: var(--text);
|
|
256
|
+
font-family: var(--mono);
|
|
257
|
+
font-size: 13px;
|
|
258
|
+
outline: none;
|
|
259
|
+
transition: border-color 200ms ease;
|
|
260
|
+
}
|
|
261
|
+
.form-input:focus { border-color: var(--accent); }
|
|
262
|
+
.form-input::placeholder { color: var(--text-muted); }
|
|
263
|
+
textarea.form-input { resize: vertical; min-height: 80px; font-family: var(--font); }
|
|
264
|
+
|
|
265
|
+
.form-hint {
|
|
266
|
+
font-size: 11px;
|
|
267
|
+
color: var(--text-muted);
|
|
268
|
+
margin-top: 4px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Test connection button + status */
|
|
272
|
+
.test-row {
|
|
273
|
+
display: flex;
|
|
274
|
+
align-items: center;
|
|
275
|
+
gap: 12px;
|
|
276
|
+
margin-top: 14px;
|
|
277
|
+
}
|
|
278
|
+
.test-status {
|
|
279
|
+
font-size: 13px;
|
|
280
|
+
font-weight: 500;
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: 6px;
|
|
284
|
+
}
|
|
285
|
+
.test-status.success { color: var(--green); }
|
|
286
|
+
.test-status.error { color: var(--red); }
|
|
287
|
+
.test-status .spinner {
|
|
288
|
+
width: 14px;
|
|
289
|
+
height: 14px;
|
|
290
|
+
border: 2px solid var(--border);
|
|
291
|
+
border-top-color: var(--accent);
|
|
292
|
+
border-radius: 50%;
|
|
293
|
+
animation: spin 0.6s linear infinite;
|
|
294
|
+
}
|
|
295
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
296
|
+
|
|
297
|
+
/* Local model list */
|
|
298
|
+
.model-list {
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-wrap: wrap;
|
|
301
|
+
gap: 6px;
|
|
302
|
+
margin-top: 8px;
|
|
303
|
+
}
|
|
304
|
+
.model-tag {
|
|
305
|
+
padding: 5px 10px;
|
|
306
|
+
background: var(--bg);
|
|
307
|
+
border: 1px solid var(--border);
|
|
308
|
+
border-radius: 6px;
|
|
309
|
+
font-size: 12px;
|
|
310
|
+
font-family: var(--mono);
|
|
311
|
+
cursor: pointer;
|
|
312
|
+
transition: all 200ms ease;
|
|
313
|
+
}
|
|
314
|
+
.model-tag:hover, .model-tag.selected {
|
|
315
|
+
border-color: var(--accent);
|
|
316
|
+
background: var(--accent-bg);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Fallback toggle */
|
|
320
|
+
.toggle-row {
|
|
321
|
+
display: flex;
|
|
322
|
+
align-items: center;
|
|
323
|
+
gap: 10px;
|
|
324
|
+
margin-top: 16px;
|
|
325
|
+
padding: 12px;
|
|
326
|
+
background: var(--bg-surface);
|
|
327
|
+
border: 1px solid var(--border);
|
|
328
|
+
border-radius: var(--radius);
|
|
329
|
+
}
|
|
330
|
+
.toggle-row label { font-size: 13px; cursor: pointer; }
|
|
331
|
+
.toggle-switch {
|
|
332
|
+
position: relative;
|
|
333
|
+
width: 36px;
|
|
334
|
+
height: 20px;
|
|
335
|
+
flex-shrink: 0;
|
|
336
|
+
}
|
|
337
|
+
.toggle-switch input { display: none; }
|
|
338
|
+
.toggle-slider {
|
|
339
|
+
position: absolute;
|
|
340
|
+
inset: 0;
|
|
341
|
+
background: var(--border);
|
|
342
|
+
border-radius: 10px;
|
|
343
|
+
cursor: pointer;
|
|
344
|
+
transition: background 200ms ease;
|
|
345
|
+
}
|
|
346
|
+
.toggle-slider::after {
|
|
347
|
+
content: '';
|
|
348
|
+
position: absolute;
|
|
349
|
+
left: 2px;
|
|
350
|
+
top: 2px;
|
|
351
|
+
width: 16px;
|
|
352
|
+
height: 16px;
|
|
353
|
+
background: var(--text);
|
|
354
|
+
border-radius: 50%;
|
|
355
|
+
transition: transform 200ms ease;
|
|
356
|
+
}
|
|
357
|
+
.toggle-switch input:checked + .toggle-slider { background: var(--accent); }
|
|
358
|
+
.toggle-switch input:checked + .toggle-slider::after { transform: translateX(16px); }
|
|
359
|
+
|
|
360
|
+
/* Channel cards */
|
|
361
|
+
.channel-grid {
|
|
362
|
+
display: grid;
|
|
363
|
+
grid-template-columns: repeat(2, 1fr);
|
|
364
|
+
gap: 8px;
|
|
365
|
+
margin-bottom: 16px;
|
|
366
|
+
}
|
|
367
|
+
.channel-card {
|
|
368
|
+
padding: 14px 16px;
|
|
369
|
+
background: var(--bg-surface);
|
|
370
|
+
border: 1px solid var(--border);
|
|
371
|
+
border-radius: var(--radius);
|
|
372
|
+
cursor: pointer;
|
|
373
|
+
transition: all 200ms ease;
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
gap: 10px;
|
|
377
|
+
}
|
|
378
|
+
.channel-card:hover {
|
|
379
|
+
border-color: var(--accent-border);
|
|
380
|
+
}
|
|
381
|
+
.channel-card.selected {
|
|
382
|
+
border-color: var(--accent);
|
|
383
|
+
background: var(--accent-bg);
|
|
384
|
+
}
|
|
385
|
+
.channel-card.locked {
|
|
386
|
+
opacity: 0.6;
|
|
387
|
+
cursor: default;
|
|
388
|
+
}
|
|
389
|
+
.channel-card .ch-icon { font-size: 18px; }
|
|
390
|
+
.channel-card .ch-name { font-size: 13px; font-weight: 500; }
|
|
391
|
+
.channel-card .ch-badge {
|
|
392
|
+
margin-left: auto;
|
|
393
|
+
font-size: 10px;
|
|
394
|
+
padding: 2px 8px;
|
|
395
|
+
border-radius: 4px;
|
|
396
|
+
background: var(--green-bg);
|
|
397
|
+
color: var(--green);
|
|
398
|
+
font-weight: 600;
|
|
399
|
+
text-transform: uppercase;
|
|
400
|
+
letter-spacing: 0.5px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* Channel config forms */
|
|
404
|
+
.channel-config {
|
|
405
|
+
background: var(--bg-surface);
|
|
406
|
+
border: 1px solid var(--border);
|
|
407
|
+
border-radius: var(--radius);
|
|
408
|
+
padding: 16px;
|
|
409
|
+
margin-top: 8px;
|
|
410
|
+
animation: slideDown 200ms ease;
|
|
411
|
+
}
|
|
412
|
+
.channel-config h4 {
|
|
413
|
+
font-size: 13px;
|
|
414
|
+
font-weight: 600;
|
|
415
|
+
margin-bottom: 12px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* Smart routing */
|
|
419
|
+
.routing-card {
|
|
420
|
+
background: var(--bg-surface);
|
|
421
|
+
border: 1px solid var(--border);
|
|
422
|
+
border-radius: var(--radius);
|
|
423
|
+
padding: 20px;
|
|
424
|
+
}
|
|
425
|
+
.routing-card p {
|
|
426
|
+
font-size: 13px;
|
|
427
|
+
color: var(--text-secondary);
|
|
428
|
+
line-height: 1.6;
|
|
429
|
+
margin-bottom: 16px;
|
|
430
|
+
}
|
|
431
|
+
.routing-option {
|
|
432
|
+
padding: 12px 14px;
|
|
433
|
+
border: 1px solid var(--border);
|
|
434
|
+
border-radius: 8px;
|
|
435
|
+
margin-bottom: 8px;
|
|
436
|
+
cursor: pointer;
|
|
437
|
+
transition: all 200ms ease;
|
|
438
|
+
display: flex;
|
|
439
|
+
align-items: center;
|
|
440
|
+
gap: 10px;
|
|
441
|
+
}
|
|
442
|
+
.routing-option:hover { border-color: var(--accent-border); }
|
|
443
|
+
.routing-option.selected {
|
|
444
|
+
border-color: var(--accent);
|
|
445
|
+
background: var(--accent-bg);
|
|
446
|
+
}
|
|
447
|
+
.routing-option .ro-label { font-size: 13px; font-weight: 500; }
|
|
448
|
+
.routing-option .ro-desc { font-size: 12px; color: var(--text-secondary); }
|
|
449
|
+
.routing-radio {
|
|
450
|
+
width: 16px;
|
|
451
|
+
height: 16px;
|
|
452
|
+
border: 2px solid var(--border);
|
|
453
|
+
border-radius: 50%;
|
|
454
|
+
flex-shrink: 0;
|
|
455
|
+
display: flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
justify-content: center;
|
|
458
|
+
}
|
|
459
|
+
.routing-option.selected .routing-radio {
|
|
460
|
+
border-color: var(--accent);
|
|
461
|
+
}
|
|
462
|
+
.routing-option.selected .routing-radio::after {
|
|
463
|
+
content: '';
|
|
464
|
+
width: 8px;
|
|
465
|
+
height: 8px;
|
|
466
|
+
background: var(--accent);
|
|
467
|
+
border-radius: 50%;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* Completion screen */
|
|
471
|
+
.completion {
|
|
472
|
+
text-align: center;
|
|
473
|
+
padding: 24px 0;
|
|
474
|
+
}
|
|
475
|
+
.completion .checkmark {
|
|
476
|
+
width: 64px;
|
|
477
|
+
height: 64px;
|
|
478
|
+
border-radius: 50%;
|
|
479
|
+
background: var(--green-bg);
|
|
480
|
+
display: flex;
|
|
481
|
+
align-items: center;
|
|
482
|
+
justify-content: center;
|
|
483
|
+
margin: 0 auto 20px;
|
|
484
|
+
font-size: 28px;
|
|
485
|
+
animation: popIn 400ms ease;
|
|
486
|
+
}
|
|
487
|
+
@keyframes popIn {
|
|
488
|
+
0% { transform: scale(0); }
|
|
489
|
+
70% { transform: scale(1.1); }
|
|
490
|
+
100% { transform: scale(1); }
|
|
491
|
+
}
|
|
492
|
+
.completion h2 {
|
|
493
|
+
font-family: var(--display);
|
|
494
|
+
font-size: 22px;
|
|
495
|
+
font-weight: 700;
|
|
496
|
+
margin-bottom: 8px;
|
|
497
|
+
}
|
|
498
|
+
.completion .sub { color: var(--text-secondary); font-size: 14px; margin-bottom: 24px; }
|
|
499
|
+
|
|
500
|
+
.summary-grid {
|
|
501
|
+
display: grid;
|
|
502
|
+
grid-template-columns: auto 1fr;
|
|
503
|
+
gap: 8px 16px;
|
|
504
|
+
text-align: left;
|
|
505
|
+
background: var(--bg-surface);
|
|
506
|
+
border: 1px solid var(--border);
|
|
507
|
+
border-radius: var(--radius);
|
|
508
|
+
padding: 20px;
|
|
509
|
+
margin-bottom: 24px;
|
|
510
|
+
font-size: 13px;
|
|
511
|
+
}
|
|
512
|
+
.summary-grid .label {
|
|
513
|
+
color: var(--text-secondary);
|
|
514
|
+
font-weight: 500;
|
|
515
|
+
}
|
|
516
|
+
.summary-grid .value {
|
|
517
|
+
font-family: var(--mono);
|
|
518
|
+
font-size: 12px;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.next-steps {
|
|
522
|
+
text-align: left;
|
|
523
|
+
background: var(--bg-surface);
|
|
524
|
+
border: 1px solid var(--border);
|
|
525
|
+
border-radius: var(--radius);
|
|
526
|
+
padding: 20px;
|
|
527
|
+
font-size: 13px;
|
|
528
|
+
color: var(--text-secondary);
|
|
529
|
+
line-height: 1.8;
|
|
530
|
+
}
|
|
531
|
+
.next-steps code {
|
|
532
|
+
font-family: var(--mono);
|
|
533
|
+
font-size: 12px;
|
|
534
|
+
background: var(--bg);
|
|
535
|
+
padding: 2px 6px;
|
|
536
|
+
border-radius: 4px;
|
|
537
|
+
color: var(--text);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.loading-overlay {
|
|
541
|
+
position: absolute;
|
|
542
|
+
inset: 0;
|
|
543
|
+
background: rgba(6,6,8,0.85);
|
|
544
|
+
border-radius: var(--radius-lg);
|
|
545
|
+
display: flex;
|
|
546
|
+
flex-direction: column;
|
|
547
|
+
align-items: center;
|
|
548
|
+
justify-content: center;
|
|
549
|
+
gap: 16px;
|
|
550
|
+
z-index: 10;
|
|
551
|
+
}
|
|
552
|
+
.loading-overlay .spinner-lg {
|
|
553
|
+
width: 32px;
|
|
554
|
+
height: 32px;
|
|
555
|
+
border: 3px solid var(--border);
|
|
556
|
+
border-top-color: var(--accent);
|
|
557
|
+
border-radius: 50%;
|
|
558
|
+
animation: spin 0.7s linear infinite;
|
|
559
|
+
}
|
|
560
|
+
.loading-overlay span { font-size: 14px; color: var(--text-secondary); }
|
|
561
|
+
|
|
562
|
+
/* Detect status badges */
|
|
563
|
+
.detect-badge {
|
|
564
|
+
display: inline-flex;
|
|
565
|
+
align-items: center;
|
|
566
|
+
gap: 5px;
|
|
567
|
+
padding: 4px 10px;
|
|
568
|
+
border-radius: 6px;
|
|
569
|
+
font-size: 12px;
|
|
570
|
+
font-weight: 500;
|
|
571
|
+
margin-bottom: 10px;
|
|
572
|
+
}
|
|
573
|
+
.detect-badge.found { background: var(--green-bg); color: var(--green); }
|
|
574
|
+
.detect-badge.not-found { background: var(--red-bg); color: var(--red); }
|
|
575
|
+
|
|
576
|
+
@media (max-width: 600px) {
|
|
577
|
+
.provider-grid { grid-template-columns: repeat(2, 1fr); }
|
|
578
|
+
.channel-grid { grid-template-columns: 1fr; }
|
|
579
|
+
.stepper-item span { display: none; }
|
|
580
|
+
}
|
|
581
|
+
</style>
|
|
582
|
+
</head>
|
|
583
|
+
<body>
|
|
584
|
+
<div class="wizard">
|
|
585
|
+
<div class="header">
|
|
586
|
+
<svg viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#7c3aed"/><path d="M50 15C52 37 63 48 85 50C63 52 52 63 50 85C48 63 37 52 15 50C37 48 48 37 50 15Z" fill="white"/></svg>
|
|
587
|
+
<h1>Zubo Setup</h1>
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
<div class="stepper" id="stepper">
|
|
591
|
+
<div class="stepper-item active" data-step="1"><div class="stepper-dot">1</div><span>AI Provider</span></div>
|
|
592
|
+
<div class="stepper-item" data-step="2"><div class="stepper-dot">2</div><span>Channels</span></div>
|
|
593
|
+
<div class="stepper-item" data-step="3"><div class="stepper-dot">3</div><span>Personalize</span></div>
|
|
594
|
+
<div class="stepper-item" data-step="4"><div class="stepper-dot">4</div><span>Cost Savings</span></div>
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
<div class="step-panel" id="stepPanel">
|
|
598
|
+
<div class="step-content" id="stepContent"></div>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
<div class="nav-bar" id="navBar">
|
|
602
|
+
<button class="btn btn-ghost" id="backBtn" onclick="prevStep()">Back</button>
|
|
603
|
+
<button class="btn btn-primary" id="nextBtn" onclick="nextStep()">Next</button>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<script>
|
|
608
|
+
// All data here is hardcoded — no external/untrusted input flows into DOM rendering.
|
|
609
|
+
const PROVIDERS = [
|
|
610
|
+
{ id: 'anthropic', name: 'Anthropic', icon: '\\u{1F9E0}', keyPrefix: 'sk-ant-', defaultModel: 'claude-sonnet-4-5-20250929', keyLabel: 'API Key', keyPlaceholder: 'sk-ant-...' },
|
|
611
|
+
{ id: 'openai', name: 'OpenAI', icon: '\\u{1F916}', keyPrefix: 'sk-', defaultModel: 'gpt-4.1', keyLabel: 'API Key', keyPlaceholder: 'sk-...' },
|
|
612
|
+
{ id: 'ollama', name: 'Ollama', icon: '\\u{1F999}', local: true, type: 'ollama', defaultModel: 'llama3.3' },
|
|
613
|
+
{ id: 'groq', name: 'Groq', icon: '\\u{26A1}', keyPrefix: 'gsk_', defaultModel: 'llama-3.3-70b-versatile', keyLabel: 'API Key', keyPlaceholder: 'gsk_...' },
|
|
614
|
+
{ id: 'together', name: 'Together', icon: '\\u{1F91D}', defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', keyLabel: 'API Key', keyPlaceholder: 'API key', baseUrl: 'https://api.together.xyz/v1' },
|
|
615
|
+
{ id: 'openrouter', name: 'OpenRouter', icon: '\\u{1F500}', defaultModel: 'anthropic/claude-sonnet-4-5', keyLabel: 'API Key', keyPlaceholder: 'API key', baseUrl: 'https://openrouter.ai/api/v1' },
|
|
616
|
+
{ id: 'deepseek', name: 'DeepSeek', icon: '\\u{1F50D}', defaultModel: 'deepseek-chat', keyLabel: 'API Key', keyPlaceholder: 'API key', baseUrl: 'https://api.deepseek.com/v1' },
|
|
617
|
+
{ id: 'xai', name: 'xAI (Grok)', icon: '\\u{2716}', defaultModel: 'grok-4.1-fast', keyLabel: 'API Key', keyPlaceholder: 'API key', baseUrl: 'https://api.x.ai/v1' },
|
|
618
|
+
{ id: 'minimax', name: 'MiniMax', icon: '\\u{303D}', defaultModel: 'MiniMax-M2.5', keyLabel: 'API Key', keyPlaceholder: 'API key' },
|
|
619
|
+
{ id: 'fireworks', name: 'Fireworks', icon: '\\u{1F386}', defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct', keyLabel: 'API Key', keyPlaceholder: 'API key', baseUrl: 'https://api.fireworks.ai/inference/v1' },
|
|
620
|
+
{ id: 'cerebras', name: 'Cerebras', icon: '\\u{1F9EA}', defaultModel: 'llama-3.3-70b', keyLabel: 'API Key', keyPlaceholder: 'API key', baseUrl: 'https://api.cerebras.ai/v1' },
|
|
621
|
+
{ id: 'lmstudio', name: 'LM Studio', icon: '\\u{1F5A5}', local: true, type: 'lmstudio', defaultModel: 'default' },
|
|
622
|
+
{ id: 'claude-code', name: 'Claude Code', icon: '\\u{1F4BB}', cli: true, cliTool: 'claude' },
|
|
623
|
+
{ id: 'codex', name: 'Codex', icon: '\\u{1F4DD}', cli: true, cliTool: 'codex' },
|
|
624
|
+
{ id: 'custom', name: 'Custom', icon: '\\u{1F527}', custom: true, defaultModel: 'default' },
|
|
625
|
+
];
|
|
626
|
+
|
|
627
|
+
const CHANNELS = [
|
|
628
|
+
{ id: 'telegram', name: 'Telegram', icon: '\\u{2708}', fields: [{key:'botToken',label:'Bot Token',placeholder:'123456:ABC-DEF...',hint:'Get from @BotFather on Telegram'}] },
|
|
629
|
+
{ id: 'discord', name: 'Discord', icon: '\\u{1F3AE}', fields: [{key:'botToken',label:'Bot Token',placeholder:'Bot token',hint:'discord.com/developers \\u{2192} Bot \\u{2192} Copy Token'}] },
|
|
630
|
+
{ id: 'slack', name: 'Slack', icon: '\\u{1F4AC}', fields: [{key:'botToken',label:'Bot Token (xoxb-)',placeholder:'xoxb-...',hint:''},{key:'appToken',label:'App Token (xapp-)',placeholder:'xapp-...',hint:'api.slack.com/apps \\u{2192} Socket Mode'}] },
|
|
631
|
+
{ id: 'whatsapp', name: 'WhatsApp', icon: '\\u{1F4F1}', fields: [], hint: 'QR code scan required on first start.' },
|
|
632
|
+
{ id: 'signal', name: 'Signal', icon: '\\u{1F512}', fields: [{key:'phoneNumber',label:'Phone Number',placeholder:'+1234567890',hint:'signal-cli must be installed and registered'}] },
|
|
633
|
+
{ id: 'email', name: 'Email', icon: '\\u{2709}', fields: [
|
|
634
|
+
{key:'imap.host',label:'IMAP Host',placeholder:'imap.gmail.com',hint:''},
|
|
635
|
+
{key:'imap.user',label:'IMAP User',placeholder:'you@gmail.com',hint:''},
|
|
636
|
+
{key:'imap.password',label:'IMAP Password',placeholder:'App password',hint:'For Gmail use myaccount.google.com/apppasswords'},
|
|
637
|
+
{key:'smtp.host',label:'SMTP Host',placeholder:'smtp.gmail.com',hint:''},
|
|
638
|
+
{key:'smtp.user',label:'SMTP User',placeholder:'you@gmail.com',hint:''},
|
|
639
|
+
{key:'smtp.password',label:'SMTP Password',placeholder:'App password',hint:''},
|
|
640
|
+
]},
|
|
641
|
+
];
|
|
642
|
+
|
|
643
|
+
// ── State ──
|
|
644
|
+
const state = {
|
|
645
|
+
providers: {},
|
|
646
|
+
activeProvider: '',
|
|
647
|
+
failover: [],
|
|
648
|
+
channels: { webchat: { enabled: true, port: 0 } },
|
|
649
|
+
agentName: 'Zubo',
|
|
650
|
+
personality: '',
|
|
651
|
+
smartRouting: { enabled: false },
|
|
652
|
+
showFallback: false,
|
|
653
|
+
fallbackProvider: '',
|
|
654
|
+
routingChoice: 'skip',
|
|
655
|
+
};
|
|
656
|
+
let currentStep = 1;
|
|
657
|
+
let completing = false;
|
|
658
|
+
|
|
659
|
+
// ── Helpers ──
|
|
660
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
661
|
+
|
|
662
|
+
function setHTML(el, html) {
|
|
663
|
+
// All HTML here is built from hardcoded constants above — safe for local-only wizard
|
|
664
|
+
if (typeof el === 'string') el = document.getElementById(el);
|
|
665
|
+
if (el) el.innerHTML = html;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ── Step rendering ──
|
|
669
|
+
function renderStep() {
|
|
670
|
+
const el = document.getElementById('stepContent');
|
|
671
|
+
el.classList.add('entering');
|
|
672
|
+
setTimeout(function() {
|
|
673
|
+
switch (currentStep) {
|
|
674
|
+
case 1: renderProvider(); break;
|
|
675
|
+
case 2: renderChannels(); break;
|
|
676
|
+
case 3: renderAgent(); break;
|
|
677
|
+
case 4: renderRouting(); break;
|
|
678
|
+
case 5: renderComplete(); break;
|
|
679
|
+
}
|
|
680
|
+
requestAnimationFrame(function() { el.classList.remove('entering'); });
|
|
681
|
+
}, 150);
|
|
682
|
+
updateStepper();
|
|
683
|
+
updateNav();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function updateStepper() {
|
|
687
|
+
document.querySelectorAll('.stepper-item').forEach(function(el) {
|
|
688
|
+
var s = +el.dataset.step;
|
|
689
|
+
el.className = 'stepper-item' + (s === currentStep && currentStep <= 4 ? ' active' : '') + (s < currentStep ? ' done' : '');
|
|
690
|
+
el.querySelector('.stepper-dot').textContent = s < currentStep ? '\\u{2713}' : s;
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function updateNav() {
|
|
695
|
+
var back = document.getElementById('backBtn');
|
|
696
|
+
var next = document.getElementById('nextBtn');
|
|
697
|
+
var nav = document.getElementById('navBar');
|
|
698
|
+
if (currentStep === 5) { nav.style.display = 'none'; return; }
|
|
699
|
+
nav.style.display = 'flex';
|
|
700
|
+
back.style.visibility = currentStep === 1 ? 'hidden' : 'visible';
|
|
701
|
+
next.textContent = currentStep === 4 ? 'Complete Setup' : 'Next';
|
|
702
|
+
next.disabled = !canProceed();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function canProceed() {
|
|
706
|
+
if (currentStep === 1) return !!state.activeProvider;
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function nextStep() {
|
|
711
|
+
if (!canProceed()) return;
|
|
712
|
+
if (currentStep === 1) collectProviderData();
|
|
713
|
+
if (currentStep === 2) collectChannelData();
|
|
714
|
+
if (currentStep === 3) collectAgentData();
|
|
715
|
+
if (currentStep === 4) { collectRoutingData(); completeSetup(); return; }
|
|
716
|
+
currentStep++;
|
|
717
|
+
renderStep();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function prevStep() {
|
|
721
|
+
if (currentStep <= 1) return;
|
|
722
|
+
currentStep--;
|
|
723
|
+
renderStep();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ── Step 1: Provider ──
|
|
727
|
+
function renderProvider() {
|
|
728
|
+
var html = '<h3 class="step-title">AI Provider</h3>';
|
|
729
|
+
html += '<p class="step-desc">Choose the AI service that powers your agent.</p>';
|
|
730
|
+
html += '<div class="provider-grid">';
|
|
731
|
+
PROVIDERS.forEach(function(p) {
|
|
732
|
+
html += '<div class="provider-card' + (state.activeProvider === p.id ? ' selected' : '') + '" data-pid="' + p.id + '">';
|
|
733
|
+
html += '<div class="provider-icon">' + p.icon + '</div>';
|
|
734
|
+
html += '<div>' + esc(p.name) + '</div></div>';
|
|
735
|
+
});
|
|
736
|
+
html += '</div>';
|
|
737
|
+
html += '<div id="providerForm"></div>';
|
|
738
|
+
html += '<div class="toggle-row"><label class="toggle-switch"><input type="checkbox" id="fallbackToggle"' + (state.showFallback ? ' checked' : '') + '><span class="toggle-slider"></span></label><label for="fallbackToggle">Add a fallback provider</label></div>';
|
|
739
|
+
html += '<div id="fallbackSection"></div>';
|
|
740
|
+
setHTML('stepContent', html);
|
|
741
|
+
|
|
742
|
+
// Attach click handlers
|
|
743
|
+
document.querySelectorAll('.provider-card[data-pid]').forEach(function(card) {
|
|
744
|
+
card.addEventListener('click', function() { selectProvider(card.dataset.pid); });
|
|
745
|
+
});
|
|
746
|
+
document.getElementById('fallbackToggle').addEventListener('change', toggleFallback);
|
|
747
|
+
|
|
748
|
+
if (state.activeProvider) showProviderForm(state.activeProvider, 'providerForm', false);
|
|
749
|
+
if (state.showFallback) showFallbackSection();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function selectProvider(id) {
|
|
753
|
+
state.activeProvider = id;
|
|
754
|
+
document.querySelectorAll('.provider-card').forEach(function(c) { c.classList.remove('selected'); });
|
|
755
|
+
var sel = document.querySelector('.provider-card[data-pid="' + id + '"]');
|
|
756
|
+
if (sel) sel.classList.add('selected');
|
|
757
|
+
showProviderForm(id, 'providerForm', false);
|
|
758
|
+
updateNav();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function showProviderForm(id, containerId, isFallback) {
|
|
762
|
+
var p = PROVIDERS.find(function(x) { return x.id === id; });
|
|
763
|
+
if (!p) return;
|
|
764
|
+
var prefix = isFallback ? 'fb_' : '';
|
|
765
|
+
var container = document.getElementById(containerId);
|
|
766
|
+
var html = '<div class="provider-form">';
|
|
767
|
+
|
|
768
|
+
if (p.local) {
|
|
769
|
+
html += '<div id="' + prefix + 'detectStatus"></div>';
|
|
770
|
+
html += '<div class="form-group"><label class="form-label">Model</label>';
|
|
771
|
+
html += '<input class="form-input" id="' + prefix + 'model" placeholder="' + esc(p.defaultModel) + '" value="' + esc(getProviderVal(id, 'model', isFallback)) + '"></div>';
|
|
772
|
+
html += '<div id="' + prefix + 'modelList"></div>';
|
|
773
|
+
html += '<div class="test-row"><button class="btn" id="' + prefix + 'detectBtn">Detect</button><span class="test-status" id="' + prefix + 'testStatus"></span></div>';
|
|
774
|
+
html += '</div>';
|
|
775
|
+
setHTML(container, html);
|
|
776
|
+
document.getElementById(prefix + 'detectBtn').addEventListener('click', function() { detectLocal(p.type, prefix); });
|
|
777
|
+
detectLocal(p.type, prefix);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (p.cli) {
|
|
782
|
+
html += '<div id="' + prefix + 'detectStatus"></div>';
|
|
783
|
+
html += '</div>';
|
|
784
|
+
setHTML(container, html);
|
|
785
|
+
detectCli(p.cliTool, prefix);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (p.custom) {
|
|
790
|
+
html += '<div class="form-group"><label class="form-label">Provider Name</label>';
|
|
791
|
+
html += '<input class="form-input" id="' + prefix + 'customName" placeholder="my-provider" value="' + esc(getProviderVal('custom', 'customName', isFallback)) + '"></div>';
|
|
792
|
+
html += '<div class="form-group"><label class="form-label">Base URL</label>';
|
|
793
|
+
html += '<input class="form-input" id="' + prefix + 'baseUrl" placeholder="https://api.example.com/v1" value="' + esc(getProviderVal('custom', 'baseUrl', isFallback)) + '"></div>';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
html += '<div class="form-group"><label class="form-label">' + esc(p.keyLabel || 'API Key') + '</label>';
|
|
797
|
+
html += '<input class="form-input" id="' + prefix + 'apiKey" type="password" placeholder="' + esc(p.keyPlaceholder || 'API key') + '" value="' + esc(getProviderVal(id, 'apiKey', isFallback)) + '"></div>';
|
|
798
|
+
html += '<div class="form-group"><label class="form-label">Model</label>';
|
|
799
|
+
html += '<input class="form-input" id="' + prefix + 'model" placeholder="' + esc(p.defaultModel) + '" value="' + esc(getProviderVal(id, 'model', isFallback)) + '"></div>';
|
|
800
|
+
html += '<div class="test-row"><button class="btn" id="' + prefix + 'testBtn">Test Connection</button><span class="test-status" id="' + prefix + 'testStatus"></span></div>';
|
|
801
|
+
html += '</div>';
|
|
802
|
+
setHTML(container, html);
|
|
803
|
+
document.getElementById(prefix + 'testBtn').addEventListener('click', function() { testConnection(prefix, id); });
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function getProviderVal(id, key, isFallback) {
|
|
807
|
+
var cfg = isFallback ? state.providers[state.fallbackProvider] : state.providers[id];
|
|
808
|
+
if (!cfg) return '';
|
|
809
|
+
return cfg[key] || '';
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function toggleFallback() {
|
|
813
|
+
state.showFallback = document.getElementById('fallbackToggle').checked;
|
|
814
|
+
if (state.showFallback) showFallbackSection();
|
|
815
|
+
else setHTML('fallbackSection', '');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function showFallbackSection() {
|
|
819
|
+
var sec = document.getElementById('fallbackSection');
|
|
820
|
+
var html = '<div style="margin-top:12px"><p class="step-desc">Choose a fallback provider:</p>';
|
|
821
|
+
html += '<div class="provider-grid">';
|
|
822
|
+
PROVIDERS.filter(function(p) { return p.id !== state.activeProvider; }).forEach(function(p) {
|
|
823
|
+
html += '<div class="provider-card' + (state.fallbackProvider === p.id ? ' selected' : '') + '" data-fbid="' + p.id + '">';
|
|
824
|
+
html += '<div class="provider-icon">' + p.icon + '</div>';
|
|
825
|
+
html += '<div>' + esc(p.name) + '</div></div>';
|
|
826
|
+
});
|
|
827
|
+
html += '</div><div id="fallbackForm"></div></div>';
|
|
828
|
+
setHTML(sec, html);
|
|
829
|
+
|
|
830
|
+
document.querySelectorAll('.provider-card[data-fbid]').forEach(function(card) {
|
|
831
|
+
card.addEventListener('click', function() { selectFallback(card.dataset.fbid); });
|
|
832
|
+
});
|
|
833
|
+
if (state.fallbackProvider) showProviderForm(state.fallbackProvider, 'fallbackForm', true);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function selectFallback(id) {
|
|
837
|
+
state.fallbackProvider = id;
|
|
838
|
+
showFallbackSection();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async function testConnection(prefix, providerId) {
|
|
842
|
+
var statusEl = document.getElementById(prefix + 'testStatus');
|
|
843
|
+
statusEl.className = 'test-status';
|
|
844
|
+
setHTML(statusEl, '<div class="spinner"></div> Testing...');
|
|
845
|
+
var p = PROVIDERS.find(function(x) { return x.id === providerId; });
|
|
846
|
+
var apiKey = document.getElementById(prefix + 'apiKey')?.value;
|
|
847
|
+
var model = document.getElementById(prefix + 'model')?.value || p.defaultModel;
|
|
848
|
+
var baseUrl = document.getElementById(prefix + 'baseUrl')?.value || p.baseUrl;
|
|
849
|
+
var config = { apiKey: apiKey, model: model };
|
|
850
|
+
if (baseUrl) config.baseUrl = baseUrl;
|
|
851
|
+
var name = providerId;
|
|
852
|
+
if (providerId === 'custom') {
|
|
853
|
+
name = document.getElementById(prefix + 'customName')?.value || 'custom';
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
var res = await fetch('/api/validate-provider', {
|
|
857
|
+
method: 'POST',
|
|
858
|
+
headers: { 'Content-Type': 'application/json' },
|
|
859
|
+
body: JSON.stringify({ name: name, config: config }),
|
|
860
|
+
});
|
|
861
|
+
var data = await res.json();
|
|
862
|
+
if (data.ok) {
|
|
863
|
+
statusEl.className = 'test-status success';
|
|
864
|
+
statusEl.textContent = '\\u{2713} Connected';
|
|
865
|
+
} else {
|
|
866
|
+
statusEl.className = 'test-status error';
|
|
867
|
+
statusEl.textContent = '\\u{2717} ' + (data.error || 'Failed');
|
|
868
|
+
}
|
|
869
|
+
} catch (e) {
|
|
870
|
+
statusEl.className = 'test-status error';
|
|
871
|
+
statusEl.textContent = '\\u{2717} Network error';
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function detectLocal(type, prefix) {
|
|
876
|
+
var statusEl = document.getElementById(prefix + 'detectStatus');
|
|
877
|
+
setHTML(statusEl, '<div class="detect-badge" style="color:var(--text-secondary);background:var(--bg-surface)"><div class="spinner" style="width:12px;height:12px;border-width:2px"></div> Detecting...</div>');
|
|
878
|
+
try {
|
|
879
|
+
var res = await fetch('/api/detect-local', {
|
|
880
|
+
method: 'POST',
|
|
881
|
+
headers: { 'Content-Type': 'application/json' },
|
|
882
|
+
body: JSON.stringify({ type: type }),
|
|
883
|
+
});
|
|
884
|
+
var data = await res.json();
|
|
885
|
+
if (data.running) {
|
|
886
|
+
statusEl.textContent = '';
|
|
887
|
+
var badge = document.createElement('div');
|
|
888
|
+
badge.className = 'detect-badge found';
|
|
889
|
+
badge.textContent = '\\u{2713} Running (' + data.models.length + ' model' + (data.models.length !== 1 ? 's' : '') + ')';
|
|
890
|
+
statusEl.appendChild(badge);
|
|
891
|
+
|
|
892
|
+
if (data.models.length) {
|
|
893
|
+
var listEl = document.getElementById(prefix + 'modelList');
|
|
894
|
+
var lbl = document.createElement('label');
|
|
895
|
+
lbl.className = 'form-label';
|
|
896
|
+
lbl.style.marginBottom = '6px';
|
|
897
|
+
lbl.textContent = 'Available Models';
|
|
898
|
+
listEl.textContent = '';
|
|
899
|
+
listEl.appendChild(lbl);
|
|
900
|
+
var listDiv = document.createElement('div');
|
|
901
|
+
listDiv.className = 'model-list';
|
|
902
|
+
data.models.slice(0, 12).forEach(function(m) {
|
|
903
|
+
var tag = document.createElement('span');
|
|
904
|
+
tag.className = 'model-tag';
|
|
905
|
+
tag.textContent = m;
|
|
906
|
+
tag.dataset.model = m;
|
|
907
|
+
tag.addEventListener('click', function() { pickModel(prefix, tag); });
|
|
908
|
+
listDiv.appendChild(tag);
|
|
909
|
+
});
|
|
910
|
+
listEl.appendChild(listDiv);
|
|
911
|
+
}
|
|
912
|
+
} else {
|
|
913
|
+
var label = type === 'ollama' ? 'Ollama' : 'LM Studio';
|
|
914
|
+
var hint = type === 'ollama' ? 'Run ollama serve or open the Ollama app' : 'Open LM Studio and start the local server';
|
|
915
|
+
statusEl.textContent = '';
|
|
916
|
+
var b = document.createElement('div');
|
|
917
|
+
b.className = 'detect-badge not-found';
|
|
918
|
+
b.textContent = '\\u{2717} ' + label + ' not detected';
|
|
919
|
+
statusEl.appendChild(b);
|
|
920
|
+
var p2 = document.createElement('p');
|
|
921
|
+
p2.className = 'form-hint';
|
|
922
|
+
p2.textContent = hint;
|
|
923
|
+
statusEl.appendChild(p2);
|
|
924
|
+
}
|
|
925
|
+
} catch (e) {
|
|
926
|
+
statusEl.textContent = '';
|
|
927
|
+
var fb = document.createElement('div');
|
|
928
|
+
fb.className = 'detect-badge not-found';
|
|
929
|
+
fb.textContent = '\\u{2717} Detection failed';
|
|
930
|
+
statusEl.appendChild(fb);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async function detectCli(tool, prefix) {
|
|
935
|
+
var statusEl = document.getElementById(prefix + 'detectStatus');
|
|
936
|
+
setHTML(statusEl, '<div class="detect-badge" style="color:var(--text-secondary);background:var(--bg-surface)"><div class="spinner" style="width:12px;height:12px;border-width:2px"></div> Checking...</div>');
|
|
937
|
+
try {
|
|
938
|
+
var res = await fetch('/api/detect-cli', {
|
|
939
|
+
method: 'POST',
|
|
940
|
+
headers: { 'Content-Type': 'application/json' },
|
|
941
|
+
body: JSON.stringify({ tool: tool }),
|
|
942
|
+
});
|
|
943
|
+
var data = await res.json();
|
|
944
|
+
statusEl.textContent = '';
|
|
945
|
+
if (data.installed) {
|
|
946
|
+
var badge = document.createElement('div');
|
|
947
|
+
badge.className = 'detect-badge found';
|
|
948
|
+
badge.textContent = '\\u{2713} ' + tool + ' CLI found';
|
|
949
|
+
statusEl.appendChild(badge);
|
|
950
|
+
} else {
|
|
951
|
+
var cmd = tool === 'claude' ? 'npm install -g @anthropic-ai/claude-code' : 'npm install -g @openai/codex';
|
|
952
|
+
var b = document.createElement('div');
|
|
953
|
+
b.className = 'detect-badge not-found';
|
|
954
|
+
b.textContent = '\\u{2717} ' + tool + ' not found';
|
|
955
|
+
statusEl.appendChild(b);
|
|
956
|
+
var p = document.createElement('p');
|
|
957
|
+
p.className = 'form-hint';
|
|
958
|
+
p.textContent = 'Install with: ' + cmd;
|
|
959
|
+
statusEl.appendChild(p);
|
|
960
|
+
}
|
|
961
|
+
} catch (e) {
|
|
962
|
+
statusEl.textContent = '';
|
|
963
|
+
var fb = document.createElement('div');
|
|
964
|
+
fb.className = 'detect-badge not-found';
|
|
965
|
+
fb.textContent = '\\u{2717} Detection failed';
|
|
966
|
+
statusEl.appendChild(fb);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function pickModel(prefix, el) {
|
|
971
|
+
document.getElementById(prefix + 'model').value = el.dataset.model;
|
|
972
|
+
el.closest('.model-list').querySelectorAll('.model-tag').forEach(function(t) { t.classList.remove('selected'); });
|
|
973
|
+
el.classList.add('selected');
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function collectProviderData() {
|
|
977
|
+
var p = PROVIDERS.find(function(x) { return x.id === state.activeProvider; });
|
|
978
|
+
if (!p) return;
|
|
979
|
+
|
|
980
|
+
if (p.cli) {
|
|
981
|
+
var model = 'default';
|
|
982
|
+
state.providers[p.id] = { model: model };
|
|
983
|
+
} else if (p.local) {
|
|
984
|
+
var m = document.getElementById('model')?.value || p.defaultModel;
|
|
985
|
+
var baseUrl = p.type === 'ollama' ? 'http://localhost:11434/v1' : 'http://localhost:1234/v1';
|
|
986
|
+
var apiKey = p.type === 'ollama' ? 'ollama' : 'lm-studio';
|
|
987
|
+
state.providers[p.id] = { baseUrl: baseUrl, apiKey: apiKey, model: m };
|
|
988
|
+
} else if (p.custom) {
|
|
989
|
+
var name = document.getElementById('customName')?.value || 'custom';
|
|
990
|
+
var bu = document.getElementById('baseUrl')?.value;
|
|
991
|
+
var ak = document.getElementById('apiKey')?.value;
|
|
992
|
+
var md = document.getElementById('model')?.value || 'default';
|
|
993
|
+
state.providers[name] = { baseUrl: bu, apiKey: ak, model: md };
|
|
994
|
+
state.activeProvider = name;
|
|
995
|
+
} else {
|
|
996
|
+
var ak2 = document.getElementById('apiKey')?.value;
|
|
997
|
+
var md2 = document.getElementById('model')?.value || p.defaultModel;
|
|
998
|
+
var cfg = { apiKey: ak2, model: md2 };
|
|
999
|
+
if (p.baseUrl) cfg.baseUrl = p.baseUrl;
|
|
1000
|
+
state.providers[p.id] = cfg;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Fallback
|
|
1004
|
+
state.failover = [];
|
|
1005
|
+
if (state.showFallback && state.fallbackProvider) {
|
|
1006
|
+
var fb = PROVIDERS.find(function(x) { return x.id === state.fallbackProvider; });
|
|
1007
|
+
if (fb) {
|
|
1008
|
+
if (fb.cli) {
|
|
1009
|
+
var fbm = 'default';
|
|
1010
|
+
state.providers[fb.id] = { model: fbm };
|
|
1011
|
+
} else if (fb.local) {
|
|
1012
|
+
var fbModel = document.getElementById('fb_model')?.value || fb.defaultModel;
|
|
1013
|
+
var fbBase = fb.type === 'ollama' ? 'http://localhost:11434/v1' : 'http://localhost:1234/v1';
|
|
1014
|
+
var fbKey = fb.type === 'ollama' ? 'ollama' : 'lm-studio';
|
|
1015
|
+
state.providers[fb.id] = { baseUrl: fbBase, apiKey: fbKey, model: fbModel };
|
|
1016
|
+
} else if (fb.custom) {
|
|
1017
|
+
var fbName = document.getElementById('fb_customName')?.value || 'custom-fb';
|
|
1018
|
+
var fbBu = document.getElementById('fb_baseUrl')?.value;
|
|
1019
|
+
var fbAk = document.getElementById('fb_apiKey')?.value;
|
|
1020
|
+
var fbMd = document.getElementById('fb_model')?.value || 'default';
|
|
1021
|
+
state.providers[fbName] = { baseUrl: fbBu, apiKey: fbAk, model: fbMd };
|
|
1022
|
+
state.fallbackProvider = fbName;
|
|
1023
|
+
} else {
|
|
1024
|
+
var fbAk2 = document.getElementById('fb_apiKey')?.value;
|
|
1025
|
+
var fbMd2 = document.getElementById('fb_model')?.value || fb.defaultModel;
|
|
1026
|
+
var fbCfg = { apiKey: fbAk2, model: fbMd2 };
|
|
1027
|
+
if (fb.baseUrl) fbCfg.baseUrl = fb.baseUrl;
|
|
1028
|
+
state.providers[fb.id] = fbCfg;
|
|
1029
|
+
}
|
|
1030
|
+
state.failover = [state.fallbackProvider];
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// ── Step 2: Channels ──
|
|
1036
|
+
function renderChannels() {
|
|
1037
|
+
var html = '<h3 class="step-title">Channels</h3>';
|
|
1038
|
+
html += '<p class="step-desc">Choose how people can talk to your agent.</p>';
|
|
1039
|
+
html += '<div class="channel-grid">';
|
|
1040
|
+
html += '<div class="channel-card locked"><span class="ch-icon">\\u{1F310}</span><span class="ch-name">WebChat</span><span class="ch-badge">Always on</span></div>';
|
|
1041
|
+
CHANNELS.forEach(function(ch) {
|
|
1042
|
+
var sel = !!state.channels[ch.id]?.enabled;
|
|
1043
|
+
html += '<div class="channel-card' + (sel ? ' selected' : '') + '" data-chid="' + ch.id + '">';
|
|
1044
|
+
html += '<span class="ch-icon">' + ch.icon + '</span><span class="ch-name">' + esc(ch.name) + '</span>';
|
|
1045
|
+
if (sel) html += '<span class="ch-badge">On</span>';
|
|
1046
|
+
html += '</div>';
|
|
1047
|
+
});
|
|
1048
|
+
html += '</div>';
|
|
1049
|
+
html += '<div id="channelConfigs"></div>';
|
|
1050
|
+
setHTML('stepContent', html);
|
|
1051
|
+
|
|
1052
|
+
document.querySelectorAll('.channel-card[data-chid]').forEach(function(card) {
|
|
1053
|
+
card.addEventListener('click', function() { toggleChannel(card.dataset.chid); });
|
|
1054
|
+
});
|
|
1055
|
+
renderChannelConfigs();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function toggleChannel(id) {
|
|
1059
|
+
if (state.channels[id]?.enabled) {
|
|
1060
|
+
delete state.channels[id];
|
|
1061
|
+
} else {
|
|
1062
|
+
state.channels[id] = { enabled: true };
|
|
1063
|
+
}
|
|
1064
|
+
renderChannels();
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function renderChannelConfigs() {
|
|
1068
|
+
var container = document.getElementById('channelConfigs');
|
|
1069
|
+
var html = '';
|
|
1070
|
+
CHANNELS.forEach(function(ch) {
|
|
1071
|
+
if (!state.channels[ch.id]?.enabled) return;
|
|
1072
|
+
if (ch.fields.length === 0 && ch.hint) {
|
|
1073
|
+
html += '<div class="channel-config"><h4>' + ch.icon + ' ' + esc(ch.name) + '</h4><p class="form-hint">' + esc(ch.hint) + '</p></div>';
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (ch.fields.length === 0) return;
|
|
1077
|
+
html += '<div class="channel-config"><h4>' + ch.icon + ' ' + esc(ch.name) + '</h4>';
|
|
1078
|
+
ch.fields.forEach(function(f) {
|
|
1079
|
+
var val = getChannelVal(ch.id, f.key);
|
|
1080
|
+
html += '<div class="form-group"><label class="form-label">' + esc(f.label) + '</label>';
|
|
1081
|
+
html += '<input class="form-input" data-channel="' + ch.id + '" data-field="' + f.key + '" placeholder="' + esc(f.placeholder) + '" value="' + esc(val) + '">';
|
|
1082
|
+
if (f.hint) html += '<p class="form-hint">' + esc(f.hint) + '</p>';
|
|
1083
|
+
html += '</div>';
|
|
1084
|
+
});
|
|
1085
|
+
html += '</div>';
|
|
1086
|
+
});
|
|
1087
|
+
setHTML(container, html);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function getChannelVal(chId, key) {
|
|
1091
|
+
var ch = state.channels[chId];
|
|
1092
|
+
if (!ch) return '';
|
|
1093
|
+
var parts = key.split('.');
|
|
1094
|
+
var val = ch;
|
|
1095
|
+
for (var i = 0; i < parts.length; i++) { val = val?.[parts[i]]; }
|
|
1096
|
+
return val || '';
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function collectChannelData() {
|
|
1100
|
+
document.querySelectorAll('.channel-config input[data-channel]').forEach(function(input) {
|
|
1101
|
+
var chId = input.dataset.channel;
|
|
1102
|
+
var field = input.dataset.field;
|
|
1103
|
+
var val = input.value;
|
|
1104
|
+
if (!state.channels[chId]) state.channels[chId] = { enabled: true };
|
|
1105
|
+
var parts = field.split('.');
|
|
1106
|
+
if (parts.length === 2) {
|
|
1107
|
+
if (!state.channels[chId][parts[0]]) state.channels[chId][parts[0]] = {};
|
|
1108
|
+
state.channels[chId][parts[0]][parts[1]] = val;
|
|
1109
|
+
} else {
|
|
1110
|
+
state.channels[chId][field] = val;
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Add defaults
|
|
1115
|
+
CHANNELS.forEach(function(ch) {
|
|
1116
|
+
if (!state.channels[ch.id]?.enabled) return;
|
|
1117
|
+
if (ch.id === 'telegram' && !state.channels.telegram.allowedUsers) state.channels.telegram.allowedUsers = [];
|
|
1118
|
+
if (ch.id === 'discord' && !state.channels.discord.allowedUsers) state.channels.discord.allowedUsers = [];
|
|
1119
|
+
if (ch.id === 'slack' && !state.channels.slack.allowedUsers) state.channels.slack.allowedUsers = [];
|
|
1120
|
+
if (ch.id === 'whatsapp' && !state.channels.whatsapp.allowedNumbers) state.channels.whatsapp.allowedNumbers = [];
|
|
1121
|
+
if (ch.id === 'signal' && !state.channels.signal.allowedNumbers) state.channels.signal.allowedNumbers = [];
|
|
1122
|
+
if (ch.id === 'email') {
|
|
1123
|
+
var e = state.channels.email;
|
|
1124
|
+
if (e.imap && !e.imap.port) e.imap.port = 993;
|
|
1125
|
+
if (e.imap && e.imap.tls === undefined) e.imap.tls = true;
|
|
1126
|
+
if (e.smtp && !e.smtp.port) e.smtp.port = 587;
|
|
1127
|
+
if (e.smtp && e.smtp.tls === undefined) e.smtp.tls = true;
|
|
1128
|
+
if (!e.allowedSenders) e.allowedSenders = [];
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// ── Step 3: Agent ──
|
|
1134
|
+
function renderAgent() {
|
|
1135
|
+
var html = '<h3 class="step-title">Personalization</h3>';
|
|
1136
|
+
html += '<p class="step-desc">Give your agent a name and personality.</p>';
|
|
1137
|
+
html += '<div class="form-group"><label class="form-label">Agent Name</label>';
|
|
1138
|
+
html += '<input class="form-input" id="agentName" placeholder="Zubo" value="' + esc(state.agentName) + '"></div>';
|
|
1139
|
+
html += '<div class="form-group"><label class="form-label">Personality <span style="color:var(--text-muted)">(optional)</span></label>';
|
|
1140
|
+
html += '<textarea class="form-input" id="personality" placeholder="Describe your agent's personality in a sentence or two...">' + esc(state.personality) + '</textarea>';
|
|
1141
|
+
html += '<p class="form-hint">Leave blank for a sensible default.</p></div>';
|
|
1142
|
+
setHTML('stepContent', html);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function collectAgentData() {
|
|
1146
|
+
state.agentName = document.getElementById('agentName')?.value || 'Zubo';
|
|
1147
|
+
state.personality = document.getElementById('personality')?.value || '';
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// ── Step 4: Smart Routing ──
|
|
1151
|
+
function renderRouting() {
|
|
1152
|
+
var html = '<h3 class="step-title">Smart Cost Savings</h3>';
|
|
1153
|
+
html += '<div class="routing-card">';
|
|
1154
|
+
html += '<p>Use a cheaper, faster AI for simple questions (greetings, one-liners) and your main AI for complex tasks. This can save 50-80% on costs.</p>';
|
|
1155
|
+
|
|
1156
|
+
var hasSecondProvider = state.failover.length > 0;
|
|
1157
|
+
|
|
1158
|
+
if (hasSecondProvider) {
|
|
1159
|
+
html += buildRoutingOption('fallback', 'Use fallback provider', 'Route simple queries to ' + esc(state.failover[0]));
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
html += buildRoutingOption('groq', 'Add Groq (free & fast)', 'Great for simple queries');
|
|
1163
|
+
|
|
1164
|
+
if (state.routingChoice === 'groq') {
|
|
1165
|
+
html += '<div class="provider-form" style="margin:8px 0 8px 34px"><div class="form-group"><label class="form-label">Groq API Key</label>';
|
|
1166
|
+
html += '<input class="form-input" id="groqKey" placeholder="gsk_..." value="' + esc(state.providers.groq?.apiKey || '') + '"></div></div>';
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
html += buildRoutingOption('smaller', 'Use a smaller model', 'Same provider, lighter model');
|
|
1170
|
+
|
|
1171
|
+
if (state.routingChoice === 'smaller') {
|
|
1172
|
+
html += '<div class="provider-form" style="margin:8px 0 8px 34px"><div class="form-group"><label class="form-label">Fast Model Name</label>';
|
|
1173
|
+
html += '<input class="form-input" id="fastModel" placeholder="e.g. claude-haiku-4-5-20251001" value=""></div></div>';
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
html += buildRoutingOption('skip', 'Skip for now', 'You can enable this later');
|
|
1177
|
+
html += '</div>';
|
|
1178
|
+
setHTML('stepContent', html);
|
|
1179
|
+
|
|
1180
|
+
// Attach click handlers
|
|
1181
|
+
document.querySelectorAll('.routing-option[data-rid]').forEach(function(opt) {
|
|
1182
|
+
opt.addEventListener('click', function() { selectRouting(opt.dataset.rid); });
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
// Pre-select fallback if available and no explicit choice yet
|
|
1186
|
+
if (hasSecondProvider && state.routingChoice === 'skip') {
|
|
1187
|
+
state.routingChoice = 'fallback';
|
|
1188
|
+
renderRouting();
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function buildRoutingOption(id, label, desc) {
|
|
1193
|
+
var selected = state.routingChoice === id;
|
|
1194
|
+
return '<div class="routing-option' + (selected ? ' selected' : '') + '" data-rid="' + id + '">' +
|
|
1195
|
+
'<div class="routing-radio"></div><div><div class="ro-label">' + esc(label) + '</div><div class="ro-desc">' + esc(desc) + '</div></div></div>';
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function selectRouting(id) {
|
|
1199
|
+
state.routingChoice = id;
|
|
1200
|
+
renderRouting();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function collectRoutingData() {
|
|
1204
|
+
if (state.routingChoice === 'fallback') {
|
|
1205
|
+
state.smartRouting = { enabled: true, fastProvider: state.failover[0] };
|
|
1206
|
+
} else if (state.routingChoice === 'groq') {
|
|
1207
|
+
var key = document.getElementById('groqKey')?.value;
|
|
1208
|
+
if (key) {
|
|
1209
|
+
state.providers.groq = { apiKey: key, model: 'llama-3.3-70b-versatile' };
|
|
1210
|
+
state.smartRouting = { enabled: true, fastProvider: 'groq' };
|
|
1211
|
+
} else {
|
|
1212
|
+
state.smartRouting = { enabled: false };
|
|
1213
|
+
}
|
|
1214
|
+
} else if (state.routingChoice === 'smaller') {
|
|
1215
|
+
var model = document.getElementById('fastModel')?.value;
|
|
1216
|
+
if (model) {
|
|
1217
|
+
state.smartRouting = { enabled: true, fastProvider: state.activeProvider, fastModel: model };
|
|
1218
|
+
} else {
|
|
1219
|
+
state.smartRouting = { enabled: false };
|
|
1220
|
+
}
|
|
1221
|
+
} else {
|
|
1222
|
+
state.smartRouting = { enabled: false };
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// ── Step 5: Completion ──
|
|
1227
|
+
async function completeSetup() {
|
|
1228
|
+
if (completing) return;
|
|
1229
|
+
completing = true;
|
|
1230
|
+
currentStep = 5;
|
|
1231
|
+
updateStepper();
|
|
1232
|
+
updateNav();
|
|
1233
|
+
|
|
1234
|
+
// Show loading
|
|
1235
|
+
var panel = document.getElementById('stepPanel');
|
|
1236
|
+
panel.textContent = '';
|
|
1237
|
+
var overlay = document.createElement('div');
|
|
1238
|
+
overlay.className = 'loading-overlay';
|
|
1239
|
+
var spinner = document.createElement('div');
|
|
1240
|
+
spinner.className = 'spinner-lg';
|
|
1241
|
+
var msg = document.createElement('span');
|
|
1242
|
+
msg.textContent = 'Saving configuration...';
|
|
1243
|
+
overlay.appendChild(spinner);
|
|
1244
|
+
overlay.appendChild(msg);
|
|
1245
|
+
panel.appendChild(overlay);
|
|
1246
|
+
|
|
1247
|
+
// Build payload
|
|
1248
|
+
var anthropicApiKey = state.activeProvider === 'anthropic' ? state.providers.anthropic?.apiKey : undefined;
|
|
1249
|
+
var telegramBotToken = state.channels.telegram?.botToken;
|
|
1250
|
+
|
|
1251
|
+
var payload = {
|
|
1252
|
+
providers: state.providers,
|
|
1253
|
+
activeProvider: state.activeProvider,
|
|
1254
|
+
failover: state.failover,
|
|
1255
|
+
anthropicApiKey: anthropicApiKey,
|
|
1256
|
+
telegramBotToken: telegramBotToken,
|
|
1257
|
+
channels: state.channels,
|
|
1258
|
+
smartRouting: state.smartRouting,
|
|
1259
|
+
agentName: state.agentName,
|
|
1260
|
+
personality: state.personality,
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
var res = await fetch('/api/complete', {
|
|
1265
|
+
method: 'POST',
|
|
1266
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1267
|
+
body: JSON.stringify(payload),
|
|
1268
|
+
});
|
|
1269
|
+
var data = await res.json();
|
|
1270
|
+
if (!data.ok) throw new Error(data.error || 'Setup failed');
|
|
1271
|
+
renderComplete();
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
panel.textContent = '';
|
|
1274
|
+
var content = document.createElement('div');
|
|
1275
|
+
content.className = 'step-content';
|
|
1276
|
+
var title = document.createElement('h3');
|
|
1277
|
+
title.className = 'step-title';
|
|
1278
|
+
title.style.color = 'var(--red)';
|
|
1279
|
+
title.textContent = 'Setup Failed';
|
|
1280
|
+
var desc = document.createElement('p');
|
|
1281
|
+
desc.className = 'step-desc';
|
|
1282
|
+
desc.textContent = e.message;
|
|
1283
|
+
var retry = document.createElement('button');
|
|
1284
|
+
retry.className = 'btn btn-primary';
|
|
1285
|
+
retry.textContent = 'Try Again';
|
|
1286
|
+
retry.addEventListener('click', function() { currentStep = 4; completing = false; renderStep(); });
|
|
1287
|
+
content.appendChild(title);
|
|
1288
|
+
content.appendChild(desc);
|
|
1289
|
+
content.appendChild(retry);
|
|
1290
|
+
panel.appendChild(content);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function renderComplete() {
|
|
1295
|
+
var panel = document.getElementById('stepPanel');
|
|
1296
|
+
var providerModel = state.providers[state.activeProvider]?.model || state.activeProvider;
|
|
1297
|
+
var channelNames = Object.keys(state.channels).filter(function(c) { return state.channels[c]?.enabled !== false; });
|
|
1298
|
+
|
|
1299
|
+
// Build completion screen using DOM methods
|
|
1300
|
+
panel.textContent = '';
|
|
1301
|
+
var wrap = document.createElement('div');
|
|
1302
|
+
wrap.className = 'step-content';
|
|
1303
|
+
var comp = document.createElement('div');
|
|
1304
|
+
comp.className = 'completion';
|
|
1305
|
+
|
|
1306
|
+
var checkmark = document.createElement('div');
|
|
1307
|
+
checkmark.className = 'checkmark';
|
|
1308
|
+
checkmark.textContent = '\\u{2713}';
|
|
1309
|
+
comp.appendChild(checkmark);
|
|
1310
|
+
|
|
1311
|
+
var h2 = document.createElement('h2');
|
|
1312
|
+
h2.textContent = 'Setup Complete!';
|
|
1313
|
+
comp.appendChild(h2);
|
|
1314
|
+
|
|
1315
|
+
var sub = document.createElement('p');
|
|
1316
|
+
sub.className = 'sub';
|
|
1317
|
+
sub.textContent = 'Your agent is ready to go.';
|
|
1318
|
+
comp.appendChild(sub);
|
|
1319
|
+
|
|
1320
|
+
// Summary grid
|
|
1321
|
+
var grid = document.createElement('div');
|
|
1322
|
+
grid.className = 'summary-grid';
|
|
1323
|
+
function addRow(label, value) {
|
|
1324
|
+
var l = document.createElement('span');
|
|
1325
|
+
l.className = 'label';
|
|
1326
|
+
l.textContent = label;
|
|
1327
|
+
var v = document.createElement('span');
|
|
1328
|
+
v.className = 'value';
|
|
1329
|
+
v.textContent = value;
|
|
1330
|
+
grid.appendChild(l);
|
|
1331
|
+
grid.appendChild(v);
|
|
1332
|
+
}
|
|
1333
|
+
addRow('Agent', state.agentName);
|
|
1334
|
+
addRow('Provider', state.activeProvider + ' (' + providerModel + ')');
|
|
1335
|
+
if (state.failover.length) addRow('Fallback', state.failover.join(', '));
|
|
1336
|
+
if (state.smartRouting.enabled) addRow('Routing', 'Smart (fast \\u{2192} ' + (state.smartRouting.fastProvider || '') + ')');
|
|
1337
|
+
addRow('Channels', channelNames.join(', '));
|
|
1338
|
+
comp.appendChild(grid);
|
|
1339
|
+
|
|
1340
|
+
// Next steps
|
|
1341
|
+
var ns = document.createElement('div');
|
|
1342
|
+
ns.className = 'next-steps';
|
|
1343
|
+
var b = document.createElement('strong');
|
|
1344
|
+
b.textContent = 'Next steps:';
|
|
1345
|
+
ns.appendChild(b);
|
|
1346
|
+
ns.appendChild(document.createElement('br'));
|
|
1347
|
+
ns.appendChild(document.createTextNode('1. Start your agent: '));
|
|
1348
|
+
var c1 = document.createElement('code');
|
|
1349
|
+
c1.textContent = 'zubo start';
|
|
1350
|
+
ns.appendChild(c1);
|
|
1351
|
+
ns.appendChild(document.createElement('br'));
|
|
1352
|
+
ns.appendChild(document.createTextNode('2. The dashboard opens automatically in your browser'));
|
|
1353
|
+
ns.appendChild(document.createElement('br'));
|
|
1354
|
+
ns.appendChild(document.createTextNode('3. Tell ' + state.agentName + ' to connect integrations'));
|
|
1355
|
+
ns.appendChild(document.createElement('br'));
|
|
1356
|
+
ns.appendChild(document.createElement('br'));
|
|
1357
|
+
var close = document.createElement('span');
|
|
1358
|
+
close.style.color = 'var(--text-muted)';
|
|
1359
|
+
close.textContent = 'You can close this tab now.';
|
|
1360
|
+
ns.appendChild(close);
|
|
1361
|
+
comp.appendChild(ns);
|
|
1362
|
+
|
|
1363
|
+
wrap.appendChild(comp);
|
|
1364
|
+
panel.appendChild(wrap);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// ── Init ──
|
|
1368
|
+
renderStep();
|
|
1369
|
+
</script>
|
|
1370
|
+
</body>
|
|
1371
|
+
</html>`;
|