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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.venv/
|
.venv/
|
||||||
|
.chatgpt/
|
||||||
fb_profile/
|
fb_profile/
|
||||||
prospects.csv
|
prospects.csv
|
||||||
config.json
|
config.json
|
||||||
|
|||||||
39
app.py
39
app.py
@@ -12,6 +12,7 @@ from flask import Flask, Response, jsonify, request, send_file, stream_with_cont
|
|||||||
|
|
||||||
import excel
|
import excel
|
||||||
import extractor
|
import extractor
|
||||||
|
import ia
|
||||||
import scraper
|
import scraper
|
||||||
import trajet
|
import trajet
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ CONFIG_DEFAUT = {
|
|||||||
"Seriez-vous disponible pour en échanger ?\n\n"
|
"Seriez-vous disponible pour en échanger ?\n\n"
|
||||||
"Bien cordialement,"
|
"Bien cordialement,"
|
||||||
),
|
),
|
||||||
|
"ia_modele": ia.MODELE_DEFAUT,
|
||||||
}
|
}
|
||||||
|
|
||||||
app = Flask(__name__, static_folder="static")
|
app = Flask(__name__, static_folder="static")
|
||||||
@@ -309,7 +311,7 @@ def api_config_lire():
|
|||||||
def api_config_ecrire():
|
def api_config_ecrire():
|
||||||
donnees = request.get_json(silent=True) or {}
|
donnees = request.get_json(silent=True) or {}
|
||||||
config = lire_config()
|
config = lire_config()
|
||||||
for cle in ("adresse_depart", "modele_message"):
|
for cle in ("adresse_depart", "modele_message", "ia_modele"):
|
||||||
if cle in donnees:
|
if cle in donnees:
|
||||||
config[cle] = str(donnees[cle])
|
config[cle] = str(donnees[cle])
|
||||||
for cle in ("conso_l_100km", "prix_carburant", "cout_peage_km"):
|
for cle in ("conso_l_100km", "prix_carburant", "cout_peage_km"):
|
||||||
@@ -327,6 +329,41 @@ def api_config_ecrire():
|
|||||||
return jsonify({"ok": True, "config": config})
|
return jsonify({"ok": True, "config": config})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/ia/status")
|
||||||
|
def api_ia_status():
|
||||||
|
return jsonify(ia.statut_login())
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/ia/login")
|
||||||
|
def api_ia_login():
|
||||||
|
try:
|
||||||
|
return jsonify(ia.lancer_login())
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": "login", "message": f"Échec du démarrage de la connexion : {e}"}), 502
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/message")
|
||||||
|
def api_message():
|
||||||
|
donnees = request.get_json(silent=True) or {}
|
||||||
|
prospect = donnees.get("prospect") or {}
|
||||||
|
mode = donnees.get("mode") or "generer"
|
||||||
|
if not prospect.get("Nom du prospect"):
|
||||||
|
return jsonify({"error": "prospect_vide", "message": "Prospect sans nom."}), 400
|
||||||
|
config = lire_config()
|
||||||
|
try:
|
||||||
|
message = ia.generer_message(
|
||||||
|
prospect, config.get("modele_message", ""), mode,
|
||||||
|
nom_modele=config.get("ia_modele") or ia.MODELE_DEFAUT,
|
||||||
|
)
|
||||||
|
return jsonify({"message": message})
|
||||||
|
except ia.IANonConnecte as e:
|
||||||
|
return jsonify({"error": "non_connecte", "message": str(e)}), 401
|
||||||
|
except ia.IAErreur as e:
|
||||||
|
return jsonify({"error": "ia", "message": str(e)}), 502
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": "erreur", "message": f"Échec de la génération : {e}"}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/distance/<int:idx>")
|
@app.get("/api/distance/<int:idx>")
|
||||||
def api_distance(idx):
|
def api_distance(idx):
|
||||||
lignes = lire_prospects()
|
lignes = lire_prospects()
|
||||||
|
|||||||
199
ia.py
Normal file
199
ia.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
"""Génération de messages par IA via l'abonnement ChatGPT (provider `chatgpt` de LiteLLM).
|
||||||
|
|
||||||
|
L'accès se fait par le flow « Sign in with ChatGPT » (device code) : aucune clé API, c'est
|
||||||
|
l'abonnement ChatGPT de l'utilisateur qui est utilisé. Le token est stocké LOCALEMENT dans le
|
||||||
|
dossier `.chatgpt/` du projet pour rester portable (utilisable en tournée sur un portable).
|
||||||
|
|
||||||
|
Usage strictement personnel : le backend `chatgpt.com/backend-api/codex` n'est pas une API
|
||||||
|
publique documentée et peut changer ; on s'appuie sur LiteLLM qui en absorbe les détails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DOSSIER = Path(__file__).parent
|
||||||
|
TOKEN_DIR = DOSSIER / ".chatgpt"
|
||||||
|
|
||||||
|
# Doit être défini AVANT tout import de litellm pour que le token soit écrit dans le projet.
|
||||||
|
os.environ.setdefault("CHATGPT_TOKEN_DIR", str(TOKEN_DIR))
|
||||||
|
|
||||||
|
MODELE_DEFAUT = "chatgpt/gpt-5.4"
|
||||||
|
|
||||||
|
# État du login device-code en cours, partagé entre la route /api/ia/login et le thread de polling.
|
||||||
|
_verrou_login = threading.Lock()
|
||||||
|
_etat_login = {
|
||||||
|
"en_cours": False,
|
||||||
|
"user_code": None,
|
||||||
|
"verification_url": None,
|
||||||
|
"erreur": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IANonConnecte(Exception):
|
||||||
|
"""Aucune session ChatGPT valide : il faut se connecter d'abord."""
|
||||||
|
|
||||||
|
|
||||||
|
class IAErreur(Exception):
|
||||||
|
"""Échec d'un appel à l'IA (réseau, rate limit, token expiré...)."""
|
||||||
|
|
||||||
|
|
||||||
|
def _authenticator():
|
||||||
|
from litellm.llms.chatgpt.authenticator import Authenticator
|
||||||
|
return Authenticator()
|
||||||
|
|
||||||
|
|
||||||
|
def est_connecte():
|
||||||
|
"""Vrai si un token ChatGPT exploitable (access ou refresh) est stocké localement."""
|
||||||
|
try:
|
||||||
|
data = _authenticator()._read_auth_file()
|
||||||
|
return bool(data and (data.get("access_token") or data.get("refresh_token")))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def lancer_login():
|
||||||
|
"""Démarre le flow device-code et renvoie le code + l'URL à afficher dans l'UI.
|
||||||
|
|
||||||
|
Le polling (attente que l'utilisateur valide dans le navigateur) tourne en arrière-plan ;
|
||||||
|
l'UI suit l'avancement via `statut_login()`.
|
||||||
|
"""
|
||||||
|
if est_connecte():
|
||||||
|
return {"deja_connecte": True}
|
||||||
|
|
||||||
|
from litellm.llms.chatgpt.common_utils import CHATGPT_DEVICE_VERIFY_URL
|
||||||
|
|
||||||
|
with _verrou_login:
|
||||||
|
if _etat_login["en_cours"]:
|
||||||
|
return {
|
||||||
|
"user_code": _etat_login["user_code"],
|
||||||
|
"verification_url": _etat_login["verification_url"],
|
||||||
|
}
|
||||||
|
auth = _authenticator()
|
||||||
|
device = auth._request_device_code()
|
||||||
|
auth._record_device_code_request()
|
||||||
|
_etat_login.update({
|
||||||
|
"en_cours": True,
|
||||||
|
"user_code": device["user_code"],
|
||||||
|
"verification_url": CHATGPT_DEVICE_VERIFY_URL,
|
||||||
|
"erreur": None,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _attendre_validation():
|
||||||
|
try:
|
||||||
|
code = auth._poll_for_authorization_code(device)
|
||||||
|
tokens = auth._exchange_code_for_tokens(code)
|
||||||
|
auth._write_auth_file(auth._build_auth_record(tokens))
|
||||||
|
except Exception as e:
|
||||||
|
_etat_login["erreur"] = str(e)
|
||||||
|
finally:
|
||||||
|
_etat_login["en_cours"] = False
|
||||||
|
|
||||||
|
threading.Thread(target=_attendre_validation, daemon=True).start()
|
||||||
|
return {
|
||||||
|
"user_code": _etat_login["user_code"],
|
||||||
|
"verification_url": _etat_login["verification_url"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def statut_login():
|
||||||
|
"""État courant pour l'UI : connecté ? login en cours ? code à afficher ? erreur ?"""
|
||||||
|
en_cours = _etat_login["en_cours"]
|
||||||
|
return {
|
||||||
|
"connecte": est_connecte(),
|
||||||
|
"en_cours": en_cours,
|
||||||
|
"user_code": _etat_login["user_code"] if en_cours else None,
|
||||||
|
"verification_url": _etat_login["verification_url"] if en_cours else None,
|
||||||
|
"erreur": _etat_login["erreur"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _message_modele(prospect, modele):
|
||||||
|
"""Substitution {nom}/{ville}/{type} — réplique de messagePour() côté frontend."""
|
||||||
|
ville = (prospect.get("Ville") or "").strip()
|
||||||
|
return (modele or "") \
|
||||||
|
.replace("{nom}", prospect.get("Nom du prospect") or "") \
|
||||||
|
.replace("{ville}", f" à {ville}" if ville else "") \
|
||||||
|
.replace("{type}", (prospect.get("Type") or "").lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _infos_prospect(prospect):
|
||||||
|
champs = [
|
||||||
|
("Nom", prospect.get("Nom du prospect")),
|
||||||
|
("Ville", prospect.get("Ville")),
|
||||||
|
("Type de lieu", prospect.get("Type")),
|
||||||
|
("Infos du lieu", prospect.get("Infos du lieu")),
|
||||||
|
]
|
||||||
|
return "\n".join(f"- {cle} : {val}" for cle, val in champs if (val or "").strip())
|
||||||
|
|
||||||
|
|
||||||
|
def _message_erreur(exc):
|
||||||
|
bas = str(exc).lower()
|
||||||
|
if "rate" in bas or "429" in bas or "limit" in bas or "quota" in bas:
|
||||||
|
return "Limite de l'abonnement ChatGPT atteinte (fenêtre de 5 h). Réessayez plus tard."
|
||||||
|
if "401" in bas or "403" in bas or "expired" in bas or "token" in bas or "unauthor" in bas:
|
||||||
|
return "Session ChatGPT expirée ou invalide. Reconnectez-vous dans les Paramètres."
|
||||||
|
return f"Échec de la génération : {exc}"
|
||||||
|
|
||||||
|
|
||||||
|
def generer_message(prospect, modele, mode="generer", nom_modele=None):
|
||||||
|
"""Génère un message de prise de contact pour un prospect.
|
||||||
|
|
||||||
|
mode == "generer" : l'IA rédige un message sur mesure (modèle = guide de ton/style).
|
||||||
|
mode == "peaufiner" : on substitue d'abord le modèle, puis l'IA le reformule sans en
|
||||||
|
changer le sens.
|
||||||
|
"""
|
||||||
|
nom_modele = nom_modele or MODELE_DEFAUT
|
||||||
|
if not est_connecte():
|
||||||
|
raise IANonConnecte(
|
||||||
|
"Connectez-vous à ChatGPT dans les Paramètres avant de générer un message."
|
||||||
|
)
|
||||||
|
|
||||||
|
infos = _infos_prospect(prospect) or "- (aucune information détaillée disponible)"
|
||||||
|
|
||||||
|
if mode == "peaufiner":
|
||||||
|
brouillon = _message_modele(prospect, modele)
|
||||||
|
systeme = (
|
||||||
|
"Tu rédiges des messages de prise de contact en français pour proposer "
|
||||||
|
"l'organisation de concerts à des établissements. On te donne un brouillon : "
|
||||||
|
"reformule-le pour le rendre plus naturel, chaleureux et engageant, sans inventer "
|
||||||
|
"d'information ni changer le sens. Réponds UNIQUEMENT par le message final, sans "
|
||||||
|
"commentaire."
|
||||||
|
)
|
||||||
|
utilisateur = f"Brouillon à améliorer :\n{brouillon}\n\nInfos sur l'établissement :\n{infos}"
|
||||||
|
else:
|
||||||
|
systeme = (
|
||||||
|
"Tu rédiges des messages de prise de contact en français pour proposer "
|
||||||
|
"l'organisation de concerts à des établissements (bars, restaurants, salles...). "
|
||||||
|
"Le message doit être court, personnalisé, chaleureux et se terminer par une "
|
||||||
|
"question ouvrant l'échange. Réponds UNIQUEMENT par le message final, sans "
|
||||||
|
"commentaire ni objet d'e-mail."
|
||||||
|
)
|
||||||
|
utilisateur = (
|
||||||
|
f"Rédige un message de prise de contact pour cet établissement :\n{infos}\n\n"
|
||||||
|
f"Inspire-toi de ce modèle pour le ton et le style :\n{modele}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Streaming OBLIGATOIRE : le backend Codex envoie le texte par deltas et renvoie un
|
||||||
|
# `output` vide dans l'événement final, ce qui casse la voie non-streaming de LiteLLM
|
||||||
|
# (« Unknown items in responses API response: [] »). On accumule donc les morceaux.
|
||||||
|
try:
|
||||||
|
import litellm
|
||||||
|
flux = litellm.completion(
|
||||||
|
model=nom_modele,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": systeme},
|
||||||
|
{"role": "user", "content": utilisateur},
|
||||||
|
],
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
morceaux = []
|
||||||
|
for chunk in flux:
|
||||||
|
if chunk.choices:
|
||||||
|
delta = chunk.choices[0].delta.content
|
||||||
|
if delta:
|
||||||
|
morceaux.append(delta)
|
||||||
|
except Exception as e:
|
||||||
|
raise IAErreur(_message_erreur(e))
|
||||||
|
|
||||||
|
return "".join(morceaux).strip()
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
flask>=3.0
|
flask>=3.0
|
||||||
playwright>=1.45
|
playwright>=1.45
|
||||||
|
litellm>=1.83
|
||||||
|
|||||||
@@ -96,6 +96,19 @@
|
|||||||
<textarea id="cfg-message" rows="6"></textarea>
|
<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>
|
<p class="indice">Variables disponibles : <code>{nom}</code> (nom du lieu), <code>{ville}</code> (devient « à Ville », ou rien), <code>{type}</code>.</p>
|
||||||
</div>
|
</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">
|
<div class="actions">
|
||||||
<button id="cfg-enregistrer">Enregistrer les paramètres</button>
|
<button id="cfg-enregistrer">Enregistrer les paramètres</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,7 +222,9 @@ async function chargerConfig() {
|
|||||||
$("cfg-peage").value = config.cout_peage_km ?? "";
|
$("cfg-peage").value = config.cout_peage_km ?? "";
|
||||||
$("cfg-relance").value = config.delai_relance_jours ?? "";
|
$("cfg-relance").value = config.delai_relance_jours ?? "";
|
||||||
$("cfg-message").value = config.modele_message || "";
|
$("cfg-message").value = config.modele_message || "";
|
||||||
|
$("cfg-ia-modele").value = config.ia_modele || "";
|
||||||
afficherRelances();
|
afficherRelances();
|
||||||
|
rafraichirStatutIA();
|
||||||
}
|
}
|
||||||
|
|
||||||
$("cfg-enregistrer").addEventListener("click", async () => {
|
$("cfg-enregistrer").addEventListener("click", async () => {
|
||||||
@@ -220,6 +235,7 @@ $("cfg-enregistrer").addEventListener("click", async () => {
|
|||||||
cout_peage_km: $("cfg-peage").value,
|
cout_peage_km: $("cfg-peage").value,
|
||||||
delai_relance_jours: $("cfg-relance").value,
|
delai_relance_jours: $("cfg-relance").value,
|
||||||
modele_message: $("cfg-message").value,
|
modele_message: $("cfg-message").value,
|
||||||
|
ia_modele: $("cfg-ia-modele").value.trim(),
|
||||||
};
|
};
|
||||||
const rep = await fetch("/api/config", {
|
const rep = await fetch("/api/config", {
|
||||||
method: "PUT",
|
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.
|
// Construit le message de contact à partir du modèle et du prospect.
|
||||||
function messagePour(p) {
|
function messagePour(p) {
|
||||||
const ville = (p["Ville"] || "").trim();
|
const ville = (p["Ville"] || "").trim();
|
||||||
@@ -591,20 +647,65 @@ function construireDetail(detail, p) {
|
|||||||
});
|
});
|
||||||
actions.append(enregistrer);
|
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");
|
const copier = document.createElement("button");
|
||||||
copier.className = "secondaire";
|
copier.className = "secondaire";
|
||||||
copier.textContent = "📋 Copier le message";
|
copier.textContent = "📋 Copier le message";
|
||||||
copier.addEventListener("click", async () => {
|
copier.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(messagePour(p));
|
await navigator.clipboard.writeText(texteContact());
|
||||||
copier.textContent = "✔ Copié";
|
copier.textContent = "✔ Copié";
|
||||||
setTimeout(() => { copier.textContent = "📋 Copier le message"; }, 1500);
|
setTimeout(() => { copier.textContent = "📋 Copier le message"; }, 1500);
|
||||||
} catch {
|
} catch {
|
||||||
copier.textContent = "Échec de la copie";
|
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
|
// Trajet + carburant
|
||||||
const trajetBtn = document.createElement("button");
|
const trajetBtn = document.createElement("button");
|
||||||
@@ -637,7 +738,7 @@ function construireDetail(detail, p) {
|
|||||||
});
|
});
|
||||||
actions.append(trajetBtn);
|
actions.append(trajetBtn);
|
||||||
|
|
||||||
detail.append(grille, actions, resu);
|
detail.append(grille, actions, iaInfo, zoneIA, resu);
|
||||||
}
|
}
|
||||||
|
|
||||||
$("recherche").addEventListener("input", afficherListe);
|
$("recherche").addEventListener("input", afficherListe);
|
||||||
|
|||||||
Reference in New Issue
Block a user