diff --git a/README.md b/README.md index 46c5eb7..81a7242 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécu - [x] Routeur Multi-IA (Groq, Gemini, Local) avec système de cascade et directives d'aiguillage strictes - [x] Continuité de service : IA de secours (Fallback automatique sur Groq) en cas de panne d'une API primaire -### Phase 3 : Refonte UI & Expérience Utilisateur (En cours) ⏳ +### Phase 3 : Refonte UI & Expérience Utilisateur ✅ - [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive) - [x] Historique (Sidebar) avec Renommage, Épinglage, Suppression et Navigation isolée par session -- [x] Zone de saisie inspirée de Gemini Web (Textarea auto-adaptatif, bouton "+" pour upload multi-fichiers) -- [x] Export complet et modulaire (PDF Pro sur fond blanc, DOC, TXT, JSON) 100% Client-Side (0 Mo RAM Serveur) -- [x] Favicon animé (Néon tech) et Intercepteur de sécurité IA intelligent (Évite les faux positifs) -- [ ] Ajout de boutons de partage social (WhatsApp, Messenger, Email) pour diffuser les réponses générées -- [ ] Script de nettoyage automatique : Suppression des discussions de > 365 jours (avec notification) -- [ ] Indicateur d'avancement dynamique (Loader) pendant le traitement des IA +- [x] Zone de saisie intelligente (Textarea auto-adaptatif, Drag & Drop avec pills/miniatures, Collage multi-fichiers jusqu'à 10) +- [x] Export modulaire et propre (PDF Pro avec fond blanc, DOC Word, TXT, JSON, et blocs de code en ZIP) avec nommage dynamique intelligent basé sur la requête utilisateur (ex: `frise_temporalite_minecraft.pdf`) +- [x] Boutons de partage direct (WhatsApp, Email) avec nettoyage automatique des balises HTML brutes +- [x] Script de nettoyage automatique : Suppression des discussions de > 365 jours +- [x] Indicateur d'avancement dynamique (Loader Néon) pendant le traitement des IA +- [x] Architecture modulaire propre (Dossier `modules/` isolant Database, Scraper, AI Engine, Router et UI) ### Phase 4 : Gestion des Utilisateurs & Sécurité - [ ] Système de connexion sécurisé (Mots de passe cryptés) diff --git a/main.py b/main.py index adf5945..f6a3151 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import os +import json import uuid import time import re @@ -6,7 +7,7 @@ from dotenv import load_dotenv from fasthtml.common import * from starlette.responses import FileResponse -# Import propre depuis le dossier modules +# Import des modules spécialisés from modules.database import load_history, save_history, nettoyer_vieilles_discussions, HISTORY_FILE from modules.scraper import rechercher_sur_google, extraire_contenu_urls from modules.ai_engine import local_client, interroger_gemini_cascade, appeler_groq_api @@ -83,7 +84,7 @@ theme_hdrs = ( @keyframes spin { to { transform: rotate(360deg); } } """), - Script(""" + Script(r""" function renderMarkdown() { try { document.querySelectorAll('.markdown-raw').forEach(el => { @@ -117,6 +118,30 @@ theme_hdrs = ( } catch(e) { console.error("Erreur markdown", e); } } + function getSmartFilename(btn, defaultName = 'Aethas38_Reponse', extension = 'pdf') { + try { + let bubble = btn.closest('.ai-bubble'); + let prevUserMsg = bubble.previousElementSibling; + if (prevUserMsg && prevUserMsg.classList.contains('user-bubble')) { + let text = prevUserMsg.innerText.replace(/📎.*$/, '').trim(); + let slug = text.toLowerCase() + .replace(/[^a-z0-9àâäéèêëîïôöùûüç\s]/g, '') + .trim() + .replace(/\s+/g, '_') + .substring(0, 35); + if (slug.length > 3) return slug + '.' + extension; + } + } catch(e) {} + return defaultName + '.' + extension; + } + + function getCleanPlainText(btn) { + let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); + let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw'); + let source = (rendered && rendered.innerHTML.trim() !== "") ? rendered : raw; + return source.innerText || source.textContent || ""; + } + function autoResize(textarea) { if (!textarea) return; textarea.style.height = '36px'; @@ -167,7 +192,9 @@ theme_hdrs = ( e.preventDefault(); let dt = new DataTransfer(); if (fileInput.files) { for (let f of fileInput.files) dt.items.add(f); } - for (let f of e.dataTransfer.files) { dt.items.add(f); } + for (let f of e.dataTransfer.files) { + if (dt.files.length < 10) dt.items.add(f); + } fileInput.files = dt.files; handleFileSelection(fileInput); }); @@ -185,22 +212,22 @@ theme_hdrs = ( let pre = btn.closest('.code-header').nextElementSibling; let code = pre.querySelector('code').innerText; let ext = lang === 'python' ? 'py' : (lang === 'javascript' ? 'js' : (lang === 'html' ? 'html' : 'txt')); - let fileName = 'aethas_snippet.' + ext; + let filename = getSmartFilename(btn, 'aethas_snippet', ext); if (typeof JSZip !== 'undefined') { let zip = new JSZip(); - zip.file(fileName, code); + zip.file(filename, code); zip.generateAsync({type: "blob"}).then(function(content) { let link = document.createElement('a'); link.href = URL.createObjectURL(content); - link.download = "aethas_code_snippet.zip"; + link.download = filename.replace('.'+ext, '_snippet.zip'); link.click(); }); } else { let blob = new Blob([code], {type: 'text/plain;charset=utf-8'}); let link = document.createElement('a'); link.href = URL.createObjectURL(blob); - link.download = fileName; + link.download = filename; link.click(); } } @@ -209,7 +236,9 @@ theme_hdrs = ( let previewList = document.getElementById('file-preview-list'); previewList.innerHTML = ''; if (input.files) { - for (let i = 0; i < input.files.length; i++) { + if (input.files.length > 10) alert("Maximum 10 fichiers autorisés."); + let count = Math.min(input.files.length, 10); + for (let i = 0; i < count; i++) { let file = input.files[i]; let pill = document.createElement('div'); pill.className = 'file-pill'; @@ -232,30 +261,44 @@ theme_hdrs = ( function downloadResponsePDF(btn) { let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); - html2pdf().set({ margin: 10, filename: 'Aethas38_Reponse.pdf', html2canvas: { scale: 2 }, jsPDF: { unit: 'mm', format: 'a4' } }).from(content).save(); + let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'pdf'); + let originalBg = content.style.backgroundColor; + let originalColor = content.style.color; + content.style.backgroundColor = '#ffffff'; + content.style.color = '#000000'; + content.style.padding = '15px'; + html2pdf().set({ margin: 10, filename: filename, html2canvas: { scale: 2, backgroundColor: '#ffffff' }, jsPDF: { unit: 'mm', format: 'a4' } }).from(content).save().then(() => { + content.style.backgroundColor = originalBg; + content.style.color = originalColor; + content.style.padding = ''; + }); } function downloadResponseTXT(btn) { - let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; + let text = getCleanPlainText(btn); + let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'txt'); let blob = new Blob([text], {type: 'text/plain;charset=utf-8'}); - let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.txt'; link.click(); + let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); } function downloadResponseDOC(btn) { let html = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML; - let blob = new Blob(['\\ufeff' + html], { type: 'application/msword' }); - let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.doc'; link.click(); + let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'doc'); + let preHtml = "Aethas Export"; + let blob = new Blob(['\ufeff', preHtml + html + ""], { type: 'application/msword' }); + let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); } function downloadResponseJSON(btn) { - let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; + let text = getCleanPlainText(btn); + let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'json'); let blob = new Blob([JSON.stringify({content: text}, null, 2)], {type: 'application/json;charset=utf-8'}); - let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.json'; link.click(); + let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click(); } function shareWhatsApp(btn) { - let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; - window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent(text), '_blank'); + let text = getCleanPlainText(btn); + window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent("🤖 *Aethas38 (IA) :*\n\n" + text), '_blank'); } function shareEmail(btn) { - let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; - window.open('mailto:?subject=Aethas38&body=' + encodeURIComponent(text), '_self'); + let text = getCleanPlainText(btn); + window.open('mailto:?subject=Aethas38 - Réponse IA&body=' + encodeURIComponent(text), '_self'); } """) ) @@ -321,6 +364,7 @@ async def post(request, session: dict): history = load_history() + # Router Qwen sec_prompt = f"""Analyse et route cette demande. Si recherche web nécessaire ("va sur", "site", "historique", "version", "cherche", etc.) -> choisis "gemini". Si code/technique -> "groq". Sinon -> "local". Renvoie STRICTEMENT un JSON : {{"intention": "...", "fournisseur": "gemini|groq|local"}} Requête : {prompt}""" @@ -339,4 +383,66 @@ async def post(request, session: dict): file_summary = [] file_names = [] for f in files[:10]: - if f && f.filename: # Wait, Python uses `and` instead of `&&` \ No newline at end of file + if f and f.filename: + file_names.append(f.filename) + content = await f.read() + file_summary.append(f"\n--- Fichier: {f.filename} ---\n{content.decode('utf-8', errors='ignore')}") + + full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "") + if web_text: + full_prompt += f"\n\n[Contexte web extrait :\n{web_text}\nUtilise ces informations pour répondre précisément.]" + + if fournisseur == "gemini": + try: + reponse, modele = interroger_gemini_cascade(full_prompt) + couleur, fournisseur_nom = "#3b82f6", "GOOGLE GEMINI" + except Exception: + reponse, couleur, fournisseur_nom, modele = appeler_groq_api(full_prompt) + fournisseur_nom, intention = "GROQ (Secours)", intention + " (Secours)" + elif fournisseur == "groq": + reponse, couleur, fournisseur_nom, modele = appeler_groq_api(full_prompt) + else: + res_local = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds en français."}, {"role": "user", "content": full_prompt}]) + reponse, couleur, fournisseur_nom, modele = res_local.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b" + + if chat_id not in history: + history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []} + + display_msg = prompt + (f"\n📎 [Fichiers: {', '.join(file_names)}]" if file_names else "") + 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}) + save_history(history) + + return render_user_msg(display_msg), render_ai_msg(reponse, intention, fournisseur_nom, modele, couleur), Script("renderMarkdown();"), render_sidebar() + + except Exception as e: + return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;") + +@rt('/favicon.ico') +def get(): return HTTPResponse(status_code=204) +@rt('/new_chat') +def get(session): session['current_chat_id'] = str(uuid.uuid4()); return RedirectResponse('/', status_code=303) +@rt('/load_chat/{cid}') +def get(cid: str, session): + if cid and cid != "null": session['current_chat_id'] = cid + return RedirectResponse('/', status_code=303) +@rt('/rename/{cid}') +def post(cid: str, req): + h = load_history() + if cid in h and (nom := req.headers.get("HX-Prompt")): h[cid]['title'] = nom; save_history(h) + return render_sidebar() +@rt('/pin/{cid}') +def post(cid: str): + h = load_history() + if cid in h: h[cid]['pinned'] = not h[cid].get('pinned', False); save_history(h) + return render_sidebar() +@rt('/delete/{cid}') +def post(cid: str): + h, cid_str = load_history(), str(cid) + if cid_str in h: del h[cid_str]; save_history(h) + return render_sidebar() +@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 diff --git a/modules/export_utils.py b/modules/export_utils.py new file mode 100644 index 0000000..3548bd4 --- /dev/null +++ b/modules/export_utils.py @@ -0,0 +1,10 @@ +# modules/export_utils.py +import json + +def generer_donnees_json(contenu): + """Prépare la structure de données pour l'export JSON.""" + return { + "app": "Aethas38 Core", + "timestamp": time.time() if 'time' in globals() else None, + "content": contenu + } \ No newline at end of file