openpalm 0.9.3 → 0.9.5
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 +40 -12
- package/package.json +2 -1
- package/src/commands/install.ts +124 -19
- package/src/commands/logs.ts +4 -15
- package/src/commands/restart.ts +39 -11
- package/src/commands/service.ts +29 -3
- package/src/commands/start.ts +47 -16
- package/src/commands/status.ts +13 -2
- package/src/commands/stop.ts +36 -11
- package/src/commands/uninstall.ts +28 -3
- package/src/commands/update.ts +22 -2
- package/src/lib/admin.ts +27 -2
- package/src/lib/docker.ts +27 -100
- package/src/lib/paths.ts +13 -23
- package/src/lib/staging.ts +72 -0
- package/src/lib/varlock.ts +8 -0
- package/src/main.test.ts +7 -2
- package/src/setup-wizard/index.html +349 -0
- package/src/setup-wizard/server.test.ts +347 -0
- package/src/setup-wizard/server.ts +297 -0
- package/src/setup-wizard/wizard.css +952 -0
- package/src/setup-wizard/wizard.js +1104 -0
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenPalm Setup Wizard — Vanilla JS
|
|
3
|
+
*
|
|
4
|
+
* Self-contained wizard logic for the CLI-hosted setup flow.
|
|
5
|
+
* No frameworks, no build step. Works with the pre-rendered HTML in index.html.
|
|
6
|
+
*
|
|
7
|
+
* API contract:
|
|
8
|
+
* GET /api/setup/status -> { ok, setupComplete }
|
|
9
|
+
* GET /api/setup/detect-providers -> { ok, providers: [{ provider, url, available }] }
|
|
10
|
+
* POST /api/setup/models/:provider { apiKey, baseUrl } -> { ok, models: [...] }
|
|
11
|
+
* POST /api/setup/complete -> { ok, error? }
|
|
12
|
+
* GET /api/setup/deploy-status -> { ok, setupComplete, deployStatus, deployError }
|
|
13
|
+
*/
|
|
14
|
+
(function () {
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
/* =========================================================================
|
|
18
|
+
Provider Constants & Defaults
|
|
19
|
+
========================================================================= */
|
|
20
|
+
|
|
21
|
+
const PROVIDER_DEFAULTS = {
|
|
22
|
+
openai: { baseUrl: "https://api.openai.com", llmModel: "gpt-4o", embModel: "text-embedding-3-small", embDims: 1536 },
|
|
23
|
+
anthropic: { baseUrl: "https://api.anthropic.com", llmModel: "claude-sonnet-4-20250514", embModel: "", embDims: 0 },
|
|
24
|
+
groq: { baseUrl: "https://api.groq.com/openai", llmModel: "llama-3.3-70b-versatile", embModel: "", embDims: 0 },
|
|
25
|
+
together: { baseUrl: "https://api.together.xyz", llmModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo", embModel: "", embDims: 0 },
|
|
26
|
+
mistral: { baseUrl: "https://api.mistral.ai", llmModel: "mistral-large-latest", embModel: "mistral-embed", embDims: 1024 },
|
|
27
|
+
deepseek: { baseUrl: "https://api.deepseek.com", llmModel: "deepseek-chat", embModel: "", embDims: 0 },
|
|
28
|
+
xai: { baseUrl: "https://api.x.ai", llmModel: "grok-2", embModel: "", embDims: 0 },
|
|
29
|
+
ollama: { baseUrl: "http://localhost:11434", llmModel: "llama3.2", embModel: "nomic-embed-text", embDims: 768 },
|
|
30
|
+
"model-runner": { baseUrl: "http://localhost:12434", llmModel: "ai/llama3.2", embModel: "ai/mxbai-embed-large-v1", embDims: 1024 },
|
|
31
|
+
lmstudio: { baseUrl: "http://localhost:1234", llmModel: "loaded-model", embModel: "", embDims: 0 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PROVIDER_LABELS = {
|
|
35
|
+
openai: "OpenAI", anthropic: "Anthropic", groq: "Groq",
|
|
36
|
+
together: "Together AI", mistral: "Mistral", deepseek: "DeepSeek",
|
|
37
|
+
xai: "xAI (Grok)", ollama: "Ollama", "model-runner": "Docker Model Runner",
|
|
38
|
+
lmstudio: "LM Studio", "ollama-instack": "Ollama (in-stack)",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
var CLOUD_PROVIDERS = ["openai", "anthropic", "groq", "together", "mistral", "deepseek", "xai"];
|
|
42
|
+
var LOCAL_PROVIDERS = ["ollama", "model-runner", "lmstudio"];
|
|
43
|
+
|
|
44
|
+
/** Known embedding dimensions for auto-fill */
|
|
45
|
+
var KNOWN_EMB_DIMS = {
|
|
46
|
+
"text-embedding-3-small": 1536, "text-embedding-3-large": 3072,
|
|
47
|
+
"text-embedding-ada-002": 1536, "nomic-embed-text": 768,
|
|
48
|
+
"mxbai-embed-large": 1024, "mxbai-embed-large-v1": 1024,
|
|
49
|
+
"ai/mxbai-embed-large-v1": 1024, "mistral-embed": 1024,
|
|
50
|
+
"all-minilm": 384, "snowflake-arctic-embed": 1024,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/* =========================================================================
|
|
54
|
+
DOM Helpers
|
|
55
|
+
========================================================================= */
|
|
56
|
+
|
|
57
|
+
function $(id) { return document.getElementById(id); }
|
|
58
|
+
function qs(sel) { return document.querySelector(sel); }
|
|
59
|
+
function qsa(sel) { return document.querySelectorAll(sel); }
|
|
60
|
+
function show(el) { if (el) el.classList.remove("hidden"); }
|
|
61
|
+
function hide(el) { if (el) el.classList.add("hidden"); }
|
|
62
|
+
function showError(el, msg) { if (el) { el.textContent = msg; show(el); } }
|
|
63
|
+
function hideError(el) { if (el) { el.textContent = ""; hide(el); } }
|
|
64
|
+
|
|
65
|
+
/* =========================================================================
|
|
66
|
+
Wizard State
|
|
67
|
+
========================================================================= */
|
|
68
|
+
|
|
69
|
+
var currentStep = 0;
|
|
70
|
+
var maxVisitedStep = 0;
|
|
71
|
+
|
|
72
|
+
/** @type {Array<{id:string, name:string, provider:string, baseUrl:string, apiKey:string, kind:string, models:string[]}>} */
|
|
73
|
+
var connections = [];
|
|
74
|
+
|
|
75
|
+
/** Provider detection results from /api/setup/detect-providers */
|
|
76
|
+
var detectedProviders = [];
|
|
77
|
+
|
|
78
|
+
/** Model lists fetched per connection id */
|
|
79
|
+
var modelCache = {};
|
|
80
|
+
|
|
81
|
+
/** Currently editing connection index (-1 = not editing) */
|
|
82
|
+
var editingIdx = -1;
|
|
83
|
+
|
|
84
|
+
/** Sub-view state for step 1: "hub" | "chooser" | "form" */
|
|
85
|
+
var connView = "hub";
|
|
86
|
+
|
|
87
|
+
/** Draft connection kind while adding: "cloud" | "local" */
|
|
88
|
+
var draftKind = "cloud";
|
|
89
|
+
|
|
90
|
+
/** Draft connection provider while in form */
|
|
91
|
+
var draftProvider = "openai";
|
|
92
|
+
|
|
93
|
+
/** Deploy polling timer */
|
|
94
|
+
var deployTimer = null;
|
|
95
|
+
|
|
96
|
+
/** Whether install is in progress */
|
|
97
|
+
var installing = false;
|
|
98
|
+
|
|
99
|
+
/* =========================================================================
|
|
100
|
+
Token Generation
|
|
101
|
+
========================================================================= */
|
|
102
|
+
|
|
103
|
+
function generateToken() {
|
|
104
|
+
var arr = new Uint8Array(16);
|
|
105
|
+
crypto.getRandomValues(arr);
|
|
106
|
+
return Array.from(arr, function (b) { return b.toString(16).padStart(2, "0"); }).join("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* =========================================================================
|
|
110
|
+
Step Navigation
|
|
111
|
+
========================================================================= */
|
|
112
|
+
|
|
113
|
+
var TOTAL_STEPS = 5; // 0..4
|
|
114
|
+
|
|
115
|
+
function goToStep(n) {
|
|
116
|
+
if (n < 0 || n > TOTAL_STEPS - 1) return;
|
|
117
|
+
// Hide all step sections
|
|
118
|
+
for (var i = 0; i < TOTAL_STEPS; i++) {
|
|
119
|
+
var sec = $("step-" + i);
|
|
120
|
+
if (sec) { if (i === n) show(sec); else hide(sec); }
|
|
121
|
+
}
|
|
122
|
+
hide($("step-deploy"));
|
|
123
|
+
|
|
124
|
+
currentStep = n;
|
|
125
|
+
if (n > maxVisitedStep) maxVisitedStep = n;
|
|
126
|
+
updateStepIndicators();
|
|
127
|
+
|
|
128
|
+
// Step-specific init
|
|
129
|
+
if (n === 1) initStep1();
|
|
130
|
+
if (n === 2) initStep2();
|
|
131
|
+
if (n === 3) initStep3();
|
|
132
|
+
if (n === 4) initStep4();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function showDeployScreen() {
|
|
136
|
+
for (var i = 0; i < TOTAL_STEPS; i++) hide($("step-" + i));
|
|
137
|
+
show($("step-deploy"));
|
|
138
|
+
hide($("step-indicators"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function updateStepIndicators() {
|
|
142
|
+
show($("step-indicators"));
|
|
143
|
+
var dots = qsa(".step-dot");
|
|
144
|
+
var lines = qsa(".step-line");
|
|
145
|
+
dots.forEach(function (dot, i) {
|
|
146
|
+
dot.classList.remove("active", "completed");
|
|
147
|
+
dot.removeAttribute("aria-current");
|
|
148
|
+
if (i === currentStep) {
|
|
149
|
+
dot.classList.add("active");
|
|
150
|
+
dot.setAttribute("aria-current", "step");
|
|
151
|
+
dot.innerHTML = String(i + 1);
|
|
152
|
+
} else if (i < maxVisitedStep || (i <= maxVisitedStep && i < currentStep)) {
|
|
153
|
+
dot.classList.add("completed");
|
|
154
|
+
dot.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
155
|
+
dot.disabled = false;
|
|
156
|
+
} else {
|
|
157
|
+
dot.innerHTML = String(i + 1);
|
|
158
|
+
dot.disabled = (i > maxVisitedStep);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
lines.forEach(function (line, i) {
|
|
162
|
+
if (i < maxVisitedStep) line.classList.add("active");
|
|
163
|
+
else line.classList.remove("active");
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* =========================================================================
|
|
168
|
+
Step 0: Welcome & Admin Token
|
|
169
|
+
========================================================================= */
|
|
170
|
+
|
|
171
|
+
function initStep0() {
|
|
172
|
+
var tokenInput = $("admin-token");
|
|
173
|
+
if (tokenInput && !tokenInput.value) {
|
|
174
|
+
tokenInput.value = generateToken();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateStep0() {
|
|
179
|
+
var errEl = $("step0-error");
|
|
180
|
+
hideError(errEl);
|
|
181
|
+
var token = ($("admin-token").value || "").trim();
|
|
182
|
+
if (token.length < 8) {
|
|
183
|
+
showError(errEl, "Admin token must be at least 8 characters.");
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* =========================================================================
|
|
190
|
+
Step 1: Connection Setup
|
|
191
|
+
========================================================================= */
|
|
192
|
+
|
|
193
|
+
function initStep1() {
|
|
194
|
+
renderConnHub();
|
|
195
|
+
setConnView("hub");
|
|
196
|
+
// Auto-detect on first visit
|
|
197
|
+
if (detectedProviders.length === 0 && connections.length === 0) {
|
|
198
|
+
detectProviders();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function setConnView(view) {
|
|
203
|
+
connView = view;
|
|
204
|
+
var hubList = $("conn-hub-list");
|
|
205
|
+
var hubEmpty = $("conn-hub-empty");
|
|
206
|
+
var chooser = $("conn-type-chooser");
|
|
207
|
+
var form = $("conn-detail-form");
|
|
208
|
+
var actions = $("step1-actions");
|
|
209
|
+
|
|
210
|
+
hide(hubList); hide(hubEmpty); hide(chooser); hide(form); hide(actions);
|
|
211
|
+
|
|
212
|
+
if (view === "hub") {
|
|
213
|
+
if (connections.length > 0) { show(hubList); } else { show(hubEmpty); }
|
|
214
|
+
show(actions);
|
|
215
|
+
$("btn-step1-next").disabled = connections.length === 0;
|
|
216
|
+
} else if (view === "chooser") {
|
|
217
|
+
show(chooser);
|
|
218
|
+
} else if (view === "form") {
|
|
219
|
+
show(form);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderConnHub() {
|
|
224
|
+
var list = $("conn-hub-list");
|
|
225
|
+
list.innerHTML = "";
|
|
226
|
+
connections.forEach(function (conn, i) {
|
|
227
|
+
var li = document.createElement("li");
|
|
228
|
+
li.className = "hub-row";
|
|
229
|
+
li.innerHTML =
|
|
230
|
+
'<div class="hub-row-info">' +
|
|
231
|
+
'<span class="hub-row-name">' + esc(conn.name || conn.provider) + "</span>" +
|
|
232
|
+
'<span class="hub-row-badge">' + (conn.kind === "local" ? "Local" : "Cloud") + "</span>" +
|
|
233
|
+
'<span class="hub-row-url">' + esc(conn.baseUrl) + "</span>" +
|
|
234
|
+
"</div>" +
|
|
235
|
+
'<div class="hub-row-actions">' +
|
|
236
|
+
'<button class="hub-action" data-action="edit" data-idx="' + i + '">Edit</button>' +
|
|
237
|
+
'<button class="hub-action hub-action--danger" data-action="remove" data-idx="' + i + '">Remove</button>' +
|
|
238
|
+
"</div>";
|
|
239
|
+
list.appendChild(li);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function openAddConnection() {
|
|
244
|
+
editingIdx = -1;
|
|
245
|
+
setConnView("chooser");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function openConnectionForm(kind) {
|
|
249
|
+
draftKind = kind;
|
|
250
|
+
if (editingIdx >= 0) {
|
|
251
|
+
var conn = connections[editingIdx];
|
|
252
|
+
draftProvider = conn.provider;
|
|
253
|
+
draftKind = conn.kind;
|
|
254
|
+
} else {
|
|
255
|
+
draftProvider = kind === "cloud" ? "openai" : "ollama";
|
|
256
|
+
}
|
|
257
|
+
renderConnectionForm();
|
|
258
|
+
setConnView("form");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderConnectionForm() {
|
|
262
|
+
var modeCard = $("conn-mode-card");
|
|
263
|
+
var badge = $("conn-mode-badge");
|
|
264
|
+
var title = $("conn-mode-title");
|
|
265
|
+
var cloudPicks = $("cloud-provider-picks");
|
|
266
|
+
var localList = $("local-provider-list");
|
|
267
|
+
var apikeyGroup = $("conn-apikey-group");
|
|
268
|
+
|
|
269
|
+
// Mode card styling
|
|
270
|
+
modeCard.className = "connection-mode-card connection-mode-card--" + draftKind;
|
|
271
|
+
if (draftKind === "cloud") {
|
|
272
|
+
badge.textContent = "Cloud";
|
|
273
|
+
title.textContent = "Remote provider";
|
|
274
|
+
show(cloudPicks);
|
|
275
|
+
hide(localList);
|
|
276
|
+
show(apikeyGroup);
|
|
277
|
+
} else {
|
|
278
|
+
badge.textContent = "Local";
|
|
279
|
+
title.textContent = "Local provider";
|
|
280
|
+
hide(cloudPicks);
|
|
281
|
+
show(localList);
|
|
282
|
+
hide(apikeyGroup);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Cloud provider chips
|
|
286
|
+
cloudPicks.innerHTML = "";
|
|
287
|
+
CLOUD_PROVIDERS.forEach(function (p) {
|
|
288
|
+
var chip = document.createElement("button");
|
|
289
|
+
chip.type = "button";
|
|
290
|
+
chip.className = "provider-chip" + (p === draftProvider ? " selected" : "");
|
|
291
|
+
chip.textContent = PROVIDER_LABELS[p] || p;
|
|
292
|
+
chip.addEventListener("click", function () {
|
|
293
|
+
draftProvider = p;
|
|
294
|
+
applyProviderDefaults();
|
|
295
|
+
renderConnectionForm();
|
|
296
|
+
});
|
|
297
|
+
cloudPicks.appendChild(chip);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Local detected providers
|
|
301
|
+
localList.innerHTML = "";
|
|
302
|
+
var localDetected = detectedProviders.filter(function (d) { return d.available; });
|
|
303
|
+
if (localDetected.length > 0) {
|
|
304
|
+
localDetected.forEach(function (dp) {
|
|
305
|
+
var opt = document.createElement("button");
|
|
306
|
+
opt.type = "button";
|
|
307
|
+
opt.className = "provider-option" + (dp.provider === draftProvider ? " selected" : "");
|
|
308
|
+
opt.innerHTML =
|
|
309
|
+
'<span class="provider-option-status"><span class="status-dot--ok"></span></span>' +
|
|
310
|
+
'<span class="provider-option-label">' + esc(PROVIDER_LABELS[dp.provider] || dp.provider) + "</span>" +
|
|
311
|
+
'<span class="provider-option-hint">Detected at ' + esc(dp.url) + "</span>";
|
|
312
|
+
opt.addEventListener("click", function () {
|
|
313
|
+
draftProvider = dp.provider;
|
|
314
|
+
$("conn-base-url").value = dp.url;
|
|
315
|
+
$("conn-name").value = PROVIDER_LABELS[dp.provider] || dp.provider;
|
|
316
|
+
renderConnectionForm();
|
|
317
|
+
});
|
|
318
|
+
localList.appendChild(opt);
|
|
319
|
+
});
|
|
320
|
+
} else {
|
|
321
|
+
// Fallback: let user pick local provider
|
|
322
|
+
LOCAL_PROVIDERS.forEach(function (p) {
|
|
323
|
+
var opt = document.createElement("button");
|
|
324
|
+
opt.type = "button";
|
|
325
|
+
opt.className = "provider-option" + (p === draftProvider ? " selected" : "");
|
|
326
|
+
opt.innerHTML =
|
|
327
|
+
'<span class="provider-option-label">' + esc(PROVIDER_LABELS[p] || p) + "</span>";
|
|
328
|
+
opt.addEventListener("click", function () {
|
|
329
|
+
draftProvider = p;
|
|
330
|
+
applyProviderDefaults();
|
|
331
|
+
renderConnectionForm();
|
|
332
|
+
});
|
|
333
|
+
localList.appendChild(opt);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Fill defaults if new connection
|
|
338
|
+
if (editingIdx < 0) {
|
|
339
|
+
applyProviderDefaults();
|
|
340
|
+
} else {
|
|
341
|
+
var c = connections[editingIdx];
|
|
342
|
+
$("conn-name").value = c.name;
|
|
343
|
+
$("conn-base-url").value = c.baseUrl;
|
|
344
|
+
$("conn-api-key").value = c.apiKey;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Reset test status
|
|
348
|
+
hide($("conn-test-success"));
|
|
349
|
+
hideError($("conn-detail-error"));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function applyProviderDefaults() {
|
|
353
|
+
var def = PROVIDER_DEFAULTS[draftProvider];
|
|
354
|
+
if (!def) return;
|
|
355
|
+
var nameInput = $("conn-name");
|
|
356
|
+
var urlInput = $("conn-base-url");
|
|
357
|
+
if (!nameInput.value || isDefaultName(nameInput.value)) {
|
|
358
|
+
nameInput.value = PROVIDER_LABELS[draftProvider] || draftProvider;
|
|
359
|
+
}
|
|
360
|
+
urlInput.value = def.baseUrl;
|
|
361
|
+
$("conn-api-key").value = "";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isDefaultName(name) {
|
|
365
|
+
for (var key in PROVIDER_LABELS) {
|
|
366
|
+
if (PROVIDER_LABELS[key] === name) return true;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function saveConnection() {
|
|
372
|
+
var errEl = $("conn-detail-error");
|
|
373
|
+
hideError(errEl);
|
|
374
|
+
|
|
375
|
+
var name = ($("conn-name").value || "").trim();
|
|
376
|
+
var baseUrl = ($("conn-base-url").value || "").trim();
|
|
377
|
+
var apiKey = ($("conn-api-key").value || "").trim();
|
|
378
|
+
|
|
379
|
+
if (!name) { showError(errEl, "Connection name is required."); return; }
|
|
380
|
+
if (!baseUrl) { showError(errEl, "Base URL is required."); return; }
|
|
381
|
+
if (draftKind === "cloud" && !apiKey && draftProvider !== "anthropic") {
|
|
382
|
+
showError(errEl, "API key is required for cloud providers.");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
var conn = {
|
|
387
|
+
id: editingIdx >= 0 ? connections[editingIdx].id : generateId(),
|
|
388
|
+
name: name,
|
|
389
|
+
provider: draftProvider,
|
|
390
|
+
baseUrl: baseUrl,
|
|
391
|
+
apiKey: apiKey,
|
|
392
|
+
kind: draftKind,
|
|
393
|
+
models: editingIdx >= 0 ? connections[editingIdx].models : [],
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
if (editingIdx >= 0) {
|
|
397
|
+
connections[editingIdx] = conn;
|
|
398
|
+
} else {
|
|
399
|
+
connections.push(conn);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
editingIdx = -1;
|
|
403
|
+
renderConnHub();
|
|
404
|
+
setConnView("hub");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function removeConnection(idx) {
|
|
408
|
+
connections.splice(idx, 1);
|
|
409
|
+
renderConnHub();
|
|
410
|
+
setConnView("hub");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function editConnectionAt(idx) {
|
|
414
|
+
editingIdx = idx;
|
|
415
|
+
var conn = connections[idx];
|
|
416
|
+
draftKind = conn.kind;
|
|
417
|
+
draftProvider = conn.provider;
|
|
418
|
+
renderConnectionForm();
|
|
419
|
+
setConnView("form");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Test connection by fetching models */
|
|
423
|
+
async function testConnection() {
|
|
424
|
+
var errEl = $("conn-detail-error");
|
|
425
|
+
hideError(errEl);
|
|
426
|
+
hide($("conn-test-success"));
|
|
427
|
+
|
|
428
|
+
var baseUrl = ($("conn-base-url").value || "").trim();
|
|
429
|
+
var apiKey = ($("conn-api-key").value || "").trim();
|
|
430
|
+
if (!baseUrl) { showError(errEl, "Base URL is required."); return; }
|
|
431
|
+
|
|
432
|
+
var btn = $("btn-conn-test");
|
|
433
|
+
btn.disabled = true;
|
|
434
|
+
btn.innerHTML = '<span class="spinner"></span> Testing...';
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
var models = await apiFetchModels(draftProvider, baseUrl, apiKey);
|
|
438
|
+
if (models && models.length > 0) {
|
|
439
|
+
show($("conn-test-success"));
|
|
440
|
+
$("conn-test-msg").textContent = "Connected -- " + models.length + " model" + (models.length !== 1 ? "s" : "") + " found.";
|
|
441
|
+
// Store models for this draft
|
|
442
|
+
if (editingIdx >= 0) {
|
|
443
|
+
connections[editingIdx].models = models;
|
|
444
|
+
} else {
|
|
445
|
+
// Will store when saved
|
|
446
|
+
modelCache["_draft"] = models;
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
show($("conn-test-success"));
|
|
450
|
+
$("conn-test-msg").textContent = "Connected (no models listed).";
|
|
451
|
+
}
|
|
452
|
+
} catch (e) {
|
|
453
|
+
showError(errEl, "Connection failed: " + (e.message || "unknown error"));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
btn.disabled = false;
|
|
457
|
+
btn.textContent = "Test";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function generateId() {
|
|
461
|
+
return Math.random().toString(36).slice(2, 10);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* =========================================================================
|
|
465
|
+
Step 2: Model Assignment
|
|
466
|
+
========================================================================= */
|
|
467
|
+
|
|
468
|
+
function initStep2() {
|
|
469
|
+
populateConnectionSelects();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function populateConnectionSelects() {
|
|
473
|
+
var llmSel = $("llm-connection");
|
|
474
|
+
var embSel = $("emb-connection");
|
|
475
|
+
var prevLlm = llmSel.value;
|
|
476
|
+
var prevEmb = embSel.value;
|
|
477
|
+
|
|
478
|
+
// Rebuild options
|
|
479
|
+
llmSel.innerHTML = '<option value="" disabled>Select a connection</option>';
|
|
480
|
+
embSel.innerHTML = '<option value="" disabled>Select a connection</option>';
|
|
481
|
+
|
|
482
|
+
connections.forEach(function (conn) {
|
|
483
|
+
var o1 = document.createElement("option");
|
|
484
|
+
o1.value = conn.id;
|
|
485
|
+
o1.textContent = conn.name || conn.provider;
|
|
486
|
+
llmSel.appendChild(o1);
|
|
487
|
+
|
|
488
|
+
var o2 = document.createElement("option");
|
|
489
|
+
o2.value = conn.id;
|
|
490
|
+
o2.textContent = conn.name || conn.provider;
|
|
491
|
+
embSel.appendChild(o2);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Restore or auto-select
|
|
495
|
+
if (prevLlm && connections.some(function (c) { return c.id === prevLlm; })) {
|
|
496
|
+
llmSel.value = prevLlm;
|
|
497
|
+
} else if (connections.length > 0) {
|
|
498
|
+
llmSel.value = connections[0].id;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (prevEmb && connections.some(function (c) { return c.id === prevEmb; })) {
|
|
502
|
+
embSel.value = prevEmb;
|
|
503
|
+
} else if (connections.length > 0) {
|
|
504
|
+
embSel.value = connections[0].id;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Fetch models for selected connections
|
|
508
|
+
if (llmSel.value) loadModelsForConnection(llmSel.value, "llm");
|
|
509
|
+
if (embSel.value) loadModelsForConnection(embSel.value, "emb");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function loadModelsForConnection(connId, target) {
|
|
513
|
+
var conn = connections.find(function (c) { return c.id === connId; });
|
|
514
|
+
if (!conn) return;
|
|
515
|
+
|
|
516
|
+
var modelSel = $(target + "-model");
|
|
517
|
+
var smallSel = target === "llm" ? $("llm-small-model") : null;
|
|
518
|
+
|
|
519
|
+
// Try cached models first
|
|
520
|
+
var models = conn.models && conn.models.length > 0
|
|
521
|
+
? conn.models
|
|
522
|
+
: (modelCache[connId] || null);
|
|
523
|
+
|
|
524
|
+
if (!models) {
|
|
525
|
+
modelSel.innerHTML = '<option value="">Fetching models...</option>';
|
|
526
|
+
if (smallSel) smallSel.innerHTML = '<option value="">(loading...)</option>';
|
|
527
|
+
try {
|
|
528
|
+
models = await apiFetchModels(conn.provider, conn.baseUrl, conn.apiKey);
|
|
529
|
+
conn.models = models;
|
|
530
|
+
modelCache[connId] = models;
|
|
531
|
+
} catch (e) {
|
|
532
|
+
models = [];
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
var def = PROVIDER_DEFAULTS[conn.provider] || {};
|
|
537
|
+
var prevVal = modelSel.value;
|
|
538
|
+
|
|
539
|
+
if (models.length > 0) {
|
|
540
|
+
modelSel.innerHTML = "";
|
|
541
|
+
models.forEach(function (m) {
|
|
542
|
+
var o = document.createElement("option");
|
|
543
|
+
o.value = m;
|
|
544
|
+
o.textContent = m;
|
|
545
|
+
modelSel.appendChild(o);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Select best default
|
|
549
|
+
var preferred;
|
|
550
|
+
if (target === "llm") {
|
|
551
|
+
preferred = def.llmModel;
|
|
552
|
+
} else {
|
|
553
|
+
preferred = def.embModel;
|
|
554
|
+
}
|
|
555
|
+
if (preferred && models.indexOf(preferred) >= 0) {
|
|
556
|
+
modelSel.value = preferred;
|
|
557
|
+
} else if (prevVal && models.indexOf(prevVal) >= 0) {
|
|
558
|
+
modelSel.value = prevVal;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Also fill small model
|
|
562
|
+
if (smallSel) {
|
|
563
|
+
smallSel.innerHTML = '<option value="">(same as chat model)</option>';
|
|
564
|
+
models.forEach(function (m) {
|
|
565
|
+
var o = document.createElement("option");
|
|
566
|
+
o.value = m;
|
|
567
|
+
o.textContent = m;
|
|
568
|
+
smallSel.appendChild(o);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Auto-fill embedding dims
|
|
573
|
+
if (target === "emb") {
|
|
574
|
+
autoFillEmbDims(modelSel.value);
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
// No models found -- allow manual entry
|
|
578
|
+
modelSel.innerHTML = "";
|
|
579
|
+
var manualOpt = document.createElement("option");
|
|
580
|
+
manualOpt.value = "";
|
|
581
|
+
manualOpt.textContent = "(enter model name below)";
|
|
582
|
+
modelSel.appendChild(manualOpt);
|
|
583
|
+
|
|
584
|
+
// Replace with text input if needed
|
|
585
|
+
replaceWithTextInput(modelSel, target + "-model", target === "emb" ? (def.embModel || "") : (def.llmModel || ""));
|
|
586
|
+
if (smallSel && target === "llm") {
|
|
587
|
+
replaceWithTextInput(smallSel, "llm-small-model", "");
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function replaceWithTextInput(selectEl, id, defaultVal) {
|
|
593
|
+
var parent = selectEl.parentNode;
|
|
594
|
+
var input = document.createElement("input");
|
|
595
|
+
input.type = "text";
|
|
596
|
+
input.id = id;
|
|
597
|
+
input.value = defaultVal;
|
|
598
|
+
input.placeholder = "Enter model name";
|
|
599
|
+
parent.replaceChild(input, selectEl);
|
|
600
|
+
|
|
601
|
+
if (id === "emb-model") {
|
|
602
|
+
input.addEventListener("input", function () { autoFillEmbDims(input.value); });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function autoFillEmbDims(modelName) {
|
|
607
|
+
if (!modelName) return;
|
|
608
|
+
var name = modelName.replace(/:.*$/, ""); // strip tag
|
|
609
|
+
var dims = KNOWN_EMB_DIMS[modelName] || KNOWN_EMB_DIMS[name];
|
|
610
|
+
if (dims) {
|
|
611
|
+
$("emb-dims").value = dims;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function validateStep2() {
|
|
616
|
+
var errEl = $("step2-error");
|
|
617
|
+
hideError(errEl);
|
|
618
|
+
|
|
619
|
+
var llmConn = $("llm-connection").value;
|
|
620
|
+
var llmModel = ($("llm-model").value || "").trim();
|
|
621
|
+
var embConn = $("emb-connection").value;
|
|
622
|
+
var embModel = ($("emb-model").value || "").trim();
|
|
623
|
+
|
|
624
|
+
if (!llmConn) { showError(errEl, "Select a chat connection."); return false; }
|
|
625
|
+
if (!llmModel) { showError(errEl, "Chat model is required."); return false; }
|
|
626
|
+
if (!embConn) { showError(errEl, "Select an embedding connection."); return false; }
|
|
627
|
+
if (!embModel) { showError(errEl, "Embedding model is required."); return false; }
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* =========================================================================
|
|
632
|
+
Step 3: Options
|
|
633
|
+
========================================================================= */
|
|
634
|
+
|
|
635
|
+
function initStep3() {
|
|
636
|
+
// Show Ollama toggle if any connection uses Ollama
|
|
637
|
+
var hasOllama = connections.some(function (c) {
|
|
638
|
+
return c.provider === "ollama" || c.provider === "ollama-instack";
|
|
639
|
+
});
|
|
640
|
+
var addon = $("ollama-addon");
|
|
641
|
+
if (hasOllama) show(addon); else hide(addon);
|
|
642
|
+
|
|
643
|
+
// Memory user ID default
|
|
644
|
+
var memInput = $("memory-user-id");
|
|
645
|
+
if (!memInput.value) {
|
|
646
|
+
var email = ($("owner-email").value || "").trim();
|
|
647
|
+
memInput.value = email || "default_user";
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/* =========================================================================
|
|
652
|
+
Step 4: Review & Install
|
|
653
|
+
========================================================================= */
|
|
654
|
+
|
|
655
|
+
function initStep4() {
|
|
656
|
+
renderReview();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function renderReview() {
|
|
660
|
+
var grid = $("review-grid");
|
|
661
|
+
grid.innerHTML = "";
|
|
662
|
+
|
|
663
|
+
// Account section
|
|
664
|
+
grid.appendChild(reviewHeader("Account", 0));
|
|
665
|
+
grid.appendChild(reviewItem("Admin Token", maskToken($("admin-token").value)));
|
|
666
|
+
var ownerName = ($("owner-name").value || "").trim();
|
|
667
|
+
if (ownerName) grid.appendChild(reviewItem("Name", ownerName));
|
|
668
|
+
var ownerEmail = ($("owner-email").value || "").trim();
|
|
669
|
+
if (ownerEmail) grid.appendChild(reviewItem("Email", ownerEmail));
|
|
670
|
+
|
|
671
|
+
// Connections section
|
|
672
|
+
grid.appendChild(reviewHeader("Connections", 1));
|
|
673
|
+
connections.forEach(function (conn) {
|
|
674
|
+
grid.appendChild(reviewItem(conn.name || conn.provider, conn.kind + " -- " + conn.baseUrl, true));
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Models section
|
|
678
|
+
grid.appendChild(reviewHeader("Models", 2));
|
|
679
|
+
var llmConnId = $("llm-connection").value;
|
|
680
|
+
var llmConn = connections.find(function (c) { return c.id === llmConnId; });
|
|
681
|
+
var llmModel = ($("llm-model").value || "").trim();
|
|
682
|
+
var smallModel = ($("llm-small-model").value || "").trim();
|
|
683
|
+
var embConnId = $("emb-connection").value;
|
|
684
|
+
var embConn = connections.find(function (c) { return c.id === embConnId; });
|
|
685
|
+
var embModel = ($("emb-model").value || "").trim();
|
|
686
|
+
var embDims = ($("emb-dims").value || "1536").trim();
|
|
687
|
+
|
|
688
|
+
grid.appendChild(reviewItem("Chat Model", llmModel + " (" + (llmConn ? llmConn.name : "?") + ")", true));
|
|
689
|
+
if (smallModel) {
|
|
690
|
+
grid.appendChild(reviewItem("Small Model", smallModel + " (" + (llmConn ? llmConn.name : "?") + ")", true));
|
|
691
|
+
}
|
|
692
|
+
grid.appendChild(reviewItem("Embedding Model", embModel + " (" + (embConn ? embConn.name : "?") + ")", true));
|
|
693
|
+
grid.appendChild(reviewItem("Embedding Dims", embDims, true));
|
|
694
|
+
|
|
695
|
+
// Options section
|
|
696
|
+
grid.appendChild(reviewHeader("Options", 3));
|
|
697
|
+
var ollamaEnabled = $("ollama-enabled") && $("ollama-enabled").checked;
|
|
698
|
+
if (ollamaEnabled) {
|
|
699
|
+
grid.appendChild(reviewItem("Ollama In-Stack", "Enabled"));
|
|
700
|
+
}
|
|
701
|
+
grid.appendChild(reviewItem("Memory User ID", $("memory-user-id").value || "default_user"));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function reviewHeader(label, editStep) {
|
|
705
|
+
var div = document.createElement("div");
|
|
706
|
+
div.className = "review-section-header";
|
|
707
|
+
var span = document.createElement("span");
|
|
708
|
+
span.textContent = label;
|
|
709
|
+
div.appendChild(span);
|
|
710
|
+
var btn = document.createElement("button");
|
|
711
|
+
btn.className = "review-edit-btn";
|
|
712
|
+
btn.type = "button";
|
|
713
|
+
btn.textContent = "Edit";
|
|
714
|
+
btn.addEventListener("click", function () { goToStep(editStep); });
|
|
715
|
+
div.appendChild(btn);
|
|
716
|
+
return div;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function reviewItem(label, value, mono) {
|
|
720
|
+
var div = document.createElement("div");
|
|
721
|
+
div.className = "review-item";
|
|
722
|
+
var lbl = document.createElement("span");
|
|
723
|
+
lbl.className = "review-label";
|
|
724
|
+
lbl.textContent = label;
|
|
725
|
+
div.appendChild(lbl);
|
|
726
|
+
var val = document.createElement("span");
|
|
727
|
+
val.className = "review-value" + (mono ? " mono" : "");
|
|
728
|
+
val.textContent = value;
|
|
729
|
+
div.appendChild(val);
|
|
730
|
+
return div;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function maskToken(token) {
|
|
734
|
+
if (!token || token.length < 8) return "(not set)";
|
|
735
|
+
return token.slice(0, 4) + "..." + token.slice(-4);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/* =========================================================================
|
|
739
|
+
Install & Deploy
|
|
740
|
+
========================================================================= */
|
|
741
|
+
|
|
742
|
+
async function handleInstall() {
|
|
743
|
+
if (installing) return;
|
|
744
|
+
|
|
745
|
+
var errEl = $("install-error");
|
|
746
|
+
hideError(errEl);
|
|
747
|
+
|
|
748
|
+
var adminToken = ($("admin-token").value || "").trim();
|
|
749
|
+
var ownerName = ($("owner-name").value || "").trim();
|
|
750
|
+
var ownerEmail = ($("owner-email").value || "").trim();
|
|
751
|
+
var memoryUserId = ($("memory-user-id").value || "").trim() || ownerEmail || "default_user";
|
|
752
|
+
var ollamaEnabled = $("ollama-enabled") ? $("ollama-enabled").checked : false;
|
|
753
|
+
|
|
754
|
+
var llmConnId = $("llm-connection").value;
|
|
755
|
+
var llmModel = ($("llm-model").value || "").trim();
|
|
756
|
+
var smallModel = ($("llm-small-model").value || "").trim();
|
|
757
|
+
var embConnId = $("emb-connection").value;
|
|
758
|
+
var embModel = ($("emb-model").value || "").trim();
|
|
759
|
+
var embDims = parseInt($("emb-dims").value, 10) || 1536;
|
|
760
|
+
|
|
761
|
+
var payload = {
|
|
762
|
+
adminToken: adminToken,
|
|
763
|
+
ownerName: ownerName || undefined,
|
|
764
|
+
ownerEmail: ownerEmail || undefined,
|
|
765
|
+
memoryUserId: memoryUserId,
|
|
766
|
+
ollamaEnabled: ollamaEnabled,
|
|
767
|
+
connections: connections.map(function (c) {
|
|
768
|
+
return {
|
|
769
|
+
id: c.id,
|
|
770
|
+
name: c.name,
|
|
771
|
+
provider: c.provider,
|
|
772
|
+
baseUrl: c.baseUrl,
|
|
773
|
+
apiKey: c.apiKey,
|
|
774
|
+
};
|
|
775
|
+
}),
|
|
776
|
+
assignments: {
|
|
777
|
+
llm: {
|
|
778
|
+
connectionId: llmConnId,
|
|
779
|
+
model: llmModel,
|
|
780
|
+
smallModel: smallModel || undefined,
|
|
781
|
+
},
|
|
782
|
+
embeddings: {
|
|
783
|
+
connectionId: embConnId,
|
|
784
|
+
model: embModel,
|
|
785
|
+
embeddingDims: embDims,
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
installing = true;
|
|
791
|
+
var installBtn = $("btn-install");
|
|
792
|
+
installBtn.disabled = true;
|
|
793
|
+
installBtn.innerHTML = '<span class="spinner"></span> Installing...';
|
|
794
|
+
|
|
795
|
+
try {
|
|
796
|
+
var res = await fetch("/api/setup/complete", {
|
|
797
|
+
method: "POST",
|
|
798
|
+
headers: { "Content-Type": "application/json" },
|
|
799
|
+
body: JSON.stringify(payload),
|
|
800
|
+
});
|
|
801
|
+
var data = await res.json();
|
|
802
|
+
|
|
803
|
+
if (!res.ok || !data.ok) {
|
|
804
|
+
showError(errEl, data.error || data.message || "Install failed.");
|
|
805
|
+
installing = false;
|
|
806
|
+
installBtn.disabled = false;
|
|
807
|
+
installBtn.textContent = "Install";
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Success -- go to deploy screen
|
|
812
|
+
showDeployScreen();
|
|
813
|
+
startDeployPolling();
|
|
814
|
+
} catch (e) {
|
|
815
|
+
showError(errEl, "Network error: " + (e.message || "unable to reach server."));
|
|
816
|
+
installing = false;
|
|
817
|
+
installBtn.disabled = false;
|
|
818
|
+
installBtn.textContent = "Install";
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function startDeployPolling() {
|
|
823
|
+
stopDeployPolling();
|
|
824
|
+
pollDeployStatus();
|
|
825
|
+
deployTimer = setInterval(pollDeployStatus, 2500);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function stopDeployPolling() {
|
|
829
|
+
if (deployTimer) { clearInterval(deployTimer); deployTimer = null; }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async function pollDeployStatus() {
|
|
833
|
+
try {
|
|
834
|
+
var res = await fetch("/api/setup/deploy-status");
|
|
835
|
+
if (!res.ok) return;
|
|
836
|
+
var data = await res.json();
|
|
837
|
+
|
|
838
|
+
updateDeployUI(data);
|
|
839
|
+
|
|
840
|
+
if (data.setupComplete && !data.deployError) {
|
|
841
|
+
stopDeployPolling();
|
|
842
|
+
showDeployDone(data);
|
|
843
|
+
} else if (data.deployError) {
|
|
844
|
+
stopDeployPolling();
|
|
845
|
+
showDeployError(data.deployError);
|
|
846
|
+
}
|
|
847
|
+
} catch (e) {
|
|
848
|
+
// silently retry
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function updateDeployUI(data) {
|
|
853
|
+
var services = data.deployStatus || [];
|
|
854
|
+
var total = services.length;
|
|
855
|
+
var running = 0;
|
|
856
|
+
var ready = 0;
|
|
857
|
+
|
|
858
|
+
var container = $("deploy-services");
|
|
859
|
+
container.innerHTML = "";
|
|
860
|
+
|
|
861
|
+
services.forEach(function (svc) {
|
|
862
|
+
if (svc.status === "running") running++;
|
|
863
|
+
if (svc.status === "running" || svc.status === "ready") ready++;
|
|
864
|
+
|
|
865
|
+
var row = document.createElement("div");
|
|
866
|
+
row.className = "deploy-service-row";
|
|
867
|
+
|
|
868
|
+
// Indicator
|
|
869
|
+
var indicator = document.createElement("div");
|
|
870
|
+
indicator.className = "deploy-service-indicator";
|
|
871
|
+
if (svc.status === "running") {
|
|
872
|
+
indicator.innerHTML = '<span class="deploy-check"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--color-success)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>';
|
|
873
|
+
} else if (svc.status === "error") {
|
|
874
|
+
indicator.innerHTML = '<span class="deploy-warning"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg></span>';
|
|
875
|
+
} else {
|
|
876
|
+
indicator.innerHTML = '<span class="deploy-spinner"><span class="spinner"></span></span>';
|
|
877
|
+
}
|
|
878
|
+
row.appendChild(indicator);
|
|
879
|
+
|
|
880
|
+
// Info
|
|
881
|
+
var info = document.createElement("div");
|
|
882
|
+
info.className = "deploy-service-info";
|
|
883
|
+
info.innerHTML =
|
|
884
|
+
'<span class="deploy-service-name">' + esc(svc.service || svc.label || "") + "</span>" +
|
|
885
|
+
'<span class="deploy-service-status">' + esc(svc.label || svc.status) + "</span>";
|
|
886
|
+
row.appendChild(info);
|
|
887
|
+
|
|
888
|
+
// Bar
|
|
889
|
+
var bar = document.createElement("div");
|
|
890
|
+
bar.className = "deploy-service-bar";
|
|
891
|
+
var fill = document.createElement("div");
|
|
892
|
+
fill.className = "deploy-bar-fill";
|
|
893
|
+
if (svc.status === "running") fill.classList.add("complete");
|
|
894
|
+
else if (svc.status === "ready") fill.classList.add("ready");
|
|
895
|
+
else if (svc.status === "error") fill.classList.add("stopped");
|
|
896
|
+
else fill.classList.add("indeterminate");
|
|
897
|
+
bar.appendChild(fill);
|
|
898
|
+
row.appendChild(bar);
|
|
899
|
+
|
|
900
|
+
container.appendChild(row);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Progress
|
|
904
|
+
var pct = total > 0 ? Math.round((running / total) * 100) : 0;
|
|
905
|
+
$("deploy-progress-value").textContent = pct + "%";
|
|
906
|
+
$("deploy-progress-fill").style.width = pct + "%";
|
|
907
|
+
|
|
908
|
+
if (pct > 0 && pct < 100) {
|
|
909
|
+
$("deploy-title").textContent = "Starting Services...";
|
|
910
|
+
$("deploy-subtitle").textContent = running + " of " + total + " services running.";
|
|
911
|
+
} else if (ready > 0 && running === 0) {
|
|
912
|
+
$("deploy-title").textContent = "Pulling Images...";
|
|
913
|
+
$("deploy-subtitle").textContent = "Downloading container images.";
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function showDeployDone(data) {
|
|
918
|
+
hide($("deploy-tips"));
|
|
919
|
+
hide($("deploy-failure"));
|
|
920
|
+
hide($("deploy-error-actions"));
|
|
921
|
+
show($("deploy-done"));
|
|
922
|
+
|
|
923
|
+
$("deploy-title").textContent = "Setup Complete";
|
|
924
|
+
$("deploy-subtitle").textContent = "All services are up and running.";
|
|
925
|
+
$("deploy-progress-value").textContent = "100%";
|
|
926
|
+
$("deploy-progress-fill").style.width = "100%";
|
|
927
|
+
|
|
928
|
+
// Service list
|
|
929
|
+
var list = $("deploy-service-list");
|
|
930
|
+
list.innerHTML = "";
|
|
931
|
+
(data.deployStatus || []).forEach(function (svc) {
|
|
932
|
+
var li = document.createElement("li");
|
|
933
|
+
li.textContent = svc.service || svc.label || "";
|
|
934
|
+
list.appendChild(li);
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function showDeployError(error) {
|
|
939
|
+
hide($("deploy-tips"));
|
|
940
|
+
hide($("deploy-done"));
|
|
941
|
+
show($("deploy-failure"));
|
|
942
|
+
show($("deploy-error-actions"));
|
|
943
|
+
|
|
944
|
+
$("deploy-title").textContent = "Deployment Issue";
|
|
945
|
+
$("deploy-subtitle").textContent = "Setup could not finish starting the stack.";
|
|
946
|
+
$("deploy-failure-summary").textContent = typeof error === "string" ? error : "Deployment failed.";
|
|
947
|
+
$("deploy-error-pre").textContent = typeof error === "string" ? error : JSON.stringify(error, null, 2);
|
|
948
|
+
|
|
949
|
+
var bar = $("deploy-progress-bar");
|
|
950
|
+
if (bar) bar.parentElement.querySelector(".deploy-progress-bar").classList.add("deploy-progress-bar--error");
|
|
951
|
+
$("deploy-progress-value").textContent = "Error";
|
|
952
|
+
$("deploy-progress-value").classList.add("deploy-progress-value--error");
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* =========================================================================
|
|
956
|
+
API Calls
|
|
957
|
+
========================================================================= */
|
|
958
|
+
|
|
959
|
+
async function detectProviders() {
|
|
960
|
+
show($("conn-detecting"));
|
|
961
|
+
try {
|
|
962
|
+
var res = await fetch("/api/setup/detect-providers");
|
|
963
|
+
if (res.ok) {
|
|
964
|
+
var data = await res.json();
|
|
965
|
+
detectedProviders = data.providers || [];
|
|
966
|
+
}
|
|
967
|
+
} catch (e) {
|
|
968
|
+
detectedProviders = [];
|
|
969
|
+
}
|
|
970
|
+
hide($("conn-detecting"));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function apiFetchModels(provider, baseUrl, apiKey) {
|
|
974
|
+
var url = "/api/setup/models/" + encodeURIComponent(provider);
|
|
975
|
+
var res = await fetch(url, {
|
|
976
|
+
method: "POST",
|
|
977
|
+
headers: { "Content-Type": "application/json" },
|
|
978
|
+
body: JSON.stringify({ apiKey: apiKey || "", baseUrl: baseUrl || "" }),
|
|
979
|
+
});
|
|
980
|
+
if (!res.ok) throw new Error("Failed to fetch models (HTTP " + res.status + ")");
|
|
981
|
+
var data = await res.json();
|
|
982
|
+
return data.models || [];
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/* =========================================================================
|
|
986
|
+
Utility
|
|
987
|
+
========================================================================= */
|
|
988
|
+
|
|
989
|
+
function esc(str) {
|
|
990
|
+
var div = document.createElement("div");
|
|
991
|
+
div.appendChild(document.createTextNode(str || ""));
|
|
992
|
+
return div.innerHTML;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/* =========================================================================
|
|
996
|
+
Event Binding
|
|
997
|
+
========================================================================= */
|
|
998
|
+
|
|
999
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
1000
|
+
// Generate initial admin token
|
|
1001
|
+
initStep0();
|
|
1002
|
+
|
|
1003
|
+
// Check setup status first
|
|
1004
|
+
fetch("/api/setup/status")
|
|
1005
|
+
.then(function (r) { return r.json(); })
|
|
1006
|
+
.then(function (data) {
|
|
1007
|
+
if (data.setupComplete) {
|
|
1008
|
+
// Already set up -- redirect
|
|
1009
|
+
window.location.href = "/";
|
|
1010
|
+
}
|
|
1011
|
+
})
|
|
1012
|
+
.catch(function () { /* ignore */ });
|
|
1013
|
+
|
|
1014
|
+
// ── Step indicator clicks ──
|
|
1015
|
+
qsa(".step-dot").forEach(function (dot) {
|
|
1016
|
+
dot.addEventListener("click", function () {
|
|
1017
|
+
var step = parseInt(dot.dataset.step, 10);
|
|
1018
|
+
if (!isNaN(step) && step <= maxVisitedStep) {
|
|
1019
|
+
goToStep(step);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// ── Step 0: Welcome ──
|
|
1025
|
+
$("btn-step0-next").addEventListener("click", function () {
|
|
1026
|
+
if (validateStep0()) goToStep(1);
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// ── Step 1: Connections ──
|
|
1030
|
+
$("btn-step1-back").addEventListener("click", function () { goToStep(0); });
|
|
1031
|
+
$("btn-step1-add").addEventListener("click", function () { openAddConnection(); });
|
|
1032
|
+
$("btn-step1-next").addEventListener("click", function () {
|
|
1033
|
+
if (connections.length > 0) goToStep(2);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
// Connection type chooser
|
|
1037
|
+
$("btn-add-cloud").addEventListener("click", function () { openConnectionForm("cloud"); });
|
|
1038
|
+
$("btn-add-local").addEventListener("click", function () { openConnectionForm("local"); });
|
|
1039
|
+
|
|
1040
|
+
// Connection detail form
|
|
1041
|
+
$("btn-conn-cancel").addEventListener("click", function () {
|
|
1042
|
+
editingIdx = -1;
|
|
1043
|
+
renderConnHub();
|
|
1044
|
+
setConnView("hub");
|
|
1045
|
+
});
|
|
1046
|
+
$("btn-conn-test").addEventListener("click", function () { testConnection(); });
|
|
1047
|
+
$("btn-conn-save").addEventListener("click", function () { saveConnection(); });
|
|
1048
|
+
|
|
1049
|
+
// Hub list delegation (edit/remove)
|
|
1050
|
+
$("conn-hub-list").addEventListener("click", function (e) {
|
|
1051
|
+
var btn = e.target.closest("[data-action]");
|
|
1052
|
+
if (!btn) return;
|
|
1053
|
+
var idx = parseInt(btn.dataset.idx, 10);
|
|
1054
|
+
if (btn.dataset.action === "edit") editConnectionAt(idx);
|
|
1055
|
+
if (btn.dataset.action === "remove") removeConnection(idx);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
// ── Step 2: Models ──
|
|
1059
|
+
$("btn-step2-back").addEventListener("click", function () { goToStep(1); });
|
|
1060
|
+
$("btn-step2-next").addEventListener("click", function () {
|
|
1061
|
+
if (validateStep2()) goToStep(3);
|
|
1062
|
+
});
|
|
1063
|
+
$("btn-models-add-conn").addEventListener("click", function () {
|
|
1064
|
+
goToStep(1);
|
|
1065
|
+
openAddConnection();
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
$("llm-connection").addEventListener("change", function () {
|
|
1069
|
+
loadModelsForConnection(this.value, "llm");
|
|
1070
|
+
});
|
|
1071
|
+
$("emb-connection").addEventListener("change", function () {
|
|
1072
|
+
loadModelsForConnection(this.value, "emb");
|
|
1073
|
+
});
|
|
1074
|
+
$("emb-model").addEventListener("change", function () {
|
|
1075
|
+
autoFillEmbDims(this.value);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
// ── Step 3: Options ──
|
|
1079
|
+
$("btn-step3-back").addEventListener("click", function () { goToStep(2); });
|
|
1080
|
+
$("btn-step3-next").addEventListener("click", function () { goToStep(4); });
|
|
1081
|
+
|
|
1082
|
+
// ── Step 4: Review ──
|
|
1083
|
+
$("btn-step4-back").addEventListener("click", function () { goToStep(3); });
|
|
1084
|
+
$("btn-install").addEventListener("click", function () { handleInstall(); });
|
|
1085
|
+
|
|
1086
|
+
// ── Deploy error actions ──
|
|
1087
|
+
$("btn-deploy-back").addEventListener("click", function () {
|
|
1088
|
+
installing = false;
|
|
1089
|
+
goToStep(4);
|
|
1090
|
+
});
|
|
1091
|
+
$("btn-deploy-retry").addEventListener("click", function () {
|
|
1092
|
+
hide($("deploy-failure"));
|
|
1093
|
+
hide($("deploy-error-actions"));
|
|
1094
|
+
show($("deploy-tips"));
|
|
1095
|
+
$("deploy-progress-value").classList.remove("deploy-progress-value--error");
|
|
1096
|
+
$("deploy-progress-value").textContent = "0%";
|
|
1097
|
+
$("deploy-progress-fill").style.width = "0%";
|
|
1098
|
+
handleInstall();
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
// Start on step 0
|
|
1102
|
+
updateStepIndicators();
|
|
1103
|
+
});
|
|
1104
|
+
})();
|