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