diff --git a/aethas_ip_logs.json b/aethas_ip_logs.json index 3aca4a8..a37d7ad 100644 --- a/aethas_ip_logs.json +++ b/aethas_ip_logs.json @@ -11824,5 +11824,41 @@ "action": "Acc\u00e8s \u00e0 /", "date": "2026-08-23 11:36:33", "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 } ] \ No newline at end of file diff --git a/modules/ai_engine.py b/modules/ai_engine.py index 1b9203a..7cb6265 100644 --- a/modules/ai_engine.py +++ b/modules/ai_engine.py @@ -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): prompt_securise = f"{SYSTEM_CONFIDENTIALITY_INSTRUCTION}\n\nRequête utilisateur :\n{prompt}" + trace_tentatives = [] if multi_ia or fournisseur in ["openrouter_code", "openrouter_droit"]: resultat_multi = executer_synthese_multi_ia(prompt_securise, fournisseur, mode_payant, chat_id) 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": 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] for fonction_api in ordre_repli: + nom_tente = fonction_api.__name__.replace("interroger_", "") try: if fonction_api.__name__ == "interroger_openrouter": 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) 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: + 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}") continue - raise Exception("Tous les fournisseurs d'IA sont indisponibles.") \ No newline at end of file + raise Exception(f"Tous les fournisseurs d'IA ont échoué. Trace : {' | '.join(trace_tentatives)}") \ No newline at end of file diff --git a/modules/chat_handler.py b/modules/chat_handler.py index cbf41f9..682fb8b 100644 --- a/modules/chat_handler.py +++ b/modules/chat_handler.py @@ -138,7 +138,9 @@ async def handle_chat_request(request, session): if prix_estime > obtenir_solde(utilisateur_nom): 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: 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)}" 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) return render_user_msg(display_msg), render_ai_msg(reponse, intention, fournisseur_nom, modele, couleur, execution_time), Script("renderMarkdown();"), render_sidebar() diff --git a/modules/model_updater.py b/modules/model_updater.py index 39100e4..88e30b1 100644 --- a/modules/model_updater.py +++ b/modules/model_updater.py @@ -30,7 +30,8 @@ def _do_update(): pricing = m.get("pricing", {}) # Gratuits purs 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) elif "claude-3.5-sonnet" in m["id"].lower() or "gpt-4o" in m["id"].lower(): modeles_payants.append(m["id"]) diff --git a/modules/router - Copie.py b/modules/router - Copie.py new file mode 100644 index 0000000..d11b0c5 --- /dev/null +++ b/modules/router - Copie.py @@ -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"} diff --git a/modules/theme.py b/modules/theme.py index 063f0ea..e83a8df 100644 --- a/modules/theme.py +++ b/modules/theme.py @@ -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="icon", type="image/svg+xml", href=f"data:image/svg+xml;utf8,{svg_favicon}"), 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; } /* --- 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: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; } .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; } - .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; } - .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; } + .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; } + + /* BULLE IA : Contraste maximal forcé pour le confort de lecture mobile */ + .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); } .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; } @@ -85,7 +89,6 @@ def get_theme_hdrs(): Script(r""" let aethasFiles = new DataTransfer(); - // JS pour le bouton Hamburger window.toggleSidebar = function() { let sidebar = document.querySelector('.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) { let pre = btn.closest('.code-header').nextElementSibling; let code = pre.querySelector('code').innerText; diff --git a/modules/ui_components.py b/modules/ui_components.py index 5d60b4a..47a44f9 100644 --- a/modules/ui_components.py +++ b/modules/ui_components.py @@ -17,14 +17,22 @@ def get_openrouter_stats(): def render_user_msg(text): 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 "" + + # 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( 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_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( 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"), @@ -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"), 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(): history = load_history() @@ -50,8 +67,18 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): messages_ui = [] if chat_id in history: for msg in history[chat_id].get('messages', []): - if msg['role'] == 'user': 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', ''))) + if msg['role'] == 'user': + 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);")) auto_refresh_script = Script('''