Public Access
refactor(modular): architecture modules/ propre, nommage intelligent des exports, nettoyage HTML pour partage et correction des erreurs de rendu
This commit is contained in:
1 parent
f91b5ae215
commit
4fb378a4c5
3 files changed
+143
-27
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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 = "<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'><head><meta charset='utf-8'><title>Aethas Export</title></head><body style='font-family: Arial, sans-serif; color: #000000; background-color: #ffffff;'>";
|
||||
let blob = new Blob(['\ufeff', preHtml + html + "</body></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 `&&`
|
||||
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)
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user