Amélioration contraste mobile, traçabilité des IA et filtrage catalogue
Build and Push Docker Image / build-and-push (push) Successful in 35s

This commit is contained in:
xavier committed 2026-08-23 15:09:57 +02:00
1 parent 3e140d7d6f
commit 9fdb2bd73e
7 files changed
+115 -19

No files matched your search

+36
View File
@@ -11824,5 +11824,41 @@
"action": "Acc\u00e8s \u00e0 /", "action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 11:36:33", "date": "2026-08-23 11:36:33",
"timestamp": 1787477793.7782843 "timestamp": 1787477793.7782843
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 14:50:01",
"timestamp": 1787489401.7572532
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /chat",
"date": "2026-08-23 14:53:10",
"timestamp": 1787489590.9684572
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 15:04:55",
"timestamp": 1787490295.170784
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 15:05:06",
"timestamp": 1787490306.0473568
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 15:05:37",
"timestamp": 1787490337.0444038
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 15:08:53",
"timestamp": 1787490533.299349
} }
] ]
+8 -3
View File
@@ -37,11 +37,12 @@ def executer_synthese_multi_ia(prompt_securise: str, fournisseur: str, mode_paya
def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False, multi_ia: bool = False, chat_id: str = None): def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False, multi_ia: bool = False, chat_id: str = None):
prompt_securise = f"{SYSTEM_CONFIDENTIALITY_INSTRUCTION}\n\nRequête utilisateur :\n{prompt}" prompt_securise = f"{SYSTEM_CONFIDENTIALITY_INSTRUCTION}\n\nRequête utilisateur :\n{prompt}"
trace_tentatives = []
if multi_ia or fournisseur in ["openrouter_code", "openrouter_droit"]: if multi_ia or fournisseur in ["openrouter_code", "openrouter_droit"]:
resultat_multi = executer_synthese_multi_ia(prompt_securise, fournisseur, mode_payant, chat_id) resultat_multi = executer_synthese_multi_ia(prompt_securise, fournisseur, mode_payant, chat_id)
if resultat_multi: if resultat_multi:
return resultat_multi return resultat_multi[0], resultat_multi[1], resultat_multi[2], resultat_multi[3], ["🟢 Multi-IA / Synthèse Gemini réussie"]
if fournisseur == "gemini": if fournisseur == "gemini":
ordre_repli = [interroger_gemini, interroger_openrouter, interroger_local_ollama] ordre_repli = [interroger_gemini, interroger_openrouter, interroger_local_ollama]
@@ -51,6 +52,7 @@ def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False
ordre_repli = [interroger_local_ollama, interroger_openrouter, interroger_gemini] ordre_repli = [interroger_local_ollama, interroger_openrouter, interroger_gemini]
for fonction_api in ordre_repli: for fonction_api in ordre_repli:
nom_tente = fonction_api.__name__.replace("interroger_", "")
try: try:
if fonction_api.__name__ == "interroger_openrouter": if fonction_api.__name__ == "interroger_openrouter":
texte, couleur, nom_fournisseur, nom_modele, _, _ = fonction_api( texte, couleur, nom_fournisseur, nom_modele, _, _ = fonction_api(
@@ -60,9 +62,12 @@ def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise) texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise)
if texte: if texte:
return texte, couleur, nom_fournisseur, nom_modele trace_tentatives.append(f"🟢 {nom_tente.upper()} ({nom_modele}) : Succès")
return texte, couleur, nom_fournisseur, nom_modele, trace_tentatives
except Exception as e: except Exception as e:
msg_err = str(e).split("-")[0].strip()[:50] # On raccourcit l'erreur pour l'affichage
trace_tentatives.append(f"🔴 {nom_tente.upper()} : Échec ({msg_err})")
log_event("WARNING", "ORCHESTRATEUR", f"Basculement suite à l'échec de {fonction_api.__name__} : {e}") log_event("WARNING", "ORCHESTRATEUR", f"Basculement suite à l'échec de {fonction_api.__name__} : {e}")
continue continue
raise Exception("Tous les fournisseurs d'IA sont indisponibles.") raise Exception(f"Tous les fournisseurs d'IA ont échoué. Trace : {' | '.join(trace_tentatives)}")
+13 -2
View File
@@ -138,7 +138,9 @@ async def handle_chat_request(request, session):
if prix_estime > obtenir_solde(utilisateur_nom): if prix_estime > obtenir_solde(utilisateur_nom):
return Div(H3("❌ Solde Insuffisant", style="color:red;"), P("Rechargez votre compte."), style="background:#300; padding:15px;") return Div(H3("❌ Solde Insuffisant", style="color:red;"), P("Rechargez votre compte."), style="background:#300; padding:15px;")
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia, chat_id=chat_id) reponse, couleur, fournisseur_nom, modele, trace_route = executer_requete_ia(
fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia, chat_id=chat_id
)
if "openrouter" in fournisseur_nom.lower() and mode_payant: if "openrouter" in fournisseur_nom.lower() and mode_payant:
verifier_et_valider_budget(full_prompt, modele, utilisateur_nom=utilisateur_nom, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia) verifier_et_valider_budget(full_prompt, modele, utilisateur_nom=utilisateur_nom, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
@@ -154,7 +156,16 @@ async def handle_chat_request(request, session):
if file_names: display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(file_names)}" if file_names: display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(file_names)}"
history[chat_id]["messages"].append({"role": "user", "content": display_msg}) history[chat_id]["messages"].append({"role": "user", "content": display_msg})
history[chat_id]["messages"].append({"role": "ai", "content": reponse, "intention": intention, "fournisseur": fournisseur_nom, "modele": modele, "couleur": couleur, "execution_time": execution_time}) history[chat_id]["messages"].append({
"role": "ai",
"content": reponse,
"intention": intention,
"fournisseur": fournisseur_nom,
"modele": modele,
"couleur": couleur,
"execution_time": execution_time,
"trace": trace_route # <--- Ajout de la traçabilité
})
save_history(history) save_history(history)
return render_user_msg(display_msg), render_ai_msg(reponse, intention, fournisseur_nom, modele, couleur, execution_time), Script("renderMarkdown();"), render_sidebar() return render_user_msg(display_msg), render_ai_msg(reponse, intention, fournisseur_nom, modele, couleur, execution_time), Script("renderMarkdown();"), render_sidebar()
+2 -1
View File
@@ -30,7 +30,8 @@ def _do_update():
pricing = m.get("pricing", {}) pricing = m.get("pricing", {})
# Gratuits purs # Gratuits purs
if pricing.get("prompt") == "0" and pricing.get("completion") == "0": if pricing.get("prompt") == "0" and pricing.get("completion") == "0":
modeles_gratuits.append(m["id"]) if "nvidia/nemotron" not in m["id"].lower():
modeles_gratuits.append(m["id"])
# Ciblage des payants ultra-performants (Claude & OpenAI) # Ciblage des payants ultra-performants (Claude & OpenAI)
elif "claude-3.5-sonnet" in m["id"].lower() or "gpt-4o" in m["id"].lower(): elif "claude-3.5-sonnet" in m["id"].lower() or "gpt-4o" in m["id"].lower():
modeles_payants.append(m["id"]) modeles_payants.append(m["id"])
+14
View File
@@ -0,0 +1,14 @@
import json
import re
def nettoyer_decision_router(raw_text):
try:
clean = re.sub(r'```(?:json)?\s*', '', raw_text)
clean = clean.replace('```', '').strip()
start = clean.find('{')
end = clean.rfind('}') + 1
if start != -1 and end != 0:
return json.loads(clean[start:end])
except Exception:
pass
return {"intention": "Analyse", "fournisseur": "gemini"}
+10 -8
View File
@@ -11,7 +11,7 @@ def get_theme_hdrs():
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"), Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"),
Link(rel="icon", type="image/svg+xml", href=f"data:image/svg+xml;utf8,{svg_favicon}"), Link(rel="icon", type="image/svg+xml", href=f"data:image/svg+xml;utf8,{svg_favicon}"),
Style(""" Style("""
:root { --bg-dark: #121212; --bg-panel: #1e1e24; --accent-blue: #00e5ff; --accent-purple: #b300ff; --text-main: #e0e0e0; } :root { --bg-dark: #121212; --bg-panel: #1e1e24; --accent-blue: #00e5ff; --accent-purple: #b300ff; --text-main: #f8fafc; }
body { background-color: var(--bg-dark); color: var(--text-main); font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; display: flex; height: 100vh; overflow: hidden; box-sizing: border-box; } body { background-color: var(--bg-dark); color: var(--text-main); font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; display: flex; height: 100vh; overflow: hidden; box-sizing: border-box; }
/* --- SIDEBAR CLASSIQUE --- */ /* --- SIDEBAR CLASSIQUE --- */
@@ -30,13 +30,17 @@ def get_theme_hdrs():
.history-btn { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em; } .history-btn { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em; }
.history-btn:hover { color: var(--accent-blue); } .history-btn:hover { color: var(--accent-blue); }
/* --- CONTENU CENTRAL --- */ /* --- CONTENU CENTRAL & CONTRASTE MOBILE ACCRU --- */
.main-content { flex: 1; display: flex; flex-direction: column; height: 100vh; position: relative; box-sizing: border-box; } .main-content { flex: 1; display: flex; flex-direction: column; height: 100vh; position: relative; box-sizing: border-box; }
.chat-container { flex: 1; padding: 20px; max-width: 1000px; margin: 0 auto; width: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; padding-bottom: 160px; box-sizing: border-box; } .chat-container { flex: 1; padding: 20px; max-width: 1000px; margin: 0 auto; width: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; padding-bottom: 160px; box-sizing: border-box; }
.user-bubble { background-color: #2a2a35; padding: 15px 20px; border-radius: 15px 15px 0 15px; align-self: flex-end; max-width: 80%; border: 1px solid #444; margin-left: auto; word-break: break-word; overflow-wrap: break-word; } .user-bubble { background-color: #2a2a35; color: #ffffff; padding: 15px 20px; border-radius: 15px 15px 0 15px; align-self: flex-end; max-width: 80%; border: 1px solid #555; margin-left: auto; word-break: break-word; overflow-wrap: break-word; }
.ai-bubble { background-color: var(--bg-panel); padding: 15px 20px; border-radius: 15px 15px 15px 0; align-self: flex-start; max-width: 90%; width: 100%; border: 1px solid #333; word-break: break-word; overflow-wrap: break-word; }
.msg-actions { display: flex; gap: 10px; margin-top: 15px; border-top: 1px dashed #333; padding-top: 10px; justify-content: flex-end; flex-wrap: wrap; } /* BULLE IA : Contraste maximal forcé pour le confort de lecture mobile */
.msg-action-btn { background: transparent; border: 1px solid #444; color: #888; border-radius: 15px; padding: 5px 12px; cursor: pointer; font-size: 0.85em; transition: 0.2s; } .ai-bubble { background-color: var(--bg-panel); color: #ffffff !important; padding: 15px 20px; border-radius: 15px 15px 15px 0; align-self: flex-start; max-width: 90%; width: 100%; border: 1px solid #444; word-break: break-word; overflow-wrap: break-word; }
.markdown-rendered, .markdown-rendered p, .markdown-rendered li, .markdown-rendered span, .markdown-rendered strong { color: #ffffff !important; }
.msg-actions { display: flex; gap: 10px; margin-top: 15px; border-top: 1px dashed #444; padding-top: 10px; justify-content: flex-end; flex-wrap: wrap; }
.msg-action-btn { background: transparent; border: 1px solid #555; color: #ccc; border-radius: 15px; padding: 5px 12px; cursor: pointer; font-size: 0.85em; transition: 0.2s; }
.msg-action-btn:hover { background: #2a2a35; color: var(--accent-blue); border-color: var(--accent-blue); } .msg-action-btn:hover { background: #2a2a35; color: var(--accent-blue); border-color: var(--accent-blue); }
.code-header { background: #16161a; padding: 8px 15px; display: flex; justify-content: space-between; align-items: center; border-radius: 8px 8px 0 0; font-family: monospace; font-size: 0.85em; border: 1px solid #333; border-bottom: none; } .code-header { background: #16161a; padding: 8px 15px; display: flex; justify-content: space-between; align-items: center; border-radius: 8px 8px 0 0; font-family: monospace; font-size: 0.85em; border: 1px solid #333; border-bottom: none; }
.code-actions-group { display: flex; gap: 8px; } .code-actions-group { display: flex; gap: 8px; }
@@ -85,7 +89,6 @@ def get_theme_hdrs():
Script(r""" Script(r"""
let aethasFiles = new DataTransfer(); let aethasFiles = new DataTransfer();
// JS pour le bouton Hamburger
window.toggleSidebar = function() { window.toggleSidebar = function() {
let sidebar = document.querySelector('.sidebar'); let sidebar = document.querySelector('.sidebar');
if (sidebar) { if (sidebar) {
@@ -250,7 +253,6 @@ def get_theme_hdrs():
}); });
}); });
// ... (Fonctions de téléchargement ZIP, PDF, DOC, TXT, JSON et Partage identiques et préservées)
function downloadCodeZip(btn, lang) { function downloadCodeZip(btn, lang) {
let pre = btn.closest('.code-header').nextElementSibling; let pre = btn.closest('.code-header').nextElementSibling;
let code = pre.querySelector('code').innerText; let code = pre.querySelector('code').innerText;
+32 -5
View File
@@ -17,14 +17,22 @@ def get_openrouter_stats():
def render_user_msg(text): def render_user_msg(text):
return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble") return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble")
def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None): def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None, trace=None):
time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else "" time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else ""
# Construction de la trace visuelle si elle existe
trace_html = ""
if trace and isinstance(trace, list):
badges = [Span(t, style="font-size:0.75em; background:#222; padding:2px 6px; border-radius:4px; border:1px solid #444;") for t in trace]
trace_html = Div(Span("Route : ", style="color:#888; font-size:0.75em;"), *badges, style="display:flex; gap:5px; align-items:center; margin-bottom:8px; flex-wrap:wrap;")
encart_info = Div( encart_info = Div(
Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"), Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
Span(f"🏢 {nom_fournisseur}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"), Span(f"🏢 {nom_fournisseur}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"), Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"),
time_badge, style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;" time_badge, style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 5px;"
) )
actions = Div( actions = Div(
Button("📄 PDF", onclick="downloadResponsePDF(this)", cls="msg-action-btn", title="Télécharger en PDF"), Button("📄 PDF", onclick="downloadResponsePDF(this)", cls="msg-action-btn", title="Télécharger en PDF"),
Button("📘 DOC", onclick="downloadResponseDOC(this)", cls="msg-action-btn", title="Télécharger en Word"), Button("📘 DOC", onclick="downloadResponseDOC(this)", cls="msg-action-btn", title="Télécharger en Word"),
@@ -34,7 +42,16 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi
Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par courriel"), Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par courriel"),
cls="msg-actions" cls="msg-actions"
) )
return Div(encart_info, Div(text, cls="markdown-raw", style="display:none;"), Div(cls="markdown-rendered", style="line-height: 1.6;"), actions, cls="ai-bubble", style=f"border-left: 4px solid {couleur};")
return Div(
trace_html, # Affichage du chemin de routage
encart_info,
Div(text, cls="markdown-raw", style="display:none;"),
Div(cls="markdown-rendered", style="line-height: 1.6;"),
actions,
cls="ai-bubble",
style=f"border-left: 4px solid {couleur};"
)
def render_sidebar(): def render_sidebar():
history = load_history() history = load_history()
@@ -50,8 +67,18 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
messages_ui = [] messages_ui = []
if chat_id in history: if chat_id in history:
for msg in history[chat_id].get('messages', []): for msg in history[chat_id].get('messages', []):
if msg['role'] == 'user': messages_ui.append(render_user_msg(msg['content'])) if msg['role'] == 'user':
else: messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333'), msg.get('execution_time', ''))) messages_ui.append(render_user_msg(msg['content']))
else:
messages_ui.append(render_ai_msg(
msg['content'],
msg.get('intention', ''),
msg.get('fournisseur', ''),
msg.get('modele', ''),
msg.get('couleur', '#333'),
msg.get('execution_time', ''),
msg.get('trace', []) # <--- C'est ici qu'on transmet la trace aux anciens messages !
))
if messages_ui: messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) if messages_ui: messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
auto_refresh_script = Script(''' auto_refresh_script = Script('''