Add OpenAI key save and test controls
This commit is contained in:
@@ -261,6 +261,19 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
height: 38px;
|
height: 38px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
.ai-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ai-actions button {
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.ai-actions button:last-child {
|
||||||
|
background: #edf3ef;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
.list-tools {
|
.list-tools {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
|
|||||||
@@ -192,6 +192,10 @@
|
|||||||
<button id="refreshAiModels" type="button">Obnovit</button>
|
<button id="refreshAiModels" type="button">Obnovit</button>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
<div class="ai-actions">
|
||||||
|
<button id="saveAiSettings" type="button">Ulozit AI nastaveni</button>
|
||||||
|
<button id="testOpenAiKey" type="button">Otestovat klic</button>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+32
-4
@@ -80,11 +80,13 @@ $("#adminSoundRepeat").addEventListener("change", (event) => {
|
|||||||
$("#adminSoundTest").addEventListener("click", () => {
|
$("#adminSoundTest").addEventListener("click", () => {
|
||||||
playNotifySound({ ignoreMuted: true });
|
playNotifySound({ ignoreMuted: true });
|
||||||
});
|
});
|
||||||
$("#translationEnabled").addEventListener("change", saveTranslationSettings);
|
$("#translationEnabled").addEventListener("change", markAiSettingsDirty);
|
||||||
$("#operatorLanguage").addEventListener("change", saveTranslationSettings);
|
$("#operatorLanguage").addEventListener("change", markAiSettingsDirty);
|
||||||
$("#translationModel").addEventListener("change", saveTranslationSettings);
|
$("#translationModel").addEventListener("change", markAiSettingsDirty);
|
||||||
$("#openaiApiKey").addEventListener("change", saveTranslationSettings);
|
$("#openaiApiKey").addEventListener("input", markAiSettingsDirty);
|
||||||
$("#refreshAiModels").addEventListener("click", loadAiModels);
|
$("#refreshAiModels").addEventListener("click", loadAiModels);
|
||||||
|
$("#saveAiSettings").addEventListener("click", saveTranslationSettings);
|
||||||
|
$("#testOpenAiKey").addEventListener("click", testOpenAiKey);
|
||||||
document.querySelectorAll("[data-settings-tab]").forEach((button) => {
|
document.querySelectorAll("[data-settings-tab]").forEach((button) => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
const tab = button.dataset.settingsTab;
|
const tab = button.dataset.settingsTab;
|
||||||
@@ -403,6 +405,7 @@ function renderSite() {
|
|||||||
|
|
||||||
async function saveTranslationSettings() {
|
async function saveTranslationSettings() {
|
||||||
if (!state.site) return;
|
if (!state.site) return;
|
||||||
|
setOpenAiStatus("Ukladam AI nastaveni...");
|
||||||
const settings = structuredClone(state.site.settings);
|
const settings = structuredClone(state.site.settings);
|
||||||
settings.translations = {
|
settings.translations = {
|
||||||
...(settings.translations || {}),
|
...(settings.translations || {}),
|
||||||
@@ -414,9 +417,34 @@ async function saveTranslationSettings() {
|
|||||||
if (apiKey) settings.translations.openaiApiKey = apiKey;
|
if (apiKey) settings.translations.openaiApiKey = apiKey;
|
||||||
await saveSite({ settings, isOnline: state.site.isOnline });
|
await saveSite({ settings, isOnline: state.site.isOnline });
|
||||||
$("#openaiApiKey").value = "";
|
$("#openaiApiKey").value = "";
|
||||||
|
setOpenAiStatus(state.site.settings.translations?.hasOpenaiApiKey
|
||||||
|
? `Klic ulozeny (${state.site.settings.translations.openaiApiKeyMasked || "sk-..."})`
|
||||||
|
: "Klic neni ulozeny. Bez nej AI preklady nepobezi.");
|
||||||
await loadAiModels().catch(() => {});
|
await loadAiModels().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markAiSettingsDirty() {
|
||||||
|
setOpenAiStatus("AI nastaveni ma neulozene zmeny.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testOpenAiKey() {
|
||||||
|
setOpenAiStatus("Testuji OpenAI klic...");
|
||||||
|
const result = await api("/api/admin/ai/test", { method: "POST" }).catch((error) => error);
|
||||||
|
if (result?.ok) {
|
||||||
|
setOpenAiStatus(`Klic funguje (${result.key || "sk-..."}), dostupnych modelu: ${result.modelCount}.`);
|
||||||
|
await loadAiModels().catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = result?.error === "missing_openai_api_key"
|
||||||
|
? "Klic neni ulozeny. Nejdřív ho vloz a klikni na Ulozit AI nastaveni."
|
||||||
|
: `Test selhal (${result?.error || "neznamá chyba"}).`;
|
||||||
|
setOpenAiStatus(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpenAiStatus(message) {
|
||||||
|
$("#openaiApiKeyStatus").textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAiModels() {
|
async function loadAiModels() {
|
||||||
const current = $("#translationModel").value || state.site?.settings?.translations?.model || "gpt-5-mini";
|
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);
|
const data = await api("/api/admin/ai/models").catch((error) => error?.models ? error : null);
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ async function adminApi(req, res, url) {
|
|||||||
if (url.pathname === "/api/admin/events" && req.method === "GET") return eventStream(req, res, url, "admin");
|
if (url.pathname === "/api/admin/events" && req.method === "GET") return eventStream(req, res, url, "admin");
|
||||||
if (url.pathname === "/api/admin/site" && req.method === "PATCH") return updateSite(req, res);
|
if (url.pathname === "/api/admin/site" && req.method === "PATCH") return updateSite(req, res);
|
||||||
if (url.pathname === "/api/admin/ai/models" && req.method === "GET") return adminAiModels(res);
|
if (url.pathname === "/api/admin/ai/models" && req.method === "GET") return adminAiModels(res);
|
||||||
|
if (url.pathname === "/api/admin/ai/test" && req.method === "POST") return adminAiTest(res);
|
||||||
if (url.pathname === "/api/admin/conversations" && req.method === "GET") return adminConversations(res);
|
if (url.pathname === "/api/admin/conversations" && req.method === "GET") return adminConversations(res);
|
||||||
if (url.pathname === "/api/admin/visitors" && req.method === "GET") return adminVisitors(res);
|
if (url.pathname === "/api/admin/visitors" && req.method === "GET") return adminVisitors(res);
|
||||||
if (url.pathname.match(/^\/api\/admin\/visitors\/[^/]+$/) && req.method === "PATCH") return updateVisitorProfile(req, res, url);
|
if (url.pathname.match(/^\/api\/admin\/visitors\/[^/]+$/) && req.method === "PATCH") return updateVisitorProfile(req, res, url);
|
||||||
@@ -471,6 +472,27 @@ async function adminAiModels(res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function adminAiTest(res) {
|
||||||
|
const settings = getTranslationSettings();
|
||||||
|
const apiKey = settings.openaiApiKey;
|
||||||
|
if (!apiKey) return json(res, 400, { ok: false, error: "missing_openai_api_key" });
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://api.openai.com/v1/models", {
|
||||||
|
headers: { "Authorization": `Bearer ${apiKey}` }
|
||||||
|
});
|
||||||
|
if (!response.ok) return json(res, 400, { ok: false, error: `openai_http_${response.status}` });
|
||||||
|
const data = await response.json();
|
||||||
|
return json(res, 200, {
|
||||||
|
ok: true,
|
||||||
|
key: maskApiKey(apiKey),
|
||||||
|
modelCount: Array.isArray(data.data) ? data.data.length : 0
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("openai_test_failed", error.message);
|
||||||
|
return json(res, 502, { ok: false, error: "openai_test_failed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateConversationStatus(req, res, url) {
|
async function updateConversationStatus(req, res, url) {
|
||||||
const body = await readJson(req);
|
const body = await readJson(req);
|
||||||
const status = ["new", "open", "resolved", "spam"].includes(body.status) ? body.status : null;
|
const status = ["new", "open", "resolved", "spam"].includes(body.status) ? body.status : null;
|
||||||
|
|||||||
Reference in New Issue
Block a user