main
js 194 lines 5.91 KB
Raw
1 import { fetchApi } from "/js/api.js";
2
3 const API_BASE = "/plugins/_model_config";
4 const API_KEY_PLACEHOLDER = "************";
5
6 export const apiKeysState = {
7 apiKeyStatus: {},
8 apiKeyValues: {},
9 apiKeyDirty: {},
10 allProviders: [],
11 };
12
13 export const apiKeysMethods = {
14 _setProviderHasKey(provider, hasKey) {
15 if (!provider) return;
16 this.apiKeyStatus = { ...this.apiKeyStatus, [provider]: !!hasKey };
17 const normalized = provider.toLowerCase();
18 this.allProviders = (this.allProviders || []).map((item) =>
19 item.value?.toLowerCase() === normalized ? { ...item, has_key: !!hasKey } : item
20 );
21 },
22
23 _syncApiKeysToSettingsStore(savedKeys) {
24 const settingsApiKeys = globalThis.Alpine?.store('settings')?.settings?.api_keys;
25 if (!settingsApiKeys) return;
26 for (const [provider, value] of Object.entries(savedKeys)) {
27 settingsApiKeys[provider] = value.trim() ? API_KEY_PLACEHOLDER : '';
28 }
29 },
30
31 _ensureApiKeySlot(provider) {
32 if (!provider) return;
33 if (!(provider in this.apiKeyValues)) {
34 this.apiKeyValues = { ...this.apiKeyValues, [provider]: '' };
35 }
36 if (!(provider in this.apiKeyDirty)) {
37 this.apiKeyDirty = { ...this.apiKeyDirty, [provider]: false };
38 }
39 },
40
41 _setApiKeyDirty(provider, isDirty) {
42 if (!provider) return;
43 this._ensureApiKeySlot(provider);
44 this.apiKeyDirty = { ...this.apiKeyDirty, [provider]: !!isDirty };
45 },
46
47 touchApiKey(provider) {
48 this._setApiKeyDirty(provider, true);
49 },
50
51 setApiKeyValue(provider, value) {
52 if (!provider) return;
53 this._ensureApiKeySlot(provider);
54 this.apiKeyValues = { ...this.apiKeyValues, [provider]: value };
55 this._setApiKeyDirty(provider, true);
56 },
57
58 async refreshApiKeyStatus() {
59 await this.ensureLoaded();
60 const res = await fetchApi(`${API_BASE}/api_keys`, {
61 method: 'POST',
62 headers: { 'Content-Type': 'application/json' },
63 body: JSON.stringify({ action: 'get' })
64 });
65 const data = await res.json();
66 const keys = data.keys || {};
67
68 const nextStatus = { ...this.apiKeyStatus };
69 const nextValues = { ...this.apiKeyValues };
70 const nextDirty = { ...this.apiKeyDirty };
71
72 for (const provider of this.allProviders) {
73 const entry = keys[provider.value] || {};
74 const hasKey = !!entry.has_key;
75 nextStatus[provider.value] = hasKey;
76 provider.has_key = hasKey;
77 if (!(provider.value in nextDirty)) {
78 nextDirty[provider.value] = false;
79 }
80 if (!hasKey && !nextDirty[provider.value]) {
81 nextValues[provider.value] = '';
82 }
83 }
84
85 this.apiKeyStatus = nextStatus;
86 this.apiKeyValues = nextValues;
87 this.apiKeyDirty = nextDirty;
88 this.allProviders = [...this.allProviders];
89 return keys;
90 },
91
92 resetApiKeyDrafts() {
93 const nextValues = {};
94 const nextDirty = {};
95 for (const provider of this.allProviders || []) {
96 if (!provider?.value) continue;
97 nextValues[provider.value] = '';
98 nextDirty[provider.value] = false;
99 }
100 this.apiKeyValues = nextValues;
101 this.apiKeyDirty = nextDirty;
102 },
103
104 async saveApiKeys(updates) {
105 const normalized = {};
106 for (const [provider, value] of Object.entries(updates || {})) {
107 if (!provider || typeof value !== 'string') continue;
108 normalized[provider] = value.trim() ? value : '';
109 }
110
111 if (Object.keys(normalized).length === 0) {
112 return { ok: true };
113 }
114
115 const res = await fetchApi(`${API_BASE}/api_keys`, {
116 method: 'POST',
117 headers: { 'Content-Type': 'application/json' },
118 body: JSON.stringify({ action: 'set', keys: normalized })
119 });
120 const data = await res.json();
121 if (!data?.ok) {
122 throw new Error(data?.error || 'Failed to save API keys.');
123 }
124
125 const nextValues = { ...this.apiKeyValues };
126 const nextDirty = { ...this.apiKeyDirty };
127 for (const [provider, value] of Object.entries(normalized)) {
128 nextValues[provider] = value;
129 nextDirty[provider] = false;
130 this._setProviderHasKey(provider, !!value.trim());
131 }
132 this.apiKeyValues = nextValues;
133 this.apiKeyDirty = nextDirty;
134
135 // Sync saved keys into the Settings store so Settings Save
136 // won't overwrite just-saved keys with stale empty values.
137 this._syncApiKeysToSettingsStore(normalized);
138
139 return data;
140 },
141
142 async saveApiKey(provider, value) {
143 return this.saveApiKeys({ [provider]: value });
144 },
145
146 saveApiKeyIfSet(provider) {
147 if (provider in this.apiKeyValues) {
148 return this.saveApiKey(provider, this.apiKeyValues[provider] || '');
149 }
150 },
151
152 async revealApiKey(provider) {
153 const res = await fetchApi(`${API_BASE}/api_keys`, {
154 method: 'POST',
155 headers: { 'Content-Type': 'application/json' },
156 body: JSON.stringify({ action: 'reveal', provider })
157 });
158 const data = await res.json();
159 if (!data?.ok) {
160 throw new Error(data?.error || 'Failed to load API key.');
161 }
162 const value = data.value || '';
163 if (provider) {
164 this._ensureApiKeySlot(provider);
165 this.apiKeyValues = { ...this.apiKeyValues, [provider]: value };
166 this._setApiKeyDirty(provider, false);
167 this._setProviderHasKey(provider, !!value.trim());
168 }
169 return value;
170 },
171
172 async persistApiKeysForConfig(config) {
173 const updates = {};
174 const seen = new Set();
175 for (const section of this.MODEL_SECTIONS) {
176 const provider = config?.[section.key]?.provider;
177 if (!provider || seen.has(provider) || !this.apiKeyDirty[provider]) continue;
178 seen.add(provider);
179 const value = this.apiKeyValues[provider];
180 updates[provider] = typeof value === 'string' ? value : '';
181 }
182 return this.saveApiKeys(updates);
183 },
184
185 async persistAllDirtyApiKeys() {
186 const updates = {};
187 for (const [provider, isDirty] of Object.entries(this.apiKeyDirty)) {
188 if (!isDirty) continue;
189 const value = this.apiKeyValues[provider];
190 updates[provider] = typeof value === 'string' ? value : '';
191 }
192 return this.saveApiKeys(updates);
193 },
194 };