openpalm 0.9.4 → 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.
@@ -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
+ })();