Add site OpenAI key and model picker
This commit is contained in:
@@ -225,6 +225,24 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
.effect-settings input[type="password"],
|
||||
.effect-settings select {
|
||||
width: 100%;
|
||||
}
|
||||
.effect-settings small {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.model-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.model-row button {
|
||||
height: 38px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.list-tools {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
|
||||
+10
-1
@@ -177,7 +177,16 @@
|
||||
<option value="de">Nemcina</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>OpenAI model <input id="translationModel" placeholder="gpt-5.6-luna"></label>
|
||||
<label>OpenAI API key
|
||||
<input id="openaiApiKey" type="password" autocomplete="off" placeholder="sk-...">
|
||||
<small id="openaiApiKeyStatus">Klic neni ulozeny.</small>
|
||||
</label>
|
||||
<label>OpenAI model
|
||||
<span class="model-row">
|
||||
<select id="translationModel"></select>
|
||||
<button id="refreshAiModels" type="button">Obnovit</button>
|
||||
</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+40
-5
@@ -18,7 +18,8 @@ const state = {
|
||||
soundRepeatTimer: null,
|
||||
attentionOn: false,
|
||||
originalTitle: document.title,
|
||||
originalFavicon: document.querySelector("link[rel='icon']")?.href || "/favicon.svg"
|
||||
originalFavicon: document.querySelector("link[rel='icon']")?.href || "/favicon.svg",
|
||||
aiModels: []
|
||||
};
|
||||
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
@@ -82,6 +83,8 @@ $("#adminSoundTest").addEventListener("click", () => {
|
||||
$("#translationEnabled").addEventListener("change", saveTranslationSettings);
|
||||
$("#operatorLanguage").addEventListener("change", saveTranslationSettings);
|
||||
$("#translationModel").addEventListener("change", saveTranslationSettings);
|
||||
$("#openaiApiKey").addEventListener("change", saveTranslationSettings);
|
||||
$("#refreshAiModels").addEventListener("click", loadAiModels);
|
||||
document.querySelectorAll("[data-settings-tab]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const tab = button.dataset.settingsTab;
|
||||
@@ -284,6 +287,7 @@ async function showApp() {
|
||||
$("#loginView").classList.add("hidden");
|
||||
$("#appView").classList.remove("hidden");
|
||||
await loadBootstrap();
|
||||
await loadAiModels().catch(() => {});
|
||||
await Promise.all([loadConversations(), loadVisitors()]);
|
||||
connectEvents();
|
||||
}
|
||||
@@ -390,7 +394,11 @@ function renderSite() {
|
||||
form.messageWiggle.checked = s.notifications?.wiggleOnAdminReply !== false;
|
||||
$("#translationEnabled").checked = Boolean(s.translations?.enabled);
|
||||
$("#operatorLanguage").value = s.translations?.operatorLanguage || "cs";
|
||||
$("#translationModel").value = s.translations?.model || "gpt-5.6-luna";
|
||||
$("#openaiApiKey").value = "";
|
||||
$("#openaiApiKeyStatus").textContent = s.translations?.hasOpenaiApiKey
|
||||
? `Klic ulozeny (${s.translations.openaiApiKeyMasked || "sk-..."})`
|
||||
: "Klic neni ulozeny.";
|
||||
renderModelOptions(s.translations?.model || "gpt-5-mini");
|
||||
}
|
||||
|
||||
async function saveTranslationSettings() {
|
||||
@@ -400,9 +408,31 @@ async function saveTranslationSettings() {
|
||||
...(settings.translations || {}),
|
||||
enabled: $("#translationEnabled").checked,
|
||||
operatorLanguage: $("#operatorLanguage").value,
|
||||
model: $("#translationModel").value.trim() || "gpt-5.6-luna"
|
||||
model: $("#translationModel").value.trim() || "gpt-5-mini"
|
||||
};
|
||||
const apiKey = $("#openaiApiKey").value.trim();
|
||||
if (apiKey) settings.translations.openaiApiKey = apiKey;
|
||||
await saveSite({ settings, isOnline: state.site.isOnline });
|
||||
$("#openaiApiKey").value = "";
|
||||
await loadAiModels().catch(() => {});
|
||||
}
|
||||
|
||||
async function loadAiModels() {
|
||||
const current = $("#translationModel").value || state.site?.settings?.translations?.model || "gpt-5-mini";
|
||||
const data = await api("/api/admin/ai/models").catch((error) => error?.models ? error : null);
|
||||
state.aiModels = data?.models || fallbackAiModels();
|
||||
renderModelOptions(current);
|
||||
}
|
||||
|
||||
function renderModelOptions(current) {
|
||||
const select = $("#translationModel");
|
||||
const models = [...new Set([current, ...state.aiModels, ...fallbackAiModels()].filter(Boolean))];
|
||||
select.innerHTML = models.map((model) => `<option value="${escapeHtml(model)}">${escapeHtml(model)}</option>`).join("");
|
||||
select.value = models.includes(current) ? current : models[0];
|
||||
}
|
||||
|
||||
function fallbackAiModels() {
|
||||
return ["gpt-5-mini", "gpt-5-nano", "gpt-5.1", "gpt-4.1-mini"];
|
||||
}
|
||||
|
||||
function renderConversations() {
|
||||
@@ -1019,8 +1049,13 @@ async function api(url, options = {}) {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: options.body ? JSON.stringify(options.body) : undefined
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json();
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
const error = new Error(`HTTP ${response.status}`);
|
||||
Object.assign(error, data);
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function debounce(fn, delay) {
|
||||
|
||||
Reference in New Issue
Block a user