Skip to content

Commit d19e534

Browse files
Merge pull request #47 from keyboardstaff/model-config
refactor(_model_config): extract reusable model-field component, spli…
2 parents db4de96 + faf08cd commit d19e534

7 files changed

Lines changed: 655 additions & 935 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
const API_BASE = "/plugins/_model_config";
2+
3+
export const apiKeysState = {
4+
apiKeyStatus: {},
5+
apiKeyValues: {},
6+
apiKeyDirty: {},
7+
allProviders: [],
8+
};
9+
10+
export const apiKeysMethods = {
11+
_setProviderHasKey(provider, hasKey) {
12+
if (!provider) return;
13+
this.apiKeyStatus = { ...this.apiKeyStatus, [provider]: !!hasKey };
14+
const normalized = provider.toLowerCase();
15+
this.allProviders = (this.allProviders || []).map((item) =>
16+
item.value?.toLowerCase() === normalized ? { ...item, has_key: !!hasKey } : item
17+
);
18+
},
19+
20+
_ensureApiKeySlot(provider) {
21+
if (!provider) return;
22+
if (!(provider in this.apiKeyValues)) {
23+
this.apiKeyValues = { ...this.apiKeyValues, [provider]: '' };
24+
}
25+
if (!(provider in this.apiKeyDirty)) {
26+
this.apiKeyDirty = { ...this.apiKeyDirty, [provider]: false };
27+
}
28+
},
29+
30+
_setApiKeyDirty(provider, isDirty) {
31+
if (!provider) return;
32+
this._ensureApiKeySlot(provider);
33+
this.apiKeyDirty = { ...this.apiKeyDirty, [provider]: !!isDirty };
34+
},
35+
36+
touchApiKey(provider) {
37+
this._setApiKeyDirty(provider, true);
38+
},
39+
40+
async refreshApiKeyStatus() {
41+
await this.ensureLoaded();
42+
const res = await fetchApi(`${API_BASE}/api_keys`, {
43+
method: 'POST',
44+
headers: { 'Content-Type': 'application/json' },
45+
body: JSON.stringify({ action: 'get' })
46+
});
47+
const data = await res.json();
48+
const keys = data.keys || {};
49+
50+
const nextStatus = { ...this.apiKeyStatus };
51+
const nextValues = { ...this.apiKeyValues };
52+
const nextDirty = { ...this.apiKeyDirty };
53+
54+
for (const provider of this.allProviders) {
55+
const entry = keys[provider.value] || {};
56+
const hasKey = !!entry.has_key;
57+
nextStatus[provider.value] = hasKey;
58+
provider.has_key = hasKey;
59+
if (!(provider.value in nextDirty)) {
60+
nextDirty[provider.value] = false;
61+
}
62+
if (!hasKey && !nextDirty[provider.value]) {
63+
nextValues[provider.value] = '';
64+
}
65+
}
66+
67+
this.apiKeyStatus = nextStatus;
68+
this.apiKeyValues = nextValues;
69+
this.apiKeyDirty = nextDirty;
70+
this.allProviders = [...this.allProviders];
71+
return keys;
72+
},
73+
74+
resetApiKeyDrafts() {
75+
const nextValues = {};
76+
const nextDirty = {};
77+
for (const provider of this.allProviders || []) {
78+
if (!provider?.value) continue;
79+
nextValues[provider.value] = '';
80+
nextDirty[provider.value] = false;
81+
}
82+
this.apiKeyValues = nextValues;
83+
this.apiKeyDirty = nextDirty;
84+
},
85+
86+
async saveApiKeys(updates) {
87+
const normalized = {};
88+
for (const [provider, value] of Object.entries(updates || {})) {
89+
if (!provider || typeof value !== 'string') continue;
90+
normalized[provider] = value.trim() ? value : '';
91+
}
92+
93+
if (Object.keys(normalized).length === 0) {
94+
return { ok: true };
95+
}
96+
97+
const res = await fetchApi(`${API_BASE}/api_keys`, {
98+
method: 'POST',
99+
headers: { 'Content-Type': 'application/json' },
100+
body: JSON.stringify({ action: 'set', keys: normalized })
101+
});
102+
const data = await res.json();
103+
if (!data?.ok) {
104+
throw new Error(data?.error || 'Failed to save API keys.');
105+
}
106+
107+
const nextValues = { ...this.apiKeyValues };
108+
const nextDirty = { ...this.apiKeyDirty };
109+
for (const [provider, value] of Object.entries(normalized)) {
110+
nextValues[provider] = value;
111+
nextDirty[provider] = false;
112+
this._setProviderHasKey(provider, !!value.trim());
113+
}
114+
this.apiKeyValues = nextValues;
115+
this.apiKeyDirty = nextDirty;
116+
return data;
117+
},
118+
119+
async saveApiKey(provider, value) {
120+
return this.saveApiKeys({ [provider]: value });
121+
},
122+
123+
saveApiKeyIfSet(provider) {
124+
if (provider in this.apiKeyValues) {
125+
return this.saveApiKey(provider, this.apiKeyValues[provider] || '');
126+
}
127+
},
128+
129+
async revealApiKey(provider) {
130+
const res = await fetchApi(`${API_BASE}/api_keys`, {
131+
method: 'POST',
132+
headers: { 'Content-Type': 'application/json' },
133+
body: JSON.stringify({ action: 'reveal', provider })
134+
});
135+
const data = await res.json();
136+
if (!data?.ok) {
137+
throw new Error(data?.error || 'Failed to load API key.');
138+
}
139+
const value = data.value || '';
140+
if (provider) {
141+
this._ensureApiKeySlot(provider);
142+
this.apiKeyValues = { ...this.apiKeyValues, [provider]: value };
143+
this._setApiKeyDirty(provider, false);
144+
this._setProviderHasKey(provider, !!value.trim());
145+
}
146+
return value;
147+
},
148+
149+
async persistApiKeysForConfig(config) {
150+
const updates = {};
151+
const seen = new Set();
152+
for (const section of this.MODEL_SECTIONS) {
153+
const provider = config?.[section.key]?.provider;
154+
if (!provider || seen.has(provider) || !this.apiKeyDirty[provider]) continue;
155+
seen.add(provider);
156+
const value = this.apiKeyValues[provider];
157+
updates[provider] = typeof value === 'string' ? value : '';
158+
}
159+
return this.saveApiKeys(updates);
160+
},
161+
};

plugins/_model_config/webui/api-keys.html

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,22 @@
1111
<div x-data>
1212
<template x-if="$store.modelConfig">
1313
<div x-data="{
14-
keys: {},
15-
originalKeys: {},
16-
touched: {},
1714
loading: true,
1815
saving: false,
1916
error: '',
2017
get hasChanges() {
21-
return Object.keys(this.touched).some((provider) => this.touched[provider]);
18+
const dirty = $store.modelConfig.apiKeyDirty;
19+
return Object.keys(dirty).some((p) => dirty[p]);
2220
},
2321
async init() {
2422
await $store.modelConfig.ensureLoaded();
2523
$store.modelConfig.resetApiKeyDrafts();
2624
await $store.modelConfig.refreshApiKeyStatus();
27-
$store.modelConfig.allProviders.forEach((provider) => {
28-
this.keys[provider.value] = '';
29-
this.originalKeys[provider.value] = '';
30-
this.touched[provider.value] = false;
31-
});
3225
this.loading = false;
3326
},
34-
markChanged(provider) {
35-
this.touched[provider] = this.keys[provider] !== this.originalKeys[provider];
36-
},
3727
async reveal(provider) {
3828
try {
39-
const value = await $store.modelConfig.revealApiKey(provider);
40-
this.keys[provider] = value || '';
41-
this.originalKeys[provider] = value || '';
42-
this.touched[provider] = false;
29+
await $store.modelConfig.revealApiKey(provider);
4330
} catch (e) {
4431
this.error = e?.message || 'Failed to reveal API key.';
4532
}
@@ -49,9 +36,11 @@
4936
this.error = '';
5037
try {
5138
const updates = {};
52-
for (const provider of Object.keys(this.touched)) {
53-
if (!this.touched[provider]) continue;
54-
updates[provider] = this.keys[provider] || '';
39+
const dirty = $store.modelConfig.apiKeyDirty;
40+
const values = $store.modelConfig.apiKeyValues;
41+
for (const provider of Object.keys(dirty)) {
42+
if (!dirty[provider]) continue;
43+
updates[provider] = values[provider] || '';
5544
}
5645
await $store.modelConfig.saveApiKeys(updates);
5746
await $store.modelConfig.refreshApiKeyStatus();
@@ -89,15 +78,15 @@
8978
</div>
9079
<div class="field-control" style="position:relative;" x-data="{ showKey: false }">
9180
<input :type="showKey ? 'text' : 'password'"
92-
x-model="keys[provider.value]"
81+
x-model="$store.modelConfig.apiKeyValues[provider.value]"
9382
:placeholder="provider.has_key ? '••••••••••••' : ''"
9483
autocomplete="off"
95-
@input="markChanged(provider.value)"
84+
@input="$store.modelConfig.touchApiKey(provider.value)"
9685
style="padding-right:32px;" />
9786
<span class="material-symbols-outlined eye-toggle"
9887
@click="
9988
showKey = !showKey;
100-
if (showKey && !keys[provider.value] && provider.has_key) {
89+
if (showKey && !$store.modelConfig.apiKeyValues[provider.value] && provider.has_key) {
10190
reveal(provider.value);
10291
}
10392
"

0 commit comments

Comments
 (0)