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:
13
app.py
13
app.py
@@ -46,6 +46,10 @@ CONFIG_DEFAUT = {
|
|||||||
"Bien cordialement,"
|
"Bien cordialement,"
|
||||||
),
|
),
|
||||||
"ia_modele": ia.MODELE_DEFAUT,
|
"ia_modele": ia.MODELE_DEFAUT,
|
||||||
|
"groupe_nom": "",
|
||||||
|
"groupe_style": "",
|
||||||
|
"groupe_description": "",
|
||||||
|
"groupe_lien": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
app = Flask(__name__, static_folder="static")
|
app = Flask(__name__, static_folder="static")
|
||||||
@@ -368,7 +372,8 @@ 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", "ia_modele"):
|
for cle in ("adresse_depart", "modele_message", "ia_modele",
|
||||||
|
"groupe_nom", "groupe_style", "groupe_description", "groupe_lien"):
|
||||||
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"):
|
||||||
@@ -411,6 +416,12 @@ def api_message():
|
|||||||
message = ia.generer_message(
|
message = ia.generer_message(
|
||||||
prospect, config.get("modele_message", ""), mode,
|
prospect, config.get("modele_message", ""), mode,
|
||||||
nom_modele=config.get("ia_modele") or ia.MODELE_DEFAUT,
|
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})
|
return jsonify({"message": message})
|
||||||
except ia.IANonConnecte as e:
|
except ia.IANonConnecte as e:
|
||||||
|
|||||||
47
ia.py
47
ia.py
@@ -127,6 +127,25 @@ def _infos_prospect(prospect):
|
|||||||
return "\n".join(f"- {cle} : {val}" for cle, val in champs if (val or "").strip())
|
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):
|
def _message_erreur(exc):
|
||||||
bas = str(exc).lower()
|
bas = str(exc).lower()
|
||||||
if "rate" in bas or "429" in bas or "limit" in bas or "quota" in bas:
|
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}"
|
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.
|
"""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 == "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
|
mode == "peaufiner" : on substitue d'abord le modèle, puis l'IA le reformule sans en
|
||||||
changer le sens.
|
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
|
nom_modele = nom_modele or MODELE_DEFAUT
|
||||||
if not est_connecte():
|
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)"
|
infos = _infos_prospect(prospect) or "- (aucune information détaillée disponible)"
|
||||||
|
bloc_groupe = _infos_groupe(groupe)
|
||||||
|
|
||||||
if mode == "peaufiner":
|
if mode == "peaufiner":
|
||||||
brouillon = _message_modele(prospect, modele)
|
brouillon = _message_modele(prospect, modele)
|
||||||
systeme = (
|
parties = [
|
||||||
"Tu rédiges des messages de prise de contact en français pour proposer "
|
"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 : "
|
"l'organisation de concerts à des établissements."
|
||||||
"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 "
|
if bloc_groupe:
|
||||||
"commentaire."
|
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}"
|
utilisateur = f"Brouillon à améliorer :\n{brouillon}\n\nInfos sur l'établissement :\n{infos}"
|
||||||
else:
|
else:
|
||||||
systeme = (
|
parties = [
|
||||||
"Tu rédiges des messages de prise de contact en français pour proposer "
|
"Tu rédiges des messages de prise de contact en français pour proposer "
|
||||||
"l'organisation de concerts à des établissements (bars, restaurants, salles...)."
|
"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 "
|
"Le message doit être court, personnalisé, chaleureux et se terminer par une "
|
||||||
"question ouvrant l'échange. Réponds UNIQUEMENT par le message final, sans "
|
"question ouvrant l'échange. Réponds UNIQUEMENT par le message final, sans "
|
||||||
"commentaire ni objet d'e-mail."
|
"commentaire ni objet d'e-mail."
|
||||||
)
|
)
|
||||||
|
systeme = "\n\n".join(parties)
|
||||||
utilisateur = (
|
utilisateur = (
|
||||||
f"Rédige un message de prise de contact pour cet établissement :\n{infos}\n\n"
|
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}"
|
f"Inspire-toi de ce modèle pour le ton et le style :\n{modele}"
|
||||||
|
|||||||
@@ -136,6 +136,23 @@
|
|||||||
.spinner.sombre { border-color: var(--accent); border-top-color: transparent; }
|
.spinner.sombre { border-color: var(--accent); border-top-color: transparent; }
|
||||||
@keyframes tourne { to { transform: rotate(360deg); } }
|
@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) {
|
@media (max-width: 860px) {
|
||||||
.deux-colonnes { grid-template-columns: 1fr; }
|
.deux-colonnes { grid-template-columns: 1fr; }
|
||||||
.colonne-detail { position: static; max-height: none; }
|
.colonne-detail { position: static; max-height: none; }
|
||||||
@@ -228,6 +245,18 @@
|
|||||||
|
|
||||||
<!-- ===== VUE RÉGLAGES ===== -->
|
<!-- ===== VUE RÉGLAGES ===== -->
|
||||||
<div class="vue" id="vue-reglages" hidden>
|
<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">
|
<section class="bloc">
|
||||||
<h2>Trajet & relance</h2>
|
<h2>Trajet & relance</h2>
|
||||||
<div class="grille">
|
<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>
|
<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>
|
||||||
|
|
||||||
|
<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">
|
<section class="bloc">
|
||||||
<h2>Message & IA</h2>
|
<h2>Message & IA</h2>
|
||||||
<div class="champ">
|
<div class="champ">
|
||||||
@@ -296,6 +348,14 @@
|
|||||||
|
|
||||||
</main>
|
</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>
|
<script>
|
||||||
const COLONNES = ["Nom du prospect","Statut","Département","Ville","Code Postal","Adresse","Date d'ajout",
|
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"];
|
"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 = {};
|
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() {
|
async function chargerConfig() {
|
||||||
try {
|
try {
|
||||||
config = await (await fetch("/api/config")).json();
|
config = await (await fetch("/api/config")).json();
|
||||||
@@ -365,6 +428,10 @@ async function chargerConfig() {
|
|||||||
$("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 || "";
|
$("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();
|
afficherRelances();
|
||||||
rafraichirStatutIA();
|
rafraichirStatutIA();
|
||||||
}
|
}
|
||||||
@@ -378,6 +445,10 @@ $("cfg-enregistrer").addEventListener("click", async () => {
|
|||||||
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(),
|
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", {
|
const rep = await fetch("/api/config", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@@ -752,18 +823,18 @@ function selectionner(index) {
|
|||||||
for (const el of $("liste").querySelectorAll(".item"))
|
for (const el of $("liste").querySelectorAll(".item"))
|
||||||
el.classList.toggle("selectionnee", +el.dataset.index === index);
|
el.classList.toggle("selectionnee", +el.dataset.index === index);
|
||||||
afficherDetail(p);
|
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() {
|
function viderDetail() {
|
||||||
selectionIndex = null;
|
selectionIndex = null;
|
||||||
$("detail-pane").innerHTML = `<div class="detail-vide">Sélectionnez un prospect dans la liste pour voir et modifier son détail.</div>`;
|
$("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");
|
for (const el of $("liste").querySelectorAll(".item.selectionnee")) el.classList.remove("selectionnee");
|
||||||
|
if (disposition === "modale") fermerModale();
|
||||||
}
|
}
|
||||||
|
|
||||||
function afficherDetail(p) {
|
function rendreFiche(container, p) {
|
||||||
const pane = $("detail-pane");
|
container.innerHTML = "";
|
||||||
pane.innerHTML = "";
|
|
||||||
|
|
||||||
const st = statutDe(p);
|
const st = statutDe(p);
|
||||||
const couleur = STATUTS[st] || "#6b7280";
|
const couleur = STATUTS[st] || "#6b7280";
|
||||||
@@ -798,9 +869,31 @@ function afficherDetail(p) {
|
|||||||
});
|
});
|
||||||
actions.append(suppr);
|
actions.append(suppr);
|
||||||
tete.append(actions);
|
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) {
|
function sectionDetail(titre) {
|
||||||
@@ -983,6 +1076,25 @@ function rafraichirLogsSiOuvert() {
|
|||||||
if ($("journal-details").open) chargerLogs();
|
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();
|
remplirFiltreStatut();
|
||||||
chargerConfig();
|
chargerConfig();
|
||||||
chargerListe();
|
chargerListe();
|
||||||
|
|||||||
Reference in New Issue
Block a user