Infos du groupe dans les réglages, injectées au prompt IA

Nouveaux champs (nom, style, description, lien) en réglages, transmis au
prompt système de génération de messages dans les deux modes (générer et
peaufiner). La consigne de format reste en dernière position, non éditable.
Bloc omis si aucun champ rempli : prompt identique à l'ancien.
This commit is contained in:
jerem
2026-06-13 23:33:30 +02:00
parent 73d1653225
commit 018add739a
3 changed files with 171 additions and 15 deletions

13
app.py
View File

@@ -46,6 +46,10 @@ CONFIG_DEFAUT = {
"Bien cordialement,"
),
"ia_modele": ia.MODELE_DEFAUT,
"groupe_nom": "",
"groupe_style": "",
"groupe_description": "",
"groupe_lien": "",
}
app = Flask(__name__, static_folder="static")
@@ -368,7 +372,8 @@ def api_config_lire():
def api_config_ecrire():
donnees = request.get_json(silent=True) or {}
config = lire_config()
for cle in ("adresse_depart", "modele_message", "ia_modele"):
for cle in ("adresse_depart", "modele_message", "ia_modele",
"groupe_nom", "groupe_style", "groupe_description", "groupe_lien"):
if cle in donnees:
config[cle] = str(donnees[cle])
for cle in ("conso_l_100km", "prix_carburant", "cout_peage_km"):
@@ -411,6 +416,12 @@ def api_message():
message = ia.generer_message(
prospect, config.get("modele_message", ""), mode,
nom_modele=config.get("ia_modele") or ia.MODELE_DEFAUT,
groupe={
"nom": config.get("groupe_nom", ""),
"style": config.get("groupe_style", ""),
"description": config.get("groupe_description", ""),
"lien": config.get("groupe_lien", ""),
},
)
return jsonify({"message": message})
except ia.IANonConnecte as e:

47
ia.py
View File

@@ -127,6 +127,25 @@ def _infos_prospect(prospect):
return "\n".join(f"- {cle} : {val}" for cle, val in champs if (val or "").strip())
def _infos_groupe(groupe):
"""Bloc décrivant le groupe (depuis les réglages), injecté dans le prompt système.
Ne liste que les champs renseignés ; renvoie "" si rien n'est fourni.
"""
if not groupe:
return ""
champs = [
("Nom du groupe", groupe.get("nom")),
("Style", groupe.get("style")),
("Description", groupe.get("description")),
("Lien", groupe.get("lien")),
]
lignes = [f"- {cle} : {(val or '').strip()}" for cle, val in champs if (val or "").strip()]
if not lignes:
return ""
return "Tu représentes le groupe suivant :\n" + "\n".join(lignes)
def _message_erreur(exc):
bas = str(exc).lower()
if "rate" in bas or "429" in bas or "limit" in bas or "quota" in bas:
@@ -136,12 +155,15 @@ def _message_erreur(exc):
return f"Échec de la génération : {exc}"
def generer_message(prospect, modele, mode="generer", nom_modele=None):
def generer_message(prospect, modele, mode="generer", nom_modele=None, groupe=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.
`groupe` : dict optionnel (nom, style, description, lien) issu des réglages ; ses champs
renseignés sont injectés dans le prompt système pour personnaliser la rédaction.
"""
nom_modele = nom_modele or MODELE_DEFAUT
if not est_connecte():
@@ -150,25 +172,36 @@ def generer_message(prospect, modele, mode="generer", nom_modele=None):
)
infos = _infos_prospect(prospect) or "- (aucune information détaillée disponible)"
bloc_groupe = _infos_groupe(groupe)
if mode == "peaufiner":
brouillon = _message_modele(prospect, modele)
systeme = (
parties = [
"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."
"l'organisation de concerts à des établissements."
]
if bloc_groupe:
parties.append(bloc_groupe)
parties.append(
"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."
)
systeme = "\n\n".join(parties)
utilisateur = f"Brouillon à améliorer :\n{brouillon}\n\nInfos sur l'établissement :\n{infos}"
else:
systeme = (
parties = [
"Tu rédiges des messages de prise de contact en français pour proposer "
"l'organisation de concerts à des établissements (bars, restaurants, salles...)."
]
if bloc_groupe:
parties.append(bloc_groupe)
parties.append(
"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."
)
systeme = "\n\n".join(parties)
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}"

View File

@@ -136,6 +136,23 @@
.spinner.sombre { border-color: var(--accent); border-top-color: transparent; }
@keyframes tourne { to { transform: rotate(360deg); } }
/* ===== Disposition « modale » : liste en grand + détail en fenêtre centrale ===== */
body[data-disposition="modale"] .deux-colonnes { grid-template-columns: 1fr; }
body[data-disposition="modale"] .colonne-detail { display: none; }
body[data-disposition="modale"] #liste { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--e3); }
body[data-disposition="modale"] .item { margin-bottom: 0; padding: var(--e3) var(--e4); }
body.modale-ouverte { overflow: hidden; }
.modale-fond { position: fixed; inset: 0; z-index: 50; background: rgba(29,27,46,.45); display: flex; align-items: flex-start; justify-content: center; padding: var(--e8) var(--e4); overflow-y: auto; }
.modale-fond[hidden] { display: none; }
.modale { position: relative; width: 100%; max-width: 720px; background: var(--fond); border: 1px solid var(--bordure); border-radius: var(--r-m); box-shadow: 0 12px 48px rgba(29,27,46,.3); padding: var(--e6); }
.modale-fermer { position: absolute; top: var(--e3); right: var(--e3); width: 32px; height: 32px; padding: 0; font-size: 16px; line-height: 1; background: var(--accent-doux); color: var(--accent); }
.modale-fermer:hover { background: #e3e0f6; }
@media (max-width: 640px) {
.modale { padding: var(--e4); }
.modale-fond { padding: var(--e4) var(--e2); }
}
@media (max-width: 860px) {
.deux-colonnes { grid-template-columns: 1fr; }
.colonne-detail { position: static; max-height: none; }
@@ -228,6 +245,18 @@
<!-- ===== VUE RÉGLAGES ===== -->
<div class="vue" id="vue-reglages" hidden>
<section class="bloc">
<h2>Affichage</h2>
<div class="champ">
<label>Disposition des prospects</label>
<select id="cfg-disposition">
<option value="colonne">Liste + détail côte à côte (deux colonnes)</option>
<option value="modale">Liste en grand + détail en fenêtre centrale</option>
</select>
<p class="indice">« Liste en grand » étale les prospects sur toute la largeur ; un clic ouvre le détail dans une fenêtre centrale. Préférence enregistrée sur cet appareil.</p>
</div>
</section>
<section class="bloc">
<h2>Trajet & relance</h2>
<div class="grille">
@@ -255,6 +284,29 @@
<p class="indice">Le péage est estimé à partir des kilomètres d'autoroute du trajet × ce tarif (≈ 0,10 €/km pour une voiture). Mettez 0 pour ne pas compter de péage.</p>
</section>
<section class="bloc">
<h2>Votre groupe</h2>
<p class="indice">Ces infos sont transmises à l'IA pour personnaliser les messages générés (elles n'apparaissent pas telles quelles : l'IA s'en sert pour rédiger). Laissez vide ce que vous ne voulez pas fournir.</p>
<div class="grille">
<div class="champ">
<label>Nom du groupe</label>
<input id="cfg-groupe-nom" placeholder="Automood">
</div>
<div class="champ">
<label>Style musical</label>
<input id="cfg-groupe-style" placeholder="rock, chanson française, reprises…">
</div>
</div>
<div class="champ" style="margin-top:12px">
<label>Description</label>
<textarea id="cfg-groupe-description" rows="3" placeholder="Trio acoustique, répertoire de reprises et compositions, format adapté aux bars et petites salles…"></textarea>
</div>
<div class="champ" style="margin-top:12px">
<label>Lien (site, réseaux, vidéo…)</label>
<input id="cfg-groupe-lien" type="text" placeholder="https://…">
</div>
</section>
<section class="bloc">
<h2>Message & IA</h2>
<div class="champ">
@@ -296,6 +348,14 @@
</main>
<!-- ===== MODALE DE DÉTAIL (disposition « modale ») ===== -->
<div id="modale-detail" class="modale-fond" hidden>
<div class="modale" role="dialog" aria-modal="true" aria-label="Détail du prospect">
<button type="button" class="modale-fermer" id="modale-fermer" aria-label="Fermer"></button>
<div id="detail-modale-corps"></div>
</div>
</div>
<script>
const COLONNES = ["Nom du prospect","Statut","Département","Ville","Code Postal","Adresse","Date d'ajout",
"Date de contact","Nom de contact","Téléphone","Email","Infos du lieu","Type","Lien Facebook","Notes"];
@@ -354,6 +414,9 @@ document.querySelectorAll(".onglet").forEach(b => b.addEventListener("click", ()
let config = {};
// Disposition de la vue Prospects : "colonne" (liste + détail côte à côte) ou "modale" (liste en grand + fenêtre centrale).
let disposition = localStorage.getItem("disposition") || "colonne";
async function chargerConfig() {
try {
config = await (await fetch("/api/config")).json();
@@ -365,6 +428,10 @@ async function chargerConfig() {
$("cfg-relance").value = config.delai_relance_jours ?? "";
$("cfg-message").value = config.modele_message || "";
$("cfg-ia-modele").value = config.ia_modele || "";
$("cfg-groupe-nom").value = config.groupe_nom || "";
$("cfg-groupe-style").value = config.groupe_style || "";
$("cfg-groupe-description").value = config.groupe_description || "";
$("cfg-groupe-lien").value = config.groupe_lien || "";
afficherRelances();
rafraichirStatutIA();
}
@@ -378,6 +445,10 @@ $("cfg-enregistrer").addEventListener("click", async () => {
delai_relance_jours: $("cfg-relance").value,
modele_message: $("cfg-message").value,
ia_modele: $("cfg-ia-modele").value.trim(),
groupe_nom: $("cfg-groupe-nom").value.trim(),
groupe_style: $("cfg-groupe-style").value.trim(),
groupe_description: $("cfg-groupe-description").value.trim(),
groupe_lien: $("cfg-groupe-lien").value.trim(),
};
const rep = await fetch("/api/config", {
method: "PUT",
@@ -752,18 +823,18 @@ function selectionner(index) {
for (const el of $("liste").querySelectorAll(".item"))
el.classList.toggle("selectionnee", +el.dataset.index === index);
afficherDetail(p);
if (window.innerWidth <= 860) $("detail-pane").scrollIntoView({ behavior: "smooth", block: "start" });
if (disposition === "colonne" && window.innerWidth <= 860) $("detail-pane").scrollIntoView({ behavior: "smooth", block: "start" });
}
function viderDetail() {
selectionIndex = null;
$("detail-pane").innerHTML = `<div class="detail-vide">Sélectionnez un prospect dans la liste pour voir et modifier son détail.</div>`;
for (const el of $("liste").querySelectorAll(".item.selectionnee")) el.classList.remove("selectionnee");
if (disposition === "modale") fermerModale();
}
function afficherDetail(p) {
const pane = $("detail-pane");
pane.innerHTML = "";
function rendreFiche(container, p) {
container.innerHTML = "";
const st = statutDe(p);
const couleur = STATUTS[st] || "#6b7280";
@@ -798,9 +869,31 @@ function afficherDetail(p) {
});
actions.append(suppr);
tete.append(actions);
pane.append(tete);
container.append(tete);
construireDetail(pane, p);
construireDetail(container, p);
}
// Affiche le détail selon la disposition : colonne latérale (« colonne ») ou fenêtre centrale (« modale »).
function afficherDetail(p) {
if (disposition === "modale") {
rendreFiche($("detail-modale-corps"), p);
ouvrirModale();
} else {
rendreFiche($("detail-pane"), p);
}
}
function ouvrirModale() {
$("modale-detail").hidden = false;
document.body.classList.add("modale-ouverte");
}
function fermerModale() {
$("modale-detail").hidden = true;
document.body.classList.remove("modale-ouverte");
selectionIndex = null;
for (const el of $("liste").querySelectorAll(".item.selectionnee")) el.classList.remove("selectionnee");
}
function sectionDetail(titre) {
@@ -983,6 +1076,25 @@ function rafraichirLogsSiOuvert() {
if ($("journal-details").open) chargerLogs();
}
// --- Disposition (colonne / modale) ---
function definirDisposition(val) {
disposition = (val === "modale") ? "modale" : "colonne";
localStorage.setItem("disposition", disposition);
document.body.dataset.disposition = disposition;
$("cfg-disposition").value = disposition;
fermerModale();
viderDetail();
}
$("cfg-disposition").addEventListener("change", () => definirDisposition($("cfg-disposition").value));
$("modale-fermer").addEventListener("click", fermerModale);
$("modale-detail").addEventListener("click", (e) => { if (e.target === $("modale-detail")) fermerModale(); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !$("modale-detail").hidden) fermerModale(); });
document.body.dataset.disposition = disposition;
$("cfg-disposition").value = disposition;
remplirFiltreStatut();
chargerConfig();
chargerListe();