diff --git a/backend/main.py b/backend/main.py index 70b49ef..85b1b96 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4022,6 +4022,25 @@ async def api_set_ai_keys(body: dict = Body(...), current_user=Depends(require_a logger.info("AI keys updated") return {"status": "ok"} + +@app.delete("/api/config/ai-keys/{provider_env}") +async def api_delete_ai_key(provider_env: str, current_user=Depends(require_admin)): + """Delete a specific AI provider key from storage.""" + allowed = {"DEEPSEEK_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", + "NVIDIA_API_KEY", "QWENCLOUD_API_KEY", "XIAOMI_API_KEY", "MISTRAL_API_KEY"} + key_name = provider_env.upper() + if key_name not in allowed: + raise HTTPException(status_code=400, detail=f"Clé inconnue: {provider_env}") + keys = _read_ai_keys() + if key_name in keys: + del keys[key_name] + _write_ai_keys(keys) + # Also clear from env at runtime so get_ai_key() no longer finds it + os.environ.pop(key_name, None) + logger.info(f"AI key deleted: {key_name}") + return {"status": "deleted", "key": key_name} + + @app.post("/api/config/ai-keys/test") async def api_test_ai_keys(current_user=Depends(require_admin)): """Test which AI providers are configured. diff --git a/frontend/js/config.js b/frontend/js/config.js index 27ba6a6..9221836 100644 --- a/frontend/js/config.js +++ b/frontend/js/config.js @@ -1340,21 +1340,77 @@ function updateRegexPreview() { // ── AI Keys management ── +const AI_KEY_MAP = { + "cfg-deepseek-key": "DEEPSEEK_API_KEY", + "cfg-openrouter-key": "OPENROUTER_API_KEY", + "cfg-gemini-key": "GEMINI_API_KEY", + "cfg-nvidia-key": "NVIDIA_API_KEY", + "cfg-qwencloud-key": "QWENCLOUD_API_KEY", + "cfg-xiaomi-key": "XIAOMI_API_KEY", + "cfg-mistral-key": "MISTRAL_API_KEY", +}; +const AI_PROVIDER_NAMES = ["deepseek","openrouter","gemini","nvidia","qwencloud","xiaomi","mistral"]; + +function _ensureAIKeyUI() { + for (const [inputId] of Object.entries(AI_KEY_MAP)) { + const input = document.getElementById(inputId); + if (!input) continue; + const row = input.closest(".config-row"); + if (!row || row.dataset.enhanced) continue; + row.dataset.enhanced = "1"; + row.style.cssText += "display:flex;align-items:center;gap:8px;flex-wrap:wrap;"; + const badge = document.createElement("span"); + badge.id = inputId.replace("-key", "-badge"); + badge.style.cssText = "font-size:11px;padding:2px 8px;border-radius:10px;white-space:nowrap;"; + row.appendChild(badge); + const delBtn = document.createElement("button"); + delBtn.type = "button"; + delBtn.id = inputId.replace("-key", "-delete"); + delBtn.className = "config-btn-secondary"; + delBtn.style.cssText = "font-size:11px;padding:4px 10px;color:var(--danger,#e74c3c);border-color:var(--danger,#e74c3c);cursor:pointer;display:none;"; + delBtn.textContent = "\u00d7 Supprimer"; + delBtn.addEventListener("click", () => deleteAIKey(inputId)); + row.appendChild(delBtn); + } +} + +function _setAIKeyBadge(inputId, hasKey) { + const badge = document.getElementById(inputId.replace("-key", "-badge")); + const delBtn = document.getElementById(inputId.replace("-key", "-delete")); + if (badge) { + if (hasKey) { + badge.textContent = "\u2713 Configur\u00e9"; + badge.style.background = "var(--success-bg, #27ae6022)"; + badge.style.color = "var(--success, #27ae60)"; + badge.style.border = "1px solid var(--success, #27ae60)"; + } else { + badge.textContent = "Non configur\u00e9"; + badge.style.background = "var(--muted-bg, #ffffff10)"; + badge.style.color = "var(--text-muted, #888)"; + badge.style.border = "1px solid var(--border, #444)"; + } + } + if (delBtn) delBtn.style.display = hasKey ? "inline-block" : "none"; +} + async function loadAIKeys() { + _ensureAIKeyUI(); try { const data = await api("/api/config/ai-keys"); - ["DEEPSEEK_API_KEY","OPENROUTER_API_KEY","GEMINI_API_KEY","NVIDIA_API_KEY","QWENCLOUD_API_KEY","XIAOMI_API_KEY","MISTRAL_API_KEY"].forEach(k => { - const id = "cfg-" + k.toLowerCase().replace(/_api_key/g, "") + "-key"; - const input = document.getElementById(id); - if (input && data[k]) input.placeholder = data[k]; - }); + for (const [inputId, envName] of Object.entries(AI_KEY_MAP)) { + const input = document.getElementById(inputId); + const val = data[envName] || ""; + if (input) { + input.placeholder = val || (inputId.includes("gemini") ? "AIza..." : inputId.includes("openrouter") ? "sk-or-..." : "sk-..."); + } + _setAIKeyBadge(inputId, !!val); + } } catch(e) {} } async function saveAIKeys() { const keys = {}; - const map = { "cfg-deepseek-key": "DEEPSEEK_API_KEY", "cfg-openrouter-key": "OPENROUTER_API_KEY", "cfg-gemini-key": "GEMINI_API_KEY", "cfg-nvidia-key": "NVIDIA_API_KEY", "cfg-qwencloud-key": "QWENCLOUD_API_KEY", "cfg-xiaomi-key": "XIAOMI_API_KEY", "cfg-mistral-key": "MISTRAL_API_KEY" }; - for (const [id, name] of Object.entries(map)) { + for (const [id, name] of Object.entries(AI_KEY_MAP)) { const input = document.getElementById(id); if (input && input.value.trim()) keys[name] = input.value.trim(); } @@ -1362,8 +1418,19 @@ async function saveAIKeys() { try { await api("/api/config/ai-keys", { method: "POST", body: JSON.stringify(keys) }); showToast(t("config.api_keys_saved"), "success"); - // Clear inputs - Object.keys(map).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; }); + Object.keys(AI_KEY_MAP).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; }); + loadAIKeys(); + } catch(e) { showToast("Erreur: " + e.message, "error"); } +} + +async function deleteAIKey(inputId) { + const envName = AI_KEY_MAP[inputId]; + if (!envName) return; + const provider = envName.replace("_API_KEY", "").replace(/_/g, " "); + if (!confirm("Supprimer la cl\u00e9 API " + provider + " ?\nLe fournisseur ne sera plus disponible.")) return; + try { + await api("/api/config/ai-keys/" + envName, { method: "DELETE" }); + showToast("Cl\u00e9 " + provider + " supprim\u00e9e", "success"); loadAIKeys(); } catch(e) { showToast("Erreur: " + e.message, "error"); } } @@ -1372,12 +1439,18 @@ async function testAIKeys() { const status = document.getElementById("cfg-ai-status"); if (status) status.textContent = "Test en cours..."; try { - // Test connectivity const results = await api("/api/config/ai-keys/test", { method: "POST" }); var msg = Object.entries(results).map(([k,v]) => k + ": " + v).join(" | "); if (status) status.textContent = msg; - // Fetch models for configured providers - for (const p of ["deepseek","openrouter","gemini","nvidia","qwencloud","xiaomi","mistral"]) { + for (const p of AI_PROVIDER_NAMES) { + const inputId = "cfg-" + p + "-key"; + if (results[p] === "ok") { + _setAIKeyBadge(inputId, true); + } else if (results[p] && results[p].includes("non")) { + _setAIKeyBadge(inputId, false); + } + } + for (const p of AI_PROVIDER_NAMES) { if (results[p] === "ok") { try { const m = await api("/api/config/ai-models?provider=" + p); @@ -1394,7 +1467,7 @@ async function testAIKeys() { } catch(e) { if (status) status.textContent = "Erreur: " + e.message; } -} +}} export {