openpalm 0.9.8 → 0.9.10
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/package.json +4 -2
- package/playwright.config.ts +16 -0
- package/src/commands/install-file.test.ts +306 -0
- package/src/commands/install-services.test.ts +12 -7
- package/src/commands/install-services.ts +1 -1
- package/src/commands/install.ts +142 -34
- package/src/commands/restart.ts +2 -35
- package/src/commands/service.ts +0 -17
- package/src/commands/start.ts +5 -43
- package/src/commands/status.ts +0 -9
- package/src/commands/stop.ts +4 -36
- package/src/commands/uninstall.ts +23 -14
- package/src/commands/update.ts +0 -9
- package/src/lib/docker.ts +25 -7
- package/src/lib/env.ts +13 -60
- package/src/lib/paths.ts +11 -1
- package/src/lib/staging.ts +3 -3
- package/src/main.test.ts +118 -80
- package/src/setup-wizard/index.html +114 -180
- package/src/setup-wizard/server-errors.test.ts +429 -0
- package/src/setup-wizard/server-integration.test.ts +511 -0
- package/src/setup-wizard/server.test.ts +6 -6
- package/src/setup-wizard/server.ts +17 -5
- package/src/setup-wizard/standalone.ts +166 -0
- package/src/setup-wizard/wizard.css +892 -299
- package/src/setup-wizard/wizard.js +1172 -559
- package/src/lib/admin.ts +0 -107
|
@@ -18,28 +18,35 @@
|
|
|
18
18
|
Provider Constants & Defaults
|
|
19
19
|
========================================================================= */
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
lmstudio: "LM Studio", "
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
21
|
+
var PROVIDER_GROUPS = [
|
|
22
|
+
{ id: "recommended", label: "Recommended", desc: "Best options to get started quickly" },
|
|
23
|
+
{ id: "local", label: "Local", desc: "Run models on your own hardware" },
|
|
24
|
+
{ id: "cloud", label: "Cloud", desc: "Hosted inference providers" },
|
|
25
|
+
{ id: "advanced", label: "Advanced", desc: "Additional providers" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
var PROVIDERS = [
|
|
29
|
+
// Recommended — best first-run experience
|
|
30
|
+
{ id: "ollama", name: "Ollama", kind: "local", group: "recommended", order: 1, icon: "\uD83E\uDD99", desc: "Run open models on your hardware", needsKey: false, placeholder: "", baseUrl: "http://localhost:11434", llmModel: "llama3.2", embModel: "nomic-embed-text", embDims: 768, canDetect: true },
|
|
31
|
+
{ id: "huggingface", name: "Hugging Face", kind: "cloud", group: "recommended", order: 2, icon: "\uD83E\uDD17", desc: "10,000+ open models via Inference Providers", needsKey: true, placeholder: "hf_...", baseUrl: "https://router.huggingface.co/v1", llmModel: "Qwen/Qwen3-32B", embModel: "intfloat/multilingual-e5-large", embDims: 1024, keyPrefix: "hf_" },
|
|
32
|
+
|
|
33
|
+
{ id: "openai", name: "OpenAI", kind: "cloud", group: "recommended", order: 3, icon: "\u25D0", desc: "GPT and o-series reasoning models", needsKey: true, placeholder: "sk-...", baseUrl: "https://api.openai.com", llmModel: "gpt-4o", embModel: "text-embedding-3-small", embDims: 1536 },
|
|
34
|
+
{ id: "google", name: "Google", kind: "cloud", group: "recommended", order: 4, icon: "\u25C6", desc: "Gemini models with large context", needsKey: true, placeholder: "AIza...", baseUrl: "https://generativelanguage.googleapis.com", llmModel: "gemini-2.5-flash", embModel: "", embDims: 0, keyPrefix: "AI" },
|
|
35
|
+
|
|
36
|
+
// Local — self-hosted model runtimes
|
|
37
|
+
{ id: "model-runner", name: "Docker Model Runner", kind: "local", group: "local", order: 1, icon: "\uD83D\uDC33", desc: "Docker-managed model runtime", needsKey: false, placeholder: "", baseUrl: "http://localhost:12434", llmModel: "ai/llama3.2", embModel: "ai/mxbai-embed-large-v1", embDims: 1024, canDetect: true },
|
|
38
|
+
{ id: "lmstudio", name: "LM Studio", kind: "local", group: "local", order: 2, icon: "\uD83D\uDD2C", desc: "Desktop app for local inference", needsKey: false, placeholder: "", baseUrl: "http://localhost:1234", llmModel: "loaded-model", embModel: "", embDims: 0, canDetect: true },
|
|
39
|
+
|
|
40
|
+
// Cloud — hosted inference APIs
|
|
41
|
+
{ id: "groq", name: "Groq", kind: "cloud", group: "cloud", order: 1, icon: "\u26A1", desc: "Ultra-fast inference", needsKey: true, placeholder: "gsk_...", baseUrl: "https://api.groq.com/openai", llmModel: "llama-3.3-70b-versatile", embModel: "", embDims: 0 },
|
|
42
|
+
{ id: "mistral", name: "Mistral", kind: "cloud", group: "cloud", order: 2, icon: "\u25C6", desc: "Mistral & Codestral models", needsKey: true, placeholder: "...", baseUrl: "https://api.mistral.ai", llmModel: "mistral-large-latest", embModel: "mistral-embed", embDims: 1024 },
|
|
43
|
+
{ id: "together", name: "Together AI", kind: "cloud", group: "cloud", order: 3, icon: "\u2726", desc: "Open models at scale", needsKey: true, placeholder: "...", baseUrl: "https://api.together.xyz", llmModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo", embModel: "", embDims: 0 },
|
|
44
|
+
|
|
45
|
+
// Advanced — niche or specialized providers
|
|
46
|
+
{ id: "deepseek", name: "DeepSeek", kind: "cloud", group: "advanced", order: 1, icon: "\u25CE", desc: "DeepSeek chat & reasoning", needsKey: true, placeholder: "sk-...", baseUrl: "https://api.deepseek.com", llmModel: "deepseek-chat", embModel: "", embDims: 0 },
|
|
47
|
+
{ id: "xai", name: "xAI (Grok)", kind: "cloud", group: "advanced", order: 2, icon: "\u2726", desc: "Grok models", needsKey: true, placeholder: "xai-...", baseUrl: "https://api.x.ai", llmModel: "grok-2", embModel: "", embDims: 0 },
|
|
48
|
+
{ id: "openai-compatible", name: "Custom (OpenAI-compatible)", kind: "cloud", group: "advanced", order: 3, icon: "\uD83D\uDD27", desc: "Any endpoint that speaks the OpenAI API", needsKey: false, needsUrl: true, optionalKey: true, placeholder: "API key (optional)", baseUrl: "", llmModel: "", embModel: "", embDims: 0 },
|
|
49
|
+
];
|
|
43
50
|
|
|
44
51
|
/** Known embedding dimensions for auto-fill */
|
|
45
52
|
var KNOWN_EMB_DIMS = {
|
|
@@ -48,15 +55,64 @@
|
|
|
48
55
|
"mxbai-embed-large": 1024, "mxbai-embed-large-v1": 1024,
|
|
49
56
|
"ai/mxbai-embed-large-v1": 1024, "mistral-embed": 1024,
|
|
50
57
|
"all-minilm": 384, "snowflake-arctic-embed": 1024,
|
|
58
|
+
"intfloat/multilingual-e5-large": 1024,
|
|
51
59
|
};
|
|
52
60
|
|
|
61
|
+
var STEP_LABELS = ["Welcome", "Providers", "Models", "Voice", "Options", "Review"];
|
|
62
|
+
var TOTAL_STEPS = 6;
|
|
63
|
+
|
|
64
|
+
/* =========================================================================
|
|
65
|
+
Voice / TTS / STT Options
|
|
66
|
+
========================================================================= */
|
|
67
|
+
|
|
68
|
+
var TTS_OPTIONS = [
|
|
69
|
+
{ id: "kokoro", name: "Kokoro TTS", type: "local", recommended: true, desc: "High-quality local TTS \u2014 runs on CPU" },
|
|
70
|
+
{ id: "piper", name: "Piper TTS", type: "local", desc: "Ultra-lightweight \u2014 great for low-power hardware" },
|
|
71
|
+
{ id: "openai-tts", name: "OpenAI TTS", type: "cloud", desc: "Cloud voices. Uses your OpenAI API key" },
|
|
72
|
+
{ id: "browser-tts", name: "Browser Built-in", type: "builtin", desc: "Native speech synthesis. No setup needed" },
|
|
73
|
+
{ id: "skip-tts", name: "Skip \u2014 text only", type: "skip", desc: "Add TTS later from the dashboard" },
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
var STT_OPTIONS = [
|
|
77
|
+
{ id: "whisper-local", name: "Whisper (local)", type: "local", recommended: true, desc: "Whisper in Docker. Accurate, private" },
|
|
78
|
+
{ id: "openai-stt", name: "OpenAI Whisper", type: "cloud", desc: "Cloud Whisper API. Uses OpenAI key" },
|
|
79
|
+
{ id: "browser-stt", name: "Browser Built-in", type: "builtin", desc: "Web Speech API. No setup" },
|
|
80
|
+
{ id: "skip-stt", name: "Skip \u2014 text only", type: "skip", desc: "Add STT later from the dashboard" },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/* =========================================================================
|
|
84
|
+
Channel & Service Constants
|
|
85
|
+
========================================================================= */
|
|
86
|
+
|
|
87
|
+
var CHANNELS = [
|
|
88
|
+
{ id: "chat", name: "Web Chat", icon: "\uD83D\uDCAC", desc: "Browser-based chat \u2014 always available", locked: true },
|
|
89
|
+
{ id: "api", name: "API", icon: "\uD83D\uDD0C", desc: "OpenAI-compatible REST API endpoint" },
|
|
90
|
+
{
|
|
91
|
+
id: "discord", name: "Discord", icon: "\uD83C\uDFAE", desc: "Connect to a Discord server",
|
|
92
|
+
credentials: [
|
|
93
|
+
{ key: "botToken", label: "Bot Token", placeholder: "Paste Discord bot token", required: true },
|
|
94
|
+
{ key: "applicationId", label: "Application ID", placeholder: "Discord application ID", secret: false },
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "slack", name: "Slack", icon: "\uD83D\uDCBC", desc: "Access via Slack bot",
|
|
99
|
+
credentials: [
|
|
100
|
+
{ key: "slackBotToken", label: "Bot Token", placeholder: "xoxb-...", required: true },
|
|
101
|
+
{ key: "slackAppToken", label: "App Token", placeholder: "xapp-...", required: true },
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
var SERVICES = [
|
|
107
|
+
{ id: "admin", name: "Admin Dashboard", icon: "\u2699\uFE0F", desc: "Web-based admin UI for managing your stack", recommended: true },
|
|
108
|
+
{ id: "openviking", name: "OpenViking", icon: "\u2694\uFE0F", desc: "Agentic task execution engine" },
|
|
109
|
+
];
|
|
110
|
+
|
|
53
111
|
/* =========================================================================
|
|
54
112
|
DOM Helpers
|
|
55
113
|
========================================================================= */
|
|
56
114
|
|
|
57
115
|
function $(id) { return document.getElementById(id); }
|
|
58
|
-
function qs(sel) { return document.querySelector(sel); }
|
|
59
|
-
function qsa(sel) { return document.querySelectorAll(sel); }
|
|
60
116
|
function show(el) { if (el) el.classList.remove("hidden"); }
|
|
61
117
|
function hide(el) { if (el) el.classList.add("hidden"); }
|
|
62
118
|
function showError(el, msg) { if (el) { el.textContent = msg; show(el); } }
|
|
@@ -68,27 +124,32 @@
|
|
|
68
124
|
|
|
69
125
|
var currentStep = 0;
|
|
70
126
|
var maxVisitedStep = 0;
|
|
127
|
+
var welcomeHeroDismissed = false;
|
|
71
128
|
|
|
72
|
-
/**
|
|
73
|
-
var
|
|
129
|
+
/** Provider selection state: { providerId: { selected, verified, verifying, error, apiKey, baseUrl, models[], ollamaMode } } */
|
|
130
|
+
var providerState = {};
|
|
74
131
|
|
|
75
|
-
/**
|
|
76
|
-
var
|
|
132
|
+
/** Expanded provider card (only one at a time) */
|
|
133
|
+
var expandedProvider = null;
|
|
77
134
|
|
|
78
|
-
/**
|
|
79
|
-
var
|
|
135
|
+
/** Provider detection results */
|
|
136
|
+
var detectedProviders = [];
|
|
80
137
|
|
|
81
|
-
/**
|
|
82
|
-
var
|
|
138
|
+
/** Model selection: { llm: {connId, model}, embedding: {connId, model, dims}, small: {connId, model} } */
|
|
139
|
+
var modelSelection = {};
|
|
83
140
|
|
|
84
|
-
/**
|
|
85
|
-
var
|
|
141
|
+
/** Voice selection state */
|
|
142
|
+
var voiceSelection = { tts: null, stt: null };
|
|
86
143
|
|
|
87
|
-
/**
|
|
88
|
-
var
|
|
144
|
+
/** Channel selection state (chat always on) */
|
|
145
|
+
var channelSelection = {
|
|
146
|
+
chat: true,
|
|
147
|
+
discord: { enabled: false, botToken: "", applicationId: "" },
|
|
148
|
+
slack: { enabled: false, slackBotToken: "", slackAppToken: "" },
|
|
149
|
+
};
|
|
89
150
|
|
|
90
|
-
/**
|
|
91
|
-
var
|
|
151
|
+
/** Services selection state (admin default on) */
|
|
152
|
+
var serviceSelection = { admin: true };
|
|
92
153
|
|
|
93
154
|
/** Deploy polling timer */
|
|
94
155
|
var deployTimer = null;
|
|
@@ -96,6 +157,20 @@
|
|
|
96
157
|
/** Whether install is in progress */
|
|
97
158
|
var installing = false;
|
|
98
159
|
|
|
160
|
+
// Initialize provider states
|
|
161
|
+
PROVIDERS.forEach(function (p) {
|
|
162
|
+
providerState[p.id] = {
|
|
163
|
+
selected: false,
|
|
164
|
+
verified: false,
|
|
165
|
+
verifying: false,
|
|
166
|
+
error: false,
|
|
167
|
+
apiKey: "",
|
|
168
|
+
baseUrl: p.baseUrl || "",
|
|
169
|
+
models: [],
|
|
170
|
+
ollamaMode: null, // null | "running" | "instack"
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
99
174
|
/* =========================================================================
|
|
100
175
|
Token Generation
|
|
101
176
|
========================================================================= */
|
|
@@ -106,15 +181,16 @@
|
|
|
106
181
|
return Array.from(arr, function (b) { return b.toString(16).padStart(2, "0"); }).join("");
|
|
107
182
|
}
|
|
108
183
|
|
|
184
|
+
function generateId() {
|
|
185
|
+
return Math.random().toString(36).slice(2, 10);
|
|
186
|
+
}
|
|
187
|
+
|
|
109
188
|
/* =========================================================================
|
|
110
189
|
Step Navigation
|
|
111
190
|
========================================================================= */
|
|
112
191
|
|
|
113
|
-
var TOTAL_STEPS = 5; // 0..4
|
|
114
|
-
|
|
115
192
|
function goToStep(n) {
|
|
116
193
|
if (n < 0 || n > TOTAL_STEPS - 1) return;
|
|
117
|
-
// Hide all step sections
|
|
118
194
|
for (var i = 0; i < TOTAL_STEPS; i++) {
|
|
119
195
|
var sec = $("step-" + i);
|
|
120
196
|
if (sec) { if (i === n) show(sec); else hide(sec); }
|
|
@@ -123,13 +199,14 @@
|
|
|
123
199
|
|
|
124
200
|
currentStep = n;
|
|
125
201
|
if (n > maxVisitedStep) maxVisitedStep = n;
|
|
126
|
-
|
|
202
|
+
renderProgressBar();
|
|
127
203
|
|
|
128
|
-
|
|
204
|
+
if (n === 0) initStep0();
|
|
129
205
|
if (n === 1) initStep1();
|
|
130
206
|
if (n === 2) initStep2();
|
|
131
207
|
if (n === 3) initStep3();
|
|
132
208
|
if (n === 4) initStep4();
|
|
209
|
+
if (n === 5) initStep5();
|
|
133
210
|
}
|
|
134
211
|
|
|
135
212
|
function showDeployScreen() {
|
|
@@ -138,34 +215,40 @@
|
|
|
138
215
|
hide($("step-indicators"));
|
|
139
216
|
}
|
|
140
217
|
|
|
141
|
-
function
|
|
218
|
+
function renderProgressBar() {
|
|
142
219
|
show($("step-indicators"));
|
|
143
|
-
var
|
|
144
|
-
var
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (i
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
220
|
+
var segHTML = "";
|
|
221
|
+
var lblHTML = "";
|
|
222
|
+
for (var i = 0; i < TOTAL_STEPS; i++) {
|
|
223
|
+
segHTML += '<div class="prog-seg ' + (i <= currentStep ? "on" : "") + '"></div>';
|
|
224
|
+
var cls = "prog-lbl";
|
|
225
|
+
if (i <= currentStep) cls += " on";
|
|
226
|
+
if (i === currentStep) cls += " active";
|
|
227
|
+
lblHTML += '<span class="' + cls + '" data-prog-step="' + i + '">' + STEP_LABELS[i] + '</span>';
|
|
228
|
+
}
|
|
229
|
+
$("prog-segments").innerHTML = segHTML;
|
|
230
|
+
$("prog-labels").innerHTML = lblHTML;
|
|
231
|
+
|
|
232
|
+
// Bind label clicks
|
|
233
|
+
var labels = document.querySelectorAll("[data-prog-step]");
|
|
234
|
+
labels.forEach(function (lbl) {
|
|
235
|
+
lbl.addEventListener("click", function () {
|
|
236
|
+
var step = parseInt(lbl.dataset.progStep, 10);
|
|
237
|
+
if (isNaN(step) || step > maxVisitedStep) return;
|
|
238
|
+
if (step > currentStep) {
|
|
239
|
+
if (step >= 1 && !validateStep0()) return;
|
|
240
|
+
if (step >= 2 && getVerifiedCount() === 0) return;
|
|
241
|
+
if (step >= 3 && !validateStep2()) return;
|
|
242
|
+
// Step 3 (voice) has no hard validation gate
|
|
243
|
+
if (step >= 5 && !validateStep4()) return;
|
|
244
|
+
}
|
|
245
|
+
goToStep(step);
|
|
246
|
+
});
|
|
164
247
|
});
|
|
165
248
|
}
|
|
166
249
|
|
|
167
250
|
/* =========================================================================
|
|
168
|
-
Step 0: Welcome &
|
|
251
|
+
Step 0: Welcome & Identity
|
|
169
252
|
========================================================================= */
|
|
170
253
|
|
|
171
254
|
function initStep0() {
|
|
@@ -173,6 +256,14 @@
|
|
|
173
256
|
if (tokenInput && !tokenInput.value) {
|
|
174
257
|
tokenInput.value = generateToken();
|
|
175
258
|
}
|
|
259
|
+
// Show hero or form based on state
|
|
260
|
+
if (!welcomeHeroDismissed) {
|
|
261
|
+
show($("welcome-hero"));
|
|
262
|
+
hide($("identity-form"));
|
|
263
|
+
} else {
|
|
264
|
+
hide($("welcome-hero"));
|
|
265
|
+
show($("identity-form"));
|
|
266
|
+
}
|
|
176
267
|
}
|
|
177
268
|
|
|
178
269
|
function validateStep0() {
|
|
@@ -187,458 +278,653 @@
|
|
|
187
278
|
}
|
|
188
279
|
|
|
189
280
|
/* =========================================================================
|
|
190
|
-
Step 1:
|
|
281
|
+
Step 1: Provider Card Grid
|
|
191
282
|
========================================================================= */
|
|
192
283
|
|
|
193
284
|
function initStep1() {
|
|
194
|
-
|
|
195
|
-
setConnView("hub");
|
|
285
|
+
renderProviderGrid();
|
|
196
286
|
// Auto-detect on first visit
|
|
197
|
-
if (detectedProviders.length === 0 &&
|
|
287
|
+
if (detectedProviders.length === 0 && getVerifiedCount() === 0) {
|
|
198
288
|
detectProviders();
|
|
199
289
|
}
|
|
200
290
|
}
|
|
201
291
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
}
|
|
292
|
+
function getVerifiedCount() {
|
|
293
|
+
var count = 0;
|
|
294
|
+
PROVIDERS.forEach(function (p) {
|
|
295
|
+
if (providerState[p.id].verified) count++;
|
|
296
|
+
});
|
|
297
|
+
return count;
|
|
221
298
|
}
|
|
222
299
|
|
|
223
|
-
function
|
|
224
|
-
var
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
300
|
+
function renderProviderGrid() {
|
|
301
|
+
var grid = $("provider-grid");
|
|
302
|
+
var html = "";
|
|
303
|
+
|
|
304
|
+
PROVIDER_GROUPS.forEach(function (g) {
|
|
305
|
+
var members = PROVIDERS.filter(function (p) { return p.group === g.id; })
|
|
306
|
+
.sort(function (a, b) { return a.order - b.order; });
|
|
307
|
+
if (members.length === 0) return;
|
|
308
|
+
|
|
309
|
+
html += '<div class="provider-group">';
|
|
310
|
+
html += '<div class="provider-group-header">';
|
|
311
|
+
html += '<h3 class="provider-group-label">' + esc(g.label) + '</h3>';
|
|
312
|
+
html += '<span class="provider-group-desc">' + esc(g.desc) + '</span>';
|
|
313
|
+
html += '</div>';
|
|
314
|
+
html += '<div class="provider-group-cards">';
|
|
315
|
+
members.forEach(function (p) { html += renderProviderCard(p); });
|
|
316
|
+
html += '</div></div>';
|
|
240
317
|
});
|
|
241
|
-
}
|
|
242
318
|
|
|
243
|
-
|
|
244
|
-
editingIdx = -1;
|
|
245
|
-
setConnView("chooser");
|
|
246
|
-
}
|
|
319
|
+
grid.innerHTML = html;
|
|
247
320
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
draftKind = conn.kind;
|
|
321
|
+
// Update nav info
|
|
322
|
+
var vc = getVerifiedCount();
|
|
323
|
+
var info = $("provider-count-info");
|
|
324
|
+
if (vc > 0) {
|
|
325
|
+
info.innerHTML = '<b>' + vc + '</b> provider' + (vc > 1 ? 's' : '') + ' ready';
|
|
254
326
|
} else {
|
|
255
|
-
|
|
327
|
+
info.textContent = 'Connect at least one';
|
|
256
328
|
}
|
|
257
|
-
|
|
258
|
-
|
|
329
|
+
$("btn-step1-next").disabled = vc === 0;
|
|
330
|
+
|
|
331
|
+
bindProviderEvents();
|
|
259
332
|
}
|
|
260
333
|
|
|
261
|
-
function
|
|
262
|
-
var
|
|
263
|
-
var
|
|
264
|
-
var
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
334
|
+
function renderProviderCard(p) {
|
|
335
|
+
var st = providerState[p.id];
|
|
336
|
+
var isExpanded = expandedProvider === p.id && st.selected;
|
|
337
|
+
var cls = "pcard";
|
|
338
|
+
if (st.selected) cls += " selected";
|
|
339
|
+
if (st.verified) cls += " verified";
|
|
340
|
+
if (isExpanded) cls += " wide";
|
|
341
|
+
|
|
342
|
+
var badgeCls = p.kind === "cloud" ? "badge-cloud" : p.kind === "local" ? "badge-local" : "badge-hybrid";
|
|
343
|
+
var vi = "";
|
|
344
|
+
if (st.verified) vi = '<span class="vs vs-ok">\u2713</span>';
|
|
345
|
+
else if (st.verifying) vi = '<span class="vs vs-wait">\u27F3</span>';
|
|
346
|
+
else if (st.error) vi = '<span class="vs vs-err">\u2717</span>';
|
|
347
|
+
|
|
348
|
+
var html = '<div class="' + cls + '" data-provider="' + p.id + '">';
|
|
349
|
+
html += '<div class="pcard-header" data-toggle-provider="' + p.id + '">';
|
|
350
|
+
html += '<div class="pcard-icon">' + esc(p.icon) + '</div>';
|
|
351
|
+
html += '<div class="pcard-info">';
|
|
352
|
+
html += '<div class="pcard-name">' + esc(p.name) + ' <span class="badge ' + badgeCls + '">' + p.kind + '</span>' + vi + '</div>';
|
|
353
|
+
html += '<div class="pcard-desc">' + esc(p.desc) + '</div>';
|
|
354
|
+
html += '</div>';
|
|
355
|
+
html += '<div class="pcard-check">' + (st.selected ? '\u2713' : '') + '</div>';
|
|
356
|
+
html += '</div>';
|
|
357
|
+
|
|
358
|
+
if (isExpanded) {
|
|
359
|
+
html += renderProviderAuth(p);
|
|
283
360
|
}
|
|
284
361
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
});
|
|
362
|
+
html += '</div>';
|
|
363
|
+
return html;
|
|
364
|
+
}
|
|
299
365
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
var
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
366
|
+
function renderProviderAuth(p) {
|
|
367
|
+
var st = providerState[p.id];
|
|
368
|
+
var html = '<div class="pcard-auth">';
|
|
369
|
+
|
|
370
|
+
if (p.id === "ollama") {
|
|
371
|
+
// Ollama: show mode selector first
|
|
372
|
+
if (!st.ollamaMode) {
|
|
373
|
+
html += '<div class="ollama-mode-prompt">';
|
|
374
|
+
html += '<p>Is Ollama already running on this machine?</p>';
|
|
375
|
+
html += '<div class="ollama-mode-buttons">';
|
|
376
|
+
html += '<button class="ollama-mode-btn ollama-mode-btn-detect" data-ollama-mode="running">Yes, detect it</button>';
|
|
377
|
+
html += '<button class="ollama-mode-btn ollama-mode-btn-stack" data-ollama-mode="instack">No, add to stack</button>';
|
|
378
|
+
html += '</div></div>';
|
|
379
|
+
} else if (st.ollamaMode === "running") {
|
|
380
|
+
html += '<div class="auth-row">';
|
|
381
|
+
html += '<input type="url" placeholder="' + esc(p.baseUrl) + '" value="' + esc(st.baseUrl || p.baseUrl) + '" data-auth-url="' + p.id + '">';
|
|
382
|
+
html += '<button class="auth-btn ' + (st.verified ? 'auth-btn-detected' : 'auth-btn-detect') + '" data-auth-verify="' + p.id + '" ' + (st.verifying ? 'disabled' : '') + '>';
|
|
383
|
+
html += st.verifying ? 'Detecting...' : st.verified ? 'Connected \u2713' : 'Detect';
|
|
384
|
+
html += '</button></div>';
|
|
385
|
+
} else {
|
|
386
|
+
// instack mode
|
|
387
|
+
if (st.verified) {
|
|
388
|
+
html += '<div class="auth-feedback auth-feedback-ok">Ollama will be added to your Docker stack with default models.</div>';
|
|
389
|
+
} else {
|
|
390
|
+
html += '<div class="ollama-mode-prompt">';
|
|
391
|
+
html += '<p>Ollama runs as a container in your stack with recommended models pre-configured.</p>';
|
|
392
|
+
html += '<button class="auth-btn auth-btn-detect" data-auth-verify="' + p.id + '" style="margin-top:4px">Enable Ollama</button>';
|
|
393
|
+
html += '</div>';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} else if (p.needsUrl) {
|
|
397
|
+
// Custom provider: URL (required) + optional API key
|
|
398
|
+
html += '<div class="auth-row">';
|
|
399
|
+
html += '<input type="url" placeholder="https://your-server.example/v1" value="' + esc(st.baseUrl || '') + '" data-auth-url="' + p.id + '">';
|
|
400
|
+
html += '</div>';
|
|
401
|
+
if (p.optionalKey) {
|
|
402
|
+
html += '<div class="auth-row" style="margin-top:6px">';
|
|
403
|
+
html += '<input type="password" placeholder="' + esc(p.placeholder || 'API key (optional)') + '" value="' + esc(st.apiKey) + '" data-auth-key="' + p.id + '">';
|
|
404
|
+
html += '</div>';
|
|
405
|
+
}
|
|
406
|
+
html += '<div class="auth-row" style="margin-top:6px">';
|
|
407
|
+
html += '<button class="auth-btn ' + (st.verified ? 'auth-btn-verified' : 'auth-btn-verify') + '" data-auth-verify="' + p.id + '" ' + (st.verifying ? 'disabled' : '') + '>';
|
|
408
|
+
html += st.verifying ? 'Checking...' : st.verified ? 'Connected \u2713' : 'Connect';
|
|
409
|
+
html += '</button></div>';
|
|
410
|
+
} else if (p.needsKey) {
|
|
411
|
+
// Cloud provider: API key + verify
|
|
412
|
+
html += '<div class="auth-row">';
|
|
413
|
+
html += '<input type="password" placeholder="' + esc(p.placeholder || 'API key') + '" value="' + esc(st.apiKey) + '" data-auth-key="' + p.id + '">';
|
|
414
|
+
html += '<button class="auth-btn ' + (st.verified ? 'auth-btn-verified' : 'auth-btn-verify') + '" data-auth-verify="' + p.id + '" ' + (st.verifying ? 'disabled' : '') + '>';
|
|
415
|
+
html += st.verifying ? 'Checking...' : st.verified ? 'Verified \u2713' : 'Verify';
|
|
416
|
+
html += '</button></div>';
|
|
320
417
|
} else {
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
});
|
|
418
|
+
// Local provider with URL
|
|
419
|
+
html += '<div class="auth-row">';
|
|
420
|
+
html += '<input type="url" placeholder="' + esc(p.baseUrl || 'http://localhost:8080') + '" value="' + esc(st.baseUrl || p.baseUrl || '') + '" data-auth-url="' + p.id + '">';
|
|
421
|
+
html += '<button class="auth-btn ' + (st.verified ? 'auth-btn-detected' : 'auth-btn-detect') + '" data-auth-verify="' + p.id + '" ' + (st.verifying ? 'disabled' : '') + '>';
|
|
422
|
+
html += st.verifying ? 'Detecting...' : st.verified ? 'Connected \u2713' : 'Detect';
|
|
423
|
+
html += '</button></div>';
|
|
335
424
|
}
|
|
336
425
|
|
|
337
|
-
//
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
} else {
|
|
341
|
-
var
|
|
342
|
-
|
|
343
|
-
$("conn-base-url").value = c.baseUrl;
|
|
344
|
-
$("conn-api-key").value = c.apiKey;
|
|
426
|
+
// Feedback messages
|
|
427
|
+
if (st.verified && p.id !== "ollama") {
|
|
428
|
+
html += '<div class="auth-feedback auth-feedback-ok">Credentials verified</div>';
|
|
429
|
+
} else if (st.error) {
|
|
430
|
+
var errMsg = st.errorMessage ? esc(st.errorMessage) : 'check your ' + (p.needsKey ? 'credentials' : 'endpoint');
|
|
431
|
+
html += '<div class="auth-feedback auth-feedback-err">Verification failed -- ' + errMsg + '</div>';
|
|
345
432
|
}
|
|
346
433
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
hideError($("conn-detail-error"));
|
|
434
|
+
html += '</div>';
|
|
435
|
+
return html;
|
|
350
436
|
}
|
|
351
437
|
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
438
|
+
function bindProviderEvents() {
|
|
439
|
+
// Card header toggle (select/expand)
|
|
440
|
+
document.querySelectorAll("[data-toggle-provider]").forEach(function (el) {
|
|
441
|
+
el.addEventListener("click", function (e) {
|
|
442
|
+
var id = el.dataset.toggleProvider;
|
|
443
|
+
var st = providerState[id];
|
|
444
|
+
if (st.selected) {
|
|
445
|
+
// Already selected: toggle expand
|
|
446
|
+
expandedProvider = expandedProvider === id ? null : id;
|
|
447
|
+
} else {
|
|
448
|
+
// Select and expand
|
|
449
|
+
st.selected = true;
|
|
450
|
+
expandedProvider = id;
|
|
451
|
+
// Auto-fill from detection
|
|
452
|
+
var detected = detectedProviders.find(function (d) { return d.provider === id && d.available; });
|
|
453
|
+
if (detected) {
|
|
454
|
+
st.baseUrl = detected.url;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
renderProviderGrid();
|
|
458
|
+
});
|
|
459
|
+
});
|
|
370
460
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
461
|
+
// Check icon: deselect provider
|
|
462
|
+
document.querySelectorAll(".pcard-check").forEach(function (el) {
|
|
463
|
+
el.addEventListener("click", function (e) {
|
|
464
|
+
e.stopPropagation();
|
|
465
|
+
var card = el.closest("[data-provider]");
|
|
466
|
+
if (!card) return;
|
|
467
|
+
var id = card.dataset.provider;
|
|
468
|
+
var st = providerState[id];
|
|
469
|
+
if (st.selected) {
|
|
470
|
+
st.selected = false;
|
|
471
|
+
st.verified = false;
|
|
472
|
+
st.verifying = false;
|
|
473
|
+
st.error = false;
|
|
474
|
+
st.apiKey = "";
|
|
475
|
+
st.models = [];
|
|
476
|
+
if (id === "ollama") st.ollamaMode = null;
|
|
477
|
+
if (expandedProvider === id) expandedProvider = null;
|
|
478
|
+
renderProviderGrid();
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
374
482
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
483
|
+
// Auth inputs (don't re-render on typing)
|
|
484
|
+
document.querySelectorAll("[data-auth-key]").forEach(function (el) {
|
|
485
|
+
el.addEventListener("input", function () {
|
|
486
|
+
providerState[el.dataset.authKey].apiKey = el.value;
|
|
487
|
+
});
|
|
488
|
+
el.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
489
|
+
});
|
|
378
490
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
491
|
+
document.querySelectorAll("[data-auth-url]").forEach(function (el) {
|
|
492
|
+
el.addEventListener("input", function () {
|
|
493
|
+
providerState[el.dataset.authUrl].baseUrl = el.value;
|
|
494
|
+
});
|
|
495
|
+
el.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
496
|
+
});
|
|
385
497
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
models: editingIdx >= 0 ? connections[editingIdx].models : [],
|
|
394
|
-
};
|
|
498
|
+
// Verify buttons
|
|
499
|
+
document.querySelectorAll("[data-auth-verify]").forEach(function (el) {
|
|
500
|
+
el.addEventListener("click", function (e) {
|
|
501
|
+
e.stopPropagation();
|
|
502
|
+
verifyProvider(el.dataset.authVerify);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
395
505
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
506
|
+
// Ollama mode buttons
|
|
507
|
+
document.querySelectorAll("[data-ollama-mode]").forEach(function (el) {
|
|
508
|
+
el.addEventListener("click", function (e) {
|
|
509
|
+
e.stopPropagation();
|
|
510
|
+
var mode = el.dataset.ollamaMode;
|
|
511
|
+
providerState.ollama.ollamaMode = mode;
|
|
512
|
+
renderProviderGrid();
|
|
513
|
+
});
|
|
514
|
+
});
|
|
401
515
|
|
|
402
|
-
editingIdx = -1;
|
|
403
|
-
renderConnHub();
|
|
404
|
-
setConnView("hub");
|
|
405
516
|
}
|
|
406
517
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
renderConnHub();
|
|
410
|
-
setConnView("hub");
|
|
411
|
-
}
|
|
518
|
+
/** Monotonic counter to discard stale verification results */
|
|
519
|
+
var verifyGeneration = {};
|
|
412
520
|
|
|
413
|
-
function
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
draftProvider = conn.provider;
|
|
418
|
-
renderConnectionForm();
|
|
419
|
-
setConnView("form");
|
|
420
|
-
}
|
|
521
|
+
async function verifyProvider(id) {
|
|
522
|
+
var p = PROVIDERS.find(function (x) { return x.id === id; });
|
|
523
|
+
if (!p) return;
|
|
524
|
+
var st = providerState[id];
|
|
421
525
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
526
|
+
// For ollama instack mode, just mark verified
|
|
527
|
+
if (id === "ollama" && st.ollamaMode === "instack") {
|
|
528
|
+
st.verified = true;
|
|
529
|
+
st.error = false;
|
|
530
|
+
renderProviderGrid();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
427
533
|
|
|
428
|
-
|
|
429
|
-
var
|
|
430
|
-
|
|
534
|
+
// Bump generation so any in-flight verify for this provider is ignored
|
|
535
|
+
var gen = (verifyGeneration[id] || 0) + 1;
|
|
536
|
+
verifyGeneration[id] = gen;
|
|
431
537
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
538
|
+
st.verifying = true;
|
|
539
|
+
st.error = false;
|
|
540
|
+
renderProviderGrid();
|
|
541
|
+
|
|
542
|
+
var baseUrl = st.baseUrl || p.baseUrl;
|
|
543
|
+
var apiKey = st.apiKey || "";
|
|
435
544
|
|
|
436
545
|
try {
|
|
437
|
-
var
|
|
438
|
-
if
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
}
|
|
546
|
+
var result = await apiFetchModels(id, baseUrl, apiKey);
|
|
547
|
+
// Discard if a newer verify was started while we were waiting
|
|
548
|
+
if (verifyGeneration[id] !== gen) return;
|
|
549
|
+
st.verified = true;
|
|
550
|
+
st.error = false;
|
|
551
|
+
st.models = result.models || [];
|
|
452
552
|
} catch (e) {
|
|
453
|
-
|
|
553
|
+
if (verifyGeneration[id] !== gen) return;
|
|
554
|
+
st.verified = false;
|
|
555
|
+
st.error = true;
|
|
556
|
+
st.errorMessage = e.message || "";
|
|
557
|
+
st.models = [];
|
|
454
558
|
}
|
|
455
559
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function generateId() {
|
|
461
|
-
return Math.random().toString(36).slice(2, 10);
|
|
560
|
+
st.verifying = false;
|
|
561
|
+
renderProviderGrid();
|
|
462
562
|
}
|
|
463
563
|
|
|
464
564
|
/* =========================================================================
|
|
465
|
-
Step 2: Model Assignment
|
|
565
|
+
Step 2: Model Assignment (Radio Options)
|
|
466
566
|
========================================================================= */
|
|
467
567
|
|
|
468
568
|
function initStep2() {
|
|
469
|
-
|
|
569
|
+
buildModelOptions();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function getVerifiedProviders() {
|
|
573
|
+
return PROVIDERS.filter(function (p) { return providerState[p.id].verified; });
|
|
470
574
|
}
|
|
471
575
|
|
|
472
|
-
function
|
|
473
|
-
var
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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);
|
|
576
|
+
function getAllModels() {
|
|
577
|
+
var result = [];
|
|
578
|
+
getVerifiedProviders().forEach(function (p) {
|
|
579
|
+
var st = providerState[p.id];
|
|
580
|
+
st.models.forEach(function (m) {
|
|
581
|
+
result.push({ id: m, provider: p.id, providerName: p.name, baseUrl: st.baseUrl || p.baseUrl, apiKey: st.apiKey });
|
|
582
|
+
});
|
|
492
583
|
});
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
493
586
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
587
|
+
var MAX_VISIBLE_MODELS = 6;
|
|
588
|
+
|
|
589
|
+
function buildModelOptions() {
|
|
590
|
+
var allModels = getAllModels();
|
|
591
|
+
var verifiedProviders = getVerifiedProviders();
|
|
592
|
+
var groupsEl = $("model-groups");
|
|
593
|
+
|
|
594
|
+
// Define model roles
|
|
595
|
+
var roles = [
|
|
596
|
+
{ id: "llm", label: "Chat Model (LLM)", tag: "required", desc: "Conversations, reasoning, and code" },
|
|
597
|
+
{ id: "embedding", label: "Embedding Model", tag: "optional", desc: "Memory search and recall" },
|
|
598
|
+
{ id: "small", label: "Small Model", tag: "optional", desc: "Lightweight tasks like memory extraction" },
|
|
599
|
+
];
|
|
600
|
+
|
|
601
|
+
var html = "";
|
|
602
|
+
|
|
603
|
+
roles.forEach(function (role) {
|
|
604
|
+
// Build options for this role from each verified provider's models
|
|
605
|
+
var options = [];
|
|
606
|
+
verifiedProviders.forEach(function (p) {
|
|
607
|
+
var st = providerState[p.id];
|
|
608
|
+
var defaultModel = role.id === "embedding" ? p.embModel : p.llmModel;
|
|
609
|
+
var models = st.models.length > 0 ? st.models : [];
|
|
610
|
+
|
|
611
|
+
// Add the default model as top pick if in the list
|
|
612
|
+
if (defaultModel && models.indexOf(defaultModel) >= 0) {
|
|
613
|
+
options.push({
|
|
614
|
+
id: defaultModel,
|
|
615
|
+
connId: p.id,
|
|
616
|
+
providerName: p.name,
|
|
617
|
+
baseUrl: st.baseUrl || p.baseUrl,
|
|
618
|
+
isDefault: true,
|
|
619
|
+
dims: role.id === "embedding" ? (KNOWN_EMB_DIMS[defaultModel] || KNOWN_EMB_DIMS[defaultModel.replace(/:.*$/, "")] || p.embDims || 0) : 0,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
500
622
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
623
|
+
// Add other models
|
|
624
|
+
models.forEach(function (m) {
|
|
625
|
+
if (m === defaultModel) return; // Already added above
|
|
626
|
+
var dims = 0;
|
|
627
|
+
if (role.id === "embedding") {
|
|
628
|
+
dims = KNOWN_EMB_DIMS[m] || KNOWN_EMB_DIMS[m.replace(/:.*$/, "")] || 0;
|
|
629
|
+
}
|
|
630
|
+
options.push({
|
|
631
|
+
id: m,
|
|
632
|
+
connId: p.id,
|
|
633
|
+
providerName: p.name,
|
|
634
|
+
baseUrl: st.baseUrl || p.baseUrl,
|
|
635
|
+
isDefault: false,
|
|
636
|
+
dims: dims,
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
});
|
|
506
640
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
641
|
+
// For embedding role, filter to models with known dims, plus provider defaults
|
|
642
|
+
if (role.id === "embedding") {
|
|
643
|
+
var embOptions = options.filter(function (o) {
|
|
644
|
+
return o.isDefault || o.dims > 0;
|
|
645
|
+
});
|
|
646
|
+
if (embOptions.length > 0) options = embOptions;
|
|
647
|
+
}
|
|
511
648
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
models = await apiFetchModels(conn.provider, conn.baseUrl, conn.apiKey);
|
|
529
|
-
conn.models = models;
|
|
530
|
-
modelCache[connId] = models;
|
|
531
|
-
} catch (e) {
|
|
532
|
-
models = [];
|
|
649
|
+
// Small model: same as LLM options but with "(same as chat)" default
|
|
650
|
+
if (role.id === "small" && options.length === 0) {
|
|
651
|
+
// Use llm options
|
|
652
|
+
var llmProvider = verifiedProviders[0];
|
|
653
|
+
if (llmProvider) {
|
|
654
|
+
providerState[llmProvider.id].models.forEach(function (m) {
|
|
655
|
+
options.push({
|
|
656
|
+
id: m,
|
|
657
|
+
connId: llmProvider.id,
|
|
658
|
+
providerName: llmProvider.name,
|
|
659
|
+
baseUrl: providerState[llmProvider.id].baseUrl || llmProvider.baseUrl,
|
|
660
|
+
isDefault: false,
|
|
661
|
+
dims: 0,
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
}
|
|
533
665
|
}
|
|
534
|
-
}
|
|
535
666
|
|
|
536
|
-
|
|
537
|
-
var prevVal = modelSel.value;
|
|
667
|
+
if (options.length === 0 && role.id !== "small") return;
|
|
538
668
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
});
|
|
669
|
+
// Auto-select default
|
|
670
|
+
if (!modelSelection[role.id] && options.length > 0) {
|
|
671
|
+
var defaultOpt = options.find(function (o) { return o.isDefault; }) || options[0];
|
|
672
|
+
if (defaultOpt) {
|
|
673
|
+
modelSelection[role.id] = { connId: defaultOpt.connId, model: defaultOpt.id, dims: defaultOpt.dims };
|
|
674
|
+
}
|
|
675
|
+
}
|
|
547
676
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
677
|
+
html += '<div class="model-group">';
|
|
678
|
+
html += '<div class="model-group-header">';
|
|
679
|
+
html += '<span class="model-group-title">' + role.label + '</span>';
|
|
680
|
+
html += '<span class="model-group-tag ' + (role.tag === "required" ? "model-group-tag-required" : "model-group-tag-optional") + '">' + role.tag + '</span>';
|
|
681
|
+
html += '</div>';
|
|
682
|
+
html += '<div class="model-group-desc">' + role.desc + '</div>';
|
|
683
|
+
|
|
684
|
+
if (role.id === "small") {
|
|
685
|
+
// Add a "same as chat" option
|
|
686
|
+
var smallSel = modelSelection.small;
|
|
687
|
+
var noneOn = !smallSel || !smallSel.model;
|
|
688
|
+
html += '<div class="model-opt ' + (noneOn ? 'on' : '') + '" data-model-select="small:">';
|
|
689
|
+
html += '<div class="model-opt-dot"><div class="model-opt-dot-inner"></div></div>';
|
|
690
|
+
html += '<div style="flex:1"><div class="model-opt-name">(same as chat model)</div>';
|
|
691
|
+
html += '<div class="model-opt-meta">No separate small model</div></div>';
|
|
692
|
+
html += '<span class="model-opt-badge model-opt-badge-auto">Default</span>';
|
|
693
|
+
html += '</div>';
|
|
554
694
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
695
|
+
|
|
696
|
+
var hasOverflow = options.length > MAX_VISIBLE_MODELS;
|
|
697
|
+
var filterId = "model-filter-" + role.id;
|
|
698
|
+
|
|
699
|
+
// Search filter for long model lists
|
|
700
|
+
if (hasOverflow) {
|
|
701
|
+
html += '<div class="model-filter-row">';
|
|
702
|
+
html += '<input type="text" class="model-filter-input" id="' + filterId + '" placeholder="Search ' + options.length + ' models\u2026" autocomplete="off">';
|
|
703
|
+
html += '</div>';
|
|
559
704
|
}
|
|
560
705
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
706
|
+
options.forEach(function (opt, idx) {
|
|
707
|
+
var sel = modelSelection[role.id];
|
|
708
|
+
var isOn = sel && sel.model === opt.id && sel.connId === opt.connId;
|
|
709
|
+
var meta = "via " + opt.providerName;
|
|
710
|
+
if (opt.dims > 0) meta += " \u00B7 " + opt.dims + "d";
|
|
711
|
+
|
|
712
|
+
// Hide items beyond MAX_VISIBLE_MODELS unless selected — filter will reveal them
|
|
713
|
+
var isHidden = hasOverflow && idx >= MAX_VISIBLE_MODELS && !isOn;
|
|
714
|
+
|
|
715
|
+
html += '<div class="model-opt ' + (isOn ? 'on' : '') + (isHidden ? ' model-opt-filtered' : '') + '" data-model-select="' + role.id + ':' + opt.connId + ':' + esc(opt.id) + ':' + opt.dims + '" data-model-name="' + esc(opt.id.toLowerCase()) + '">';
|
|
716
|
+
html += '<div class="model-opt-dot"><div class="model-opt-dot-inner"></div></div>';
|
|
717
|
+
html += '<div style="flex:1;min-width:0"><div class="model-opt-name">' + esc(opt.id) + '</div>';
|
|
718
|
+
html += '<div class="model-opt-meta">' + esc(meta) + '</div></div>';
|
|
719
|
+
if (idx === 0 && opt.isDefault) {
|
|
720
|
+
html += '<span class="model-opt-badge model-opt-badge-top">Top Pick</span>';
|
|
721
|
+
}
|
|
722
|
+
html += '</div>';
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
html += '</div>';
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
groupsEl.innerHTML = html;
|
|
729
|
+
|
|
730
|
+
// Sync hidden fields for backward compat
|
|
731
|
+
syncHiddenModelFields();
|
|
732
|
+
|
|
733
|
+
// Bind model filter inputs
|
|
734
|
+
document.querySelectorAll(".model-filter-input").forEach(function (input) {
|
|
735
|
+
input.addEventListener("input", function () {
|
|
736
|
+
var query = input.value.toLowerCase().trim();
|
|
737
|
+
var group = input.closest(".model-group");
|
|
738
|
+
if (!group) return;
|
|
739
|
+
var opts = group.querySelectorAll("[data-model-name]");
|
|
740
|
+
var shown = 0;
|
|
741
|
+
opts.forEach(function (el, idx) {
|
|
742
|
+
var name = el.dataset.modelName || "";
|
|
743
|
+
if (query) {
|
|
744
|
+
// When filtering, show all matches
|
|
745
|
+
var match = name.indexOf(query) >= 0;
|
|
746
|
+
el.classList.toggle("model-opt-filtered", !match);
|
|
747
|
+
if (match) shown++;
|
|
748
|
+
} else {
|
|
749
|
+
// No query — show top MAX_VISIBLE_MODELS + selected
|
|
750
|
+
var isOn = el.classList.contains("on");
|
|
751
|
+
el.classList.toggle("model-opt-filtered", idx >= MAX_VISIBLE_MODELS && !isOn);
|
|
752
|
+
shown++;
|
|
753
|
+
}
|
|
569
754
|
});
|
|
570
|
-
}
|
|
755
|
+
});
|
|
756
|
+
});
|
|
571
757
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
758
|
+
// Bind model option clicks
|
|
759
|
+
document.querySelectorAll("[data-model-select]").forEach(function (el) {
|
|
760
|
+
el.addEventListener("click", function () {
|
|
761
|
+
var parts = el.dataset.modelSelect.split(":");
|
|
762
|
+
var role = parts[0];
|
|
763
|
+
if (parts.length < 2 || !parts[1]) {
|
|
764
|
+
// "same as chat" for small model
|
|
765
|
+
delete modelSelection[role];
|
|
766
|
+
} else {
|
|
767
|
+
var connId = parts[1];
|
|
768
|
+
var modelId = parts.slice(2, -1).join(":"); // Model id may contain colons
|
|
769
|
+
var dims = parseInt(parts[parts.length - 1], 10) || 0;
|
|
770
|
+
modelSelection[role] = { connId: connId, model: modelId, dims: dims };
|
|
771
|
+
}
|
|
772
|
+
buildModelOptions();
|
|
773
|
+
});
|
|
774
|
+
});
|
|
590
775
|
}
|
|
591
776
|
|
|
592
|
-
function
|
|
593
|
-
var
|
|
594
|
-
var
|
|
595
|
-
|
|
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
|
-
}
|
|
777
|
+
function syncHiddenModelFields() {
|
|
778
|
+
var llm = modelSelection.llm;
|
|
779
|
+
var emb = modelSelection.embedding;
|
|
780
|
+
var small = modelSelection.small;
|
|
605
781
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
var dims = KNOWN_EMB_DIMS[modelName] || KNOWN_EMB_DIMS[name];
|
|
610
|
-
if (dims) {
|
|
611
|
-
$("emb-dims").value = dims;
|
|
782
|
+
if (llm) {
|
|
783
|
+
$("llm-connection").value = llm.connId;
|
|
784
|
+
$("llm-model").value = llm.model;
|
|
612
785
|
}
|
|
786
|
+
if (emb) {
|
|
787
|
+
$("emb-connection").value = emb.connId;
|
|
788
|
+
$("emb-model").value = emb.model;
|
|
789
|
+
$("emb-dims").value = emb.dims || 1536;
|
|
790
|
+
}
|
|
791
|
+
$("llm-small-model").value = small ? small.model : "";
|
|
613
792
|
}
|
|
614
793
|
|
|
615
794
|
function validateStep2() {
|
|
616
795
|
var errEl = $("step2-error");
|
|
617
796
|
hideError(errEl);
|
|
618
797
|
|
|
619
|
-
var
|
|
620
|
-
var
|
|
621
|
-
var embConn = $("emb-connection").value;
|
|
622
|
-
var embModel = ($("emb-model").value || "").trim();
|
|
798
|
+
var llm = modelSelection.llm;
|
|
799
|
+
var emb = modelSelection.embedding;
|
|
623
800
|
|
|
624
|
-
if (!
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
801
|
+
if (!llm || !llm.model) {
|
|
802
|
+
showError(errEl, "Select a chat model.");
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
if (!emb || !emb.model) {
|
|
806
|
+
showError(errEl, "Select an embedding model.");
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
628
809
|
return true;
|
|
629
810
|
}
|
|
630
811
|
|
|
631
812
|
/* =========================================================================
|
|
632
|
-
Step 3:
|
|
813
|
+
Step 3: Voice (TTS / STT)
|
|
633
814
|
========================================================================= */
|
|
634
815
|
|
|
816
|
+
function getVoiceDefaults() {
|
|
817
|
+
var hasOpenAI = PROVIDERS.some(function (p) {
|
|
818
|
+
return p.id === "openai" && providerState[p.id].verified;
|
|
819
|
+
});
|
|
820
|
+
if (hasOpenAI) return { tts: "openai-tts", stt: "openai-stt" };
|
|
821
|
+
return { tts: "browser-tts", stt: "browser-stt" };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function activeTts() { return voiceSelection.tts || getVoiceDefaults().tts; }
|
|
825
|
+
function activeStt() { return voiceSelection.stt || getVoiceDefaults().stt; }
|
|
826
|
+
|
|
635
827
|
function initStep3() {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
828
|
+
renderVoiceStep();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function renderVoiceStep() {
|
|
832
|
+
var container = $("voice-groups");
|
|
833
|
+
var curTts = activeTts();
|
|
834
|
+
var curStt = activeStt();
|
|
835
|
+
var hasOpenAI = PROVIDERS.some(function (p) {
|
|
836
|
+
return p.id === "openai" && providerState[p.id].verified;
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
var hint = hasOpenAI
|
|
840
|
+
? "OpenAI selected as voice defaults. Kokoro and Whisper recommended for better quality."
|
|
841
|
+
: "Browser voice works out of the box. Kokoro and Whisper recommended for higher quality.";
|
|
842
|
+
|
|
843
|
+
var html = '<p class="voice-hint">' + esc(hint) + '</p>';
|
|
844
|
+
|
|
845
|
+
// TTS group
|
|
846
|
+
html += '<div class="model-group">';
|
|
847
|
+
html += '<div class="model-group-header">';
|
|
848
|
+
html += '<span class="model-group-title">Text-to-Speech</span>';
|
|
849
|
+
html += '<span class="model-group-tag model-group-tag-optional">Optional</span>';
|
|
850
|
+
html += '</div>';
|
|
851
|
+
html += '<div class="model-group-desc">How your assistant speaks</div>';
|
|
852
|
+
|
|
853
|
+
TTS_OPTIONS.forEach(function (o) {
|
|
854
|
+
var isOn = curTts === o.id;
|
|
855
|
+
var defs = getVoiceDefaults();
|
|
856
|
+
var badge = "";
|
|
857
|
+
if (o.recommended) badge = '<span class="model-opt-badge model-opt-badge-top">Recommended</span>';
|
|
858
|
+
else if (defs.tts === o.id && !voiceSelection.tts) badge = '<span class="model-opt-badge model-opt-badge-auto">Auto</span>';
|
|
859
|
+
|
|
860
|
+
html += '<div class="model-opt ' + (isOn ? "on" : "") + '" data-voice-select="tts:' + o.id + '">';
|
|
861
|
+
html += '<div class="model-opt-dot"><div class="model-opt-dot-inner"></div></div>';
|
|
862
|
+
html += '<div style="flex:1;min-width:0"><div class="model-opt-name">' + esc(o.name) + '</div>';
|
|
863
|
+
html += '<div class="model-opt-meta">' + esc(o.desc) + '</div></div>';
|
|
864
|
+
html += badge;
|
|
865
|
+
html += '</div>';
|
|
866
|
+
});
|
|
867
|
+
html += '</div>';
|
|
868
|
+
|
|
869
|
+
// STT group
|
|
870
|
+
html += '<div class="model-group">';
|
|
871
|
+
html += '<div class="model-group-header">';
|
|
872
|
+
html += '<span class="model-group-title">Speech-to-Text</span>';
|
|
873
|
+
html += '<span class="model-group-tag model-group-tag-optional">Optional</span>';
|
|
874
|
+
html += '</div>';
|
|
875
|
+
html += '<div class="model-group-desc">How your assistant hears you</div>';
|
|
876
|
+
|
|
877
|
+
STT_OPTIONS.forEach(function (o) {
|
|
878
|
+
var isOn = curStt === o.id;
|
|
879
|
+
var defs = getVoiceDefaults();
|
|
880
|
+
var badge = "";
|
|
881
|
+
if (o.recommended) badge = '<span class="model-opt-badge model-opt-badge-top">Recommended</span>';
|
|
882
|
+
else if (defs.stt === o.id && !voiceSelection.stt) badge = '<span class="model-opt-badge model-opt-badge-auto">Auto</span>';
|
|
883
|
+
|
|
884
|
+
html += '<div class="model-opt ' + (isOn ? "on" : "") + '" data-voice-select="stt:' + o.id + '">';
|
|
885
|
+
html += '<div class="model-opt-dot"><div class="model-opt-dot-inner"></div></div>';
|
|
886
|
+
html += '<div style="flex:1;min-width:0"><div class="model-opt-name">' + esc(o.name) + '</div>';
|
|
887
|
+
html += '<div class="model-opt-meta">' + esc(o.desc) + '</div></div>';
|
|
888
|
+
html += badge;
|
|
889
|
+
html += '</div>';
|
|
890
|
+
});
|
|
891
|
+
html += '</div>';
|
|
892
|
+
|
|
893
|
+
container.innerHTML = html;
|
|
894
|
+
|
|
895
|
+
// Bind voice option clicks
|
|
896
|
+
document.querySelectorAll("[data-voice-select]").forEach(function (el) {
|
|
897
|
+
el.addEventListener("click", function () {
|
|
898
|
+
var parts = el.dataset.voiceSelect.split(":");
|
|
899
|
+
var kind = parts[0]; // "tts" or "stt"
|
|
900
|
+
var id = parts[1];
|
|
901
|
+
if (kind === "tts") voiceSelection.tts = id;
|
|
902
|
+
if (kind === "stt") voiceSelection.stt = id;
|
|
903
|
+
renderVoiceStep();
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/* =========================================================================
|
|
909
|
+
Step 4: Options (Channels + Services + Memory)
|
|
910
|
+
========================================================================= */
|
|
911
|
+
|
|
912
|
+
function initStep4() {
|
|
913
|
+
// Show Ollama toggle if any verified provider is Ollama
|
|
914
|
+
var hasOllama = PROVIDERS.some(function (p) {
|
|
915
|
+
return p.id === "ollama" && providerState[p.id].verified;
|
|
639
916
|
});
|
|
640
917
|
var addon = $("ollama-addon");
|
|
641
|
-
if (hasOllama)
|
|
918
|
+
if (hasOllama) {
|
|
919
|
+
show(addon);
|
|
920
|
+
// Pre-check if ollamaMode is instack
|
|
921
|
+
var ollamaCb = $("ollama-enabled");
|
|
922
|
+
if (providerState.ollama.ollamaMode === "instack") {
|
|
923
|
+
ollamaCb.checked = true;
|
|
924
|
+
}
|
|
925
|
+
} else {
|
|
926
|
+
hide(addon);
|
|
927
|
+
}
|
|
642
928
|
|
|
643
929
|
// Memory user ID default
|
|
644
930
|
var memInput = $("memory-user-id");
|
|
@@ -646,21 +932,291 @@
|
|
|
646
932
|
var email = ($("owner-email").value || "").trim();
|
|
647
933
|
memInput.value = email || "default_user";
|
|
648
934
|
}
|
|
935
|
+
|
|
936
|
+
// Render channels and services
|
|
937
|
+
renderChannels();
|
|
938
|
+
renderServices();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Helper: check if a channel is enabled (handles both boolean and object state) */
|
|
942
|
+
function isChannelEnabled(ch) {
|
|
943
|
+
if (ch.locked) return true;
|
|
944
|
+
var sel = channelSelection[ch.id];
|
|
945
|
+
if (typeof sel === "object" && sel !== null) return sel.enabled;
|
|
946
|
+
return !!sel;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function renderChannels() {
|
|
950
|
+
var container = $("channels-grid");
|
|
951
|
+
var html = "";
|
|
952
|
+
|
|
953
|
+
CHANNELS.forEach(function (ch) {
|
|
954
|
+
var isOn = isChannelEnabled(ch);
|
|
955
|
+
var cls = "toggle-card" + (isOn ? " on" : "") + (ch.locked ? " locked" : "");
|
|
956
|
+
if (ch.credentials && isOn) cls += " wide";
|
|
957
|
+
|
|
958
|
+
html += '<div class="' + cls + '" data-channel="' + ch.id + '">';
|
|
959
|
+
html += '<div class="toggle-card-header" data-channel-toggle="' + ch.id + '">';
|
|
960
|
+
html += '<div class="toggle-card-icon">' + esc(ch.icon) + '</div>';
|
|
961
|
+
html += '<div class="toggle-card-info">';
|
|
962
|
+
html += '<div class="toggle-card-name">' + esc(ch.name) + (ch.locked ? ' <span class="badge badge-local">Always on</span>' : '') + '</div>';
|
|
963
|
+
html += '<div class="toggle-card-desc">' + esc(ch.desc) + '</div>';
|
|
964
|
+
html += '</div>';
|
|
965
|
+
html += '<div class="toggle-card-switch">';
|
|
966
|
+
if (ch.locked) {
|
|
967
|
+
html += '<div class="toggle-track on locked"><div class="toggle-thumb"></div></div>';
|
|
968
|
+
} else {
|
|
969
|
+
html += '<div class="toggle-track ' + (isOn ? "on" : "") + '"><div class="toggle-thumb"></div></div>';
|
|
970
|
+
}
|
|
971
|
+
html += '</div>';
|
|
972
|
+
html += '</div>';
|
|
973
|
+
|
|
974
|
+
// Credential fields (expanded when channel with credentials is toggled ON)
|
|
975
|
+
if (ch.credentials && isOn) {
|
|
976
|
+
html += renderChannelCredentials(ch);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
html += '</div>';
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
container.innerHTML = html;
|
|
983
|
+
|
|
984
|
+
// Bind toggle clicks on header
|
|
985
|
+
document.querySelectorAll("[data-channel-toggle]").forEach(function (el) {
|
|
986
|
+
el.addEventListener("click", function () {
|
|
987
|
+
var id = el.dataset.channelToggle;
|
|
988
|
+
var ch = CHANNELS.find(function (c) { return c.id === id; });
|
|
989
|
+
if (ch && ch.locked) return; // Cannot toggle locked channels
|
|
990
|
+
var sel = channelSelection[id];
|
|
991
|
+
if (typeof sel === "object" && sel !== null) {
|
|
992
|
+
sel.enabled = !sel.enabled;
|
|
993
|
+
} else {
|
|
994
|
+
channelSelection[id] = !sel;
|
|
995
|
+
}
|
|
996
|
+
renderChannels();
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
// Bind credential inputs (don't re-render on typing)
|
|
1001
|
+
document.querySelectorAll("[data-channel-cred]").forEach(function (el) {
|
|
1002
|
+
el.addEventListener("input", function () {
|
|
1003
|
+
var sep = el.dataset.channelCred.indexOf(":");
|
|
1004
|
+
var chId = el.dataset.channelCred.slice(0, sep);
|
|
1005
|
+
var credKey = el.dataset.channelCred.slice(sep + 1);
|
|
1006
|
+
var sel = channelSelection[chId];
|
|
1007
|
+
if (typeof sel === "object" && sel !== null) {
|
|
1008
|
+
sel[credKey] = el.value;
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
el.addEventListener("click", function (e) { e.stopPropagation(); });
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function renderChannelCredentials(ch) {
|
|
1016
|
+
var sel = channelSelection[ch.id];
|
|
1017
|
+
var html = '<div class="pcard-auth">';
|
|
1018
|
+
|
|
1019
|
+
ch.credentials.forEach(function (cred) {
|
|
1020
|
+
var val = (typeof sel === "object" && sel !== null) ? (sel[cred.key] || "") : "";
|
|
1021
|
+
var inputType = cred.secret === false ? "text" : "password";
|
|
1022
|
+
html += '<div class="auth-row">';
|
|
1023
|
+
html += '<label class="channel-cred-label">' + esc(cred.label) + (cred.required ? ' <span class="channel-cred-required">*</span>' : '') + '</label>';
|
|
1024
|
+
html += '<input type="' + inputType + '" placeholder="' + esc(cred.placeholder || '') + '" value="' + esc(val) + '" data-channel-cred="' + ch.id + ':' + cred.key + '">';
|
|
1025
|
+
html += '</div>';
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
html += '</div>';
|
|
1029
|
+
return html;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function renderServices() {
|
|
1033
|
+
var container = $("services-grid");
|
|
1034
|
+
var html = "";
|
|
1035
|
+
|
|
1036
|
+
SERVICES.forEach(function (svc) {
|
|
1037
|
+
var isOn = serviceSelection[svc.id];
|
|
1038
|
+
var cls = "toggle-card" + (isOn ? " on" : "");
|
|
1039
|
+
|
|
1040
|
+
html += '<div class="' + cls + '" data-service="' + svc.id + '">';
|
|
1041
|
+
html += '<div class="toggle-card-header">';
|
|
1042
|
+
html += '<div class="toggle-card-icon">' + esc(svc.icon) + '</div>';
|
|
1043
|
+
html += '<div class="toggle-card-info">';
|
|
1044
|
+
html += '<div class="toggle-card-name">' + esc(svc.name) + (svc.recommended ? ' <span class="badge badge-cloud">Recommended</span>' : '') + '</div>';
|
|
1045
|
+
html += '<div class="toggle-card-desc">' + esc(svc.desc) + '</div>';
|
|
1046
|
+
html += '</div>';
|
|
1047
|
+
html += '<div class="toggle-card-switch">';
|
|
1048
|
+
html += '<div class="toggle-track ' + (isOn ? "on" : "") + '"><div class="toggle-thumb"></div></div>';
|
|
1049
|
+
html += '</div>';
|
|
1050
|
+
html += '</div>';
|
|
1051
|
+
html += '</div>';
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
container.innerHTML = html;
|
|
1055
|
+
|
|
1056
|
+
// Bind toggle clicks
|
|
1057
|
+
document.querySelectorAll("[data-service]").forEach(function (el) {
|
|
1058
|
+
el.addEventListener("click", function () {
|
|
1059
|
+
var id = el.dataset.service;
|
|
1060
|
+
serviceSelection[id] = !serviceSelection[id];
|
|
1061
|
+
renderServices();
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function validateStep4() {
|
|
1067
|
+
var errEl = $("step4-error");
|
|
1068
|
+
hideError(errEl);
|
|
1069
|
+
|
|
1070
|
+
var errors = [];
|
|
1071
|
+
CHANNELS.forEach(function (ch) {
|
|
1072
|
+
if (!ch.credentials) return;
|
|
1073
|
+
if (!isChannelEnabled(ch)) return;
|
|
1074
|
+
var sel = channelSelection[ch.id];
|
|
1075
|
+
if (typeof sel !== "object" || sel === null) return;
|
|
1076
|
+
ch.credentials.forEach(function (cred) {
|
|
1077
|
+
if (cred.required && !(sel[cred.key] || "").trim()) {
|
|
1078
|
+
errors.push(ch.name + ": " + cred.label + " is required.");
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
if (errors.length > 0) {
|
|
1084
|
+
showError(errEl, errors.join(" "));
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
return true;
|
|
649
1088
|
}
|
|
650
1089
|
|
|
651
1090
|
/* =========================================================================
|
|
652
|
-
Step
|
|
1091
|
+
Step 5: Review & Install
|
|
653
1092
|
========================================================================= */
|
|
654
1093
|
|
|
655
|
-
function
|
|
1094
|
+
function initStep5() {
|
|
656
1095
|
renderReview();
|
|
1096
|
+
// TODO: Remove renderReviewLegacy() once e2e tests (setup-wizard.test.ts)
|
|
1097
|
+
// are updated to use the new #review-summary selectors instead of #review-grid.
|
|
1098
|
+
renderReviewLegacy();
|
|
657
1099
|
}
|
|
658
1100
|
|
|
659
1101
|
function renderReview() {
|
|
1102
|
+
var container = $("review-summary");
|
|
1103
|
+
var html = "";
|
|
1104
|
+
|
|
1105
|
+
// Account section
|
|
1106
|
+
var adminToken = ($("admin-token").value || "").trim();
|
|
1107
|
+
var ownerName = ($("owner-name").value || "").trim();
|
|
1108
|
+
var ownerEmail = ($("owner-email").value || "").trim();
|
|
1109
|
+
|
|
1110
|
+
html += '<div class="review-card">';
|
|
1111
|
+
html += '<div class="review-card-title"><span>Account</span><button class="review-edit-btn" type="button" data-review-edit="0">Edit</button></div>';
|
|
1112
|
+
html += '<div class="review-row"><span class="review-row-label">Admin Token</span><span class="review-row-value">' + maskToken(adminToken) + '</span></div>';
|
|
1113
|
+
if (ownerName) html += '<div class="review-row"><span class="review-row-label">Name</span><span class="review-row-value">' + esc(ownerName) + '</span></div>';
|
|
1114
|
+
if (ownerEmail) html += '<div class="review-row"><span class="review-row-label">Email</span><span class="review-row-value">' + esc(ownerEmail) + '</span></div>';
|
|
1115
|
+
html += '</div>';
|
|
1116
|
+
|
|
1117
|
+
// Providers section
|
|
1118
|
+
var vp = getVerifiedProviders();
|
|
1119
|
+
html += '<div class="review-card">';
|
|
1120
|
+
html += '<div class="review-card-title"><span>Providers</span><button class="review-edit-btn" type="button" data-review-edit="1">Edit</button></div>';
|
|
1121
|
+
vp.forEach(function (p) {
|
|
1122
|
+
html += '<div class="review-row"><span class="review-row-label">' + esc(p.icon) + ' ' + esc(p.name) + '</span><span class="review-row-value review-row-value-ok">Connected \u2713</span></div>';
|
|
1123
|
+
});
|
|
1124
|
+
html += '</div>';
|
|
1125
|
+
|
|
1126
|
+
// Models section
|
|
1127
|
+
html += '<div class="review-card">';
|
|
1128
|
+
html += '<div class="review-card-title"><span>Models</span><button class="review-edit-btn" type="button" data-review-edit="2">Edit</button></div>';
|
|
1129
|
+
var llm = modelSelection.llm;
|
|
1130
|
+
var emb = modelSelection.embedding;
|
|
1131
|
+
var small = modelSelection.small;
|
|
1132
|
+
if (llm) {
|
|
1133
|
+
var llmProv = PROVIDERS.find(function (p) { return p.id === llm.connId; });
|
|
1134
|
+
html += '<div class="review-row"><span class="review-row-label">Chat Model</span><span class="review-row-value">' + esc(llm.model) + (llmProv ? ' (' + esc(llmProv.name) + ')' : '') + '</span></div>';
|
|
1135
|
+
}
|
|
1136
|
+
if (small && small.model) {
|
|
1137
|
+
var smallProv = PROVIDERS.find(function (p) { return p.id === small.connId; });
|
|
1138
|
+
html += '<div class="review-row"><span class="review-row-label">Small Model</span><span class="review-row-value">' + esc(small.model) + (smallProv ? ' (' + esc(smallProv.name) + ')' : '') + '</span></div>';
|
|
1139
|
+
}
|
|
1140
|
+
if (emb) {
|
|
1141
|
+
var embProv = PROVIDERS.find(function (p) { return p.id === emb.connId; });
|
|
1142
|
+
html += '<div class="review-row"><span class="review-row-label">Embedding Model</span><span class="review-row-value">' + esc(emb.model) + (embProv ? ' (' + esc(embProv.name) + ')' : '') + '</span></div>';
|
|
1143
|
+
html += '<div class="review-row"><span class="review-row-label">Embedding Dims</span><span class="review-row-value">' + (emb.dims || 1536) + '</span></div>';
|
|
1144
|
+
}
|
|
1145
|
+
html += '</div>';
|
|
1146
|
+
|
|
1147
|
+
// Voice section
|
|
1148
|
+
var ttsOpt = TTS_OPTIONS.find(function (o) { return o.id === activeTts(); });
|
|
1149
|
+
var sttOpt = STT_OPTIONS.find(function (o) { return o.id === activeStt(); });
|
|
1150
|
+
html += '<div class="review-card">';
|
|
1151
|
+
html += '<div class="review-card-title"><span>Voice</span><button class="review-edit-btn" type="button" data-review-edit="3">Edit</button></div>';
|
|
1152
|
+
html += '<div class="review-row"><span class="review-row-label">Text-to-Speech</span><span class="review-row-value">' + (ttsOpt ? esc(ttsOpt.name) : "Disabled") + '</span></div>';
|
|
1153
|
+
html += '<div class="review-row"><span class="review-row-label">Speech-to-Text</span><span class="review-row-value">' + (sttOpt ? esc(sttOpt.name) : "Disabled") + '</span></div>';
|
|
1154
|
+
html += '</div>';
|
|
1155
|
+
|
|
1156
|
+
// Channels section
|
|
1157
|
+
var activeChannels = CHANNELS.filter(function (ch) { return isChannelEnabled(ch); });
|
|
1158
|
+
html += '<div class="review-card">';
|
|
1159
|
+
html += '<div class="review-card-title"><span>Channels</span><button class="review-edit-btn" type="button" data-review-edit="4">Edit</button></div>';
|
|
1160
|
+
activeChannels.forEach(function (ch) {
|
|
1161
|
+
html += '<div class="review-row"><span class="review-row-label">' + esc(ch.icon) + ' ' + esc(ch.name) + '</span><span class="review-row-value review-row-value-ok">Enabled \u2713</span></div>';
|
|
1162
|
+
// Show masked credentials if present
|
|
1163
|
+
if (ch.credentials) {
|
|
1164
|
+
var sel = channelSelection[ch.id];
|
|
1165
|
+
if (typeof sel === "object" && sel !== null && sel.enabled) {
|
|
1166
|
+
ch.credentials.forEach(function (cred) {
|
|
1167
|
+
var val = sel[cred.key] || "";
|
|
1168
|
+
if (val) {
|
|
1169
|
+
html += '<div class="review-row"><span class="review-row-label" style="padding-left:24px">' + esc(cred.label) + '</span><span class="review-row-value">' + maskToken(val) + '</span></div>';
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
html += '</div>';
|
|
1176
|
+
|
|
1177
|
+
// Services section
|
|
1178
|
+
var activeServices = SERVICES.filter(function (svc) { return serviceSelection[svc.id]; });
|
|
1179
|
+
html += '<div class="review-card">';
|
|
1180
|
+
html += '<div class="review-card-title"><span>Services</span><button class="review-edit-btn" type="button" data-review-edit="4">Edit</button></div>';
|
|
1181
|
+
if (activeServices.length > 0) {
|
|
1182
|
+
activeServices.forEach(function (svc) {
|
|
1183
|
+
html += '<div class="review-row"><span class="review-row-label">' + esc(svc.icon) + ' ' + esc(svc.name) + '</span><span class="review-row-value review-row-value-ok">Enabled \u2713</span></div>';
|
|
1184
|
+
});
|
|
1185
|
+
} else {
|
|
1186
|
+
html += '<div class="review-row"><span class="review-row-label">No extra services</span><span class="review-row-value">Core only</span></div>';
|
|
1187
|
+
}
|
|
1188
|
+
html += '</div>';
|
|
1189
|
+
|
|
1190
|
+
// Options section
|
|
1191
|
+
var ollamaEnabled = $("ollama-enabled") && $("ollama-enabled").checked;
|
|
1192
|
+
var memUserId = ($("memory-user-id").value || "").trim() || "default_user";
|
|
1193
|
+
html += '<div class="review-card">';
|
|
1194
|
+
html += '<div class="review-card-title"><span>Options</span><button class="review-edit-btn" type="button" data-review-edit="4">Edit</button></div>';
|
|
1195
|
+
if (ollamaEnabled) {
|
|
1196
|
+
html += '<div class="review-row"><span class="review-row-label">Ollama In-Stack</span><span class="review-row-value">Enabled</span></div>';
|
|
1197
|
+
}
|
|
1198
|
+
html += '<div class="review-row"><span class="review-row-label">Memory User ID</span><span class="review-row-value">' + esc(memUserId) + '</span></div>';
|
|
1199
|
+
html += '</div>';
|
|
1200
|
+
|
|
1201
|
+
container.innerHTML = html;
|
|
1202
|
+
|
|
1203
|
+
// Build JSON for review
|
|
1204
|
+
var jsonObj = buildPayload();
|
|
1205
|
+
$("review-json-pre").textContent = JSON.stringify(jsonObj, null, 2);
|
|
1206
|
+
|
|
1207
|
+
// Bind edit buttons
|
|
1208
|
+
document.querySelectorAll("[data-review-edit]").forEach(function (btn) {
|
|
1209
|
+
btn.addEventListener("click", function () {
|
|
1210
|
+
goToStep(parseInt(btn.dataset.reviewEdit, 10));
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function renderReviewLegacy() {
|
|
1216
|
+
// Keep the old review-grid populated for test backward compat
|
|
660
1217
|
var grid = $("review-grid");
|
|
661
1218
|
grid.innerHTML = "";
|
|
662
1219
|
|
|
663
|
-
// Account section
|
|
664
1220
|
grid.appendChild(reviewHeader("Account", 0));
|
|
665
1221
|
grid.appendChild(reviewItem("Admin Token", maskToken($("admin-token").value)));
|
|
666
1222
|
var ownerName = ($("owner-name").value || "").trim();
|
|
@@ -668,36 +1224,60 @@
|
|
|
668
1224
|
var ownerEmail = ($("owner-email").value || "").trim();
|
|
669
1225
|
if (ownerEmail) grid.appendChild(reviewItem("Email", ownerEmail));
|
|
670
1226
|
|
|
671
|
-
// Connections section
|
|
672
1227
|
grid.appendChild(reviewHeader("Connections", 1));
|
|
673
|
-
|
|
674
|
-
|
|
1228
|
+
getVerifiedProviders().forEach(function (p) {
|
|
1229
|
+
var st = providerState[p.id];
|
|
1230
|
+
grid.appendChild(reviewItem(p.name, p.kind + " -- " + (st.baseUrl || p.baseUrl), true));
|
|
675
1231
|
});
|
|
676
1232
|
|
|
677
|
-
// Models section
|
|
678
1233
|
grid.appendChild(reviewHeader("Models", 2));
|
|
679
|
-
var
|
|
680
|
-
var
|
|
681
|
-
var
|
|
682
|
-
var
|
|
683
|
-
var
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (smallModel) {
|
|
690
|
-
grid.appendChild(reviewItem("Small Model", smallModel + " (" + (llmConn ? llmConn.name : "?") + ")", true));
|
|
1234
|
+
var llm = modelSelection.llm;
|
|
1235
|
+
var small = modelSelection.small;
|
|
1236
|
+
var emb = modelSelection.embedding;
|
|
1237
|
+
var llmProv = llm ? PROVIDERS.find(function (pp) { return pp.id === llm.connId; }) : null;
|
|
1238
|
+
var embProv = emb ? PROVIDERS.find(function (pp) { return pp.id === emb.connId; }) : null;
|
|
1239
|
+
if (llm) grid.appendChild(reviewItem("Chat Model", llm.model + " (" + (llmProv ? llmProv.name : "?") + ")", true));
|
|
1240
|
+
if (small && small.model) grid.appendChild(reviewItem("Small Model", small.model + " (" + (llmProv ? llmProv.name : "?") + ")", true));
|
|
1241
|
+
if (emb) {
|
|
1242
|
+
grid.appendChild(reviewItem("Embedding Model", emb.model + " (" + (embProv ? embProv.name : "?") + ")", true));
|
|
1243
|
+
grid.appendChild(reviewItem("Embedding Dims", String(emb.dims || 1536), true));
|
|
691
1244
|
}
|
|
692
|
-
grid.appendChild(reviewItem("Embedding Model", embModel + " (" + (embConn ? embConn.name : "?") + ")", true));
|
|
693
|
-
grid.appendChild(reviewItem("Embedding Dims", embDims, true));
|
|
694
1245
|
|
|
695
|
-
|
|
696
|
-
|
|
1246
|
+
grid.appendChild(reviewHeader("Voice", 3));
|
|
1247
|
+
var ttsOpt = TTS_OPTIONS.find(function (o) { return o.id === activeTts(); });
|
|
1248
|
+
var sttOpt = STT_OPTIONS.find(function (o) { return o.id === activeStt(); });
|
|
1249
|
+
grid.appendChild(reviewItem("TTS", ttsOpt ? ttsOpt.name : "Disabled"));
|
|
1250
|
+
grid.appendChild(reviewItem("STT", sttOpt ? sttOpt.name : "Disabled"));
|
|
1251
|
+
|
|
1252
|
+
grid.appendChild(reviewHeader("Channels", 4));
|
|
1253
|
+
CHANNELS.forEach(function (ch) {
|
|
1254
|
+
if (isChannelEnabled(ch)) {
|
|
1255
|
+
grid.appendChild(reviewItem(ch.name, "Enabled"));
|
|
1256
|
+
// Show masked credentials in legacy review
|
|
1257
|
+
if (ch.credentials) {
|
|
1258
|
+
var sel = channelSelection[ch.id];
|
|
1259
|
+
if (typeof sel === "object" && sel !== null && sel.enabled) {
|
|
1260
|
+
ch.credentials.forEach(function (cred) {
|
|
1261
|
+
var val = sel[cred.key] || "";
|
|
1262
|
+
if (val) {
|
|
1263
|
+
grid.appendChild(reviewItem(" " + cred.label, maskToken(val)));
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
grid.appendChild(reviewHeader("Services", 4));
|
|
1272
|
+
SERVICES.forEach(function (svc) {
|
|
1273
|
+
if (serviceSelection[svc.id]) {
|
|
1274
|
+
grid.appendChild(reviewItem(svc.name, "Enabled"));
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
grid.appendChild(reviewHeader("Options", 4));
|
|
697
1279
|
var ollamaEnabled = $("ollama-enabled") && $("ollama-enabled").checked;
|
|
698
|
-
if (ollamaEnabled)
|
|
699
|
-
grid.appendChild(reviewItem("Ollama In-Stack", "Enabled"));
|
|
700
|
-
}
|
|
1280
|
+
if (ollamaEnabled) grid.appendChild(reviewItem("Ollama In-Stack", "Enabled"));
|
|
701
1281
|
grid.appendChild(reviewItem("Memory User ID", $("memory-user-id").value || "default_user"));
|
|
702
1282
|
}
|
|
703
1283
|
|
|
@@ -739,53 +1319,92 @@
|
|
|
739
1319
|
Install & Deploy
|
|
740
1320
|
========================================================================= */
|
|
741
1321
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1322
|
+
function buildChannelsConfig() {
|
|
1323
|
+
var result = {};
|
|
1324
|
+
CHANNELS.forEach(function (ch) {
|
|
1325
|
+
var sel = channelSelection[ch.id];
|
|
1326
|
+
if (ch.locked) {
|
|
1327
|
+
result[ch.id] = true;
|
|
1328
|
+
} else if (typeof sel === "object" && sel !== null) {
|
|
1329
|
+
if (sel.enabled) {
|
|
1330
|
+
// Include credentials (copy enabled + credential fields)
|
|
1331
|
+
var entry = { enabled: true };
|
|
1332
|
+
if (ch.credentials) {
|
|
1333
|
+
ch.credentials.forEach(function (cred) {
|
|
1334
|
+
if (sel[cred.key]) entry[cred.key] = sel[cred.key];
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
result[ch.id] = entry;
|
|
1338
|
+
}
|
|
1339
|
+
} else if (sel) {
|
|
1340
|
+
result[ch.id] = true;
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
747
1345
|
|
|
1346
|
+
function buildPayload() {
|
|
748
1347
|
var adminToken = ($("admin-token").value || "").trim();
|
|
749
1348
|
var ownerName = ($("owner-name").value || "").trim();
|
|
750
1349
|
var ownerEmail = ($("owner-email").value || "").trim();
|
|
751
|
-
var memoryUserId = ($("memory-user-id").value || "").trim() ||
|
|
1350
|
+
var memoryUserId = ($("memory-user-id").value || "").trim() || (ownerName ? ownerName.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "") : "") || "default_user";
|
|
752
1351
|
var ollamaEnabled = $("ollama-enabled") ? $("ollama-enabled").checked : false;
|
|
753
1352
|
|
|
754
|
-
var
|
|
755
|
-
var
|
|
756
|
-
var
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
var
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
1353
|
+
var llm = modelSelection.llm;
|
|
1354
|
+
var emb = modelSelection.embedding;
|
|
1355
|
+
var small = modelSelection.small;
|
|
1356
|
+
|
|
1357
|
+
// Build connections from verified providers
|
|
1358
|
+
var connections = getVerifiedProviders().map(function (p) {
|
|
1359
|
+
var st = providerState[p.id];
|
|
1360
|
+
return {
|
|
1361
|
+
id: p.id,
|
|
1362
|
+
name: p.name,
|
|
1363
|
+
provider: p.id,
|
|
1364
|
+
baseUrl: st.baseUrl || p.baseUrl,
|
|
1365
|
+
apiKey: st.apiKey || "",
|
|
1366
|
+
};
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
var ttsVal = activeTts() !== "skip-tts" ? activeTts() : null;
|
|
1370
|
+
var sttVal = activeStt() !== "skip-stt" ? activeStt() : null;
|
|
1371
|
+
|
|
1372
|
+
return {
|
|
1373
|
+
version: 1,
|
|
1374
|
+
owner: (ownerName || ownerEmail) ? { name: ownerName || undefined, email: ownerEmail || undefined } : undefined,
|
|
1375
|
+
security: { adminToken: adminToken },
|
|
1376
|
+
connections: connections,
|
|
776
1377
|
assignments: {
|
|
777
1378
|
llm: {
|
|
778
|
-
connectionId:
|
|
779
|
-
model:
|
|
780
|
-
smallModel:
|
|
1379
|
+
connectionId: llm ? llm.connId : "",
|
|
1380
|
+
model: llm ? llm.model : "",
|
|
1381
|
+
smallModel: (small && small.model) ? small.model : undefined,
|
|
781
1382
|
},
|
|
782
1383
|
embeddings: {
|
|
783
|
-
connectionId:
|
|
784
|
-
model:
|
|
785
|
-
embeddingDims:
|
|
1384
|
+
connectionId: emb ? emb.connId : "",
|
|
1385
|
+
model: emb ? emb.model : "",
|
|
1386
|
+
embeddingDims: emb ? (emb.dims || 1536) : 1536,
|
|
786
1387
|
},
|
|
1388
|
+
tts: ttsVal,
|
|
1389
|
+
stt: sttVal,
|
|
1390
|
+
},
|
|
1391
|
+
memory: { userId: memoryUserId },
|
|
1392
|
+
channels: buildChannelsConfig(),
|
|
1393
|
+
services: {
|
|
1394
|
+
admin: serviceSelection.admin || false,
|
|
1395
|
+
openviking: serviceSelection.openviking || false,
|
|
1396
|
+
ollama: ollamaEnabled,
|
|
787
1397
|
},
|
|
788
1398
|
};
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function handleInstall() {
|
|
1402
|
+
if (installing) return;
|
|
1403
|
+
|
|
1404
|
+
var errEl = $("install-error");
|
|
1405
|
+
hideError(errEl);
|
|
1406
|
+
|
|
1407
|
+
var payload = buildPayload();
|
|
789
1408
|
|
|
790
1409
|
installing = true;
|
|
791
1410
|
var installBtn = $("btn-install");
|
|
@@ -808,7 +1427,6 @@
|
|
|
808
1427
|
return;
|
|
809
1428
|
}
|
|
810
1429
|
|
|
811
|
-
// Success -- go to deploy screen
|
|
812
1430
|
showDeployScreen();
|
|
813
1431
|
startDeployPolling();
|
|
814
1432
|
} catch (e) {
|
|
@@ -837,12 +1455,15 @@
|
|
|
837
1455
|
|
|
838
1456
|
updateDeployUI(data);
|
|
839
1457
|
|
|
840
|
-
if (data.
|
|
841
|
-
stopDeployPolling();
|
|
842
|
-
showDeployDone(data);
|
|
843
|
-
} else if (data.deployError) {
|
|
1458
|
+
if (data.deployError) {
|
|
844
1459
|
stopDeployPolling();
|
|
845
1460
|
showDeployError(data.deployError);
|
|
1461
|
+
} else if (data.setupComplete && data.deployStatus && data.deployStatus.length > 0) {
|
|
1462
|
+
var allRunning = data.deployStatus.every(function (s) { return s.status === "running"; });
|
|
1463
|
+
if (allRunning) {
|
|
1464
|
+
stopDeployPolling();
|
|
1465
|
+
showDeployDone(data);
|
|
1466
|
+
}
|
|
846
1467
|
}
|
|
847
1468
|
} catch (e) {
|
|
848
1469
|
// silently retry
|
|
@@ -865,7 +1486,6 @@
|
|
|
865
1486
|
var row = document.createElement("div");
|
|
866
1487
|
row.className = "deploy-service-row";
|
|
867
1488
|
|
|
868
|
-
// Indicator
|
|
869
1489
|
var indicator = document.createElement("div");
|
|
870
1490
|
indicator.className = "deploy-service-indicator";
|
|
871
1491
|
if (svc.status === "running") {
|
|
@@ -877,7 +1497,6 @@
|
|
|
877
1497
|
}
|
|
878
1498
|
row.appendChild(indicator);
|
|
879
1499
|
|
|
880
|
-
// Info
|
|
881
1500
|
var info = document.createElement("div");
|
|
882
1501
|
info.className = "deploy-service-info";
|
|
883
1502
|
info.innerHTML =
|
|
@@ -885,7 +1504,6 @@
|
|
|
885
1504
|
'<span class="deploy-service-status">' + esc(svc.label || svc.status) + "</span>";
|
|
886
1505
|
row.appendChild(info);
|
|
887
1506
|
|
|
888
|
-
// Bar
|
|
889
1507
|
var bar = document.createElement("div");
|
|
890
1508
|
bar.className = "deploy-service-bar";
|
|
891
1509
|
var fill = document.createElement("div");
|
|
@@ -900,7 +1518,6 @@
|
|
|
900
1518
|
container.appendChild(row);
|
|
901
1519
|
});
|
|
902
1520
|
|
|
903
|
-
// Progress
|
|
904
1521
|
var pct = total > 0 ? Math.round((running / total) * 100) : 0;
|
|
905
1522
|
$("deploy-progress-value").textContent = pct + "%";
|
|
906
1523
|
$("deploy-progress-fill").style.width = pct + "%";
|
|
@@ -925,7 +1542,6 @@
|
|
|
925
1542
|
$("deploy-progress-value").textContent = "100%";
|
|
926
1543
|
$("deploy-progress-fill").style.width = "100%";
|
|
927
1544
|
|
|
928
|
-
// Service list
|
|
929
1545
|
var list = $("deploy-service-list");
|
|
930
1546
|
list.innerHTML = "";
|
|
931
1547
|
(data.deployStatus || []).forEach(function (svc) {
|
|
@@ -946,8 +1562,6 @@
|
|
|
946
1562
|
$("deploy-failure-summary").textContent = typeof error === "string" ? error : "Deployment failed.";
|
|
947
1563
|
$("deploy-error-pre").textContent = typeof error === "string" ? error : JSON.stringify(error, null, 2);
|
|
948
1564
|
|
|
949
|
-
var bar = $("deploy-progress-bar");
|
|
950
|
-
if (bar) bar.parentElement.querySelector(".deploy-progress-bar").classList.add("deploy-progress-bar--error");
|
|
951
1565
|
$("deploy-progress-value").textContent = "Error";
|
|
952
1566
|
$("deploy-progress-value").classList.add("deploy-progress-value--error");
|
|
953
1567
|
}
|
|
@@ -963,11 +1577,30 @@
|
|
|
963
1577
|
if (res.ok) {
|
|
964
1578
|
var data = await res.json();
|
|
965
1579
|
detectedProviders = data.providers || [];
|
|
1580
|
+
|
|
1581
|
+
// Auto-select and pre-verify detected providers
|
|
1582
|
+
detectedProviders.forEach(function (dp) {
|
|
1583
|
+
if (!dp.available) return;
|
|
1584
|
+
var st = providerState[dp.provider];
|
|
1585
|
+
if (st) {
|
|
1586
|
+
st.baseUrl = dp.url;
|
|
1587
|
+
// Auto-select detected providers
|
|
1588
|
+
if (!st.selected) {
|
|
1589
|
+
st.selected = true;
|
|
1590
|
+
if (dp.provider === "ollama") {
|
|
1591
|
+
st.ollamaMode = "running";
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
// Auto-verify by fetching models
|
|
1595
|
+
verifyProvider(dp.provider);
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
966
1598
|
}
|
|
967
1599
|
} catch (e) {
|
|
968
1600
|
detectedProviders = [];
|
|
969
1601
|
}
|
|
970
1602
|
hide($("conn-detecting"));
|
|
1603
|
+
renderProviderGrid();
|
|
971
1604
|
}
|
|
972
1605
|
|
|
973
1606
|
async function apiFetchModels(provider, baseUrl, apiKey) {
|
|
@@ -977,9 +1610,11 @@
|
|
|
977
1610
|
headers: { "Content-Type": "application/json" },
|
|
978
1611
|
body: JSON.stringify({ apiKey: apiKey || "", baseUrl: baseUrl || "" }),
|
|
979
1612
|
});
|
|
980
|
-
if (!res.ok) throw new Error("Failed to fetch models (HTTP " + res.status + ")");
|
|
981
1613
|
var data = await res.json();
|
|
982
|
-
|
|
1614
|
+
if (!res.ok || data.status === "recoverable_error") {
|
|
1615
|
+
throw new Error(data.error || "Failed to fetch models (HTTP " + res.status + ")");
|
|
1616
|
+
}
|
|
1617
|
+
return data;
|
|
983
1618
|
}
|
|
984
1619
|
|
|
985
1620
|
/* =========================================================================
|
|
@@ -1005,54 +1640,26 @@
|
|
|
1005
1640
|
.then(function (r) { return r.json(); })
|
|
1006
1641
|
.then(function (data) {
|
|
1007
1642
|
if (data.setupComplete) {
|
|
1008
|
-
// Already set up -- redirect
|
|
1009
1643
|
window.location.href = "/";
|
|
1010
1644
|
}
|
|
1011
1645
|
})
|
|
1012
1646
|
.catch(function () { /* ignore */ });
|
|
1013
1647
|
|
|
1014
|
-
// ── Step
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
goToStep(step);
|
|
1020
|
-
}
|
|
1021
|
-
});
|
|
1648
|
+
// ── Step 0: Welcome ──
|
|
1649
|
+
$("btn-get-started").addEventListener("click", function () {
|
|
1650
|
+
welcomeHeroDismissed = true;
|
|
1651
|
+
hide($("welcome-hero"));
|
|
1652
|
+
show($("identity-form"));
|
|
1022
1653
|
});
|
|
1023
1654
|
|
|
1024
|
-
// ── Step 0: Welcome ──
|
|
1025
1655
|
$("btn-step0-next").addEventListener("click", function () {
|
|
1026
1656
|
if (validateStep0()) goToStep(1);
|
|
1027
1657
|
});
|
|
1028
1658
|
|
|
1029
|
-
// ── Step 1:
|
|
1659
|
+
// ── Step 1: Providers ──
|
|
1030
1660
|
$("btn-step1-back").addEventListener("click", function () { goToStep(0); });
|
|
1031
|
-
$("btn-step1-add").addEventListener("click", function () { openAddConnection(); });
|
|
1032
1661
|
$("btn-step1-next").addEventListener("click", function () {
|
|
1033
|
-
if (
|
|
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);
|
|
1662
|
+
if (getVerifiedCount() > 0) goToStep(2);
|
|
1056
1663
|
});
|
|
1057
1664
|
|
|
1058
1665
|
// ── Step 2: Models ──
|
|
@@ -1060,35 +1667,41 @@
|
|
|
1060
1667
|
$("btn-step2-next").addEventListener("click", function () {
|
|
1061
1668
|
if (validateStep2()) goToStep(3);
|
|
1062
1669
|
});
|
|
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
1670
|
|
|
1078
|
-
// ── Step 3:
|
|
1671
|
+
// ── Step 3: Voice ──
|
|
1079
1672
|
$("btn-step3-back").addEventListener("click", function () { goToStep(2); });
|
|
1080
1673
|
$("btn-step3-next").addEventListener("click", function () { goToStep(4); });
|
|
1081
1674
|
|
|
1082
|
-
// ── Step 4:
|
|
1675
|
+
// ── Step 4: Options ──
|
|
1083
1676
|
$("btn-step4-back").addEventListener("click", function () { goToStep(3); });
|
|
1677
|
+
$("btn-step4-next").addEventListener("click", function () {
|
|
1678
|
+
if (validateStep4()) goToStep(5);
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
// ── Step 5: Review ──
|
|
1682
|
+
$("btn-step5-back").addEventListener("click", function () { goToStep(4); });
|
|
1084
1683
|
$("btn-install").addEventListener("click", function () { handleInstall(); });
|
|
1085
1684
|
|
|
1685
|
+
// ── JSON toggle ──
|
|
1686
|
+
$("btn-toggle-json").addEventListener("click", function () {
|
|
1687
|
+
var jsonEl = $("review-json");
|
|
1688
|
+
var btn = $("btn-toggle-json");
|
|
1689
|
+
if (jsonEl.classList.contains("hidden")) {
|
|
1690
|
+
show(jsonEl);
|
|
1691
|
+
btn.textContent = "Hide Setup JSON";
|
|
1692
|
+
} else {
|
|
1693
|
+
hide(jsonEl);
|
|
1694
|
+
btn.textContent = "Show Setup JSON";
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
|
|
1086
1698
|
// ── Deploy error actions ──
|
|
1087
1699
|
$("btn-deploy-back").addEventListener("click", function () {
|
|
1088
1700
|
installing = false;
|
|
1089
|
-
goToStep(
|
|
1701
|
+
goToStep(5);
|
|
1090
1702
|
});
|
|
1091
1703
|
$("btn-deploy-retry").addEventListener("click", function () {
|
|
1704
|
+
installing = false;
|
|
1092
1705
|
hide($("deploy-failure"));
|
|
1093
1706
|
hide($("deploy-error-actions"));
|
|
1094
1707
|
show($("deploy-tips"));
|
|
@@ -1099,6 +1712,6 @@
|
|
|
1099
1712
|
});
|
|
1100
1713
|
|
|
1101
1714
|
// Start on step 0
|
|
1102
|
-
|
|
1715
|
+
renderProgressBar();
|
|
1103
1716
|
});
|
|
1104
1717
|
})();
|