From d6a7d10e3ae159f1bdd322bb32c07ab500ebe35e Mon Sep 17 00:00:00 2001 From: Xavier Date: Tue, 6 Oct 2026 13:59:17 +0200 Subject: [PATCH] MAJ majeur --- README.md | 6 +- backend/ai_engine.py | 39 +++++ auth.py => backend/auth.py | 5 +- backend/catalog_manager.py | 57 +++++++ backend/history_manager.py | 36 ++++ backend/main.py | 321 +++++++++++++++++++++-------------- setup.py => backend/setup.py | 0 frontend/index.html | 301 +++++++++++++++++++++----------- 8 files changed, 529 insertions(+), 236 deletions(-) create mode 100644 backend/ai_engine.py rename auth.py => backend/auth.py (93%) create mode 100644 backend/catalog_manager.py create mode 100644 backend/history_manager.py rename setup.py => backend/setup.py (100%) diff --git a/README.md b/README.md index b4fb004..1eaa53c 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ Le développement suit un versioning strict (format `vX.Y.Z`) synchronisé avec ## 🚀 ROADMAP (Ordre d'exécution strict) - [X] **Étape 1 :** Création de la page d'administration (changement des clés, ajout de fournisseurs, configuration serveur mail, etc.). -- [ ] **Étape 2 :** Ajout des fournisseurs pour le traitement d'images, de sons (génération de voix, musique, calage sur vidéo) et de vidéos. -- [ ] **Étape 3 :** Possibilité de télécharger les prompts en PDF, TXT, MD et JSON. -- [ ] **Étape 4 :** Possibilité de télécharger les réponses en PDF, TXT, MD et JSON. +- [X] **Étape 2 :** Ajout des fournisseurs pour le traitement d'images, de sons (génération de voix, musique, calage sur vidéo) et de vidéos. +- [X] **Étape 3 :** Possibilité de télécharger les prompts en PDF, TXT, MD et JSON. +- [X] **Étape 4 :** Possibilité de télécharger les réponses en PDF, TXT, MD et JSON. - [ ] **Étape 5 :** Rédaction du Wiki détaillé pour Gitea et GitHub. - [ ] **Étape 6 :** Ne plus créer systématiquement un nouveau projet à l'ouverture de l'application. - [ ] **Étape 7 :** Refonte de la gestion des projets (séparation Épinglés / Non Épinglés, avec le dernier utilisé en tête de liste). diff --git a/backend/ai_engine.py b/backend/ai_engine.py new file mode 100644 index 0000000..dfdc401 --- /dev/null +++ b/backend/ai_engine.py @@ -0,0 +1,39 @@ +# ai_engine.py +import os +import requests +import asyncio +from datetime import datetime + +os.makedirs("logs", exist_ok=True) +APP_LAUNCH_TIME = datetime.now().strftime("%Y%m%d_%H%M%S") +LOG_FILE = f"logs/AETHAS38_Run_{APP_LAUNCH_TIME}.log" + +def log_event(session_id, category, action): + sid_short = session_id[:8] if session_id and session_id != "system" else "SYSTEM" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(LOG_FILE, "a", encoding="utf-8") as f: + f.write(f"[{timestamp}] [Session:{sid_short}] [{category}] {action}\n")[cite: 11] + +def ask_llm(session_id, full_id, msg): + parts = full_id.split("|", 1) + provider, actual_model = parts[0], parts[1] if len(parts) > 1 else full_id + + log_event(session_id, "API_REQ", f"Provider: {provider} | Modèle: {actual_model}")[cite: 11] + try: + if provider == "openrouter": + res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]}) + if res.status_code == 200: return res.json()["choices"][0]["message"]["content"] + return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11] + elif provider == "groq": + res = requests.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]}) + if res.status_code == 200: return res.json()["choices"][0]["message"]["content"] + return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11] + elif provider == "gemini": + res = requests.post(f"https://generativelanguage.googleapis.com/v1beta/models/{actual_model}:generateContent?key={os.getenv('GEMINI_API_KEY')}", json={"contents": [{"parts": [{"text": msg}]}]}) + if res.status_code == 200: return res.json()["candidates"][0]["content"]["parts"][0]["text"] + return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11] + return "⚠️ Fournisseur API non implémenté ou ID invalide[cite: 11]." + except Exception as e: return f"⚠ Erreur de connexion : {str(e)}"[cite: 11] + +async def async_ask_llm(session_id, full_id, msg): + return await asyncio.to_thread(ask_llm, session_id, full_id, msg)[cite: 11] \ No newline at end of file diff --git a/auth.py b/backend/auth.py similarity index 93% rename from auth.py rename to backend/auth.py index 0d4c3fa..00c4c84 100644 --- a/auth.py +++ b/backend/auth.py @@ -1,3 +1,4 @@ +# auth.py import re import pyotp import hashlib @@ -35,7 +36,6 @@ def send_invite_email(to_email, setup_link): smtp_pass = os.getenv("SMTP_PASS") if not all([smtp_server, smtp_user, smtp_pass]): - print("Erreur: Configuration SMTP incomplète dans .env") return False msg = MIMEMultipart() @@ -52,6 +52,5 @@ def send_invite_email(to_email, setup_link): server.send_message(msg) server.quit() return True - except Exception as e: - print(f"Erreur SMTP: {e}") + except Exception: return False \ No newline at end of file diff --git a/backend/catalog_manager.py b/backend/catalog_manager.py new file mode 100644 index 0000000..f4c2edf --- /dev/null +++ b/backend/catalog_manager.py @@ -0,0 +1,57 @@ +# catalog_manager.py +import os +import json +import re +import requests + +CATALOG_FILE = "sessions/catalog.json" +MODELS_DATA = [] + +def load_catalog(): + global MODELS_DATA + if os.path.exists(CATALOG_FILE): + with open(CATALOG_FILE, "r", encoding="utf-8") as f: + try: MODELS_DATA = json.load(f) + except: MODELS_DATA = [] + if not MODELS_DATA: + MODELS_DATA = [ + {"id": "groq|llama-3.1-8b-instant", "name": "Groq - Llama 3.1 (8B)", "is_free": True, "provider": "Général / Texte", "description": "Modèle par défaut."}, + {"id": "gemini|gemini-1.5-flash", "name": "Google - Gemini 1.5 Flash", "is_free": True, "provider": "Vision", "description": "Modèle par défaut."} + ] +load_catalog() + +def save_catalog(data): + global MODELS_DATA + MODELS_DATA = data + with open(CATALOG_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4, ensure_ascii=False)[cite: 11] + +def get_budget(): + mgmt_key = os.getenv("OPENROUTER_MANAGEMENT_KEY") + if not mgmt_key: return "Clé manquante[cite: 11]" + try: + data = requests.get("https://openrouter.ai/api/v1/credits", headers={"Authorization": f"Bearer {mgmt_key}"}, timeout=5).json().get("data", {}) + return f"{data.get('total_credits', 0) - data.get('total_usage', 0):.4f} $"[cite: 11] + except: return "Erreur réseau[cite: 11]" + +def parse_txt_catalog(content: str): + lines = content.split('\n') + models = [] + current = {} + ext_date, ext_time = "Inconnue", "" + + for line in lines: + line_trim = line.strip() + if "Généré le" in line_trim: + match = re.search(r"Généré le (\d{2}/\d{2}/\d{4}) à (\d{2}:\d{2}:\d{2})", line_trim) + if match: + ext_date, ext_time = match.group(1), match.group(2) + if line_trim.startswith("Nom : "): current['name'] = line_trim.replace("Nom : ", "").strip() + elif line_trim.startswith("ID : "): current['id'] = line_trim.replace("ID : ", "").strip() + elif line_trim.startswith("Tarif : "): current['is_free'] = ("Gratuit" in line_trim) + elif line_trim.startswith("Spécialité : "): current['provider'] = line_trim.replace("Spécialité : ", "").strip() + elif line_trim.startswith("Description : "): current['description'] = line_trim.replace("Description : ", "").strip() + elif line_trim.startswith("---"): + if 'id' in current and 'name' in current: models.append(current) + current = {} + return models, ext_date, ext_time[cite: 11] \ No newline at end of file diff --git a/backend/history_manager.py b/backend/history_manager.py new file mode 100644 index 0000000..a4718e3 --- /dev/null +++ b/backend/history_manager.py @@ -0,0 +1,36 @@ +# history_manager.py +import os +import json +import uuid +from datetime import datetime + +HISTORY_FILE = "sessions/history.json" + +def load_index(): + if not os.path.exists(HISTORY_FILE): return {} + with open(HISTORY_FILE, "r", encoding="utf-8") as f: + try: return json.load(f) + except: return {}[cite: 11] + +def save_index(idx): + with open(HISTORY_FILE, "w", encoding="utf-8") as f: + json.dump(idx, f, indent=4, ensure_ascii=False)[cite: 11] + +def load_session_data(session_id): + path = f"sessions/{session_id}.json" + if not os.path.exists(path): return [] + with open(path, "r", encoding="utf-8") as f: + try: return json.load(f) + except: return [][cite: 11] + +def save_session_data(session_id, messages): + with open(f"sessions/{session_id}.json", "w", encoding="utf-8") as f: + json.dump(messages, f, indent=4, ensure_ascii=False)[cite: 11] + +def create_new_session(): + session_id = str(uuid.uuid4()) + idx = load_index() + idx[session_id] = {"title": f"Projet du {datetime.now().strftime('%d/%m %H:%M')}", "pinned": False} + save_index(idx) + save_session_data(session_id, []) + return session_id[cite: 11] \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index b40f2ba..f742235 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,140 +1,207 @@ -from fastapi import FastAPI, HTTPException -from fastapi.staticfiles import StaticFiles -from fastapi.responses import HTMLResponse, Response -from pydantic import BaseModel -import requests -import asyncio +# main.py +from fasthtml.common import * import os -import uuid -import json -import subprocess -import urllib3 -import socket -import config +import time +import requests +from dotenv import load_dotenv -# Forçage IPv4 -old_getaddrinfo = socket.getaddrinfo -def force_ipv4_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): - return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags) -socket.getaddrinfo = force_ipv4_getaddrinfo +from auth import verify_password, verify_totp +from catalog_manager import MODELS_DATA, load_catalog, save_catalog, get_budget, parse_txt_catalog +from history_manager import load_index, save_index, load_session_data, save_session_data, create_new_session +from ai_engine import ask_llm, async_ask_llm, log_event -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +load_dotenv() -app = FastAPI(title="AETHAS38 API") +theme_hdrs = [ + Script(src="https://cdn.tailwindcss.com"), + 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"), + Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/tokyo-night-dark.min.css"), + Style(""" + body { background-color: #0f172a; color: #f8fafc; font-family: system-ui, sans-serif; margin: 0;} + .sidebar { background-color: #1e293b; border-right: 1px solid #333; height: 100vh; padding: 20px; display: flex; flex-direction: column;} + .main-content { padding: 20px; height: 100vh; display: flex; flex-direction: column; position: relative;} + .chat-container { flex-grow: 1; overflow-y: auto; margin-bottom: 20px; border: 1px solid #333; padding: 15px; border-radius: 8px; background: #151e2e; } + .input-row { display: flex; gap: 10px; align-items: flex-end; width: 100%; background: #1e293b; padding: 10px; border-radius: 10px; border: 1px solid #333;} + .input-box { flex-grow: 1; padding: 12px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: white; outline: none; } + .file-upload-btn { background: #2a2a35; border: 1px solid #444; color: #94a3b8; padding: 12px; border-radius: 5px; cursor: pointer; font-size:14px; display: flex; align-items:center; justify-content:center;} + .model-selector { padding: 8px; border-radius: 5px; background: #1e293b; border: 1px solid #444; color: #00e5ff; width: 100%; font-size: 12px; outline:none; } + .send-btn { padding: 12px 24px; border-radius: 5px; background: #00e5ff; color: #0f172a; font-weight: bold; cursor: pointer; border: none; } + .msg-user { color: #00e5ff; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;} + .msg-ia { color: #a855f7; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;} + .bubble-user { background: #1e293b; padding: 10px; border-radius: 5px; display: inline-block; color: white; border: 1px solid #333;} + .bubble-ia { background: #1e293b; padding: 15px; border-radius: 8px; display: block; color: #e2e8f0; border: 1px solid #333;} + .htmx-indicator { display: none; position: absolute; bottom: 180px; left: 50%; transform: translateX(-50%); background: rgba(15, 23, 42, 0.9); border: 1px solid #00e5ff; padding: 15px 30px; border-radius: 50px; color: #00e5ff; font-weight: bold; z-index: 50;} + .htmx-request .htmx-indicator { display: block; } + .export-btn { display: block; background: #0f172a; border: 1px solid #00e5ff; color: #00e5ff; font-weight: bold; padding: 10px; border-radius: 5px; text-align: center; cursor: pointer; width: 100%;} + """), + Script(""" + function filterModels() { + let filter = document.querySelector('input[name="filter_type"]:checked').value; + document.querySelectorAll('.model-selector option').forEach(opt => { + if(opt.value === "") return; + let isFree = opt.getAttribute('data-free') === 'true'; + if(filter === 'free' && !isFree) opt.style.display = 'none'; + else if(filter === 'paid' && isFree) opt.style.display = 'none'; + else opt.style.display = 'block'; + }); + } + function validateMultiIA() { + let wCount = 0; + for(let i=1; i<=2; i++) { if(document.getElementById('worker'+i).value !== "") wCount++; } + if(wCount === 0) { alert("Erreur : Sélectionnez au moins une IA de travail."); return false; } + return true; + } + function updateFileCount(input) { + let label = document.getElementById('file-label-text'); + if(input.files && input.files.length > 0) { label.innerText = `📎 ${input.files.length} fichier(s)`; label.style.color = '#00e5ff'; } + } + """) +] -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -FRONTEND_DIR = os.path.join(BASE_DIR, "frontend") -SESSIONS_DIR = os.path.join(BASE_DIR, "sessions") -MEDIA_DIR = os.path.join(SESSIONS_DIR, "media") -TEMP_DIR = os.path.join(SESSIONS_DIR, "temp") +app, rt = fast_app(secret_key=os.getenv("SESSION_SECRET", "super-secret-key-fallback"), hdrs=theme_hdrs) -os.makedirs(MEDIA_DIR, exist_ok=True) -os.makedirs(TEMP_DIR, exist_ok=True) +os.makedirs("sessions", exist_ok=True) -app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static") -app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") +@rt('/login') +def get(): + return Title("Connexion - AETHAS38"), Body( + Div( + H2("AETHAS 38 - Accès Sécurisé", style="color:#00e5ff; margin-bottom:20px; text-align:center;"), + Form( + Input(type="email", name="email", placeholder="E-mail", cls="input-box mb-3 w-full", required=True), + Input(type="password", name="password", placeholder="Mot de passe", cls="input-box mb-3 w-full", required=True), + Input(type="text", name="totp_code", placeholder="Code 2FA (6 chiffres)", cls="input-box mb-4 w-full", required=True), + Button("Se connecter", type="submit", cls="send-btn w-full"), + hx_post="/auth/login", hx_target="#auth-response" + ), + Div(id="auth-response", style="color:red; margin-top:10px; text-align:center;"), + cls="sidebar", style="margin: auto; width: 400px; height: auto; border-radius: 10px; margin-top: 15vh;" + ), style="background-color: #0f172a; height: 100vh; display: flex;" + ) -class ExportRequest(BaseModel): - content: str - format: str # pdf, txt, md, json - type: str # prompt ou response +@rt('/auth/login') +def post(email: str, password: str, totp_code: str, session): + if email == os.getenv("ADMIN_EMAIL") and verify_password(os.getenv("ADMIN_PASSWORD_HASH"), password) and verify_totp(os.getenv("ADMIN_2FA_SECRET"), totp_code): + session["authenticated"] = True + session["role"] = "admin" + return Script("window.location.href = '/';") + return Div("Identifiants ou code 2FA incorrects.", style="color:#ef4444; font-weight:bold;") -@app.get("/", response_class=HTMLResponse) -async def serve_admin(): - index_path = os.path.join(FRONTEND_DIR, "index.html") - with open(index_path, "r", encoding="utf-8") as f: - return f.read() +@rt('/logout') +def get(session): + session.clear() + return RedirectResponse('/login') -@app.get("/api/config") -async def get_config(): - return config.load_config() +@rt('/check_updates') +def get(session): + if not session.get("authenticated"): return Span("") + try: + res = requests.get("https://gitea.aethas38.duckdns.org/api/v1/repos/xavier/MULTI-IA-CODAGE/commits?limit=1", timeout=3) + if res.status_code == 200: + return Span(f"🟢 Gitea Connecté ({res.json()[0].get('sha', '')[:7]})", style="color:#10b981;") + except: pass + return Span("🟠 Gitea hors ligne", style="color:#fbbf24;") -@app.post("/api/config") -async def update_config(data: dict): - config.save_config(data) - return {"status": "success", "message": "Configuration sauvegardée avec succès."} +def render_history_list(): + idx = load_index() + items = [Button("➕ Nouveau Projet", cls="export-btn mb-3", style="background:#10b981; color:#0f172a;", hx_get="/session/new", hx_target="#chat-history")] + for sid, data in idx.items(): + items.append(Div(Div(data['title'], cls="truncate flex-grow cursor-pointer text-sm hover:text-cyan-400", hx_get=f"/session/load/{sid}", hx_target="#chat-history"), cls="flex justify-between items-center w-full mb-2 bg-[#2a2a35] p-2 rounded border border-[#444]")) + return Div(*items, id="history-list-container", hx_swap_oob="true") -@app.post("/api/export") -async def export_content(req: ExportRequest): - filename_base = f"aethas38_{req.type}_{uuid.uuid4().hex[:8]}" +def render_model_dropdown(field_id, placeholder): + groups = {} + for m in MODELS_DATA: + prov = m.get('provider', 'Général') + if prov not in groups: groups[prov] = [] + groups[prov].append(m) + opts = [Option("--- Laisser vide ---", value="")] + for prov in sorted(groups.keys()): + grp_opts = [Option(f"{m['name']} ({'Gratuit' if str(m.get('is_free'))=='True' else 'Payant'})", value=m['id'], **{"data-free": str(m.get('is_free', False)).lower()}) for m in groups[prov]] + opts.append(Optgroup(label=prov)(*grp_opts)) + return Div(P(placeholder, style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:2px;"), Select(*opts, name=field_id, id=field_id, cls="model-selector", onchange="filterModels()"), style="margin-bottom: 8px;") + +SESSION_PROGRESS = {} + +@rt('/status/{sid}') +def get_status(sid: str): + return SESSION_PROGRESS.get(sid, "⚙️ Traitement en cours...") + +@rt('/') +def get(session): + if not session.get("authenticated"): return RedirectResponse('/login') + session_id = create_new_session() + return Title("AETHAS38 Orchestrateur"), Body( + Div( + Div( + H2("AETHAS 38", style="color:#00e5ff; font-weight:bold; font-size:24px;"), + Div("⏳ Vérification GitHub...", hx_get="/check_updates", hx_trigger="load", style="font-size:10px; margin-bottom:15px;"), + Div(H3("Historique", style="color:#94a3b8; font-size:12px; font-weight:bold; margin-bottom:10px;"), render_history_list(), style="flex-grow:1; overflow:hidden;"), + Button("📥 Exporter Modèles", type="button", cls="export-btn mb-2", onclick="window.location.href='/export_models'"), + A("Déconnexion", href="/logout", cls="export-btn text-center", style="color:#ef4444; border-color:#ef4444;"), + Div(P("BUDGET", style="color:#94a3b8; font-size:10px; font-weight:bold; margin-top:8px;"), P(get_budget(), id="budget-display", style="color:#10b981; font-size:16px; font-weight:bold;")), + cls="sidebar" + ), + Div( + Div(id="chat-history", cls="chat-container"), + Div(Span("⚙️ Préparation...", id="loading-text"), id="loading-tracker", cls="htmx-indicator"), + Form( + Input(type="hidden", name="session_id", value=session_id, id="current-session-id"), + Div( + Div(render_model_dropdown("writer", "1. Rédacteur"), render_model_dropdown("synthesizer", "7. Concaténeur"), style="border-right: 1px solid #444; padding-right:15px;"), + Div(render_model_dropdown("worker1", "2. Travailleur 1"), render_model_dropdown("worker2", "3. Travailleur 2"), style="padding: 0 15px;"), + cls="grid grid-cols-2 gap-4 mb-4 bg-[#151e2e] p-4 rounded border border-[#333]" + ), + Div( + Input(type="file", name="fichiers", id="file-upload", multiple=True, style="display:none;", onchange="updateFileCount(this)"), + Label(Span("📎 Fichiers", id="file-label-text"), _for="file-upload", cls="file-upload-btn w-40"), + Input(type="text", name="msg", placeholder="Votre consigne...", cls="input-box", required=True), + Button("Lancer", type="submit", cls="send-btn"), + cls="input-row" + ), + hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#loading-tracker", enctype="multipart/form-data", + onsubmit="return validateMultiIA();" + ), + cls="main-content" + ), + style="display: grid; grid-template-columns: 280px 1fr;" + ) + ) + +@rt('/chat') +async def post(msg: str, session_id: str, session, writer: str = "", worker1: str = "", worker2: str = "", synthesizer: str = "", fichiers: list[UploadFile] = None): + if not session.get("authenticated"): return RedirectResponse('/login') + workers = [w for w in [worker1, worker2] if w] - if req.format == "txt": - return Response( - content=req.content, - media_type="text/plain", - headers={"Content-Disposition": f"attachment; filename={filename_base}.txt"} - ) - - elif req.format == "md": - return Response( - content=f"# AETHAS38 Export ({req.type.upper()})\n\n{req.content}", - media_type="text/markdown", - headers={"Content-Disposition": f"attachment; filename={filename_base}.md"} - ) - - elif req.format == "json": - json_data = json.dumps({"type": req.type, "content": req.content}, indent=4, ensure_ascii=False) - return Response( - content=json_data, - media_type="application/json", - headers={"Content-Disposition": f"attachment; filename={filename_base}.json"} - ) - - elif req.format == "pdf": - # Utilisation de LibreOffice en mode headless - html_filename = os.path.join(TEMP_DIR, f"{filename_base}.html") - pdf_filename = os.path.join(TEMP_DIR, f"{filename_base}.pdf") - - # Création d'un HTML propre pour la conversion - html_content = f""" - - - - - - - -

AETHAS38 - Export {req.type.upper()}

-

{req.content}

- - - """ - - with open(html_filename, "w", encoding="utf-8") as f: - f.write(html_content) - - try: - # Commande LibreOffice headless pour convertir le HTML en PDF - # Note: selon l'OS (Windows vs Linux Docker), la commande peut s'appeler 'soffice' ou 'libreoffice' - cmd = ["soffice", "--headless", "--convert-to", "pdf", "--outdir", TEMP_DIR, html_filename] - - # Exécution synchrone dans un thread pour ne pas bloquer l'API - def run_libreoffice(): - subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - await asyncio.to_thread(run_libreoffice) - - if not os.path.exists(pdf_filename): - raise Exception("La génération du PDF par LibreOffice a échoué.") - - with open(pdf_filename, "rb") as pdf_file: - pdf_bytes = pdf_file.read() - - # Nettoyage des fichiers temporaires - os.remove(html_filename) - os.remove(pdf_filename) - - return Response( - content=pdf_bytes, - media_type="application/pdf", - headers={"Content-Disposition": f"attachment; filename={filename_base}.pdf"} - ) - - except Exception as e: - raise HTTPException(status_code=500, detail=f"Erreur de génération PDF via LibreOffice : {str(e)}") - - raise HTTPException(status_code=400, detail="Format d'export non supporté.") \ No newline at end of file + SESSION_PROGRESS[session_id] = "⚡ Traitement des IA en cours..." + tasks = [async_ask_llm(session_id, w, msg) for w in workers] + results = await asyncio.gather(*tasks) + final_response = results[0] if results else "Aucun travailleur configuré." + + session_data = load_session_data(session_id) + session_data.append({"user": msg, "ia": final_response}) + save_session_data(session_id, session_data) + + return Div(P("Vous", cls="msg-user"), Div(msg, cls="bubble-user"), P("AETHAS38", cls="msg-ia"), Div(final_response, cls="bubble-ia")), P(get_budget(), id="budget-display", style="color:#10b981; font-size:16px; font-weight:bold;", hx_swap_oob="true") + +@rt('/export_models') +def get(session): + if not session.get("authenticated"): return RedirectResponse('/login') + return Response("=== CATALOGUE ===\nID: groq|llama-3.1-8b-instant\nTarif: Gratuit", media_type="text/plain", headers={"Content-Disposition": "attachment; filename=modeles.txt"}) + +@rt('/session/new') +def get(session): + if not session.get("authenticated"): return RedirectResponse('/login') + sid = create_new_session() + return Div(), Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true"), render_history_list() + +@rt('/session/load/{sid}') +def get(sid: str, session): + if not session.get("authenticated"): return RedirectResponse('/login') + messages = load_session_data(sid) + return tuple(Div(P("Vous", cls="msg-user"), Div(m['user'], cls="bubble-user"), P("AETHAS38", cls="msg-ia"), Div(m['ia'], cls="bubble-ia")) for m in messages) + (Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true"),) + +if __name__ == '__main__': + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=5001) \ No newline at end of file diff --git a/setup.py b/backend/setup.py similarity index 100% rename from setup.py rename to backend/setup.py diff --git a/frontend/index.html b/frontend/index.html index 766228e..df4139e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,6 +14,8 @@ .btn-primary { background-color: #00e5ff; color: #0f172a; font-weight: bold; padding: 10px 20px; border-radius: 5px; cursor: pointer; transition: 0.2s; } .btn-primary:hover { background-color: #38bdf8; } .btn-primary:disabled { background-color: #334155; color: #94a3b8; cursor: not-allowed; } + .btn-export { background-color: #334155; color: #38bdf8; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 4px; cursor: pointer; border: 1px solid #475569; } + .btn-export:hover { background-color: #475569; color: #00e5ff; } .btn-danger { background-color: #f43f5e; color: white; padding: 5px 10px; border-radius: 5px; cursor: pointer; font-size: 12px; } .tab-btn { padding: 10px 20px; font-weight: bold; cursor: pointer; transition: 0.2s; border-bottom: 2px solid transparent; } .tab-active { color: #00e5ff; border-bottom-color: #00e5ff; } @@ -24,77 +26,30 @@
- +

🤖 AETHAS38

- v2.1.0 +
+ 💰 OpenRouter - Crédit : {{ orCredits.limit }} + Consommation : {{ orCredits.usage }} +
- - + + +
{{ notification }}
- -
-
-

Générateur Média (HuggingFace)

- -
-
- - -
-
- - -
-
- -
- -
-
- - -
-

Résultat :

- - - - - - - - -
-
- - +
-
-

🔑 Clés API Principales

+

🔑 Clés API & Suivi

@@ -104,37 +59,43 @@
-
- - -
- +
-

🔌 Fournisseurs Personnalisés

- -
-
-
-

Fournisseur #{{ index + 1 }}

- +

🎛️ Sélection & Filtrage des Modèles

+
+ +
-
+
+ +
+
+ + +
+
+ + +
+
+ +
+
- - -
-
- - -
-
- - + {{ m.name }} ({{ m.id }}) +

{{ m.description }}

+ + {{ m.pricing === 'free' ? 'GRATUIT' : 'PAYANT' }} +
@@ -144,26 +105,121 @@
+ +
+
+

🎬 Studio Multimédia

+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+

Résultat :

+ + +
+
+ + +
+
+

📂 Analyse de Documents & Exports Multi-formats

+
+ + +
+
+ + +
+
+ +
+
+ +
+
+

Rapport : {{ docResult.filename }}

+ +
+ Exporter en : + + + + +
+
+
+ {{ docResult.extracted_preview }} +
+
+
+