Ajout génération de messages par IA via l'abonnement ChatGPT
- Provider chatgpt de LiteLLM (Sign in with ChatGPT, sans clé API) - Module ia.py : login device-code, token local portable (.chatgpt/), génération streaming - Routes /api/message, /api/ia/login, /api/ia/status - UI : boutons Générer/Peaufiner par prospect, connexion ChatGPT + modèle IA dans les Paramètres
This commit is contained in:
@@ -96,6 +96,19 @@
|
||||
<textarea id="cfg-message" rows="6"></textarea>
|
||||
<p class="indice">Variables disponibles : <code>{nom}</code> (nom du lieu), <code>{ville}</code> (devient « à Ville », ou rien), <code>{type}</code>.</p>
|
||||
</div>
|
||||
<div class="champ" style="margin-top:12px">
|
||||
<label>Modèle IA (génération de messages)</label>
|
||||
<input id="cfg-ia-modele" type="text" placeholder="chatgpt/gpt-5.4">
|
||||
<p class="indice">Utilisé via votre abonnement ChatGPT (sans clé API). Ex. <code>chatgpt/gpt-5.4</code>, ou <code>chatgpt/gpt-5.3-instant</code> (plus rapide).</p>
|
||||
</div>
|
||||
<div class="champ" style="margin-top:12px">
|
||||
<label>Connexion ChatGPT (pour la génération par IA)</label>
|
||||
<div id="ia-statut" class="indice">Vérification…</div>
|
||||
<div class="actions">
|
||||
<button type="button" class="secondaire" id="ia-connexion">🔗 Se connecter à ChatGPT</button>
|
||||
</div>
|
||||
<div id="ia-code" class="banniere-relance" style="display:none"></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="cfg-enregistrer">Enregistrer les paramètres</button>
|
||||
</div>
|
||||
@@ -209,7 +222,9 @@ async function chargerConfig() {
|
||||
$("cfg-peage").value = config.cout_peage_km ?? "";
|
||||
$("cfg-relance").value = config.delai_relance_jours ?? "";
|
||||
$("cfg-message").value = config.modele_message || "";
|
||||
$("cfg-ia-modele").value = config.ia_modele || "";
|
||||
afficherRelances();
|
||||
rafraichirStatutIA();
|
||||
}
|
||||
|
||||
$("cfg-enregistrer").addEventListener("click", async () => {
|
||||
@@ -220,6 +235,7 @@ $("cfg-enregistrer").addEventListener("click", async () => {
|
||||
cout_peage_km: $("cfg-peage").value,
|
||||
delai_relance_jours: $("cfg-relance").value,
|
||||
modele_message: $("cfg-message").value,
|
||||
ia_modele: $("cfg-ia-modele").value.trim(),
|
||||
};
|
||||
const rep = await fetch("/api/config", {
|
||||
method: "PUT",
|
||||
@@ -235,6 +251,46 @@ $("cfg-enregistrer").addEventListener("click", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Connexion ChatGPT (génération IA) ---
|
||||
|
||||
let iaPolling = null;
|
||||
|
||||
async function rafraichirStatutIA() {
|
||||
let s;
|
||||
try { s = await (await fetch("/api/ia/status")).json(); } catch { return; }
|
||||
const statut = $("ia-statut"), bouton = $("ia-connexion"), code = $("ia-code");
|
||||
if (s.connecte) {
|
||||
statut.textContent = "✅ Connecté à ChatGPT.";
|
||||
bouton.textContent = "🔄 Se reconnecter";
|
||||
code.style.display = "none";
|
||||
} else if (s.en_cours) {
|
||||
statut.textContent = "⏳ En attente de validation dans le navigateur…";
|
||||
code.style.display = "block";
|
||||
code.innerHTML = `Ouvrez <a href="${echap(s.verification_url)}" target="_blank" rel="noopener">${echap(s.verification_url)}</a> ` +
|
||||
`puis saisissez le code : <strong style="font-size:18px">${echap(s.user_code)}</strong>`;
|
||||
} else {
|
||||
statut.textContent = s.erreur ? `❌ ${s.erreur}` : "Non connecté à ChatGPT.";
|
||||
bouton.textContent = "🔗 Se connecter à ChatGPT";
|
||||
code.style.display = "none";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
$("ia-connexion").addEventListener("click", async () => {
|
||||
$("ia-connexion").disabled = true;
|
||||
try {
|
||||
const r = await (await fetch("/api/ia/login", { method: "POST" })).json();
|
||||
if (r.verification_url) window.open(r.verification_url, "_blank", "noopener");
|
||||
} catch {}
|
||||
$("ia-connexion").disabled = false;
|
||||
await rafraichirStatutIA();
|
||||
clearInterval(iaPolling);
|
||||
iaPolling = setInterval(async () => {
|
||||
const s = await rafraichirStatutIA();
|
||||
if (s && !s.en_cours) clearInterval(iaPolling); // connecté, échec ou expiration
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// Construit le message de contact à partir du modèle et du prospect.
|
||||
function messagePour(p) {
|
||||
const ville = (p["Ville"] || "").trim();
|
||||
@@ -591,20 +647,65 @@ function construireDetail(detail, p) {
|
||||
});
|
||||
actions.append(enregistrer);
|
||||
|
||||
// Prise de contact
|
||||
// Prise de contact — zone éditable partagée (modèle statique ou message généré par IA)
|
||||
const zoneIA = document.createElement("textarea");
|
||||
zoneIA.className = "ia-zone";
|
||||
zoneIA.rows = 6;
|
||||
zoneIA.style.cssText = "width:100%;margin-top:8px;display:none";
|
||||
const iaInfo = document.createElement("div");
|
||||
iaInfo.className = "indice";
|
||||
|
||||
// Texte courant : message généré/édité s'il existe, sinon le modèle statique.
|
||||
const texteContact = () => (zoneIA.value.trim() ? zoneIA.value : messagePour(p));
|
||||
|
||||
const copier = document.createElement("button");
|
||||
copier.className = "secondaire";
|
||||
copier.textContent = "📋 Copier le message";
|
||||
copier.addEventListener("click", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(messagePour(p));
|
||||
await navigator.clipboard.writeText(texteContact());
|
||||
copier.textContent = "✔ Copié";
|
||||
setTimeout(() => { copier.textContent = "📋 Copier le message"; }, 1500);
|
||||
} catch {
|
||||
copier.textContent = "Échec de la copie";
|
||||
}
|
||||
});
|
||||
actions.append(copier);
|
||||
|
||||
async function genererIA(mode, bouton) {
|
||||
const ancien = bouton.textContent;
|
||||
bouton.disabled = true;
|
||||
bouton.innerHTML = '<span class="spinner sombre"></span> Génération…';
|
||||
iaInfo.textContent = "";
|
||||
try {
|
||||
const rep = await fetch("/api/message", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ prospect: p, mode }),
|
||||
});
|
||||
const d = await rep.json();
|
||||
if (!rep.ok) { iaInfo.innerHTML = `<span style="color:#b03a2e">${echap(d.message || "Échec de la génération.")}</span>`; return; }
|
||||
zoneIA.value = d.message || "";
|
||||
zoneIA.style.display = "block";
|
||||
iaInfo.textContent = "Message généré — éditable, puis « Copier le message ».";
|
||||
} catch {
|
||||
iaInfo.innerHTML = '<span style="color:#b03a2e">Le serveur ne répond pas.</span>';
|
||||
} finally {
|
||||
bouton.disabled = false;
|
||||
bouton.textContent = ancien;
|
||||
}
|
||||
}
|
||||
|
||||
const genererBtn = document.createElement("button");
|
||||
genererBtn.className = "secondaire";
|
||||
genererBtn.textContent = "✨ Générer (IA)";
|
||||
genererBtn.addEventListener("click", () => genererIA("generer", genererBtn));
|
||||
|
||||
const peaufinerBtn = document.createElement("button");
|
||||
peaufinerBtn.className = "secondaire";
|
||||
peaufinerBtn.textContent = "✨ Peaufiner (IA)";
|
||||
peaufinerBtn.addEventListener("click", () => genererIA("peaufiner", peaufinerBtn));
|
||||
|
||||
actions.append(copier, genererBtn, peaufinerBtn);
|
||||
|
||||
// Trajet + carburant
|
||||
const trajetBtn = document.createElement("button");
|
||||
@@ -637,7 +738,7 @@ function construireDetail(detail, p) {
|
||||
});
|
||||
actions.append(trajetBtn);
|
||||
|
||||
detail.append(grille, actions, resu);
|
||||
detail.append(grille, actions, iaInfo, zoneIA, resu);
|
||||
}
|
||||
|
||||
$("recherche").addEventListener("input", afficherListe);
|
||||
|
||||
Reference in New Issue
Block a user