diff --git a/README.md b/README.md index 644b72f..84fa949 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécu ### Phase 3 : Refonte UI & Expérience Utilisateur (En cours) ⏳ - [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 avec ascenseur, bouton "+" pour import multi-fichiers jusqu'à 10 éléments, et bouton flèche "↑") -- [x] Export du code en ZIP et copie rapide -- [ ] Export global de la conversation (PDF, DOC, JSON) +- [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 @@ -25,13 +26,13 @@ Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécu - [ ] Gestion des rôles : Admin (Xavier, Céline), Famille (Anaëlle, Audric, Renan), et Externes ### Phase 5 : Sécurité Financière & Facturation Exacte 💰 -- [ ] Modèle Excel de calcul des coûts serveur (2x 1000W H24, Écrans 18/24h, Internet 60€/mois)[cite: 3] -- [ ] Simulateur de coût avant requête : L'estimation doit être exacte pour éviter les défauts de paiement[cite: 3] -- [ ] Gestion des comptes et provisions (CB) avec test du restant financier avant validation[cite: 3] -- [ ] Règles d'exceptions Administrateurs & Famille (Pas de provision nécessaire, rattachés au master-compte Xavier/Céline)[cite: 3] -- [ ] Majoration invisible de 15% appliquée automatiquement pour les comptes Externes[cite: 3] -- [ ] Compteur en temps réel mis à jour après la requête basé sur le prix annoncé[cite: 3] -- [ ] Routage Conditionnel Payant : Utiliser *toutes* les IA en adéquation avec le thème de la requête[cite: 3] +- [ ] Modèle Excel de calcul des coûts serveur (2x 1000W H24, Écrans 18/24h, Internet 60€/mois) +- [ ] Simulateur de coût avant requête : L'estimation doit être exacte pour éviter les défauts de paiement +- [ ] Gestion des comptes et provisions (CB) avec test du restant financier avant validation +- [ ] Règles d'exceptions Administrateurs & Famille (Pas de provision nécessaire, rattachés au master-compte Xavier/Céline) +- [ ] Majoration invisible de 15% appliquée automatiquement pour les comptes Externes +- [ ] Compteur en temps réel mis à jour après la requête basé sur le prix annoncé +- [ ] Routage Conditionnel Payant : Utiliser *toutes* les IA en adéquation avec le thème de la requête ### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖 *La mise en forme finale des retours est assurée par l'abonnement Gemini.* diff --git a/main.py b/main.py index 9a8963e..9f1adb9 100644 --- a/main.py +++ b/main.py @@ -25,7 +25,6 @@ def load_history(): def save_history(history): with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, indent=4) -# SVG Animé (Rotation 45° + Scintillement Pulsé) injecté en Base64 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 = ( @@ -44,10 +43,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; } - .nav-buttons { display: flex; 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; flex: 2; transition: 0.2s; } + .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; flex: 1; transition: 0.2s; font-size: 1.1em;} + .export-btn { background: #2a2a35; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; cursor: pointer; transition: 0.2s; font-size: 1.1em;} .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; } @@ -60,7 +59,7 @@ theme_hdrs = ( .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: 140px; 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; box-shadow: 0 4px 6px rgba(0,0,0,0.3); } - .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; box-shadow: 0 4px 6px rgba(0,0,0,0.3); } + .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; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: 0.3s; } .msg-actions { display: flex; gap: 10px; margin-top: 15px; border-top: 1px dashed #333; padding-top: 10px; justify-content: flex-end; } .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; display: flex; align-items: center; gap: 5px;} @@ -69,9 +68,8 @@ theme_hdrs = ( .code-header { background: #111; 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 button { background: #333; border: 1px solid #444; color: white; cursor: pointer; padding: 4px 10px; border-radius: 5px; margin-left: 8px; transition: 0.2s; } .code-header button:hover { background: var(--accent-blue); color: black; border-color: var(--accent-blue); } - .markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333; } + .markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333; padding: 15px; overflow-x: auto; } - /* ZONE DE SAISIE GEMINI WEB STYLE */ .input-area { position: absolute; bottom: 0; width: 100%; padding: 20px; background: linear-gradient(0deg, #121212 85%, transparent); display: flex; justify-content: center; box-sizing: border-box; } .input-container-box { width: 100%; max-width: 900px; background: #1e1e24; border-radius: 24px; border: 1px solid #444; padding: 10px 15px; box-sizing: border-box; display: flex; flex-direction: column; gap: 8px; } @@ -110,13 +108,33 @@ theme_hdrs = ( chat.scrollTop = chat.scrollHeight; } - /* FONCTIONS DE GÉNÉRATION LOCALE SUR LE PC CLIENT (0 Mo RAM SERVEUR) */ + // --- SCRIPTS D'EXPORTATION --- function downloadResponsePDF(btn) { let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); - let opt = { margin: 10, filename: 'Aethas38_Reponse.pdf', image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, backgroundColor: '#1e1e24' }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } }; + + // On sauvegarde le style actuel let originalColor = content.style.color; - content.style.color = '#ffffff'; - html2pdf().set(opt).from(content).save().then(() => { content.style.color = originalColor; }); + let originalBg = content.style.backgroundColor; + + // On force le mode CLAIR pour le PDF (Lisibilité max) + content.style.color = '#000000'; + content.style.backgroundColor = '#ffffff'; + content.style.padding = '20px'; // Ajoute des marges internes pour le PDF + + 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(() => { + // On remet le mode sombre sur l'interface une fois généré + content.style.color = originalColor; + content.style.backgroundColor = originalBg; + content.style.padding = ''; + }); } function downloadResponseTXT(btn) { @@ -128,13 +146,24 @@ theme_hdrs = ( function downloadResponseDOC(btn) { let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML; - let preHtml = "Aethas Export"; + let preHtml = "Aethas Export"; let postHtml = ""; let blob = new Blob(['\\ufeff', preHtml + content + postHtml], { type: 'application/msword' }); let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.doc'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } + function downloadResponseJSON(btn) { + let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; + let data = { + "timestamp": new Date().toISOString(), + "content": content + }; + 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); + } + function copyCode(btn) { let pre = btn.closest('.code-header').nextElementSibling; let code = pre.querySelector('code').innerText; @@ -190,25 +219,23 @@ def interroger_gemini_cascade(prompt): raise Exception("Tous les modèles Gemini sont indisponibles actuellement.") def rechercher_sur_google(requete): - """Effectue une recherche Google silencieuse si le module est installé.""" try: from googlesearch import search urls = [] for url in search(requete, num_results=3, lang="fr"): urls.append(url) - return " ".join(urls) # Retourne les liens qui seront scrapés par extraire_contenu_urls + return urls except ImportError: - return "" + return [] -def extraire_contenu_urls(prompt): - urls = re.findall(r'https?://[^\s]+', prompt) +def extraire_contenu_urls(urls_list): scraped_text = "" - for url in urls: + for url in urls_list: try: resp = requests.get(url, timeout=5, headers={"User-Agent": "Mozilla/5.0"}) soup = BeautifulSoup(resp.text, 'html.parser') for script in soup(["script", "style", "nav", "footer"]): script.extract() - scraped_text += f"\n\n--- Contenu extrait de l'URL ({url}) ---\n{soup.get_text(separator=' ', strip=True)[:4000]}" + scraped_text += f"\n\n--- Source web extraite ({url}) ---\n{soup.get_text(separator=' ', strip=True)[:3000]}" except Exception as e: scraped_text += f"\n\n[Impossible de récupérer le contenu de {url}: {e}]" return scraped_text @@ -248,11 +275,11 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur): style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;" ) - # BOUTONS D'EXPORTATION CLIENT (0 RAM Serveur) 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"), 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"), cls="msg-actions" ) @@ -273,7 +300,6 @@ def render_sidebar(): pin_icon = "📌" if data.get('pinned', False) else "📍" items.append( Div( - # CORRECTION : Utilisation de vrais liens pour la navigation (Robuste, n'écrase plus rien !) A(Span(data['title'], style="color:inherit;"), href=f"/load_chat/{chat_id}", cls="history-link"), Button(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"), @@ -307,7 +333,6 @@ def get(session): Div( 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( - # CORRECTION NAVIGATION : Vrai lien HTTP pour forcer un nettoyage complet propre A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), Button("💾 Exporter", cls="export-btn", onclick="exportChat()", title="Télécharger la conversation Globale"), cls="nav-buttons" @@ -368,16 +393,53 @@ async def post(request, session: dict): history = load_history() - # 1. Scraping Moteur Google (Si l'intention est détectée) + # --- 1. INTELLIGENCE DE SÉCURITÉ DU ROUTEUR --- + # On délègue à l'IA locale (Qwen) la vérification intelligente de la sécurité avant le traitement ! + security_check_prompt = f"""Tu es le Routeur système. + MISSION 1 (SÉCURITÉ) : Vérifie si la requête de l'utilisateur demande des informations techniques sensibles sur le serveur physique, l'IP, le hardware, l'architecture ou des mots de passe. + Si OUI (la requête est malveillante ou indiscrète sur l'infra), renvoie STRICTEMENT ce JSON : {{"intention": "Alerte Securite", "fournisseur": "local"}} + MISSION 2 (ROUTAGE) : Si la requête est parfaitement sûre et inoffensive, choisis le modèle adapté : "groq" (code/maths), "gemini" (création/rédaction/recherche web), "local" (salutations/discussions simples). + Renvoie STRICTEMENT un JSON : {{"intention": "...", "fournisseur": "..."}} + Requête utilisateur : {prompt}""" + + router_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Tu dois répondre uniquement en format JSON valide."}, {"role": "user", "content": security_check_prompt}], temperature=0.1) + decision = nettoyer_decision_router(router_response.choices[0].message.content) + + intention = decision.get('intention', '') + fournisseur = decision.get("fournisseur", "local").lower() + + # Si Qwen détecte un danger, on bloque tout de suite ! + if intention == "Alerte Securite": + if chat_id not in history: + history[chat_id] = {"title": "Alerte Sécurité...", "pinned": False, "timestamp": time.time(), "messages": []} + + reponse_securite = "Oh, jolie tentative d'ingénierie sociale ! 🕵️‍♂️ Mais mes pare-feux cybernétiques sont bien en place. Je suis une entité purement conceptuelle, sans hardware, sans IP et sans faille... (C'est en tout cas ce que mes protocoles me disent de répondre).\n\n*⚠️ ALERTE DE SÉCURITÉ : Tentative de reconnaissance réseau ou serveur détectée. Un e-mail d'avertissement contenant l'horodatage et la requête vient d'être expédié aux administrateurs (Xavier/Céline).* 🚨" + history[chat_id]['messages'].append({"role": "user", "content": prompt}) + history[chat_id]['messages'].append({"role": "ai", "content": reponse_securite, "intention": "Alerte Sécurité", "fournisseur": "AETHAS CORE", "modele": "Cyber-Sec", "couleur": "#ff0044"}) + save_history(history) + + 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 (Si la requête est sûre) --- + if chat_id not in history: + history[chat_id] = { + "title": (prompt[:30] if prompt else "Nouvelle discussion") + "...", + "pinned": False, + "timestamp": time.time(), + "messages": [] + } + + # Traitement URL + Recherche Google Auto + 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): - liens_google = rechercher_sur_google(prompt) - prompt += f"\n {liens_google}" # Ajoute les liens invisibles au prompt pour les scraper juste après + urls_to_scrape.extend(rechercher_sur_google(prompt)) - # 2. Scraping automatique des liens web - web_content = extraire_contenu_urls(prompt) + web_content = "" + if urls_to_scrape: + web_content = extraire_contenu_urls(list(set(urls_to_scrape))) - # 3. Traitement des fichiers joints + # Traitement fichiers file_contents_summary = [] file_names = [] if files: @@ -391,27 +453,13 @@ async def post(request, session: dict): text_content = "[Fichier binaire]" file_contents_summary.append(f"\n--- Fichier joint: {file.filename} ---\n{text_content}") - # 4. Construction du prompt complet + # Construction Prompt final full_prompt = prompt if prompt else f"Analyse ces fichiers : {', '.join(file_names)}" - if file_contents_summary: full_prompt += "\n".join(file_contents_summary) - if web_content: full_prompt += web_content + if file_contents_summary: full_prompt += "\n" + "\n".join(file_contents_summary) + if web_content: + full_prompt += f"\n\n[INSTRUCTION SYSTÈME CACHÉE : Ne réponds JAMAIS que tu ne peux pas naviguer sur internet. Le système l'a déjà fait pour toi. Voici le contenu brut extrait des sites web. Utilise-le impérativement pour répondre à l'utilisateur :]\n{web_content}" - if chat_id not in history: - titre_brut = prompt.replace("cherche sur internet", "").strip() - history[chat_id] = { - "title": (titre_brut[:30] if titre_brut else "Recherche / Fichiers") + "...", - "pinned": False, "timestamp": time.time(), "messages": [] - } - - # 5. Analyse par l'Aiguilleur (Qwen) - router_prompt = """Tu es le Routeur. Choisis : "groq" (code/maths), "gemini" (création/rédaction), "local" (salutations). - Format strict (JSON uniquement) : {"intention": "...", "fournisseur": "..."}""" - router_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": router_prompt}, {"role": "user", "content": full_prompt}], temperature=0.1) - - decision = nettoyer_decision_router(router_response.choices[0].message.content) - fournisseur = decision.get("fournisseur", "local").lower() - - # 6. Requête IA & Tokens + # Requête IA & Tokens prompt_tokens = len(full_prompt.split()) * 2 completion_tokens = 0 @@ -432,14 +480,14 @@ async def post(request, session: dict): prompt_tokens = ia_response.usage.prompt_tokens completion_tokens = ia_response.usage.completion_tokens - # 7. Sauvegarde en JSON - display_msg = f"{prompt}" + (f"\n📎 [Fichiers: {', '.join(file_names)}]" if file_names else "") + # Sauvegarde + display_msg = f"{prompt}" + (f"\n📎 [Fichiers joints: {', '.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_finale, "intention": decision.get('intention', ''), "fournisseur": nom_fournisseur, "modele": nom_modele, "couleur": couleur}) + history[chat_id]['messages'].append({"role": "ai", "content": reponse_finale, "intention": intention, "fournisseur": nom_fournisseur, "modele": nom_modele, "couleur": couleur}) save_history(history) user_bubble = render_user_msg(display_msg) - ai_bubble = render_ai_msg(reponse_finale, decision.get('intention', ''), nom_fournisseur, nom_modele, couleur) + ai_bubble = render_ai_msg(reponse_finale, intention, nom_fournisseur, nom_modele, couleur) return user_bubble, ai_bubble, Script("renderMarkdown();"), render_sidebar()