agent-zero/plugins/_model_config/webui/api-keys-mixin.js
Alessandro 539d809789 feat: add agent profile switcher to chat composer
Surface the active Agent Profile beside the model preset switcher and let users switch profiles through the existing settings flow.

- add agent profile metadata to state snapshots
- list available profiles in the chat composer profile dropdown
- persist profile changes via settings_get/settings_set
- add a Create new Agent Profile action that opens a guided a0-create-agent chat
- rename the agent-profile creation skill/docs from a0-new-agent to a0-create-agent
- clean up fetchApi imports for related WebUI modules
2026-04-22 14:25:18 +02:00

194 lines
5.9 KiB
JavaScript

import { fetchApi } from "/js/api.js";
const API_BASE = "/plugins/_model_config";
const API_KEY_PLACEHOLDER = "************";
export const apiKeysState = {
apiKeyStatus: {},
apiKeyValues: {},
apiKeyDirty: {},
allProviders: [],
};
export const apiKeysMethods = {
_setProviderHasKey(provider, hasKey) {
if (!provider) return;
this.apiKeyStatus = { ...this.apiKeyStatus, [provider]: !!hasKey };
const normalized = provider.toLowerCase();
this.allProviders = (this.allProviders || []).map((item) =>
item.value?.toLowerCase() === normalized ? { ...item, has_key: !!hasKey } : item
);
},
_syncApiKeysToSettingsStore(savedKeys) {
const settingsApiKeys = globalThis.Alpine?.store('settings')?.settings?.api_keys;
if (!settingsApiKeys) return;
for (const [provider, value] of Object.entries(savedKeys)) {
settingsApiKeys[provider] = value.trim() ? API_KEY_PLACEHOLDER : '';
}
},
_ensureApiKeySlot(provider) {
if (!provider) return;
if (!(provider in this.apiKeyValues)) {
this.apiKeyValues = { ...this.apiKeyValues, [provider]: '' };
}
if (!(provider in this.apiKeyDirty)) {
this.apiKeyDirty = { ...this.apiKeyDirty, [provider]: false };
}
},
_setApiKeyDirty(provider, isDirty) {
if (!provider) return;
this._ensureApiKeySlot(provider);
this.apiKeyDirty = { ...this.apiKeyDirty, [provider]: !!isDirty };
},
touchApiKey(provider) {
this._setApiKeyDirty(provider, true);
},
setApiKeyValue(provider, value) {
if (!provider) return;
this._ensureApiKeySlot(provider);
this.apiKeyValues = { ...this.apiKeyValues, [provider]: value };
this._setApiKeyDirty(provider, true);
},
async refreshApiKeyStatus() {
await this.ensureLoaded();
const res = await fetchApi(`${API_BASE}/api_keys`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'get' })
});
const data = await res.json();
const keys = data.keys || {};
const nextStatus = { ...this.apiKeyStatus };
const nextValues = { ...this.apiKeyValues };
const nextDirty = { ...this.apiKeyDirty };
for (const provider of this.allProviders) {
const entry = keys[provider.value] || {};
const hasKey = !!entry.has_key;
nextStatus[provider.value] = hasKey;
provider.has_key = hasKey;
if (!(provider.value in nextDirty)) {
nextDirty[provider.value] = false;
}
if (!hasKey && !nextDirty[provider.value]) {
nextValues[provider.value] = '';
}
}
this.apiKeyStatus = nextStatus;
this.apiKeyValues = nextValues;
this.apiKeyDirty = nextDirty;
this.allProviders = [...this.allProviders];
return keys;
},
resetApiKeyDrafts() {
const nextValues = {};
const nextDirty = {};
for (const provider of this.allProviders || []) {
if (!provider?.value) continue;
nextValues[provider.value] = '';
nextDirty[provider.value] = false;
}
this.apiKeyValues = nextValues;
this.apiKeyDirty = nextDirty;
},
async saveApiKeys(updates) {
const normalized = {};
for (const [provider, value] of Object.entries(updates || {})) {
if (!provider || typeof value !== 'string') continue;
normalized[provider] = value.trim() ? value : '';
}
if (Object.keys(normalized).length === 0) {
return { ok: true };
}
const res = await fetchApi(`${API_BASE}/api_keys`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'set', keys: normalized })
});
const data = await res.json();
if (!data?.ok) {
throw new Error(data?.error || 'Failed to save API keys.');
}
const nextValues = { ...this.apiKeyValues };
const nextDirty = { ...this.apiKeyDirty };
for (const [provider, value] of Object.entries(normalized)) {
nextValues[provider] = value;
nextDirty[provider] = false;
this._setProviderHasKey(provider, !!value.trim());
}
this.apiKeyValues = nextValues;
this.apiKeyDirty = nextDirty;
// Sync saved keys into the Settings store so Settings Save
// won't overwrite just-saved keys with stale empty values.
this._syncApiKeysToSettingsStore(normalized);
return data;
},
async saveApiKey(provider, value) {
return this.saveApiKeys({ [provider]: value });
},
saveApiKeyIfSet(provider) {
if (provider in this.apiKeyValues) {
return this.saveApiKey(provider, this.apiKeyValues[provider] || '');
}
},
async revealApiKey(provider) {
const res = await fetchApi(`${API_BASE}/api_keys`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'reveal', provider })
});
const data = await res.json();
if (!data?.ok) {
throw new Error(data?.error || 'Failed to load API key.');
}
const value = data.value || '';
if (provider) {
this._ensureApiKeySlot(provider);
this.apiKeyValues = { ...this.apiKeyValues, [provider]: value };
this._setApiKeyDirty(provider, false);
this._setProviderHasKey(provider, !!value.trim());
}
return value;
},
async persistApiKeysForConfig(config) {
const updates = {};
const seen = new Set();
for (const section of this.MODEL_SECTIONS) {
const provider = config?.[section.key]?.provider;
if (!provider || seen.has(provider) || !this.apiKeyDirty[provider]) continue;
seen.add(provider);
const value = this.apiKeyValues[provider];
updates[provider] = typeof value === 'string' ? value : '';
}
return this.saveApiKeys(updates);
},
async persistAllDirtyApiKeys() {
const updates = {};
for (const [provider, isDirty] of Object.entries(this.apiKeyDirty)) {
if (!isDirty) continue;
const value = this.apiKeyValues[provider];
updates[provider] = typeof value === 'string' ? value : '';
}
return this.saveApiKeys(updates);
},
};