feat(ai): selection fournisseur/modele par defaut + guide architecture

- Persistance ai_default_provider / ai_default_models dans data/config.json
- Lecture + rechargement a chaud dans backend/ai.py (get_default_provider, reload_ai_config)
- UI: selecteurs Fournisseur/Modele par defaut dans la section Cles API IA (i18n FR/EN)
- docs/AI_ARCHITECTURE_GUIDE.md: architecture cible, catalogue d'outils, decisions MCP
This commit is contained in:
2026-09-11 12:03:16 -04:00
parent e2153435dc
commit a3642caa3d
8 changed files with 451 additions and 8 deletions
+30
View File
@@ -1975,6 +1975,36 @@
<strong data-i18n="config.test">Tester</strong> pour charger les
modeles.
</p>
<div class="config-row">
<label
class="config-label"
for="cfg-ai-default-provider"
data-i18n="config.ai_default_provider"
>Fournisseur par defaut</label
>
<select
id="cfg-ai-default-provider"
class="config-select"
style="width: 200px"
>
<option value="">--</option>
</select>
</div>
<div class="config-row">
<label
class="config-label"
for="cfg-ai-default-model"
data-i18n="config.ai_default_model"
>Modele par defaut</label
>
<select
id="cfg-ai-default-model"
class="config-select"
style="width: 220px"
>
<option value="">--</option>
</select>
</div>
<div class="config-row">
<label
class="config-label"
+73 -3
View File
@@ -615,8 +615,19 @@ function initConfigModal() {
if (saveAIKeysBtn) saveAIKeysBtn.addEventListener("click", saveAIKeys);
const testAIKeysBtn = document.getElementById("cfg-test-ai-keys");
if (testAIKeysBtn) testAIKeysBtn.addEventListener("click", testAIKeys);
// Load existing keys on modal open
// Default provider/model selection
const aiDefaultProviderSel = document.getElementById("cfg-ai-default-provider");
if (aiDefaultProviderSel) {
aiDefaultProviderSel.addEventListener("change", async () => {
let cfg = {};
try { cfg = await api("/api/config"); } catch(e) {}
const models = cfg.ai_default_models || {};
await _populateAIModelOptions(aiDefaultProviderSel.value, models[aiDefaultProviderSel.value] || "");
});
}
// Load existing keys/defaults on modal open
document.getElementById("config-open-btn").addEventListener("click", loadAIKeys);
document.getElementById("config-open-btn").addEventListener("click", loadAIDefaults);
// Profile menu button: open config modal and scroll to profile section
var profileBtn = document.getElementById("profile-open-btn");
@@ -1407,18 +1418,77 @@ async function loadAIKeys() {
} catch(e) {}
}
async function loadAIDefaults() {
const providerSel = document.getElementById("cfg-ai-default-provider");
if (!providerSel) return;
if (!providerSel.dataset.populated) {
providerSel.innerHTML = '<option value="">--</option>';
AI_PROVIDER_NAMES.forEach(function(p) {
providerSel.innerHTML += '<option value="' + p + '">' + p + '</option>';
});
providerSel.dataset.populated = "1";
}
let cfg = {};
try { cfg = await api("/api/config"); } catch(e) {}
const provider = cfg.ai_default_provider || "deepseek";
providerSel.value = provider;
const models = cfg.ai_default_models || {};
await _populateAIModelOptions(provider, models[provider] || "");
}
async function _populateAIModelOptions(provider, selected) {
const modelSel = document.getElementById("cfg-ai-default-model");
if (!modelSel) return;
modelSel.innerHTML = '<option value="">--</option>';
if (!provider) return;
try {
const m = await api("/api/config/ai-models?provider=" + encodeURIComponent(provider));
const list = (m && m.models) || [];
list.forEach(function(md) {
modelSel.innerHTML += '<option value="' + md + '">' + md + '</option>';
});
if (selected && list.indexOf(selected) === -1) {
modelSel.innerHTML += '<option value="' + selected + '">' + selected + '</option>';
}
modelSel.value = selected || "";
} catch(e) { console.warn("Failed to load default models for", provider, e); }
}
async function saveAIKeys() {
const keys = {};
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();
}
if (!Object.keys(keys).length) { showToast(t("config.no_keys"), "warning"); return; }
// Collect default provider + per-provider default models
const providerSel = document.getElementById("cfg-ai-default-provider");
const modelSel = document.getElementById("cfg-ai-default-model");
const defaults = {};
const models = {};
for (const p of AI_PROVIDER_NAMES) {
const sel = document.getElementById("cfg-" + p + "-model");
if (sel && sel.value) models[p] = sel.value;
}
if (providerSel && providerSel.value) {
defaults.ai_default_provider = providerSel.value;
if (modelSel && modelSel.value) models[providerSel.value] = modelSel.value;
}
if (Object.keys(models).length) defaults.ai_default_models = models;
if (!Object.keys(keys).length && !Object.keys(defaults).length) {
showToast(t("config.no_keys"), "warning"); return;
}
try {
await api("/api/config/ai-keys", { method: "POST", body: JSON.stringify(keys) });
if (Object.keys(keys).length) {
await api("/api/config/ai-keys", { method: "POST", body: JSON.stringify(keys) });
}
if (Object.keys(defaults).length) {
await api("/api/config", { method: "POST", body: JSON.stringify(defaults) });
}
showToast(t("config.api_keys_saved"), "success");
Object.keys(AI_KEY_MAP).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; });
loadAIKeys();
loadAIDefaults();
} catch(e) { showToast("Erreur: " + e.message, "error"); }
}
+1
View File
@@ -306,6 +306,7 @@
"config.about_loading": "Loading...",
"config.add_webhook": "Add",
"config.ai_deepseek_label": "DeepSeek API Key",
"config.ai_default_model": "Default model",
"config.ai_default_provider": "Default provider",
"config.ai_gemini_label": "Gemini API Key",
"config.ai_keys": "AI API keys",
+1
View File
@@ -306,6 +306,7 @@
"config.about_loading": "Chargement...",
"config.add_webhook": "Ajouter",
"config.ai_deepseek_label": "DeepSeek API Key",
"config.ai_default_model": "Modèle par défaut",
"config.ai_default_provider": "Fournisseur par défaut",
"config.ai_gemini_label": "Gemini API Key",
"config.ai_keys": "Clés API IA",