From db2b137d6bb7aa932534ea0697403f54f81d57a6 Mon Sep 17 00:00:00 2001 From: Xavier Date: Mon, 17 Aug 2026 11:45:02 +0200 Subject: [PATCH] maj auto 1 --- .gitignore | 3 +- Logo/logo_aethas.jpg | Bin 0 -> 61819 bytes Logo/logo_aethas.svg | 36 ++++ README.md | 6 +- main.py | 424 ++------------------------------------- modules/chat_handler.py | 72 +++++++ modules/paypal.py | 18 ++ modules/theme.py | 270 +++++++++++++++++++++++++ modules/ui_components.py | 60 +++++- 9 files changed, 479 insertions(+), 410 deletions(-) create mode 100644 Logo/logo_aethas.jpg create mode 100644 Logo/logo_aethas.svg create mode 100644 modules/chat_handler.py create mode 100644 modules/paypal.py create mode 100644 modules/theme.py diff --git a/.gitignore b/.gitignore index 7505227..67519cc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ venv/ .env .sesskey aethas_history.json -Echange GEMINY.txt \ No newline at end of file +Echange GEMINY.txt +.vscode/ \ No newline at end of file diff --git a/Logo/logo_aethas.jpg b/Logo/logo_aethas.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f600187e7c32a4dd3a5bb67ee1fe7e4f1d9acf90 GIT binary patch literal 61819 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 z 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 W{Y`Z0Rcysjt literal 0 HcmV?d00001 diff --git a/Logo/logo_aethas.svg b/Logo/logo_aethas.svg new file mode 100644 index 0000000..d4f7af6 --- /dev/null +++ b/Logo/logo_aethas.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AETHAS + 38 + + + TECH CORE + diff --git a/README.md b/README.md index a9e6f34..892820f 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,14 @@ Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécu - [x] Architecture modulaire propre (Dossier `modules/` isolant Database, Scraper, AI Engine, Router et UI) ### Phase 4 : Gestion des Utilisateurs & Sécurité -- [ ] Mettre en place une bibliothèque lociel à installer avec le serveur +- [ ] Céer un lien Paypal pour les dons des utilisateurs. +- [ ] Mettre en place une bibliothèque logiciel à installer avec le serveur (déporter tout ce qui peut l'être comme la création de pdf, docx, html, etc.) - [ ] Choix entre l'utilisation de la version payante ou gratuite. Routage suivant le choix. - [ ] Système de connexion sécurisé (Mots de passe cryptés) - [ ] Fail2Ban (Blocage 15 mins après 5 erreurs) - [ ] Intégration SMTP (`mail.aethas38.duckdns.org`) pour mot de passe oublié et notifications de thèmes spécifiques - [ ] Gestion des rôles : Admin (Xavier, Céline), Famille (Anaëlle, Audric, Renan), Testeur (Nogapra) et Externes. Testeur juste accès à la partie gratuite. +- [ ] Prévoir un script automatique envoyant un mail sympathique aux utilisateurs lors de leur anniversaire. ### Phase 5 : Sécurité Financière & Facturation Exacte 💰 - [ ] Modèle Excel de calcul des coûts serveur (1x 1000W H24, Internet 60€/mois). Prendre en compte le tarif réglemanté de l'électricité en France et la consommation réel du serveur. @@ -51,7 +53,7 @@ Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécu ### Phase 7 : RAG & Maintenance - [ ] "RAG" : Base de données vectorielle (ChromaDB) pour permettre l'apprentissage et la mémorisation de l'IA locale -- [ ] Script de surveillance de l'espace disque du serveur +- [ ] Script de surveillance de l'espace disque du serveur. Affichage d'une barre d'état (statuts des disques, remplissage, utilisations pour Xavier) - [ ] Création de logs centralisés consultables et téléchargeables par l'Admin ### Phase 8 : Déploiement Public & Configuration diff --git a/main.py b/main.py index f6a3151..ef3ed01 100644 --- a/main.py +++ b/main.py @@ -1,446 +1,62 @@ -import os -import json import uuid -import time -import re -from dotenv import load_dotenv from fasthtml.common import * +from starlette.staticfiles import StaticFiles from starlette.responses import FileResponse -# 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 -from modules.router import nettoyer_decision_router -from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar +# Imports ultra-propres depuis les modules dédiés +from modules.theme import get_theme_hdrs +from modules.database import nettoyer_vieilles_discussions, HISTORY_FILE, load_history, save_history +from modules.ui_components import render_main_page, render_sidebar +from modules.chat_handler import handle_chat_request -load_dotenv() +# On exécute le nettoyage au démarrage 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" +# Initialisation du serveur FastHTML +app, rt = fast_app(hdrs=get_theme_hdrs(), secret_key="super_secret_aethas") -theme_hdrs = ( - Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"), - Script(src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"), - Script(src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"), - Script(src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"), - 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; } - 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 { width: 280px; background-color: #0a0a0c; border-right: 1px solid #333; padding: 20px; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; } - .sidebar-top { flex-shrink: 0; } - .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; 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; 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; } - .history-item:hover { background: #1a1a20; } - .history-link { flex: 1; text-decoration: none; color: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; padding-right: 10px; } - .history-btn { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em; } - .history-btn:hover { color: var(--accent-blue); } - - .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: 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; 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; } - .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; } - .code-header button { background: #2a2a35; border: 1px solid #444; color: #e0e0e0; cursor: pointer; padding: 4px 10px; border-radius: 5px; font-size: 0.85em; 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; padding: 15px; overflow-x: auto; background-color: #0d0d0f !important; } - - .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; } - - #file-preview-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 80px; overflow-y: auto; padding-bottom: 5px; } - .file-pill { background: #2a2a35; border: 1px solid #555; color: var(--accent-blue); padding: 5px 12px; border-radius: 12px; font-size: 0.8em; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } - - .input-row { display: flex; align-items: flex-end; gap: 10px; width: 100%; } - .input-field { background: transparent; border: none; color: white; resize: none; flex: 1; font-size: 1em; height: 36px; max-height: 140px; overflow-y: hidden; padding: 8px; font-family: inherit; box-sizing: border-box; line-height: 20px; } - .input-field:focus { outline: none; } - - .file-btn { background: #2a2a35; border: 1px solid #444; color: var(--accent-blue); width: 36px !important; height: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.3em; text-decoration: none; flex-shrink: 0; } - .btn-submit { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; width: 36px !important; height: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.1em; flex-shrink: 0; } - - .htmx-indicator { display: none; } - .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; } - .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(r""" - function renderMarkdown() { - try { - document.querySelectorAll('.markdown-raw').forEach(el => { - if (el.dataset.rendered) return; - let container = el.nextElementSibling; - container.innerHTML = (typeof marked !== 'undefined') ? marked.parse(el.textContent) : el.textContent; - el.dataset.rendered = "true"; - el.classList.remove('markdown-raw'); - - container.querySelectorAll('pre').forEach(pre => { - try { - let code = pre.querySelector('code'); - if (code && !pre.previousElementSibling?.classList.contains('code-header')) { - let rawLang = code.className.replace('language-', '') || 'snippet'; - let header = document.createElement('div'); - header.className = 'code-header'; - header.innerHTML = ` - ${rawLang.toUpperCase()} -
- - -
`; - pre.parentNode.insertBefore(header, pre); - if (typeof hljs !== 'undefined') hljs.highlightElement(code); - } - } catch(err) { console.warn("Erreur entête code", err); } - }); - }); - let chat = document.getElementById('chat-history'); - if (chat) chat.scrollTop = chat.scrollHeight; - } 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'; - let scrollH = textarea.scrollHeight; - if (scrollH > 100) { - textarea.style.height = '100px'; - textarea.style.overflowY = 'auto'; - } else { - textarea.style.height = scrollH + 'px'; - textarea.style.overflowY = 'hidden'; - } - } - - document.addEventListener('DOMContentLoaded', () => { - const ta = document.getElementById('prompt-input'); - const dropZone = document.querySelector('.input-container-box'); - const fileInput = document.getElementById('file-upload'); - - if (ta) { - ta.addEventListener('input', () => autoResize(ta)); - ta.addEventListener('paste', (e) => { - let items = (e.clipboardData || e.originalEvent.clipboardData).items; - let dt = new DataTransfer(); - if (fileInput && fileInput.files) { - for (let f of fileInput.files) dt.items.add(f); - } - let hasFiles = false; - for (let i = 0; i < items.length; i++) { - if (items[i].kind === 'file') { - let blob = items[i].getAsFile(); - if (blob) { dt.items.add(blob); hasFiles = true; } - } - } - if (hasFiles) { - e.preventDefault(); - fileInput.files = dt.files; - handleFileSelection(fileInput); - } else { - setTimeout(() => autoResize(ta), 20); - } - }); - } - - if (dropZone && fileInput) { - ['dragenter', 'dragover'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent-blue)'; })); - ['dragleave', 'drop'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = '#444'; })); - dropZone.addEventListener('drop', (e) => { - 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) { - if (dt.files.length < 10) dt.items.add(f); - } - fileInput.files = dt.files; - handleFileSelection(fileInput); - }); - } - }); - - function copyCodeBlock(btn) { - let pre = btn.closest('.code-header').nextElementSibling; - navigator.clipboard.writeText(pre.querySelector('code').innerText); - btn.innerText = "✅ Copié !"; - setTimeout(() => btn.innerText = "📋 Copier", 2000); - } - - function downloadCodeZip(btn, lang) { - 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 = getSmartFilename(btn, 'aethas_snippet', ext); - - if (typeof JSZip !== 'undefined') { - let zip = new JSZip(); - zip.file(filename, code); - zip.generateAsync({type: "blob"}).then(function(content) { - let link = document.createElement('a'); - link.href = URL.createObjectURL(content); - 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.click(); - } - } - - function handleFileSelection(input) { - let previewList = document.getElementById('file-preview-list'); - previewList.innerHTML = ''; - if (input.files) { - 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'; - pill.innerHTML = `📎 ${file.name} ✕`; - previewList.appendChild(pill); - } - } - } - - function removeFile(index) { - let fileInput = document.getElementById('file-upload'); - let dt = new DataTransfer(); - let { files } = fileInput; - for (let i = 0; i < files.length; i++) { - if (i !== index) dt.items.add(files[i]); - } - fileInput.files = dt.files; - handleFileSelection(fileInput); - } - - function downloadResponsePDF(btn) { - let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); - 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 = 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 = filename; link.click(); - } - function downloadResponseDOC(btn) { - let html = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML; - 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 = 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 = filename; link.click(); - } - function shareWhatsApp(btn) { - let text = getCleanPlainText(btn); - window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent("🤖 *Aethas38 (IA) :*\n\n" + text), '_blank'); - } - function shareEmail(btn) { - let text = getCleanPlainText(btn); - window.open('mailto:?subject=Aethas38 - Réponse IA&body=' + encodeURIComponent(text), '_self'); - } - """) -) - -app, rt = fast_app(hdrs=theme_hdrs, secret_key="super_secret_aethas") +# Montage du dossier statique pour rendre accessible le logo +app.mount("/Logo", StaticFiles(directory="Logo"), name="Logo") @rt('/') def get(session): if 'current_chat_id' not in session or not session['current_chat_id']: session['current_chat_id'] = str(uuid.uuid4()) - chat_id = session['current_chat_id'] - history = load_history() - - msgs = [render_user_msg(m['content']) if m['role']=='user' else render_ai_msg(m['content'], m.get('intention',''), m.get('fournisseur',''), m.get('modele',''), m.get('couleur','#333')) for m in history.get(chat_id, {}).get('messages', [])] - if msgs: msgs.append(Script("setTimeout(renderMarkdown, 150);")) - - return Title("Aethas38 - Core"), Body( - Div( - 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(A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn"), cls="nav-buttons"), - cls="sidebar-top" - ), - render_sidebar(), - Div(P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer"), - cls="sidebar" - ), - Div( - Div(*msgs, id="chat-history", cls="chat-container"), - 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), - Div( - Div(id="file-preview-list"), - Div( - Label("+", _for="file-upload", cls="file-btn"), - Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"), - Textarea(name="prompt", id="prompt-input", placeholder="Requête, lien ou glissez vos fichiers ici...", cls="input-field", rows="1", oninput="autoResize(this)"), - Button("↑", type="submit", cls="btn-submit"), - cls="input-row" - ), - cls="input-container-box" - ), - 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='36px';", - enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;" - ), - cls="input-area" - ), - cls="main-content" - ) - ) + return render_main_page(session['current_chat_id']) @rt('/chat') async def post(request, session: dict): - try: - form = await request.form() - prompt = form.get("prompt", "") - chat_id = form.get("chat_id", str(uuid.uuid4())) - files = form.getlist("files") - session['current_chat_id'] = chat_id - - 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}""" - - router_res = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "JSON uniquement."}, {"role": "user", "content": sec_prompt}], temperature=0.1) - decision = nettoyer_decision_router(router_res.choices[0].message.content) - intention, fournisseur = decision.get('intention', 'Analyse'), decision.get('fournisseur', 'gemini').lower() - - urls = re.findall(r'https?://[^\s]+', prompt) - mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "va sur", "trouve", "site", "donne", "historique", "version"] - if any(m in prompt.lower() for m in mots_cles): - urls.extend(rechercher_sur_google(prompt)) - - web_text = extraire_contenu_urls(list(set(urls))) if urls else "" - - file_summary = [] - file_names = [] - for f in files[:10]: - 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;") + return await handle_chat_request(request, session) @rt('/favicon.ico') -def get(): return HTTPResponse(status_code=204) +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') diff --git a/modules/chat_handler.py b/modules/chat_handler.py new file mode 100644 index 0000000..d6e5804 --- /dev/null +++ b/modules/chat_handler.py @@ -0,0 +1,72 @@ +import uuid +import time +import re +from fasthtml.common import * +from modules.database import load_history, save_history +from modules.scraper import rechercher_sur_google, extraire_contenu_urls +from modules.router import nettoyer_decision_router +from modules.ai_engine import local_client, interroger_gemini_cascade, appeler_groq_api +from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar + +async def handle_chat_request(request, session): + try: + form = await request.form() + prompt = form.get("prompt", "") + chat_id = form.get("chat_id", str(uuid.uuid4())) + files = form.getlist("files") + session['current_chat_id'] = chat_id + + history = load_history() + + 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}""" + + router_res = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "JSON uniquement."}, {"role": "user", "content": sec_prompt}], temperature=0.1) + decision = nettoyer_decision_router(router_res.choices[0].message.content) + intention, fournisseur = decision.get('intention', 'Analyse'), decision.get('fournisseur', 'gemini').lower() + + urls = re.findall(r'https?://[^\s]+', prompt) + mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "va sur", "trouve", "site", "donne", "historique", "version"] + if any(m in prompt.lower() for m in mots_cles): + urls.extend(rechercher_sur_google(prompt)) + + web_text = extraire_contenu_urls(list(set(urls))) if urls else "" + + file_summary = [] + file_names = [] + for f in files[:10]: + 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;") \ No newline at end of file diff --git a/modules/paypal.py b/modules/paypal.py new file mode 100644 index 0000000..3157c52 --- /dev/null +++ b/modules/paypal.py @@ -0,0 +1,18 @@ +# modules/paypal.py +from fasthtml.common import * + +def render_paypal_donation_widget(): + """Génère le bouton de don PayPal pour l'interface sidebar.""" + # Remplacez l'URL ci-dessous par votre véritable lien ou bouton de don PayPal + paypal_url = "https://www.paypal.com/donate?hosted_button_id=VOTRE_ID_PAYPAL" + + return Div( + A( + "☕ Faire un don PayPal", + href=paypal_url, + target="_blank", + cls="export-btn", + style="text-align: center; color: #00e5ff; border-color: #00e5ff; display: block; text-decoration: none;" + ), + style="margin-top: 10px;" + ) \ No newline at end of file diff --git a/modules/theme.py b/modules/theme.py new file mode 100644 index 0000000..d680ef8 --- /dev/null +++ b/modules/theme.py @@ -0,0 +1,270 @@ +from fasthtml.common import * + +def get_theme_hdrs(): + 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" + + return ( + Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"), + Script(src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"), + Script(src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"), + Script(src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"), + 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; } + 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 { width: 280px; background-color: #0a0a0c; border-right: 1px solid #333; padding: 20px; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; } + .sidebar-top { flex-shrink: 0; } + .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; 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; 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; } + .history-item:hover { background: #1a1a20; } + .history-link { flex: 1; text-decoration: none; color: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; padding-right: 10px; } + .history-btn { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em; } + .history-btn:hover { color: var(--accent-blue); } + .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: 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; 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; } + .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; } + .code-header button { background: #2a2a35; border: 1px solid #444; color: #e0e0e0; cursor: pointer; padding: 4px 10px; border-radius: 5px; font-size: 0.85em; 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; padding: 15px; overflow-x: auto; background-color: #0d0d0f !important; } + .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; } + #file-preview-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 80px; overflow-y: auto; padding-bottom: 5px; } + .file-pill { background: #2a2a35; border: 1px solid #555; color: var(--accent-blue); padding: 5px 12px; border-radius: 12px; font-size: 0.8em; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } + .input-row { display: flex; align-items: flex-end; gap: 10px; width: 100%; } + .input-field { background: transparent; border: none; color: white; resize: none; flex: 1; font-size: 1em; height: 36px; max-height: 140px; overflow-y: hidden; padding: 8px; font-family: inherit; box-sizing: border-box; line-height: 20px; } + .input-field:focus { outline: none; } + .file-btn { background: #2a2a35; border: 1px solid #444; color: var(--accent-blue); width: 36px !important; height: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.3em; text-decoration: none; flex-shrink: 0; } + .btn-submit { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; width: 36px !important; height: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.1em; flex-shrink: 0; } + .htmx-indicator { display: none; } + .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; } + .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(r""" + function renderMarkdown() { + try { + document.querySelectorAll('.markdown-raw').forEach(el => { + if (el.dataset.rendered) return; + let container = el.nextElementSibling; + container.innerHTML = (typeof marked !== 'undefined') ? marked.parse(el.textContent) : el.textContent; + el.dataset.rendered = "true"; + el.classList.remove('markdown-raw'); + + container.querySelectorAll('pre').forEach(pre => { + try { + let code = pre.querySelector('code'); + if (code && !pre.previousElementSibling?.classList.contains('code-header')) { + let rawLang = code.className.replace('language-', '') || 'snippet'; + let header = document.createElement('div'); + header.className = 'code-header'; + header.innerHTML = ` + ${rawLang.toUpperCase()} +
+ + +
`; + pre.parentNode.insertBefore(header, pre); + if (typeof hljs !== 'undefined') hljs.highlightElement(code); + } + } catch(err) { console.warn("Erreur entête code", err); } + }); + }); + let chat = document.getElementById('chat-history'); + if (chat) chat.scrollTop = chat.scrollHeight; + } 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'; + let scrollH = textarea.scrollHeight; + if (scrollH > 100) { + textarea.style.height = '100px'; + textarea.style.overflowY = 'auto'; + } else { + textarea.style.height = scrollH + 'px'; + textarea.style.overflowY = 'hidden'; + } + } + + document.addEventListener('DOMContentLoaded', () => { + const ta = document.getElementById('prompt-input'); + const dropZone = document.querySelector('.input-container-box'); + const fileInput = document.getElementById('file-upload'); + + if (ta) { + ta.addEventListener('input', () => autoResize(ta)); + ta.addEventListener('paste', (e) => { + let items = (e.clipboardData || e.originalEvent.clipboardData).items; + let dt = new DataTransfer(); + if (fileInput && fileInput.files) { + for (let f of fileInput.files) dt.items.add(f); + } + let hasFiles = false; + for (let i = 0; i < items.length; i++) { + if (items[i].kind === 'file') { + let blob = items[i].getAsFile(); + if (blob) { dt.items.add(blob); hasFiles = true; } + } + } + if (hasFiles) { + e.preventDefault(); + fileInput.files = dt.files; + handleFileSelection(fileInput); + } else { + setTimeout(() => autoResize(ta), 20); + } + }); + } + + if (dropZone && fileInput) { + ['dragenter', 'dragover'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent-blue)'; })); + ['dragleave', 'drop'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = '#444'; })); + dropZone.addEventListener('drop', (e) => { + 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) { + if (dt.files.length < 10) dt.items.add(f); + } + fileInput.files = dt.files; + handleFileSelection(fileInput); + }); + } + }); + + function copyCodeBlock(btn) { + let pre = btn.closest('.code-header').nextElementSibling; + navigator.clipboard.writeText(pre.querySelector('code').innerText); + btn.innerText = "✅ Copié !"; + setTimeout(() => btn.innerText = "📋 Copier", 2000); + } + + function downloadCodeZip(btn, lang) { + 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 = getSmartFilename(btn, 'aethas_snippet', ext); + + if (typeof JSZip !== 'undefined') { + let zip = new JSZip(); + zip.file(filename, code); + zip.generateAsync({type: "blob"}).then(function(content) { + let link = document.createElement('a'); + link.href = URL.createObjectURL(content); + 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.click(); + } + } + + function handleFileSelection(input) { + let previewList = document.getElementById('file-preview-list'); + previewList.innerHTML = ''; + if (input.files) { + 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'; + pill.innerHTML = `📎 ${file.name} ✕`; + previewList.appendChild(pill); + } + } + } + + function removeFile(index) { + let fileInput = document.getElementById('file-upload'); + let dt = new DataTransfer(); + let { files } = fileInput; + for (let i = 0; i < files.length; i++) { + if (i !== index) dt.items.add(files[i]); + } + fileInput.files = dt.files; + handleFileSelection(fileInput); + } + + function downloadResponsePDF(btn) { + let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); + 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 = 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 = filename; link.click(); + } + function downloadResponseDOC(btn) { + let html = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML; + 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 = 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 = filename; link.click(); + } + function shareWhatsApp(btn) { + let text = getCleanPlainText(btn); + window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent("🤖 *Aethas38 (IA) :*\\n\\n" + text), '_blank'); + } + function shareEmail(btn) { + let text = getCleanPlainText(btn); + window.open('mailto:?subject=Aethas38 - Réponse IA&body=' + encodeURIComponent(text), '_self'); + } + """) + ) \ No newline at end of file diff --git a/modules/ui_components.py b/modules/ui_components.py index 9e1027f..de24c6d 100644 --- a/modules/ui_components.py +++ b/modules/ui_components.py @@ -1,5 +1,6 @@ from fasthtml.common import * from modules.database import load_history +from modules.paypal import render_paypal_donation_widget def render_user_msg(text): return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble") @@ -11,7 +12,6 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur): Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"), style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; 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"), @@ -21,7 +21,6 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur): Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par Email"), cls="msg-actions" ) - return Div( encart_info, Div(text, cls="markdown-raw", style="display:none;"), @@ -33,7 +32,6 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur): def render_sidebar(): history = load_history() sorted_chats = sorted(history.items(), key=lambda x: (not x[1].get('pinned', False), -x[1].get('timestamp', 0))) - items = [] for chat_id, data in sorted_chats: pin_icon = "📌" if data.get('pinned', False) else "📍" @@ -47,3 +45,59 @@ def render_sidebar(): ) ) return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true") + +def render_main_page(chat_id): + history = load_history() + 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'))) + if messages_ui: + messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) + + return Title("Aethas38 - Core"), Body( + Div( + Div( + # L'image est appelée depuis le dossier local "Logo" monté dans FastHTML + Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"), + Div( + A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), + A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn"), + render_paypal_donation_widget(), + cls="nav-buttons" + ), + cls="sidebar-top" + ), + render_sidebar(), + Div(P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer"), + cls="sidebar" + ), + Div( + Div(*messages_ui, id="chat-history", cls="chat-container"), + 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), + Div( + Div(id="file-preview-list"), + Div( + Label("+", _for="file-upload", cls="file-btn"), + Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"), + Textarea(name="prompt", id="prompt-input", placeholder="Requête, lien ou glissez vos fichiers ici...", cls="input-field", rows="1", oninput="autoResize(this)"), + Button("↑", type="submit", cls="btn-submit"), + cls="input-row" + ), + cls="input-container-box" + ), + 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='36px';", + enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;" + ), + cls="input-area" + ), + cls="main-content" + ) + ) \ No newline at end of file