main
js 295 lines 10.2 KB
Raw
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 };