main
html 453 lines 16.9 KB
Raw
1 <!-- Model Switcher - Floating Preset Selector -->
2 <script type="module">
3 import { store } from "/plugins/_model_config/webui/model-config-store.js";
4 </script>
5
6 <div x-data>
7 <template x-if="$store.modelConfig">
8 <div x-data="{ showDropdown: false, showProfileDropdown: false }"
9 x-init="
10 await Promise.all([
11 $store.modelConfig.refreshSwitcher($store.chats?.selected || ''),
12 $store.modelConfig.loadAgentProfiles(),
13 ]);
14 $watch('$store.chats.selected', v => $store.modelConfig.refreshSwitcher(v || ''));
15 ">
16 <template x-if="($store.modelConfig.switcherAllowed && !$store.modelConfig.switcherLoading) || $store.chats?.selectedContext?.agent_profile">
17 <div class="model-switcher-container">
18 <div class="model-context-strip">
19 <template x-if="$store.modelConfig.switcherAllowed && !$store.modelConfig.switcherLoading">
20 <div class="model-switcher-anchor" @click.outside="showDropdown = false">
21 <button class="btn-icon-action model-switcher-btn"
22 :class="{ 'has-override': !!$store.modelConfig.switcherOverride }"
23 @click="showDropdown = !showDropdown; showProfileDropdown = false">
24 <x-icon style="font-size: 16px;" name="neurology"></x-icon>
25 <span class="model-switcher-label" x-text="$store.modelConfig.getSwitcherLabel()"></span>
26 <x-icon style="font-size: 0.7rem;"
27 :name="showDropdown ? 'expand_less' : 'expand_more'"></x-icon>
28 </button>
29 <!-- Dropdown (opens upward) -->
30 <div class="model-switcher-dropdown" x-show="showDropdown" x-transition.opacity style="display: none;">
31 <!-- Return to the project/profile selection -->
32 <template x-if="$store.modelConfig.switcherOverride">
33 <div class="model-switcher-item revert" @click="
34 $store.modelConfig.clearOverrideSwitch($store.chats?.selected || '');
35 showDropdown = false;
36 ">
37 <x-icon style="font-size: 15px;" name="undo"></x-icon>
38 <span>Use scoped preset</span>
39 </div>
40 </template>
41 <div class="model-switcher-divider" x-show="$store.modelConfig.switcherOverride"></div>
42
43 <!-- No presets message -->
44 <template x-if="$store.modelConfig.switcherPresets.length === 0">
45 <div class="model-switcher-item disabled">No presets configured</div>
46 </template>
47
48 <!-- Preset list -->
49 <template x-for="preset in $store.modelConfig.switcherPresets" :key="preset.name">
50 <div class="model-switcher-item"
51 :class="{ 'active': $store.modelConfig.switcherEffectivePreset === preset.name }"
52 @click="
53 $store.modelConfig.selectPresetSwitch($store.chats?.selected || '', preset.name);
54 showDropdown = false;
55 ">
56 <div class="model-switcher-preset-name" x-text="preset.name"></div>
57 <div class="model-switcher-preset-models">
58 <template x-if="preset.chat?.name">
59 <div class="model-switcher-model-row">
60 <span class="model-switcher-model-label">Main</span>
61 <span class="model-switcher-model-value">
62 <span x-text="preset.chat.provider" style="opacity:0.5;"></span>
63 <span style="opacity:0.3; margin:0 3px;">/</span>
64 <span x-text="preset.chat.name"></span>
65 </span>
66 </div>
67 </template>
68 <template x-if="preset.vision?.name && (!preset.chat?.vision || preset.vision?.override_main)">
69 <div class="model-switcher-model-row">
70 <span class="model-switcher-model-label">Vision</span>
71 <span class="model-switcher-model-value">
72 <span x-text="preset.vision.provider" style="opacity:0.5;"></span>
73 <span style="opacity:0.3; margin:0 3px;">/</span>
74 <span x-text="preset.vision.name"></span>
75 </span>
76 </div>
77 </template>
78 <template x-if="preset.utility?.name">
79 <div class="model-switcher-model-row">
80 <span class="model-switcher-model-label">Utility</span>
81 <span class="model-switcher-model-value">
82 <span x-text="preset.utility.provider" style="opacity:0.5;"></span>
83 <span style="opacity:0.3; margin:0 3px;">/</span>
84 <span x-text="preset.utility.name"></span>
85 </span>
86 </div>
87 </template>
88 <template x-if="preset.embedding?.name">
89 <div class="model-switcher-model-row">
90 <span class="model-switcher-model-label">Embed</span>
91 <span class="model-switcher-model-value">
92 <span x-text="preset.embedding.provider" style="opacity:0.5;"></span>
93 <span style="opacity:0.3; margin:0 3px;">/</span>
94 <span x-text="preset.embedding.name"></span>
95 </span>
96 </div>
97 </template>
98 </div>
99 </div>
100 </template>
101
102 <!-- Edit Presets shortcut -->
103 <div class="model-switcher-divider" style="opacity:0.2;"></div>
104 <div class="model-switcher-item model-switcher-edit" @click="
105 $store.modelConfig.openPresetEditor(
106 $store.modelConfig.switcherEffectivePreset,
107 $store.chats?.selected || ''
108 );
109 showDropdown = false;
110 ">
111 <x-icon style="font-size: 14px;" name="tune"></x-icon>
112 <span>Edit Presets</span>
113 </div>
114 </div>
115 </div>
116 </template>
117
118 <template x-if="$store.chats?.selectedContext?.agent_profile">
119 <div class="agent-profile-anchor" @click.outside="showProfileDropdown = false">
120 <button class="btn-icon-action agent-profile-btn"
121 title="Active agent profile"
122 @click="showProfileDropdown = !showProfileDropdown; showDropdown = false">
123 <span class="agent-profile-avatar"
124 :style="`background:${$store.modelConfig.getAgentProfileVisual($store.chats.selectedContext.agent_profile, $store.chats.selectedContext.agent_profile_label).color}`">
125 <img x-show="$store.modelConfig.getAgentProfileVisual($store.chats.selectedContext.agent_profile, $store.chats.selectedContext.agent_profile_label).url"
126 :src="$store.modelConfig.getAgentProfileVisual($store.chats.selectedContext.agent_profile, $store.chats.selectedContext.agent_profile_label).url"
127 alt="">
128 <span x-show="!$store.modelConfig.getAgentProfileVisual($store.chats.selectedContext.agent_profile, $store.chats.selectedContext.agent_profile_label).url"
129 x-text="$store.modelConfig.getAgentProfileVisual($store.chats.selectedContext.agent_profile, $store.chats.selectedContext.agent_profile_label).initials"></span>
130 </span>
131 <span class="agent-profile-label"
132 x-text="$store.chats.selectedContext.agent_profile_label || $store.chats.selectedContext.agent_profile"></span>
133 <x-icon style="font-size: 0.7rem;"
134 :name="showProfileDropdown ? 'expand_less' : 'expand_more'"></x-icon>
135 </button>
136
137 <div class="agent-profile-dropdown" x-show="showProfileDropdown" x-transition.opacity style="display: none;">
138 <template x-if="$store.modelConfig.agentProfilesLoading">
139 <div class="model-switcher-item disabled">Loading profiles...</div>
140 </template>
141
142 <template x-for="profile in $store.modelConfig.getAgentProfileList($store.chats.selectedContext.agent_profile, $store.chats.selectedContext.agent_profile_label)" :key="profile.key">
143 <div class="agent-profile-row"
144 x-show="!$store.modelConfig.agentProfilesLoading"
145 :class="{ 'active': profile.key === $store.chats.selectedContext.agent_profile }">
146 <button type="button" class="model-switcher-item agent-profile-item"
147 :disabled="$store.chats.selectedContext.running || $store.modelConfig.agentProfileSaving"
148 :class="{ 'disabled': $store.chats.selectedContext.running || $store.modelConfig.agentProfileSaving }"
149 @click="
150 if (profile.key === $store.chats.selectedContext.agent_profile) {
151 showProfileDropdown = false;
152 } else {
153 $store.modelConfig.selectAgentProfile($store.chats?.selected || '', profile.key)
154 .then(ok => { if (ok) showProfileDropdown = false; });
155 }
156 ">
157 <span class="agent-profile-avatar" :style="`background:${$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).color}`">
158 <img x-show="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" :src="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" alt="">
159 <span x-show="!$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).url" x-text="$store.modelConfig.getAgentProfileVisual(profile.key, profile.label).initials"></span>
160 </span>
161 <div class="model-switcher-preset-name" x-text="profile.label || profile.key"></div>
162 </button>
163 <button type="button" class="agent-profile-row-edit"
164 :aria-label="`Edit ${profile.label || profile.key}`"
165 @click="
166 showProfileDropdown = false;
167 window.openAgentEditor?.({
168 view: 'edit',
169 profileId: profile.key,
170 contextId: $store.chats?.selected || ''
171 });
172 ">
173 <x-icon name="edit"></x-icon>
174 <span>Edit</span>
175 </button>
176 </div>
177 </template>
178
179 <div class="model-switcher-divider" style="opacity:0.2;"></div>
180 <button class="model-switcher-item agent-profile-settings" @click="
181 showProfileDropdown = false;
182 window.openAgentEditor?.({ view: 'manage', contextId: $store.chats?.selected || '' });
183 ">
184 <x-icon style="font-size: 14px;" name="manage_accounts"></x-icon>
185 <span>Manage agents</span>
186 </button>
187 </div>
188 </div>
189 </template>
190
191 <x-extension id="model-context-strip-end"></x-extension>
192 </div>
193 </div>
194 </template>
195 </div>
196 </template>
197 </div>
198
199 <style>
200 .model-switcher-container {
201 position: relative;
202 z-index: 10;
203 }
204 .model-context-strip {
205 display: flex;
206 align-items: center;
207 gap: 8px;
208 min-width: 0;
209 flex-wrap: wrap;
210 }
211 .model-context-strip > x-extension,
212 .model-context-strip > x-extension > x-component,
213 .model-context-strip > x-extension > x-component > div[x-data] {
214 display: contents;
215 }
216 .agent-profile-avatar {
217 width: 1.35rem;
218 height: 1.35rem;
219 display: inline-grid;
220 place-items: center;
221 flex: 0 0 auto;
222 overflow: hidden;
223 border-radius: 50%;
224 color: #fff;
225 font-size: .58rem;
226 font-weight: 700;
227 }
228 .agent-profile-avatar img {
229 width: 100%;
230 height: 100%;
231 object-fit: cover;
232 }
233 .model-switcher-anchor {
234 position: relative;
235 display: flex;
236 align-items: center;
237 gap: 6px;
238 }
239 .model-switcher-btn {
240 width: auto;
241 height: auto;
242 gap: 4px;
243 padding: var(--spacing-xs) var(--spacing-sm);
244 border: none;
245 font-size: 0.75rem;
246 white-space: nowrap;
247 flex-shrink: 0;
248 }
249 .model-switcher-btn:hover {
250 border: none;
251 }
252 .model-switcher-btn.has-override {
253 /* color: var(--color-text); */
254 }
255 .model-switcher-label {
256 max-width: min(240px, 38vw);
257 overflow: hidden;
258 text-overflow: ellipsis;
259 }
260 .agent-profile-anchor {
261 position: relative;
262 display: flex;
263 align-items: center;
264 }
265 .agent-profile-btn {
266 width: auto;
267 height: auto;
268 gap: 4px;
269 padding: var(--spacing-xs) var(--spacing-sm);
270 border: none;
271 font-size: 0.75rem;
272 white-space: nowrap;
273 flex-shrink: 0;
274 }
275 .agent-profile-btn:hover {
276 border: none;
277 }
278 .agent-profile-label {
279 max-width: 140px;
280 overflow: hidden;
281 text-overflow: ellipsis;
282 }
283 /* Dropdown - opens upward, aligned to left */
284 .model-switcher-dropdown {
285 position: absolute;
286 bottom: calc(100% + 6px);
287 left: 0;
288 min-width: 280px;
289 max-height: 550px;
290 overflow-y: auto;
291 background-color: var(--color-panel);
292 border: 1px solid var(--color-border);
293 border-radius: 8px;
294 padding: 6px;
295 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
296 z-index: 100;
297 }
298 .agent-profile-dropdown {
299 position: absolute;
300 bottom: calc(100% + 6px);
301 left: 0;
302 min-width: 260px;
303 max-width: min(320px, 90vw);
304 max-height: 550px;
305 overflow-y: auto;
306 background-color: var(--color-panel);
307 border: 1px solid var(--color-border);
308 border-radius: 8px;
309 padding: 6px;
310 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
311 z-index: 100;
312 }
313 .model-switcher-item {
314 padding: 6px 10px;
315 border-radius: 6px;
316 cursor: pointer;
317 font-size: 0.8rem;
318 transition: background 0.1s ease;
319 }
320 .model-switcher-item:hover {
321 background: var(--color-background-hover, rgba(255,255,255,0.06));
322 }
323 .model-switcher-item.active {
324 background: color-mix(in srgb, var(--color-highlight) 12%, transparent);
325 }
326 .agent-profile-row {
327 display: grid;
328 grid-template-columns: minmax(0, 1fr) auto;
329 align-items: center;
330 border-radius: 6px;
331 transition: background 0.1s ease;
332 }
333 .agent-profile-row:hover {
334 background: var(--color-background-hover, rgba(255,255,255,0.06));
335 }
336 .agent-profile-row.active {
337 background: color-mix(in srgb, var(--color-highlight) 12%, transparent);
338 }
339 .agent-profile-item {
340 display: flex;
341 align-items: center;
342 gap: 7px;
343 width: 100%;
344 min-width: 0;
345 border: 0;
346 background: transparent;
347 color: var(--color-text);
348 text-align: left;
349 }
350 .agent-profile-row .agent-profile-item:hover {
351 background: transparent;
352 }
353 .agent-profile-row-edit {
354 display: flex;
355 align-items: center;
356 gap: 4px;
357 margin-right: 4px;
358 min-height: 24px;
359 padding: 4px 6px;
360 border: 0;
361 border-radius: 4px;
362 background: transparent;
363 color: var(--color-text);
364 font: inherit;
365 font-size: 0.72rem;
366 cursor: pointer;
367 opacity: 0.72;
368 white-space: nowrap;
369 }
370 .agent-profile-row-edit:hover {
371 background: color-mix(in srgb, var(--color-text) 8%, transparent);
372 opacity: 1;
373 }
374 .agent-profile-row-edit x-icon {
375 font-size: 0.8rem;
376 }
377 .model-switcher-item.revert {
378 display: flex;
379 align-items: center;
380 gap: 6px;
381 opacity: 0.8;
382 }
383 .model-switcher-item.disabled {
384 opacity: 0.4;
385 cursor: default;
386 font-style: italic;
387 }
388 .model-switcher-item.disabled:hover {
389 background: transparent;
390 }
391 .model-switcher-divider {
392 border-top: 1px solid var(--color-border);
393 margin: 4px 0;
394 opacity: 0.3;
395 }
396 .model-switcher-preset-name {
397 font-weight: 500;
398 }
399 .model-switcher-preset-models {
400 display: flex;
401 flex-direction: column;
402 gap: 2px;
403 margin-top: 4px;
404 }
405 .model-switcher-model-row {
406 display: flex;
407 align-items: center;
408 gap: 6px;
409 font-size: 0.7rem;
410 opacity: 0.7;
411 }
412 .model-switcher-model-label {
413 width: 40px;
414 flex-shrink: 0;
415 font-weight: 500;
416 opacity: 0.7;
417 }
418 .model-switcher-model-value {
419 flex: 1;
420 min-width: 0;
421 overflow: hidden;
422 text-overflow: ellipsis;
423 white-space: nowrap;
424 }
425 .model-switcher-edit {
426 display: flex;
427 align-items: center;
428 gap: 6px;
429 opacity: 0.6;
430 font-size: 0.75rem;
431 }
432 .agent-profile-settings {
433 display: flex;
434 align-items: center;
435 gap: 6px;
436 width: 100%;
437 border: none;
438 background: transparent;
439 color: var(--color-text);
440 opacity: 0.7;
441 text-align: left;
442 font-family: inherit;
443 }
444
445 @media (max-width: 600px) {
446 .model-switcher-label {
447 max-width: 160px;
448 }
449 .agent-profile-label {
450 max-width: 92px;
451 }
452 }
453 </style>