| 1 | <html> |
| 2 | <head> |
| 3 | <title>Manage API Keys</title> |
| 4 | </head> |
| 5 | |
| 6 | <body> |
| 7 | <script type="module"> |
| 8 | import { store } from "/plugins/_model_config/webui/model-config-store.js"; |
| 9 | </script> |
| 10 | |
| 11 | <div x-data> |
| 12 | <template x-if="$store.modelConfig"> |
| 13 | <div x-data="{ |
| 14 | loading: true, |
| 15 | saving: false, |
| 16 | error: '', |
| 17 | get hasChanges() { |
| 18 | const dirty = $store.modelConfig.apiKeyDirty; |
| 19 | return Object.keys(dirty).some((p) => dirty[p]); |
| 20 | }, |
| 21 | async init() { |
| 22 | await $store.modelConfig.ensureLoaded(); |
| 23 | $store.modelConfig.resetApiKeyDrafts(); |
| 24 | await $store.modelConfig.refreshApiKeyStatus(); |
| 25 | this.loading = false; |
| 26 | }, |
| 27 | async reveal(provider) { |
| 28 | try { |
| 29 | await $store.modelConfig.revealApiKey(provider); |
| 30 | } catch (e) { |
| 31 | this.error = e?.message || 'Failed to reveal API key.'; |
| 32 | } |
| 33 | }, |
| 34 | async saveAndClose() { |
| 35 | this.saving = true; |
| 36 | this.error = ''; |
| 37 | try { |
| 38 | await $store.modelConfig.persistAllDirtyApiKeys(); |
| 39 | await $store.modelConfig.refreshApiKeyStatus(); |
| 40 | window.closeModal?.(); |
| 41 | } catch (e) { |
| 42 | this.error = e?.message || 'Failed to save API keys.'; |
| 43 | } finally { |
| 44 | this.saving = false; |
| 45 | } |
| 46 | } |
| 47 | }" |
| 48 | x-init="init()"> |
| 49 | |
| 50 | <div class="api-keys-section"> |
| 51 | <div class="section-title">API Keys</div> |
| 52 | <div class="section-description"> |
| 53 | API keys for model providers and services used by Agent Zero. You can set multiple API keys separated by a comma (,). They will be used in round-robin fashion.<br> |
| 54 | For more information about Agent Zero Venice provider, see <a href="http://agent-zero.ai/?community/api-dashboard/about" target="_blank">Agent Zero Venice</a>. |
| 55 | </div> |
| 56 | |
| 57 | <div x-show="error" class="plugin-settings-error" style="margin-top: 1rem;"> |
| 58 | <x-icon name="error"></x-icon> |
| 59 | <span x-text="error"></span> |
| 60 | </div> |
| 61 | |
| 62 | <div x-show="loading" style="text-align:center; padding: 20px;"> |
| 63 | <x-icon class="spinning" name="progress_activity"></x-icon> |
| 64 | </div> |
| 65 | |
| 66 | <div x-show="!loading"> |
| 67 | <template x-for="provider in $store.modelConfig.allProviders" :key="provider.value"> |
| 68 | <div class="field"> |
| 69 | <div class="field-label"> |
| 70 | <div class="field-title" x-text="provider.label"></div> |
| 71 | </div> |
| 72 | <div class="field-control" style="position:relative;" x-data="{ showKey: false }"> |
| 73 | <input type="text" |
| 74 | :class="{ 'masked-text-input': !showKey }" |
| 75 | :value="$store.modelConfig.apiKeyValues[provider.value]" |
| 76 | :placeholder="provider.has_key ? '••••••••••••' : ''" |
| 77 | autocomplete="off" |
| 78 | @input="$store.modelConfig.setApiKeyValue(provider.value, $el.value)" |
| 79 | style="padding-right:32px;" /> |
| 80 | <x-icon class="eye-toggle" |
| 81 | @click=" |
| 82 | showKey = !showKey; |
| 83 | if (showKey && !$store.modelConfig.apiKeyValues[provider.value] && provider.has_key) { |
| 84 | reveal(provider.value); |
| 85 | } |
| 86 | " |
| 87 | :name="showKey ? 'visibility' : 'visibility_off'"></x-icon> |
| 88 | </div> |
| 89 | </div> |
| 90 | </template> |
| 91 | </div> |
| 92 | </div> |
| 93 | |
| 94 | <div class="modal-footer" data-modal-footer> |
| 95 | <button class="btn btn-ok" |
| 96 | @click="saveAndClose()" |
| 97 | :disabled="loading || saving || !hasChanges"> |
| 98 | Save |
| 99 | </button> |
| 100 | <button class="btn btn-cancel" |
| 101 | @click="window.closeModal?.()" |
| 102 | :disabled="saving"> |
| 103 | Close |
| 104 | </button> |
| 105 | </div> |
| 106 | </div> |
| 107 | </template> |
| 108 | </div> |
| 109 | |
| 110 | <style> |
| 111 | .api-keys-section { |
| 112 | border: 1px solid var(--color-border); |
| 113 | border-radius: 8px; |
| 114 | padding: 16px; |
| 115 | } |
| 116 | .api-keys-section .section-title { |
| 117 | margin-top: 0; |
| 118 | } |
| 119 | .eye-toggle { |
| 120 | position: absolute; |
| 121 | right: 8px; |
| 122 | top: 50%; |
| 123 | transform: translateY(-50%); |
| 124 | font-size: 18px; |
| 125 | cursor: pointer; |
| 126 | user-select: none; |
| 127 | opacity: 0.6; |
| 128 | z-index: 1; |
| 129 | } |
| 130 | .eye-toggle:hover { |
| 131 | opacity: 1; |
| 132 | } |
| 133 | </style> |
| 134 | </body> |
| 135 | </html> |