| 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> |