main
js 867 lines 30 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import { callJsonApi, fetchApi } from "/js/api.js";
3 import { store as modelConfigStore } from "/plugins/_model_config/webui/model-config-store.js";
4 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
5 import {
6 LOCAL_PROVIDER_IDS,
7 MORE_CLOUD_PROVIDER_IDS,
8 ONBOARDING_PROVIDER_OVERRIDES,
9 TOP_CLOUD_PROVIDER_IDS,
10 } from "/plugins/_onboarding/webui/onboarding-providers.js";
11
12 const MODEL_CONFIG_API = "/plugins/_model_config";
13 const OAUTH_STATUS_API = "/plugins/_oauth/status";
14 const OAUTH_START_API = "/plugins/_oauth/start_login";
15 const OAUTH_POLL_API = "/plugins/_oauth/poll_device_login";
16 const OAUTH_MANUAL_CALLBACK_API = "/plugins/_oauth/manual_callback";
17 const OAUTH_MODELS_API = "/plugins/_oauth/models";
18 const MAX_OAUTH_POLL_MS = 120000;
19
20 const TOP_CLOUD_IDS = TOP_CLOUD_PROVIDER_IDS;
21 const MORE_CLOUD_IDS = MORE_CLOUD_PROVIDER_IDS;
22
23 const OAUTH_MARKS = {
24 github: "terminal",
25 google: "cloud",
26 openai: "key",
27 xai: "neurology",
28 };
29
30 const FALLBACKS = {
31 other: {
32 id: "other",
33 name: "Other OpenAI-compatible",
34 logo: "/public/darkSymbol.svg",
35 api_key_mode: "optional",
36 short_description: "Use a compatible endpoint you control.",
37 },
38 };
39
40 function clone(value) {
41 return JSON.parse(JSON.stringify(value || {}));
42 }
43
44 function detailsById(details = []) {
45 const result = {};
46 for (const item of details || []) {
47 const id = String(item?.id || item?.value || "").trim();
48 if (id) result[id] = item;
49 }
50 return result;
51 }
52
53 function ensureSlot(config, key) {
54 if (!config[key] || typeof config[key] !== "object") config[key] = {};
55 config[key] = {
56 provider: "",
57 name: "",
58 api_base: "",
59 api_key: "",
60 ctx_length: key === "utility_model" ? 128000 : 200000,
61 ctx_history: key === "chat_model" ? 0.7 : undefined,
62 ctx_input: key === "utility_model" ? 0.7 : undefined,
63 vision: key === "chat_model" ? true : undefined,
64 rl_requests: 0,
65 rl_input: 0,
66 rl_output: 0,
67 kwargs: {},
68 ...config[key],
69 };
70 }
71
72 function normalizeUrl(value) {
73 return String(value || "").trim();
74 }
75
76 function safeProviderName(provider) {
77 return provider?.name || provider?.label || provider?.id || "Provider";
78 }
79
80 export const store = createStore("onboarding", {
81 step: "connect",
82 providerMode: "cloud",
83 presetMode: "",
84 loading: true,
85 saving: false,
86 config: null,
87 providerDetails: {},
88 selectedProviderId: "",
89 selectedProviderOrigin: "cloud",
90 userTouchedModel: {
91 chat_model: false,
92 utility_model: false,
93 },
94 modelDropdown: {
95 chat_model: { models: [], open: false, loading: false, error: "", source: "" },
96 utility_model: { models: [], open: false, loading: false, error: "", source: "" },
97 },
98 oauthStatus: null,
99 oauthLoading: false,
100 oauthConnecting: false,
101 oauthConnectingProvider: "",
102 oauthDevice: null,
103 oauthPollTimer: null,
104 oauthCallbackPollTimer: null,
105 oauthPollStartedAt: 0,
106 oauthModels: {},
107 oauthProviderUi: {},
108
109 steps: [
110 { step: "connect", label: "Choose provider" },
111 { step: "setup", label: "Connect" },
112 { step: "utility", label: "Utility" },
113 { step: "ready", label: "Ready" },
114 ],
115
116 async init() {
117 this.resetState();
118 },
119
120 resetState() {
121 this.step = "connect";
122 this.providerMode = ["local", "account"].includes(this.presetMode) ? this.presetMode : "cloud";
123 this.presetMode = "";
124 this.loading = true;
125 this.saving = false;
126 this.config = null;
127 this.providerDetails = {};
128 this.selectedProviderId = "";
129 this.selectedProviderOrigin = "cloud";
130 this.userTouchedModel = { chat_model: false, utility_model: false };
131 this.modelDropdown = {
132 chat_model: { models: [], open: false, loading: false, error: "", source: "" },
133 utility_model: { models: [], open: false, loading: false, error: "", source: "" },
134 };
135 this.oauthStatus = null;
136 this.oauthLoading = false;
137 this.oauthConnecting = false;
138 this.oauthConnectingProvider = "";
139 this.oauthDevice = null;
140 this.oauthModels = {};
141 this.oauthProviderUi = {};
142 this.stopOauthPolling();
143 this.stopOauthCallbackPolling();
144 },
145
146 async onOpen() {
147 await this.init();
148 await modelConfigStore.ensureLoaded();
149 modelConfigStore.resetApiKeyDrafts();
150 await modelConfigStore.refreshApiKeyStatus();
151 await this.loadConfig();
152 await this.loadOauthStatus({ silent: true });
153 this.loading = false;
154 },
155
156 cleanup() {
157 this.stopOauthPolling();
158 this.resetState();
159 },
160
161 async loadConfig() {
162 const response = await fetchApi(`${MODEL_CONFIG_API}/model_config_get`, {
163 method: "POST",
164 headers: { "Content-Type": "application/json" },
165 body: JSON.stringify({}),
166 });
167 const data = await response.json().catch(() => ({}));
168 this.config = clone(data.config || {});
169 ensureSlot(this.config, "chat_model");
170 ensureSlot(this.config, "utility_model");
171 ensureSlot(this.config, "embedding_model");
172 modelConfigStore.initConfigFields(this.config);
173 this.providerDetails = detailsById(data.chat_provider_details || modelConfigStore.chatProviderDetails || []);
174 if (this.config.chat_model.provider) {
175 this.selectedProviderId = this.config.chat_model.provider;
176 }
177 },
178
179 providerMeta(id) {
180 const providerId = String(id || "").trim();
181 const fromDetails = this.providerDetails[providerId] || {};
182 const fallback = FALLBACKS[providerId] || {};
183 const override = ONBOARDING_PROVIDER_OVERRIDES[providerId] || {};
184 const oauth = this.oauthProviderStatus(providerId);
185 if (oauth) {
186 const defaultModels = Array.isArray(oauth.default_models) ? oauth.default_models : [];
187 return {
188 ...fallback,
189 ...fromDetails,
190 ...override,
191 id: providerId,
192 name: oauth.display_name || fromDetails.name || override.name || providerId,
193 short_description: oauth.connected
194 ? (oauth.account_label || "Connected")
195 : (oauth.note || oauth.warning || "Connect this account-backed provider."),
196 logo: override.logo || fromDetails.logo || "/public/darkSymbol.svg",
197 api_key_mode: "oauth",
198 default_chat_model: oauth.default_model || defaultModels[0] || fromDetails.default_chat_model || "",
199 default_utility_model: defaultModels[1] || oauth.default_model || fromDetails.default_utility_model || "",
200 model_list_autoload: Boolean(oauth.connected),
201 auth_flow: oauth.auth_flow || "",
202 };
203 }
204 return {
205 ...fallback,
206 ...fromDetails,
207 ...override,
208 id: providerId,
209 name: override.name || fromDetails.name || fallback.name || providerId,
210 short_description: override.short_description || fromDetails.short_description || fallback.short_description || "Connect this provider to Agent Zero.",
211 logo: override.logo || fromDetails.logo || fallback.logo || "/public/darkSymbol.svg",
212 api_key_mode: override.api_key_mode || fromDetails.api_key_mode || fallback.api_key_mode || "required",
213 };
214 },
215
216 cloudProviders() {
217 return [...TOP_CLOUD_IDS, ...MORE_CLOUD_IDS].map((id) => this.providerMeta(id));
218 },
219
220 localProviderCards() {
221 return LOCAL_PROVIDER_IDS.map((id) => {
222 const meta = this.providerMeta(id);
223 if (id === "other") {
224 return {
225 ...meta,
226 name: "Other local endpoint",
227 short_description: "Point Agent Zero at a local compatible server.",
228 default_api_base: "",
229 api_key_mode: "optional",
230 };
231 }
232 return meta;
233 });
234 },
235
236 oauthProviderStatus(providerId) {
237 const key = String(providerId || "").trim();
238 if (!key) return null;
239 return this.oauthStatus?.provider_map?.[key] || null;
240 },
241
242 oauthProviderCards() {
243 const providers = Array.isArray(this.oauthStatus?.providers) ? this.oauthStatus.providers : [];
244 return providers.filter((provider) => provider?.provider_id);
245 },
246
247 oauthProviderUiFor(providerId) {
248 const key = String(providerId || "").trim();
249 if (!key) return {};
250 if (!this.oauthProviderUi[key]) {
251 this.oauthProviderUi = {
252 ...this.oauthProviderUi,
253 [key]: {
254 enterprise_domain: "",
255 client_id: "",
256 client_secret: "",
257 quota_project_id: "",
258 manualCallback: "",
259 },
260 };
261 }
262 return this.oauthProviderUi[key];
263 },
264
265 oauthMark(provider) {
266 return provider?.connected ? "check" : (provider?.mark || OAUTH_MARKS[provider?.icon] || "account_circle");
267 },
268
269 oauthCardTitle(provider) {
270 return provider?.display_name || provider?.short_name || provider?.provider_id || "Account";
271 },
272
273 oauthCardSubtitle(provider) {
274 if (this.oauthLoading) return "Checking";
275 if (provider?.connected) return provider.account_label || provider.email || "Connected";
276 if (!this.oauthSetupReady(provider?.provider_id)) return "Needs OAuth client details";
277 if (provider?.warning) return "Available with restrictions";
278 return "Not connected";
279 },
280
281 oauthAccountActionLabel(providerId) {
282 const provider = this.oauthProviderStatus(providerId);
283 if (provider?.connected) return "Use account";
284 if (this.oauthConnectingProvider === providerId) return "Waiting for sign-in";
285 return this.oauthSetupReady(providerId) ? "Connect account" : "Configure";
286 },
287
288 selectedProvider() {
289 return this.providerMeta(this.selectedProviderId || this.config?.chat_model?.provider || "");
290 },
291
292 selectedProviderName() {
293 return safeProviderName(this.selectedProvider());
294 },
295
296 titleText() {
297 if (this.step === "setup") return "Choose your main model";
298 if (this.step === "utility") return "Choose your utility model";
299 if (this.step === "ready") return "Agent Zero is ready";
300 return "Choose your AI provider";
301 },
302
303 stepNumber(stepName) {
304 const index = this.steps.findIndex((item) => item.step === stepName);
305 return index >= 0 ? index + 1 : 1;
306 },
307
308 currentStepNumber() {
309 return this.stepNumber(this.step);
310 },
311
312 isStep(name) {
313 return this.step === name;
314 },
315
316 setProviderMode(mode) {
317 this.providerMode = ["local", "account"].includes(mode) ? mode : "cloud";
318 },
319
320 goBack() {
321 if (this.step === "setup") {
322 this.step = "connect";
323 return;
324 }
325 if (this.step === "utility") {
326 this.step = "setup";
327 return;
328 }
329 if (this.step === "ready") {
330 this.step = "utility";
331 }
332 },
333
334 showBackButton() {
335 return !["connect", "ready"].includes(this.step);
336 },
337
338 showPrimaryButton() {
339 return ["setup", "utility", "ready"].includes(this.step);
340 },
341
342 primaryButtonLabel() {
343 if (this.step === "setup") return "Choose utility model";
344 if (this.step === "utility") return this.saving ? "Saving" : "Finish setup";
345 if (this.step === "ready") return "Start Chatting";
346 return "Continue";
347 },
348
349 primaryDisabled() {
350 if (this.loading || this.saving) return true;
351 if (this.step === "setup") {
352 if (this.isOAuthProvider() && !this.oauthConnected(this.selectedProviderId)) return true;
353 if (this.providerNeedsKey(this.selectedProviderId) && !this.hasProviderKey(this.selectedProviderId)) return true;
354 return !this.config?.chat_model?.provider || !this.config?.chat_model?.name;
355 }
356 if (this.step === "utility") {
357 const utilityProvider = this.config?.utility_model?.provider || "";
358 if (this.isOAuthProvider(utilityProvider) && !this.oauthConnected(utilityProvider)) return true;
359 return !utilityProvider || !this.config?.utility_model?.name;
360 }
361 return false;
362 },
363
364 async primaryAction() {
365 if (this.primaryDisabled()) return;
366 if (this.step === "setup") {
367 this.prepareUtilityDefaults();
368 this.step = "utility";
369 await this.loadModels("utility_model");
370 return;
371 }
372 if (this.step === "utility") {
373 await this.completeSetup();
374 return;
375 }
376 if (this.step === "ready") {
377 await this.startChatting();
378 }
379 },
380
381 async selectProvider(providerId, origin = "cloud") {
382 this.selectedProviderId = providerId;
383 this.selectedProviderOrigin = origin;
384 this.providerMode = ["local", "account"].includes(origin) ? origin : "cloud";
385 const meta = this.providerMeta(providerId);
386 this.applyProviderToSlot("chat_model", providerId, meta, { forceApiBase: origin === "local" });
387 if (this.isOAuthProvider(providerId)) {
388 await this.loadOauthStatus({ silent: true });
389 }
390 this.step = "setup";
391 if (this.isOAuthProvider(providerId)) {
392 if (this.oauthConnected(providerId)) {
393 await this.loadModels("chat_model", { openDropdown: false });
394 }
395 } else if (meta.model_list_autoload !== false) {
396 await this.loadModels("chat_model", { openDropdown: false });
397 }
398 },
399
400 async selectOAuthProvider(providerId) {
401 await this.selectProvider(providerId, "account");
402 },
403
404 applyProviderToSlot(slotKey, providerId, meta, options = {}) {
405 ensureSlot(this.config, slotKey);
406 const slot = this.config[slotKey];
407 const previousProvider = slot.provider;
408 slot.provider = providerId;
409 const defaultApiBase = meta.default_api_base || meta.kwargs?.api_base || "";
410 if (defaultApiBase && (options.forceApiBase || !slot.api_base)) {
411 slot.api_base = defaultApiBase;
412 }
413
414 const defaultModel = slotKey === "utility_model"
415 ? meta.default_utility_model || meta.default_chat_model || ""
416 : meta.default_chat_model || "";
417 if (defaultModel && (!slot.name || !this.userTouchedModel[slotKey])) {
418 slot.name = defaultModel;
419 } else if (previousProvider && previousProvider !== providerId && !this.userTouchedModel[slotKey]) {
420 slot.name = "";
421 }
422
423 if (!slot.kwargs || typeof slot.kwargs !== "object") slot.kwargs = {};
424 },
425
426 localGuidance() {
427 return "";
428 },
429
430 showApiBaseField() {
431 return this.selectedProviderOrigin === "local" || this.selectedProviderId === "other";
432 },
433
434 setupPurpose() {
435 if (this.isOAuthProvider()) return "Connect this account, then choose the account-backed model Agent Zero should use.";
436 if (this.selectedProviderOrigin === "local") return "Choose a local model and confirm where Agent Zero can reach it.";
437 return "Choose a model and add the key Agent Zero will use for this provider.";
438 },
439
440 selectedProviderDocsUrl() {
441 const provider = this.selectedProvider();
442 return provider.docs_url || provider.api_key_url || provider.setup_url || "";
443 },
444
445 openSelectedProviderDocs() {
446 const url = this.selectedProviderDocsUrl();
447 if (url) window.open(url, "_blank", "noopener,noreferrer");
448 },
449
450 providerNeedsKey(providerId) {
451 return this.providerMeta(providerId).api_key_mode === "required";
452 },
453
454 providerKeyOptional(providerId) {
455 return this.providerMeta(providerId).api_key_mode === "optional";
456 },
457
458 providerHasNoKey(providerId) {
459 const mode = this.providerMeta(providerId).api_key_mode;
460 return mode === "none" || mode === "oauth";
461 },
462
463 hasProviderKey(providerId) {
464 if (!providerId) return false;
465 if (this.providerHasNoKey(providerId)) return true;
466 const draft = modelConfigStore.apiKeyValues?.[providerId] || "";
467 return Boolean(draft.trim() || modelConfigStore.apiKeyStatus?.[providerId]);
468 },
469
470 isOAuthProvider(providerId = "") {
471 const key = String(providerId || this.selectedProviderId || this.config?.chat_model?.provider || "").trim();
472 if (!key) return false;
473 return Boolean(this.oauthProviderStatus(key) || this.providerMeta(key).api_key_mode === "oauth");
474 },
475
476 oauthConnected(providerId = "") {
477 const key = String(providerId || this.selectedProviderId || this.config?.chat_model?.provider || "").trim();
478 return Boolean(this.oauthProviderStatus(key)?.connected);
479 },
480
481 oauthEmail(providerId = "") {
482 const status = this.oauthProviderStatus(providerId || this.selectedProviderId) || {};
483 return status.account_label || status.email || status.account_email || status.account_id || "";
484 },
485
486 oauthStatusLabel(providerId = "") {
487 if (this.oauthLoading) return "Checking";
488 const status = this.oauthProviderStatus(providerId || this.selectedProviderId);
489 if (!status) return "Not connected";
490 if (status.connected) return this.oauthEmail(status.provider_id) || "Connected";
491 if (!this.oauthSetupReady(status.provider_id)) return "Needs OAuth client details";
492 return "Not connected";
493 },
494
495 oauthSetupReady(providerId = "") {
496 const status = this.oauthProviderStatus(providerId || this.selectedProviderId);
497 if (!status?.supports_oauth_client_config) return true;
498 const ui = this.oauthProviderUiFor(status.provider_id);
499 const clientId = ui.client_id || status.client_id || "";
500 const hasSecret = Boolean(ui.client_secret || status.client_secret_configured);
501 return Boolean(String(clientId).trim() && hasSecret);
502 },
503
504 async loadOauthStatus({ silent = false } = {}) {
505 if (this.oauthLoading) return;
506 this.oauthLoading = true;
507 try {
508 this.oauthStatus = await callJsonApi(OAUTH_STATUS_API, {});
509 for (const provider of this.oauthProviderCards()) {
510 const ui = this.oauthProviderUiFor(provider.provider_id);
511 if (provider.enterprise_domain && !ui.enterprise_domain) ui.enterprise_domain = provider.enterprise_domain;
512 if (provider.client_id && !ui.client_id) ui.client_id = provider.client_id;
513 if (provider.quota_project_id && !ui.quota_project_id) ui.quota_project_id = provider.quota_project_id;
514 }
515 } catch (error) {
516 if (!silent) globalThis.justToast?.("Could not check account connection", "error");
517 } finally {
518 this.oauthLoading = false;
519 }
520 },
521
522 async connectOAuth(providerId = "") {
523 const selectedProvider = String(providerId || this.selectedProviderId || "").trim();
524 if (!selectedProvider || this.oauthConnecting) return;
525 this.oauthConnecting = true;
526 this.oauthConnectingProvider = selectedProvider;
527 const popup = window.open("about:blank", "_blank");
528 if (popup) popup.opener = null;
529 try {
530 const payload = this.oauthLoginPayload(selectedProvider);
531 const response = await callJsonApi(OAUTH_START_API, payload);
532 if (!response?.ok) {
533 throw new Error(response?.error || "Could not start account connection.");
534 }
535 this.oauthDevice = response;
536 if (response.flow === "device_code" && response.verification_url && response.attempt_id) {
537 if (popup && !popup.closed) {
538 popup.location.assign(response.verification_url);
539 } else {
540 window.open(response.verification_url, "_blank", "noopener,noreferrer");
541 }
542 this.startOauthPolling(selectedProvider);
543 return;
544 }
545 if (response.flow === "browser_pkce" && response.auth_url) {
546 if (popup && !popup.closed) {
547 popup.location.assign(response.auth_url);
548 } else {
549 window.open(response.auth_url, "_blank", "noopener,noreferrer");
550 }
551 this.startOauthCallbackPolling(selectedProvider);
552 return;
553 }
554 throw new Error(response?.error || "Could not start account connection.");
555 } catch (error) {
556 if (popup && !popup.closed) popup.close();
557 this.oauthConnecting = false;
558 this.oauthConnectingProvider = "";
559 globalThis.justToast?.(error?.message || "Could not connect account", "error");
560 }
561 },
562
563 oauthLoginPayload(providerId) {
564 const status = this.oauthProviderStatus(providerId) || {};
565 const ui = this.oauthProviderUiFor(providerId);
566 const payload = { provider_id: providerId };
567 if (status.supports_enterprise_domain) {
568 payload.enterprise_domain = ui.enterprise_domain || "";
569 }
570 if (status.supports_oauth_client_config) {
571 payload.client_id = ui.client_id || "";
572 payload.client_secret = ui.client_secret || "";
573 }
574 if (status.supports_quota_project) {
575 payload.quota_project_id = ui.quota_project_id || "";
576 }
577 return payload;
578 },
579
580 startOauthPolling(providerId = "") {
581 this.stopOauthPolling();
582 this.oauthPollStartedAt = Date.now();
583 const tick = async () => {
584 if (!this.oauthDevice?.attempt_id) return;
585 try {
586 const response = await callJsonApi(OAUTH_POLL_API, {
587 provider_id: providerId,
588 attempt_id: this.oauthDevice.attempt_id,
589 });
590 if (!response?.ok) {
591 if (response?.expired) {
592 this.oauthDevice = null;
593 }
594 throw new Error(response?.error || "Could not finish account connection.");
595 }
596 if (response.completed) {
597 this.oauthConnecting = false;
598 this.oauthConnectingProvider = "";
599 this.oauthDevice = null;
600 this.stopOauthPolling();
601 await this.loadOauthStatus();
602 this.applyProviderToSlot("chat_model", providerId, this.providerMeta(providerId));
603 await this.loadOauthModels(providerId, "chat_model");
604 return;
605 }
606 } catch (error) {
607 this.oauthConnecting = false;
608 this.oauthConnectingProvider = "";
609 this.stopOauthPolling();
610 globalThis.justToast?.(error?.message || "Could not connect account", "error");
611 return;
612 }
613 if (Date.now() - this.oauthPollStartedAt > MAX_OAUTH_POLL_MS) {
614 this.oauthConnecting = false;
615 this.oauthConnectingProvider = "";
616 this.oauthDevice = null;
617 this.stopOauthPolling();
618 }
619 };
620 void tick();
621 const parsedInterval = Number(this.oauthDevice.interval);
622 const intervalSeconds = Number.isFinite(parsedInterval) ? parsedInterval : 5;
623 const delay = Math.max(1500, intervalSeconds * 1000);
624 this.oauthPollTimer = window.setInterval(tick, delay);
625 },
626
627 stopOauthPolling() {
628 if (this.oauthPollTimer) window.clearInterval(this.oauthPollTimer);
629 this.oauthPollTimer = null;
630 },
631
632 startOauthCallbackPolling(providerId = "") {
633 this.stopOauthCallbackPolling();
634 this.oauthPollStartedAt = Date.now();
635 const tick = async () => {
636 await this.loadOauthStatus({ silent: true });
637 if (this.oauthConnected(providerId)) {
638 this.oauthConnecting = false;
639 this.oauthConnectingProvider = "";
640 this.oauthDevice = null;
641 this.stopOauthCallbackPolling();
642 this.applyProviderToSlot("chat_model", providerId, this.providerMeta(providerId));
643 await this.loadOauthModels(providerId, "chat_model");
644 return;
645 }
646 if (Date.now() - this.oauthPollStartedAt > MAX_OAUTH_POLL_MS) {
647 this.oauthConnecting = false;
648 this.oauthConnectingProvider = "";
649 this.oauthDevice = null;
650 this.stopOauthCallbackPolling();
651 }
652 };
653 void tick();
654 this.oauthCallbackPollTimer = window.setInterval(tick, 2500);
655 },
656
657 stopOauthCallbackPolling() {
658 if (this.oauthCallbackPollTimer) window.clearInterval(this.oauthCallbackPollTimer);
659 this.oauthCallbackPollTimer = null;
660 },
661
662 async submitOAuthManualCallback(providerId = "") {
663 const selectedProvider = String(providerId || this.selectedProviderId || "").trim();
664 const callback = String(this.oauthProviderUiFor(selectedProvider).manualCallback || "").trim();
665 if (!selectedProvider || !callback) {
666 globalThis.justToast?.("Paste callback URL, query string, or code.", "error");
667 return;
668 }
669 this.oauthConnecting = true;
670 this.oauthConnectingProvider = selectedProvider;
671 try {
672 const response = await callJsonApi(OAUTH_MANUAL_CALLBACK_API, {
673 provider_id: selectedProvider,
674 callback,
675 });
676 if (!response?.ok) {
677 throw new Error(response?.error || "Could not finish account connection.");
678 }
679 this.oauthProviderUiFor(selectedProvider).manualCallback = "";
680 this.oauthDevice = null;
681 this.stopOauthCallbackPolling();
682 await this.loadOauthStatus();
683 this.applyProviderToSlot("chat_model", selectedProvider, this.providerMeta(selectedProvider));
684 await this.loadOauthModels(selectedProvider, "chat_model");
685 } catch (error) {
686 globalThis.justToast?.(error?.message || "Could not connect account", "error");
687 } finally {
688 this.oauthConnecting = false;
689 this.oauthConnectingProvider = "";
690 }
691 },
692
693 async loadOauthModels(providerId = "", slotKey = "chat_model", { openDropdown = true } = {}) {
694 const selectedProvider = String(providerId || this.config?.[slotKey]?.provider || this.selectedProviderId || "").trim();
695 if (!selectedProvider) return;
696 const dropdown = this.modelDropdown[slotKey];
697 dropdown.loading = true;
698 dropdown.error = "";
699 dropdown.source = "";
700 try {
701 const response = await callJsonApi(OAUTH_MODELS_API, { provider_id: selectedProvider });
702 if (!response?.ok) {
703 throw new Error(response?.error || "Could not load account models.");
704 }
705 const models = Array.isArray(response?.models) ? response.models : [];
706 this.oauthModels = { ...this.oauthModels, [selectedProvider]: models };
707 dropdown.models = models;
708 dropdown.source = "oauth";
709 dropdown.open = openDropdown && models.length > 0;
710 if (models.length && !this.userTouchedModel[slotKey]) {
711 const meta = this.providerMeta(selectedProvider);
712 const preferred = slotKey === "utility_model" ? meta.default_utility_model : meta.default_chat_model;
713 this.config[slotKey].name = preferred && models.includes(preferred) ? preferred : models[0];
714 }
715 } catch (error) {
716 this.oauthModels = { ...this.oauthModels, [selectedProvider]: [] };
717 dropdown.models = [];
718 dropdown.error = error?.message || "Could not load account models.";
719 dropdown.open = false;
720 } finally {
721 dropdown.loading = false;
722 }
723 },
724
725 cancelOauthConnect() {
726 this.oauthConnecting = false;
727 this.oauthConnectingProvider = "";
728 this.oauthDevice = null;
729 this.stopOauthPolling();
730 this.stopOauthCallbackPolling();
731 },
732
733 async loadModels(slotKey, { openDropdown = true } = {}) {
734 if (!this.config?.[slotKey]?.provider) return;
735 if (this.isOAuthProvider(this.config[slotKey].provider)) {
736 await this.loadOauthModels(this.config[slotKey].provider, slotKey, { openDropdown });
737 return;
738 }
739 const dropdown = this.modelDropdown[slotKey];
740 dropdown.loading = true;
741 dropdown.error = "";
742 dropdown.source = "";
743 try {
744 const slot = this.config[slotKey];
745 const response = await fetchApi(`${MODEL_CONFIG_API}/model_search`, {
746 method: "POST",
747 headers: { "Content-Type": "application/json" },
748 body: JSON.stringify({
749 provider: slot.provider,
750 model_type: slotKey === "embedding_model" ? "embedding" : "chat",
751 query: "",
752 api_base: slot.api_base || "",
753 }),
754 });
755 const data = await response.json().catch(() => ({}));
756 dropdown.models = Array.isArray(data.models) ? data.models : [];
757 dropdown.source = data.source || "";
758 dropdown.error = data.error || "";
759 dropdown.open = openDropdown && dropdown.models.length > 0;
760 this.selectDefaultModelIfSafe(slotKey);
761 } catch (error) {
762 dropdown.models = [];
763 dropdown.error = error?.message || "Could not load models.";
764 dropdown.open = false;
765 } finally {
766 dropdown.loading = false;
767 }
768 },
769
770 selectDefaultModelIfSafe(slotKey) {
771 const slot = this.config?.[slotKey];
772 if (!slot || this.userTouchedModel[slotKey]) return;
773 const models = this.modelDropdown[slotKey]?.models || [];
774 if (!models.length) return;
775 if (slot.name && models.includes(slot.name)) return;
776 const meta = this.providerMeta(slot.provider);
777 const preferred = slotKey === "utility_model" ? meta.default_utility_model : meta.default_chat_model;
778 if (preferred && models.includes(preferred)) {
779 slot.name = preferred;
780 }
781 },
782
783 filteredModels(slotKey) {
784 const slot = this.config?.[slotKey] || {};
785 const query = String(slot.name || "").trim().toLowerCase();
786 const models = this.modelDropdown[slotKey]?.models || [];
787 if (!query) return models.slice(0, 80);
788 return models.filter((name) => String(name).toLowerCase().includes(query)).slice(0, 80);
789 },
790
791 openModelDropdown(slotKey) {
792 this.modelDropdown[slotKey].open = true;
793 if (!this.modelDropdown[slotKey].models.length && !this.modelDropdown[slotKey].loading) {
794 void this.loadModels(slotKey);
795 }
796 },
797
798 closeModelDropdown(slotKey) {
799 this.modelDropdown[slotKey].open = false;
800 },
801
802 selectModel(slotKey, modelName) {
803 this.config[slotKey].name = modelName;
804 this.userTouchedModel[slotKey] = true;
805 this.modelDropdown[slotKey].open = false;
806 },
807
808 markModelTouched(slotKey) {
809 this.userTouchedModel[slotKey] = true;
810 },
811
812 prepareUtilityDefaults() {
813 const mainProvider = this.config.chat_model.provider;
814 this.applyProviderToSlot("utility_model", mainProvider, this.providerMeta(mainProvider));
815 if (!this.config.utility_model.api_base) {
816 this.config.utility_model.api_base = this.config.chat_model.api_base || "";
817 }
818 },
819
820 async utilityProviderChanged() {
821 const providerId = this.config.utility_model.provider;
822 this.userTouchedModel.utility_model = false;
823 this.config.utility_model.api_base = "";
824 this.applyProviderToSlot("utility_model", providerId, this.providerMeta(providerId));
825 if (!this.config.utility_model.api_base && providerId === this.config.chat_model.provider) {
826 this.config.utility_model.api_base = this.config.chat_model.api_base || "";
827 }
828 await this.loadModels("utility_model");
829 },
830
831 async completeSetup() {
832 this.saving = true;
833 try {
834 await modelConfigStore.persistApiKeysForConfig(this.config);
835 const response = await fetchApi(`${MODEL_CONFIG_API}/model_config_set`, {
836 method: "POST",
837 headers: { "Content-Type": "application/json" },
838 body: JSON.stringify({
839 project_name: "",
840 agent_profile: "",
841 config: this.config,
842 }),
843 });
844 const data = await response.json().catch(() => ({}));
845 if (!data?.ok) throw new Error(data?.error || "Could not save model setup.");
846 await modelConfigStore.refreshApiKeyStatus();
847 this.step = "ready";
848 document.dispatchEvent(new CustomEvent("onboarding-configured"));
849 } catch (error) {
850 globalThis.justToast?.(error?.message || "Could not save setup", "error");
851 } finally {
852 this.saving = false;
853 }
854 },
855
856 async startChatting() {
857 window.closeModal?.();
858 await chatsStore.newChat();
859 },
860
861 async openAdvancedSettings() {
862 await window.closeModal?.();
863 await modelConfigStore.openPresetEditor(
864 this.config?.model_preset || "Default"
865 );
866 },
867 });