From d47bd673911980e8744160be42ccac0f1bdb5931 Mon Sep 17 00:00:00 2001 From: Xavier Date: Sun, 16 Aug 2026 22:26:02 +0200 Subject: [PATCH] maj auto 1 --- .gitignore | 1 + main.py | 222 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 151 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index 1f2b607..7505227 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ venv/ .env .sesskey aethas_history.json +Echange GEMINY.txt \ No newline at end of file diff --git a/main.py b/main.py index 8a98323..7134014 100644 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ import requests from bs4 import BeautifulSoup from dotenv import load_dotenv from fasthtml.common import * +from starlette.responses import FileResponse from openai import OpenAI from groq import Groq from google import genai @@ -25,6 +26,26 @@ def load_history(): def save_history(history): with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, indent=4) +# --- SCRIPT DE NETTOYAGE AUTO --- +def nettoyer_vieilles_discussions(): + history = load_history() + now = time.time() + un_an_en_secondes = 365 * 24 * 60 * 60 + a_supprimer = [] + + for chat_id, data in history.items(): + if not data.get('pinned', False): + if now - data.get('timestamp', now) > un_an_en_secondes: + a_supprimer.append(chat_id) + + if a_supprimer: + for chat_id in a_supprimer: + del history[chat_id] + save_history(history) + print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) de plus de 365 jours supprimée(s).") + +nettoyer_vieilles_discussions() + svg_favicon = "%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cstyle%3E@keyframes pulse { 0% { opacity: 1; stroke-width: 5; } 50% { opacity: 0.5; stroke-width: 7; } 100% { opacity: 1; stroke-width: 5; } } .neon { animation: pulse 2s infinite; transform-origin: 50px 50px; transform: rotate(45deg); }%3C/style%3E%3Crect width='100' height='100' fill='%23121212'/%3E%3Cg class='neon'%3E%3Crect x='25' y='25' width='50' height='50' fill='none' stroke='%2300e5ff'/%3E%3Cpath d='M25 50 L75 50 M50 25 L50 75' stroke='%23b300ff'/%3E%3Ccircle cx='50' cy='50' r='8' fill='%23e0e0e0'/%3E%3C/g%3E%3C/svg%3E" theme_hdrs = ( @@ -43,11 +64,10 @@ theme_hdrs = ( .sidebar-history { flex: 1; overflow-y: auto; margin-top: 15px; margin-bottom: 15px; } .sidebar-footer { flex-shrink: 0; border-top: 1px solid #333; padding-top: 15px; text-align: center; } - /* CORRECTION: Boutons disposés en colonne proprement */ .nav-buttons { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; } .new-chat-btn { display:block; text-align:center; text-decoration:none; background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: 0.2s; } .new-chat-btn:hover { background: rgba(0, 229, 255, 0.1); } - .export-btn { background: #2a2a35; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; cursor: pointer; transition: 0.2s; font-size: 1em; text-align:center;} + .export-btn { background: #2a2a35; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; cursor: pointer; transition: 0.2s; font-size: 1em; text-align:center; text-decoration:none; display:block;} .export-btn:hover { background: #444; } .history-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #222; font-size: 0.9em; transition: 0.2s; } @@ -86,64 +106,107 @@ theme_hdrs = ( .btn-submit { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; width: 36px !important; height: 36px !important; min-width: 36px !important; max-width: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.1em; flex-shrink: 0 !important; box-sizing: border-box; } .btn-submit:active { transform: scale(0.95); } + + .htmx-indicator { display: none; } + .htmx-request .htmx-indicator { display: flex; } + .htmx-request.htmx-indicator { display: flex; } + + .tech-loader-container { display: flex; align-items: center; gap: 15px; color: var(--accent-blue); font-style: italic; background: var(--bg-panel); padding: 15px 20px; border-radius: 15px 15px 15px 0; border: 1px dashed var(--accent-blue); width: fit-content; margin-bottom: 20px; font-size: 0.9em; box-shadow: 0 0 10px rgba(0, 229, 255, 0.1); } + .tech-spinner { width: 20px; height: 20px; border: 3px solid rgba(0, 229, 255, 0.2); border-top-color: var(--accent-blue); border-radius: 50%; animation: spin 1s linear infinite; } + @keyframes spin { to { transform: rotate(360deg); } } """), Script(""" function renderMarkdown() { - document.querySelectorAll('.markdown-raw').forEach(el => { - let container = el.nextElementSibling; - container.innerHTML = marked.parse(el.textContent); - el.classList.remove('markdown-raw'); - - container.querySelectorAll('pre').forEach(pre => { - let code = pre.querySelector('code'); - let lang = code.className.replace('language-', '') || 'texte'; - let header = document.createElement('div'); - header.className = 'code-header'; - header.innerHTML = `${lang.toUpperCase()}
`; - pre.parentNode.insertBefore(header, pre); - hljs.highlightElement(code); + try { + document.querySelectorAll('.markdown-raw').forEach(el => { + if (el.dataset.rendered) return; + let container = el.nextElementSibling; + + if (typeof marked !== 'undefined') { + container.innerHTML = marked.parse(el.textContent); + } else { + container.innerText = el.textContent; + } + + el.dataset.rendered = "true"; + el.classList.remove('markdown-raw'); + + container.querySelectorAll('pre').forEach(pre => { + try { + let code = pre.querySelector('code'); + if(code) { + let rawLang = code.className.replace('language-', '') || 'texte'; + let lang = rawLang.toUpperCase(); + let header = document.createElement('div'); + header.className = 'code-header'; + header.innerHTML = '' + lang + '
'; + pre.parentNode.insertBefore(header, pre); + if (typeof hljs !== 'undefined') hljs.highlightElement(code); + } + } catch(err) { console.warn("Aethas - Erreur formatage code", err); } + }); }); - }); - let chat = document.getElementById('chat-history'); - chat.scrollTop = chat.scrollHeight; + let chat = document.getElementById('chat-history'); + if (chat) chat.scrollTop = chat.scrollHeight; + } catch(globalErr) { + console.error("Aethas - Erreur fatale rendu Markdown", globalErr); + } } - // --- SCRIPTS D'EXPORTATION --- function downloadResponsePDF(btn) { - let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); - let originalColor = content.style.color; - let originalBg = content.style.backgroundColor; - - // Forçage du texte en noir sur blanc pour le PDF - content.style.color = '#000000'; - content.style.backgroundColor = '#ffffff'; - content.style.padding = '20px'; - - let opt = { - margin: 10, - filename: 'Aethas38_Reponse.pdf', - image: { type: 'jpeg', quality: 0.98 }, - html2canvas: { scale: 2, backgroundColor: '#ffffff' }, - jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } - }; - - html2pdf().set(opt).from(content).save().then(() => { - content.style.color = originalColor; - content.style.backgroundColor = originalBg; - content.style.padding = ''; - }); + try { + let renderedNode = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); + let rawNode = btn.closest('.ai-bubble').querySelector('.markdown-raw'); + let originalContent = (renderedNode && renderedNode.innerHTML.trim() !== "") ? renderedNode : rawNode; + + let content = originalContent.cloneNode(true); + let wrapper = document.createElement('div'); + wrapper.style.backgroundColor = '#ffffff'; + wrapper.style.padding = '20px'; + wrapper.style.fontFamily = 'Arial, sans-serif'; + wrapper.style.color = '#000000'; + + content.querySelectorAll('*').forEach(el => { + el.style.color = '#000000'; + if (el.tagName === 'PRE' || el.tagName === 'CODE' || el.tagName === 'TABLE' || el.tagName === 'TH' || el.tagName === 'TD') { + el.style.backgroundColor = '#f4f4f4'; + el.style.borderColor = '#aaaaaa'; + el.style.borderWidth = '1px'; + el.style.borderStyle = 'solid'; + el.style.borderCollapse = 'collapse'; + } else { + el.style.backgroundColor = 'transparent'; + } + }); + + wrapper.appendChild(content); + + let opt = { + margin: 10, + filename: 'Aethas38_Reponse.pdf', + image: { type: 'jpeg', quality: 0.98 }, + html2canvas: { scale: 2, backgroundColor: '#ffffff', useCORS: true }, + jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } + }; + html2pdf().set(opt).from(wrapper).save(); + } catch (err) { + alert("Erreur lors de la génération du PDF."); + } } function downloadResponseTXT(btn) { - let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); - let blob = new Blob([content.innerText], {type: 'text/plain;charset=utf-8'}); + let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; + let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; + let finalContent = rendered ? rendered : raw; + let blob = new Blob([finalContent], {type: 'text/plain;charset=utf-8'}); let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.txt'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function downloadResponseDOC(btn) { - let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML; + let renderedNode = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); + let content = (renderedNode && renderedNode.innerHTML.trim() !== "") ? renderedNode.innerHTML : btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; let preHtml = "Aethas Export"; let postHtml = ""; let blob = new Blob(['\\ufeff', preHtml + content + postHtml], { type: 'application/msword' }); @@ -152,8 +215,10 @@ theme_hdrs = ( } function downloadResponseJSON(btn) { - let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; - let data = { "timestamp": new Date().toISOString(), "content": content }; + let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; + let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; + let finalContent = rendered ? rendered : raw; + let data = { "timestamp": new Date().toISOString(), "content": finalContent }; let blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json;charset=utf-8'}); let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.json'; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -167,29 +232,36 @@ theme_hdrs = ( setTimeout(() => btn.innerText = "📋 Copier", 2000); } - function exportChat() { - let chat = document.getElementById('chat-history'); - if(!chat) return; - let blob = new Blob([chat.innerText], {type: 'text/plain;charset=utf-8'}); - let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Discussion_Globale.txt'; - document.body.appendChild(link); link.click(); document.body.removeChild(link); - } - function handleFileSelection(input) { let previewList = document.getElementById('file-preview-list'); previewList.innerHTML = ''; if (input.files) { for (let i = 0; i < input.files.length; i++) { - let pill = document.createElement('div'); pill.className = 'file-pill'; pill.innerHTML = `📎 ${input.files[i].name}`; + let pill = document.createElement('div'); pill.className = 'file-pill'; pill.innerHTML = '📎 ' + input.files[i].name; previewList.appendChild(pill); } } } function autoResize(textarea) { + if(!textarea) return; textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; } + + function shareWhatsApp(btn) { + let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; + let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; + let content = rendered ? rendered : raw; + window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent("🤖 *Aethas38 (IA) :*\n\n" + content), '_blank'); + } + + function shareEmail(btn) { + let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; + let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; + let content = rendered ? rendered : raw; + window.open('mailto:?subject=Réponse Aethas38 IA&body=' + encodeURIComponent(content), '_self'); + } """) ) @@ -206,8 +278,11 @@ def interroger_gemini_cascade(prompt): modeles_a_tester = ["gemini-3.1-pro-preview", "gemini-3.1-pro", "gemini-2.5-pro", "gemini-3.7-flash", "gemini-3.6-flash"] for nom_modele in modeles_a_tester: try: - chat = gemini_client.chats.create(model=nom_modele, config={"system_instruction": "Réponds toujours dans la langue de l'utilisateur."}) + config = {"system_instruction": "Réponds toujours dans la langue de l'utilisateur. Utilise exclusivement le format Markdown pour les tableaux et la mise en forme. N'utilise JAMAIS de code LaTeX."} + chat = gemini_client.chats.create(model=nom_modele, config=config) ia_response = chat.send_message(prompt) + if not ia_response.text: + raise Exception("Réponse vide de Google.") return ia_response.text, nom_modele except Exception: continue @@ -217,7 +292,6 @@ def rechercher_sur_google(requete): try: from googlesearch import search urls = [] - # Modification ici : num_results=5 au lieu de 3 for url in search(requete, num_results=5, lang="fr"): urls.append(url) return urls @@ -276,6 +350,8 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur): Button("📘 DOC", onclick="downloadResponseDOC(this)", cls="msg-action-btn", title="Télécharger en Word"), Button("📝 TXT", onclick="downloadResponseTXT(this)", cls="msg-action-btn", title="Télécharger en Texte"), Button("🗄️ JSON", onclick="downloadResponseJSON(this)", cls="msg-action-btn", title="Télécharger en JSON"), + Button("💬 WhatsApp", onclick="shareWhatsApp(this)", cls="msg-action-btn", title="Partager sur WhatsApp"), + Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par Email"), cls="msg-actions" ) @@ -322,7 +398,7 @@ def get(session): messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333'))) if messages_ui: - messages_ui.append(Script("setTimeout(renderMarkdown, 100);")) + messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) return Title("Aethas38 - Mine_AI"), Body( Div( @@ -330,7 +406,7 @@ def get(session): Div(H2("AETHAS 38", style="color:#00e5ff; margin:0; text-align:center;"), P("Tech Core", style="font-size:0.7em; color:#888; text-align:center;"), margin_bottom="20px"), Div( A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), - Button("💾 Exporter JSON", cls="export-btn", onclick="exportChat()", title="Télécharger la conversation Globale"), + A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn", title="Télécharger la conversation Globale"), cls="nav-buttons" ), cls="sidebar-top" @@ -344,7 +420,13 @@ def get(session): ), Div( - Div(*messages_ui, id="chat-history", cls="chat-container"), + Div(*messages_ui, id="chat-history", cls="chat-container", style="padding-bottom: 20px;"), + + Div( + Div(Div(cls="tech-spinner"), Span("Aethas Core analyse la requête..."), cls="tech-loader-container"), + id="global-loader", cls="htmx-indicator", style="max-width: 1000px; margin: 0 auto; width: 100%; padding: 0 20px; box-sizing: border-box; padding-bottom: 120px;" + ), + Div( Form( Input(type="hidden", name="chat_id", value=chat_id), @@ -362,6 +444,7 @@ def get(session): hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", + hx_indicator="#global-loader", hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='auto';", enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;" @@ -389,7 +472,6 @@ async def post(request, session: dict): history = load_history() - # --- 1. INTELLIGENCE DE SÉCURITÉ DU ROUTEUR (Smarter Interceptor) --- security_check_prompt = f"""Tu es l'Aiguilleur du système Aethas38. Ton rôle EXCLUSIF est d'analyser et de router la demande. RÈGLE DE SÉCURITÉ ABSOLUE : Les recherches web, les serveurs Minecraft, les modpacks (ex: ATM10) SONT AUTORISÉS. @@ -411,7 +493,6 @@ async def post(request, session: dict): intention = decision.get('intention', '') fournisseur = decision.get("fournisseur", "local").lower() - # Si Qwen détecte un VRAI danger if intention == "Alerte Securite": if chat_id not in history: history[chat_id] = {"title": "Alerte Sécurité...", "pinned": False, "timestamp": time.time(), "messages": []} @@ -423,7 +504,6 @@ async def post(request, session: dict): return render_user_msg(prompt), render_ai_msg(reponse_securite, "Alerte Sécurité", "AETHAS CORE", "Cyber-Sec", "#ff0044"), Script("renderMarkdown();"), render_sidebar() - # --- 2. TRAITEMENT CLASSIQUE --- urls_to_scrape = re.findall(r'https?://[^\s]+', prompt) mots_cles = ["cherche sur", "recherche sur", "sur internet", "sur le web", "sur le net"] if any(mot in prompt.lower() for mot in mots_cles): @@ -451,25 +531,18 @@ async def post(request, session: dict): if web_content: full_prompt += f"\n\n[INSTRUCTION SYSTÈME STRICTE : Ne dis JAMAIS que tu ne peux pas naviguer sur internet ou télécharger des fichiers. Le système a déjà fait la recherche web pour toi. Voici le contenu brut extrait des sites. Utilise-le IMPÉRATIVEMENT pour formuler ta réponse sans mentionner tes propres limitations :]\n{web_content}" - prompt_tokens = len(full_prompt.split()) * 2 - completion_tokens = 0 - - # Fonction d'appel à Groq centralisée pour éviter la duplication de code def appeler_groq_api(prompt_text): resp = groq_client.chat.completions.create( - messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur. Tu as accès aux recherches web fournies."}, {"role": "user", "content": prompt_text}], + messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur. Tu as accès aux recherches web fournies. Utilise uniquement Markdown. Pas de code LaTeX."}, {"role": "user", "content": prompt_text}], model="llama-3.3-70b-versatile" ) return resp.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b" if fournisseur == "gemini": try: - # On tente Gemini en premier reponse_finale, nom_modele = interroger_gemini_cascade(full_prompt) couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI" - completion_tokens = len(reponse_finale.split()) * 2 except Exception as e_gemini: - # 🔄 IA DE SECOURS : Si Gemini échoue, on bascule silencieusement sur Groq print(f"⚠️ Alerte système : Gemini indisponible ({e_gemini}). Bascule sur l'IA de secours (Groq)...") intention = intention + " (Secours Groq)" try: @@ -487,7 +560,7 @@ async def post(request, session: dict): else: try: - ia_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": full_prompt}]) + ia_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur. Utilise le Markdown."}, {"role": "user", "content": full_prompt}]) reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b" except Exception as e: reponse_finale, couleur, nom_fournisseur, nom_modele = f"Erreur Serveur Local : {e}", "#ff0000", "ERREUR LOCAL", "Qwen HS" @@ -560,5 +633,10 @@ def post(chat_id: str): save_history(history) return render_sidebar() +# --- ROUTE : EXPORT JSON COMPLET --- +@rt('/export_json') +def get(): + return FileResponse(HISTORY_FILE, media_type='application/json', filename='Aethas38_Discussion_Globale.json') + if __name__ == '__main__': serve(port=5001) \ No newline at end of file