| 1 | import { callJsonApi, fetchApi } from "/js/api.js"; |
| 2 | |
| 3 | const API_BASE = "/plugins/_model_config"; |
| 4 | const BUILT_IN_AGENT_COLORS = { |
| 5 | agent0: "#8E44AD", |
| 6 | default: "#D35400", |
| 7 | developer: "#202124", |
| 8 | hacker: "#C0392B", |
| 9 | researcher: "#6C5CE7", |
| 10 | "tiny-local": "#E67E22", |
| 11 | }; |
| 12 | function normalizeModelIdentity(value) { |
| 13 | if (!value || typeof value !== "object") return null; |
| 14 | const provider = String(value.provider || "").trim(); |
| 15 | const name = String(value.name || "").trim(); |
| 16 | if (!provider && !name) return null; |
| 17 | return { provider, name }; |
| 18 | } |
| 19 | |
| 20 | export function getModelLeafName(value) { |
| 21 | const name = String(typeof value === "string" ? value : value?.name || "").trim(); |
| 22 | if (!name) return ""; |
| 23 | const leaf = name.slice(name.lastIndexOf("/") + 1).trim(); |
| 24 | return leaf || name; |
| 25 | } |
| 26 | |
| 27 | export const switcherState = { |
| 28 | switcherAllowed: false, |
| 29 | switcherOverride: null, |
| 30 | switcherConfiguredPreset: "Default", |
| 31 | switcherEffectivePreset: "Default", |
| 32 | switcherPresets: [], |
| 33 | switcherLoading: true, |
| 34 | agentProfiles: [], |
| 35 | agentProfilesLoading: true, |
| 36 | agentProfilesLoaded: false, |
| 37 | agentProfilesLoadSeq: 0, |
| 38 | agentProfileSaving: false, |
| 39 | }; |
| 40 | |
| 41 | let agentProfilesRequest = null; |
| 42 | let agentProfilesRequestContext = ""; |
| 43 | |
| 44 | export const switcherMethods = { |
| 45 | async loadAgentProfiles(force = false) { |
| 46 | const contextId = window.Alpine?.store("chats")?.selected || ""; |
| 47 | if (agentProfilesRequest && agentProfilesRequestContext === contextId) { |
| 48 | return agentProfilesRequest; |
| 49 | } |
| 50 | if (!force && this.agentProfilesLoaded) return this.agentProfiles; |
| 51 | const requestSeq = ++this.agentProfilesLoadSeq; |
| 52 | this.agentProfilesLoading = true; |
| 53 | const request = (async () => { |
| 54 | try { |
| 55 | const data = await callJsonApi("/plugins/_agent_editor/agent_editor", { |
| 56 | action: "list", |
| 57 | context_id: contextId, |
| 58 | }); |
| 59 | if (requestSeq !== this.agentProfilesLoadSeq) return this.agentProfiles; |
| 60 | this.agentProfiles = (data.profiles || []) |
| 61 | .filter(profile => profile.id && !["_example", "default"].includes(profile.id) && profile.enabled !== false) |
| 62 | .map(profile => ({ |
| 63 | key: profile.id, |
| 64 | label: profile.title || profile.id, |
| 65 | avatar: profile.avatar || null, |
| 66 | avatarUrl: profile.avatar_url || "", |
| 67 | })); |
| 68 | this.agentProfilesLoaded = true; |
| 69 | } catch (e) { |
| 70 | if (requestSeq !== this.agentProfilesLoadSeq) return this.agentProfiles; |
| 71 | console.error("Agent profile list load failed:", e); |
| 72 | this.agentProfiles = []; |
| 73 | this.agentProfilesLoaded = false; |
| 74 | } finally { |
| 75 | if (requestSeq === this.agentProfilesLoadSeq) { |
| 76 | this.agentProfilesLoading = false; |
| 77 | } |
| 78 | } |
| 79 | return this.agentProfiles; |
| 80 | })(); |
| 81 | agentProfilesRequest = request; |
| 82 | agentProfilesRequestContext = contextId; |
| 83 | try { |
| 84 | return await request; |
| 85 | } finally { |
| 86 | if (agentProfilesRequest === request) agentProfilesRequest = null; |
| 87 | } |
| 88 | }, |
| 89 | |
| 90 | async loadSwitcherState(contextId) { |
| 91 | const result = { allowed: false, presets: [], override: null, configuredPreset: "Default", effectivePreset: "Default" }; |
| 92 | try { |
| 93 | await this.loadGlobalPresets(); |
| 94 | result.presets = this.globalPresets.filter(p => p.name); |
| 95 | if (contextId) { |
| 96 | const overRes = await fetchApi(`${API_BASE}/model_override`, { |
| 97 | method: "POST", |
| 98 | headers: { "Content-Type": "application/json" }, |
| 99 | body: JSON.stringify({ action: "get", context_id: contextId }), |
| 100 | }); |
| 101 | const overData = await overRes.json(); |
| 102 | result.allowed = !!overData.allowed; |
| 103 | result.override = overData.override || null; |
| 104 | result.configuredPreset = overData.configured_preset || "Default"; |
| 105 | result.effectivePreset = overData.effective_preset || result.configuredPreset; |
| 106 | } |
| 107 | } catch (e) { |
| 108 | console.error("Model switcher load failed:", e); |
| 109 | } |
| 110 | return result; |
| 111 | }, |
| 112 | |
| 113 | async setPresetOverride(contextId, presetName) { |
| 114 | try { |
| 115 | const res = await fetchApi(`${API_BASE}/model_override`, { |
| 116 | method: "POST", |
| 117 | headers: { "Content-Type": "application/json" }, |
| 118 | body: JSON.stringify({ action: "set_preset", context_id: contextId, preset_name: presetName }), |
| 119 | }); |
| 120 | const data = await res.json(); |
| 121 | return data?.ok ? data : null; |
| 122 | } catch (e) { |
| 123 | console.error("Failed to set preset override:", e); |
| 124 | return false; |
| 125 | } |
| 126 | }, |
| 127 | |
| 128 | async clearOverride(contextId) { |
| 129 | try { |
| 130 | const res = await fetchApi(`${API_BASE}/model_override`, { |
| 131 | method: "POST", |
| 132 | headers: { "Content-Type": "application/json" }, |
| 133 | body: JSON.stringify({ action: "clear", context_id: contextId }), |
| 134 | }); |
| 135 | const data = await res.json(); |
| 136 | return data?.ok ? data : null; |
| 137 | } catch (e) { |
| 138 | console.error("Failed to clear override:", e); |
| 139 | return false; |
| 140 | } |
| 141 | }, |
| 142 | |
| 143 | getAgentProfileList(activeKey = "", activeLabel = "") { |
| 144 | const profiles = [...(this.agentProfiles || [])]; |
| 145 | if (activeKey && activeKey !== "default" && !profiles.some(profile => profile.key === activeKey)) { |
| 146 | profiles.unshift({ key: activeKey, label: activeLabel || activeKey }); |
| 147 | } |
| 148 | return profiles; |
| 149 | }, |
| 150 | |
| 151 | getAgentProfileVisual(profileKey, profileLabel = "") { |
| 152 | const profile = this.agentProfiles.find(item => item.key === profileKey) || {}; |
| 153 | const label = profile.label || profileLabel || profileKey || "Agent"; |
| 154 | const palette = ["#6C5CE7", "#0984E3", "#00A884", "#D35400", "#C0392B", "#8E44AD"]; |
| 155 | let hash = 0; |
| 156 | for (const char of profileKey || label) hash = ((hash * 31) + char.charCodeAt(0)) >>> 0; |
| 157 | return { |
| 158 | url: profile.avatarUrl || "", |
| 159 | color: profile.avatar?.kind === "color" |
| 160 | ? profile.avatar.value |
| 161 | : BUILT_IN_AGENT_COLORS[profileKey] || palette[hash % palette.length], |
| 162 | initials: label.trim().split(/\s+/).slice(0, 2).map(word => word[0]).join("").toUpperCase() || "A", |
| 163 | }; |
| 164 | }, |
| 165 | |
| 166 | async selectAgentProfile(contextId, agentProfile) { |
| 167 | if (!contextId || !agentProfile) return false; |
| 168 | if (this.agentProfileSaving) return false; |
| 169 | const chatsStore = window.Alpine?.store("chats"); |
| 170 | const selectedContext = chatsStore?.selectedContext; |
| 171 | if (selectedContext?.running) { |
| 172 | window.justToast?.("Agent profile can be changed after the current run finishes.", "warning", 2500, "agent-profile-switch"); |
| 173 | return false; |
| 174 | } |
| 175 | |
| 176 | const activeProfile = selectedContext?.agent_profile || ""; |
| 177 | if (activeProfile === agentProfile) return true; |
| 178 | |
| 179 | this.agentProfileSaving = true; |
| 180 | try { |
| 181 | await this.loadAgentProfiles(); |
| 182 | const res = await fetchApi("/agent_profile_set", { |
| 183 | method: "POST", |
| 184 | headers: { "Content-Type": "application/json" }, |
| 185 | body: JSON.stringify({ context_id: contextId, agent_profile: agentProfile }), |
| 186 | }); |
| 187 | if (!res.ok) throw new Error(await res.text()); |
| 188 | const data = await res.json(); |
| 189 | if (!data.ok) return false; |
| 190 | |
| 191 | const label = data.agent_profile_label || this.agentProfiles.find(profile => profile.key === agentProfile)?.label || agentProfile; |
| 192 | if (selectedContext) { |
| 193 | selectedContext.agent_profile = data.agent_profile || agentProfile; |
| 194 | selectedContext.agent_profile_label = label; |
| 195 | } |
| 196 | await this.refreshSwitcher(contextId); |
| 197 | window.justToast?.(`Agent profile: ${label}`, "success", 1600, "agent-profile-switch"); |
| 198 | return true; |
| 199 | } catch (e) { |
| 200 | console.error("Failed to set active agent profile:", e); |
| 201 | window.toastFetchError?.("Failed to set agent profile", e); |
| 202 | return false; |
| 203 | } finally { |
| 204 | this.agentProfileSaving = false; |
| 205 | } |
| 206 | }, |
| 207 | |
| 208 | getPresetLabel(preset) { |
| 209 | return preset?.name || "Unnamed"; |
| 210 | }, |
| 211 | |
| 212 | getPresetSummary(preset) { |
| 213 | if (!preset) return ""; |
| 214 | const parts = []; |
| 215 | if (preset.chat?.name) parts.push(preset.chat.name); |
| 216 | if (preset.utility?.name) parts.push(preset.utility.name); |
| 217 | return parts.join(" / "); |
| 218 | }, |
| 219 | |
| 220 | async refreshSwitcher(contextId) { |
| 221 | this.switcherLoading = true; |
| 222 | try { |
| 223 | const state = await this.loadSwitcherState(contextId); |
| 224 | this.switcherAllowed = state.allowed; |
| 225 | this.switcherPresets = state.presets; |
| 226 | this.switcherOverride = state.override; |
| 227 | this.switcherConfiguredPreset = state.configuredPreset; |
| 228 | this.switcherEffectivePreset = state.effectivePreset; |
| 229 | } catch (e) { |
| 230 | console.error('Model switcher refresh failed:', e); |
| 231 | } finally { |
| 232 | this.switcherLoading = false; |
| 233 | } |
| 234 | }, |
| 235 | |
| 236 | async selectPresetSwitch(contextId, presetName) { |
| 237 | const data = await this.setPresetOverride(contextId, presetName); |
| 238 | if (data) { |
| 239 | const selected = data.preset_name || presetName; |
| 240 | this.switcherOverride = { preset_name: selected }; |
| 241 | this.switcherEffectivePreset = selected; |
| 242 | } |
| 243 | return !!data; |
| 244 | }, |
| 245 | |
| 246 | async clearOverrideSwitch(contextId) { |
| 247 | const data = await this.clearOverride(contextId); |
| 248 | if (data) { |
| 249 | this.switcherOverride = null; |
| 250 | this.switcherEffectivePreset = data.effective_preset || this.switcherConfiguredPreset || 'Default'; |
| 251 | } |
| 252 | return !!data; |
| 253 | }, |
| 254 | |
| 255 | getSwitcherLabel() { |
| 256 | const o = this.switcherOverride; |
| 257 | const presetName = this.switcherEffectivePreset || o?.preset_name || 'Default'; |
| 258 | const preset = this.getActivePreset(); |
| 259 | if (preset) { |
| 260 | const mainModelName = getModelLeafName(preset.chat); |
| 261 | return mainModelName ? `${presetName} ${mainModelName}` : presetName; |
| 262 | } |
| 263 | if (!o || o.preset_name) return presetName; |
| 264 | |
| 265 | const models = this.getCustomOverrideModels(); |
| 266 | const mainModelName = getModelLeafName(models.main); |
| 267 | return mainModelName ? `Custom ${mainModelName}` : 'Custom'; |
| 268 | }, |
| 269 | |
| 270 | getActivePreset() { |
| 271 | return this.switcherPresets.find(p => p.name === this.switcherEffectivePreset) || null; |
| 272 | }, |
| 273 | |
| 274 | getActiveModels() { |
| 275 | const preset = this.getActivePreset(); |
| 276 | if (preset) { |
| 277 | return { |
| 278 | main: normalizeModelIdentity(preset.chat), |
| 279 | utility: normalizeModelIdentity(preset.utility), |
| 280 | embedding: normalizeModelIdentity(preset.embedding), |
| 281 | }; |
| 282 | } |
| 283 | return this.getCustomOverrideModels(); |
| 284 | }, |
| 285 | |
| 286 | getCustomOverrideModels() { |
| 287 | const o = this.switcherOverride; |
| 288 | if (!o || o.preset_name) return { main: null, utility: null, embedding: null }; |
| 289 | return { |
| 290 | main: normalizeModelIdentity(o.chat || o), |
| 291 | utility: normalizeModelIdentity(o.utility), |
| 292 | embedding: normalizeModelIdentity(o.embedding), |
| 293 | }; |
| 294 | }, |
| 295 | }; |