Add site OpenAI key and model picker

This commit is contained in:
rajchales-monito
2026-07-29 21:17:12 +02:00
parent 46d18c69eb
commit 227fcc2084
4 changed files with 157 additions and 13 deletions
+40 -5
View File
@@ -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) {