| 1 | <html> |
| 2 | <head> |
| 3 | <title>Edit Model Presets</title> |
| 4 | <script type="module"> |
| 5 | import { store } from "/plugins/_model_config/webui/model-config-store.js"; |
| 6 | </script> |
| 7 | </head> |
| 8 | |
| 9 | <body> |
| 10 | <div x-data |
| 11 | x-init=" |
| 12 | await $store.modelConfig.ensureLoaded(); |
| 13 | $store.modelConfig.resetApiKeyDrafts(); |
| 14 | await $store.modelConfig.refreshApiKeyStatus(); |
| 15 | await $store.modelConfig.loadGlobalPresets(); |
| 16 | await $store.modelConfig.preparePresetEditor($store.chats?.selected || ''); |
| 17 | "> |
| 18 | <template x-if="$store.modelConfig._loaded && $store.modelConfig._presetsLoaded"> |
| 19 | <div class="preset-editor" |
| 20 | x-data="$store.modelConfig.createPresetEditor($store.modelConfig.presetEditorInitialName)"> |
| 21 | <div class="preset-editor-intro"> |
| 22 | <div class="field-title">Model Presets</div> |
| 23 | <div class="field-description"> |
| 24 | Each preset contains the main, utility, embedding, and optional vision-sidecar setup. Default is always available. |
| 25 | </div> |
| 26 | </div> |
| 27 | |
| 28 | <div class="preset-editor-toolbar"> |
| 29 | <label class="preset-editor-select"> |
| 30 | <span class="field-title">Preset</span> |
| 31 | <select x-model.number="selectedKey" x-init="$nextTick(() => $el.focus())"> |
| 32 | <template x-for="preset in presets" :key="preset._key"> |
| 33 | <option :value="preset._key" x-text="preset.name"></option> |
| 34 | </template> |
| 35 | </select> |
| 36 | </label> |
| 37 | <button type="button" class="button" @click="addPreset()"> |
| 38 | <x-icon class="icon" name="add"></x-icon> |
| 39 | Add |
| 40 | </button> |
| 41 | <button type="button" class="button" |
| 42 | x-show="canDeleteSelected" |
| 43 | @click="$confirmClick($event, () => removeSelectedPreset())"> |
| 44 | <x-icon class="icon" name="delete"></x-icon> |
| 45 | Delete |
| 46 | </button> |
| 47 | </div> |
| 48 | |
| 49 | <template x-if="selectedPreset"> |
| 50 | <div class="preset-editor-body"> |
| 51 | <template x-if="canRenameSelected"> |
| 52 | <div class="field preset-name-field"> |
| 53 | <div class="field-label"> |
| 54 | <div class="field-title">Preset name</div> |
| 55 | <div class="field-description">Shown in settings and the chat model switcher.</div> |
| 56 | </div> |
| 57 | <div class="field-control"> |
| 58 | <input type="text" |
| 59 | x-model="selectedPreset.name" |
| 60 | placeholder="Preset name"> |
| 61 | </div> |
| 62 | </div> |
| 63 | </template> |
| 64 | |
| 65 | <section class="preset-model-section"> |
| 66 | <div class="preset-model-heading"> |
| 67 | <div class="section-title">Main Model</div> |
| 68 | <div class="section-description">Primary model for conversations, reasoning, and tools.</div> |
| 69 | </div> |
| 70 | <div x-data="{ get model() { return selectedPreset.chat; }, get visionModel() { return selectedPreset.vision; }, modelType: 'chat', providers: $store.modelConfig.chatProviders, searchType: 'chat', apiKeyMode: 'store' }"> |
| 71 | <x-component path="/plugins/_model_config/webui/model-field.html"></x-component> |
| 72 | </div> |
| 73 | <template x-if="!selectedPreset.chat.vision || selectedPreset.vision.override_main"> |
| 74 | <div class="vision-sidecar-selector"> |
| 75 | <div class="field-title">Vision sidecar</div> |
| 76 | <div class="field-description" |
| 77 | x-text="selectedPreset.chat.vision |
| 78 | ? 'Overrides Main model vision for vision_load.' |
| 79 | : 'Used by vision_load while Main model vision is disabled. Leave empty to disable image analysis.'"></div> |
| 80 | <div x-data="{ get model() { return selectedPreset.vision; }, modelType: 'vision', providers: $store.modelConfig.chatProviders, searchType: 'chat', apiKeyMode: 'store' }"> |
| 81 | <x-component path="/plugins/_model_config/webui/model-field.html"></x-component> |
| 82 | </div> |
| 83 | </div> |
| 84 | </template> |
| 85 | </section> |
| 86 | |
| 87 | <section class="preset-model-section"> |
| 88 | <div class="preset-model-heading"> |
| 89 | <div class="section-title">Utility Model</div> |
| 90 | <div class="section-description">Faster helper model for summaries and background work.</div> |
| 91 | </div> |
| 92 | <div x-data="{ get model() { return selectedPreset.utility; }, modelType: 'utility', providers: $store.modelConfig.chatProviders, searchType: 'chat', apiKeyMode: 'store', get providerFallback() { return selectedPreset.chat.provider; }, get apiBaseFallback() { return selectedPreset.chat.api_base; } }"> |
| 93 | <x-component path="/plugins/_model_config/webui/model-field.html"></x-component> |
| 94 | </div> |
| 95 | </section> |
| 96 | |
| 97 | <section class="preset-model-section"> |
| 98 | <div class="preset-model-heading"> |
| 99 | <div class="section-title">Embedding Model</div> |
| 100 | <div class="section-description">Creates vectors for memory and knowledge retrieval.</div> |
| 101 | </div> |
| 102 | <div x-data="{ get model() { return selectedPreset.embedding; }, modelType: 'embedding', providers: $store.modelConfig.embeddingProviders, searchType: 'embedding', apiKeyMode: 'store' }"> |
| 103 | <x-component path="/plugins/_model_config/webui/model-field.html"></x-component> |
| 104 | </div> |
| 105 | </section> |
| 106 | </div> |
| 107 | </template> |
| 108 | |
| 109 | <div class="preset-editor-secondary-actions"> |
| 110 | <button type="button" class="text-button" @click="$store.modelConfig.openApiKeysFromSummary()"> |
| 111 | <x-icon name="key"></x-icon> |
| 112 | API Keys |
| 113 | </button> |
| 114 | <button type="button" class="text-button" |
| 115 | @click="$confirmClick($event, () => resetPresets())"> |
| 116 | <x-icon name="restart_alt"></x-icon> |
| 117 | Restore bundled presets |
| 118 | </button> |
| 119 | </div> |
| 120 | |
| 121 | <div class="modal-footer" data-modal-footer> |
| 122 | <button type="button" class="btn btn-ok" |
| 123 | @click="if (await savePresets()) window.closeModal?.()">Save</button> |
| 124 | <button type="button" class="btn btn-cancel" |
| 125 | @click="window.closeModal?.()">Close</button> |
| 126 | </div> |
| 127 | </div> |
| 128 | </template> |
| 129 | </div> |
| 130 | |
| 131 | <style> |
| 132 | .preset-editor, |
| 133 | .preset-editor-body { |
| 134 | display: flex; |
| 135 | flex-direction: column; |
| 136 | gap: 1rem; |
| 137 | } |
| 138 | |
| 139 | .preset-editor-intro .field-title { |
| 140 | font-size: 1rem; |
| 141 | } |
| 142 | |
| 143 | .preset-editor-toolbar { |
| 144 | display: flex; |
| 145 | align-items: flex-end; |
| 146 | gap: 0.5rem; |
| 147 | padding: 0.8rem; |
| 148 | border: 1px solid var(--color-border); |
| 149 | border-radius: 8px; |
| 150 | background: var(--color-input); |
| 151 | } |
| 152 | |
| 153 | .preset-editor-select { |
| 154 | display: flex; |
| 155 | flex: 1 1 auto; |
| 156 | flex-direction: column; |
| 157 | gap: 0.35rem; |
| 158 | min-width: 0; |
| 159 | margin: 0; |
| 160 | } |
| 161 | |
| 162 | .preset-editor-select select { |
| 163 | width: 100%; |
| 164 | } |
| 165 | |
| 166 | .preset-name-field, |
| 167 | .preset-model-section { |
| 168 | padding: 1rem; |
| 169 | border: 1px solid var(--color-border); |
| 170 | border-radius: 8px; |
| 171 | } |
| 172 | |
| 173 | .preset-model-heading { |
| 174 | margin-bottom: 0.75rem; |
| 175 | } |
| 176 | |
| 177 | .preset-model-heading .section-title { |
| 178 | margin-top: 0; |
| 179 | } |
| 180 | |
| 181 | .vision-sidecar-selector { |
| 182 | margin: 0.75rem 0 0; |
| 183 | padding: 0.25rem 0 0; |
| 184 | } |
| 185 | |
| 186 | .preset-editor-secondary-actions { |
| 187 | display: flex; |
| 188 | flex-wrap: wrap; |
| 189 | gap: 1rem; |
| 190 | } |
| 191 | |
| 192 | @media (max-width: 640px) { |
| 193 | .preset-editor-toolbar { |
| 194 | align-items: stretch; |
| 195 | flex-wrap: wrap; |
| 196 | } |
| 197 | |
| 198 | .preset-editor-select { |
| 199 | flex-basis: 100%; |
| 200 | } |
| 201 | |
| 202 | .preset-editor-toolbar .button { |
| 203 | flex: 1 1 auto; |
| 204 | justify-content: center; |
| 205 | } |
| 206 | } |
| 207 | </style> |
| 208 | </body> |
| 209 | </html> |