Test : Intégration bouton PayPal Sandbox et activation du Serveur Mail
Build and Push Docker Image / build-and-push (push) Successful in 1m45s

This commit is contained in:
xavier committed 2026-08-23 09:47:14 +02:00
1 parent 632c23069a
commit 2ea18701b5
12 files changed
+486 -187

No files matched your search

+15 -10
View File
@@ -9,8 +9,6 @@ load_dotenv()
api_key_openrouter = os.getenv("OPENROUTER_API_KEY") or "sk-or-v1-cle-non-definie"
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key_openrouter)
# Chemin ABSOLU strict
CATALOGUE_FILE = "/DATA/AppData/MULTI-IA-AETHAS38/catalogue_dynamique.json"
def verifier_cle_api():
@@ -19,8 +17,10 @@ def verifier_cle_api():
raise Exception("Clé OPENROUTER_API_KEY absente dans le fichier .env.")
return cle
def obtenir_modele_dynamique(premium: bool):
"""Charge le catalogue dynamiquement selon l'état Premium ou Gratuit."""
def obtenir_modele_dynamique(premium: bool, prompt: str = ""):
"""Charge le catalogue dynamiquement et adapte le modèle si la règle Éducation est détectée."""
est_education = "RÈGLE ÉDUCATION NATIONALE" in prompt
try:
if os.path.exists(CATALOGUE_FILE):
with open(CATALOGUE_FILE, "r", encoding="utf-8") as f:
@@ -30,20 +30,25 @@ def obtenir_modele_dynamique(premium: bool):
claude = [m for m in paid_models if "claude-3.5-sonnet" in m.lower()]
return claude[0] if claude else (paid_models[0] if paid_models else "anthropic/claude-3.5-sonnet")
else:
# En mode gratuit + Education, Mistral est le plus pointu en syntaxe française
if est_education:
return "mistralai/mistral-small-24b-instruct-2501:free"
free_models = data.get("free_models", [])
priorites = [m for m in free_models if "gemma" in m.lower() or "nemotron" in m.lower()]
return priorites[0] if priorites else (free_models[0] if free_models else "google/gemma-2-9b-it:free")
except Exception:
pass
return "anthropic/claude-3.5-sonnet" if premium else "google/gemma-2-9b-it:free"
if premium: return "anthropic/claude-3.5-sonnet"
return "mistralai/mistral-small-24b-instruct-2501:free" if est_education else "google/gemma-2-9b-it:free"
def interroger_openrouter(prompt: str, type_modele: str = "openrouter_chat", premium: bool = False, max_retries: int = 2, chat_id: str = None):
verifier_cle_api()
nom_modele_api = obtenir_modele_dynamique(premium)
nom_modele_api = obtenir_modele_dynamique(premium, prompt) # On passe le prompt pour détecter le marqueur
nom_affichage = f"OpenRouter ({nom_modele_api.split('/')[-1]})"
couleur = "#d97757" if premium else "#4a90e2"
# Injection du Tracker Financier OpenRouter
extra_body_data = {"session_id": chat_id} if chat_id else {}
for tentative in range(max_retries):
@@ -53,7 +58,7 @@ def interroger_openrouter(prompt: str, type_modele: str = "openrouter_chat", pre
messages=[{"role": "user", "content": prompt}],
extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"},
extra_body=extra_body_data,
timeout=90.0 # TIMEOUT MONTE A 90s
timeout=90.0
)
texte = response.choices[0].message.content
usage = response.usage
@@ -78,7 +83,7 @@ def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code
verifier_cle_api()
extra_body_data = {"session_id": chat_id} if chat_id else {}
modeles = [obtenir_modele_dynamique(premium)]
modeles = [obtenir_modele_dynamique(premium, prompt)]
if premium:
modeles.append("openai/gpt-4o-mini")
else:
@@ -92,7 +97,7 @@ def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code
messages=[{"role": "user", "content": prompt}],
extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"},
extra_body=extra_body_data,
timeout=90.0 # TIMEOUT MONTE A 90s
timeout=90.0
)
contenu = res.choices[0].message.content
if contenu:
+68 -12
View File
@@ -1,6 +1,7 @@
import os
import json
import time
import requests
from datetime import datetime
import tiktoken
from modules.logger import log_event
@@ -10,19 +11,50 @@ BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38"
os.makedirs(BASE_DIR, exist_ok=True)
BALANCES_FILE = os.path.join(BASE_DIR, "aethas_balances.json")
AUDIT_FILE = os.path.join(BASE_DIR, "aethas_financial_audit.json")
TARIFS_OPENROUTER = {
"meta-llama/llama-3.1-8b-instruct:free": 0.0,
"qwen/qwen-2.5-coder-32b-instruct:free": 0.0,
"mistralai/mistral-small-24b-instruct-2501:free": 0.0,
"openai/gpt-4o-mini": 0.0000015,
"anthropic/claude-3.5-sonnet": 0.000015,
"mistralai/mistral-large-2411": 0.000006,
"google/gemini-2.5-pro": 0.000005
}
TARIFS_FILE = os.path.join(BASE_DIR, "tarifs_openrouter.json")
VIP_ADMIN_USERS = ["xavier", "céline", "celine", "anaëlle", "anaelle", "renan", "audric"]
def obtenir_tarifs_dynamiques() -> dict:
"""Récupère les tarifs via API toutes les 2 heures maximum pour éviter le hardcoding."""
maintenant = time.time()
if os.path.exists(TARIFS_FILE):
if maintenant - os.path.getmtime(TARIFS_FILE) < 7200:
try:
with open(TARIFS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
tarifs = {}
try:
resp = requests.get("https://openrouter.ai/api/v1/models", timeout=10)
if resp.status_code == 200:
data = resp.json().get("data", [])
for m in data:
pricing = m.get("pricing", {})
p_prompt = float(pricing.get("prompt", 0.0))
p_comp = float(pricing.get("completion", 0.0))
# On sécurise l'estimation en prenant la valeur la plus haute entre tokens in et out
tarifs[m["id"]] = max(p_prompt, p_comp)
with open(TARIFS_FILE, "w", encoding="utf-8") as f:
json.dump(tarifs, f, indent=4)
log_event("INFO", "BILLING", "Tarifs OpenRouter mis à jour dynamiquement.")
return tarifs
except Exception as e:
log_event("ERROR", "BILLING", f"Échec récupération des tarifs : {e}")
# Repli sur le cache si l'API est injoignable
if os.path.exists(TARIFS_FILE):
try:
with open(TARIFS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {}
def charger_soldes() -> dict:
if os.path.exists(BALANCES_FILE):
try:
@@ -71,7 +103,8 @@ def compter_tokens(texte: str, modele: str = "cl100k_base") -> int:
def estimer_cout_facture(prompt: str, modele_id: str, rang_utilisateur: str = "standard", multi_ia: bool = False) -> tuple[float, float, int]:
tokens = compter_tokens(prompt)
cout_unitaire = TARIFS_OPENROUTER.get(modele_id, 0.0)
tarifs = obtenir_tarifs_dynamiques()
cout_unitaire = tarifs.get(modele_id, 0.0)
cout_brut = tokens * cout_unitaire
if multi_ia and cout_brut > 0:
@@ -144,4 +177,27 @@ def generer_dossier_litige(utilisateur_nom: str, est_admin: bool = False) -> lis
"tokens": item.get("tokens"),
"montant_facture_ttc": item.get("prix_facture_client")
})
return dossier
return dossier
def calculer_prix_recharge_ttc(montant_credits_usd: float, methode: str = "paypal") -> float:
"""
Calcule le coût final pour l'utilisateur qui souhaite acheter une recharge de crédits.
Le coût final répercute les frais bancaires pour que l'entreprise soit à l'équilibre.
Frais estimés (Variables selon devises, basés sur des profils Sandbox/Standard) :
- PayPal : 2.9% + 0.35$ fixes par transaction
- Revolut Business : 1.0% + 0.20$ fixes par transaction en ligne
Formule d'absorption des frais : Montant_Final = (Credits + Frais_Fixes) / (1 - Pourcentage_Frais)
"""
if methode.lower() == "paypal":
frais_fixes = 0.35
frais_variable = 0.029
elif methode.lower() == "revolut":
frais_fixes = 0.20
frais_variable = 0.010
else:
return montant_credits_usd # Aucune méthode bancaire spécifiée
montant_a_payer = (montant_credits_usd + frais_fixes) / (1 - frais_variable)
return round(montant_a_payer, 2)
+52 -38
View File
@@ -1,6 +1,10 @@
import os
import json
from datetime import date
import uuid
import time
import re
import psutil
from fasthtml.common import *
from modules.database import load_history, save_history
from modules.scraper import rechercher_sur_google, extraire_contenu_urls
@@ -13,10 +17,27 @@ from modules.billing_manager import verifier_et_valider_budget, obtenir_solde
from modules.rag_manager import rechercher_contexte_rag, auto_apprendre_rag
from modules.moderator import analyser_intention_illicite, appliquer_sanction_et_archiver
def check_and_update_resource_quota(utilisateur_nom, role_utilisateur):
quota_file = "/DATA/AppData/MULTI-IA-AETHAS38/Utilisateurs/quotas_ressources.json"
today = str(date.today())
limites = {"super admin": 10, "admin": 10, "vip": 5, "famille": 5, "standard": 3}
limite_max = limites.get(role_utilisateur.lower(), 3)
try:
if os.path.exists(quota_file):
with open(quota_file, "r") as f: data = json.load(f)
else: data = {}
except: data = {}
if data.get("date") != today: data = {"date": today, "users": {}}
user_uses = data["users"].get(utilisateur_nom, 0)
if user_uses >= limite_max: return False
data["users"][utilisateur_nom] = user_uses + 1
os.makedirs(os.path.dirname(quota_file), exist_ok=True)
with open(quota_file, "w") as f: json.dump(data, f)
return True
async def handle_chat_request(request, session):
start_time = time.time()
try:
# Psutil et la gestion de surcharge retirés comme demandé
form = await request.form()
prompt = form.get("prompt", "")
chat_id = form.get("chat_id", str(uuid.uuid4()))
@@ -24,6 +45,7 @@ async def handle_chat_request(request, session):
mode_payant = form.get("mode_payant") == "on"
mode_multi_ia = form.get("mode_multi_ia") == "on"
mode_education = form.get("mode_education") == "on" # INTERCEPTION DU MODE EDU
validation_acquise = form.get("validation_acquise") == "true"
choix_utilisateur = form.get("choix_utilisateur", "aucun")
@@ -39,10 +61,16 @@ async def handle_chat_request(request, session):
role_utilisateur = "Super Admin" if utilisateur_nom.lower() == "xavier" else ("VIP" if utilisateur_nom.lower() in ["céline", "anaëlle", "renan", "audric"] else "standard")
ip_client = request.client.host if request.client else "Inconnue"
cpu_usage = psutil.cpu_percent(interval=0.1)
ram_usage = psutil.virtual_memory().percent
if cpu_usage > 60.0 or ram_usage > 60.0:
if not check_and_update_resource_quota(utilisateur_nom, role_utilisateur):
return Div(H3("⚠️ Limite de requêtes atteintes", style="color:#ff9800;"), P(f"Serveur très sollicité (>60%). Quota épuisé."), style="background:#333; padding:15px; border-radius:10px; border: 1px solid #ff9800;")
est_illicite, raison = analyser_intention_illicite(prompt)
if est_illicite:
sanction = appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client)
return Div(H3("⚠️ Avertissement de Sécurité"), P("Votre requête enfreint les conditions d'utilisation de la plateforme."), P(f"Sanction : {sanction}"), style="color:red; background:#300; padding:15px; border-radius:10px;")
return Div(H3("⚠️ Avertissement de Sécurité"), P(f"Sanction : {sanction}"), style="color:red; background:#300; padding:15px; border-radius:10px;")
file_summary = []
file_names = []
@@ -53,24 +81,14 @@ async def handle_chat_request(request, session):
file_summary.append(f"\n--- Fichier : {f.filename} ---\n{content.decode('utf-8', errors='ignore')}")
liste_fichiers_str = ", ".join(file_names) if file_names else "Aucun fichier"
log_mode = "PAYANT 👑" if mode_payant else "GRATUIT 🆓"
log_event("INFO", "SYSTEM", f"Requête reçue [{log_mode}]. Fichiers joints : {liste_fichiers_str}")
log_event("INFO", "SYSTEM", f"Requête reçue (Edu: {mode_education} / Payant: {mode_payant})")
sec_prompt = f"""Tu es l'Orchestrateur système. Analyse la requête et choisis le meilleur fournisseur IA :
- "gemini" : (OBLIGATOIRE si fichiers joints), rédaction complexe, comparaisons.
- "openrouter_chat" : requêtes générales, texte rapide, traduction.
- "openrouter_code" : programmation, Minecraft/KubeJS, scripts, mathématiques.
- "openrouter_droit" : droit français, CGV, CGU, RGPD.
- "local" : salutations basiques, questions simples.
Tu dois aussi déterminer si la requête nécessite de lire la documentation interne (RAG). Thèmes : "minecraft", "droit", "serveur", "regles" ou "aucun".
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé...", "fournisseur": "gemini|openrouter_chat|openrouter_code|openrouter_droit|local", "theme_rag": "minecraft|droit|serveur|regles|aucun"}}
Requête : {prompt}"""
sec_prompt = f"""Tu es l'Orchestrateur système. Analyse la requête et choisis le meilleur fournisseur IA..."""
try:
texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt)
decision = nettoyer_decision_router(texte_routeur)
except Exception:
decision = {"intention": "Analyse de secours", "fournisseur": "openrouter_code" if "minecraft" in prompt.lower() else "openrouter_chat", "theme_rag": "aucun"}
except:
decision = {"intention": "Analyse", "fournisseur": "openrouter_chat", "theme_rag": "aucun"}
intention = decision.get("intention", "Analyse")
fournisseur = decision.get("fournisseur", "openrouter_chat").lower()
@@ -78,28 +96,32 @@ async def handle_chat_request(request, session):
if mode_payant or mode_multi_ia:
fournisseur = "openrouter_code"
intention += " (Forcé API Premium/Multi via UI)"
intention += " (API Premium/Multi)"
if file_names:
fournisseur = "gemini"
intention += " (Forcé Gemini via Sécurité Fichiers)"
# --- LOGIQUE EDUCATION NATIONALE ---
if mode_education:
fournisseur = "openrouter_chat"
thematique_rag = "Education_Nationale"
intention = "Éducation Nationale " + ("(Payant)" if mode_payant else "(IA Gratuite)")
# Marqueur secret pour ai_openrouter.py
instruction_edu = "[SYSTÈME] RÈGLE ÉDUCATION NATIONALE : Agis comme un expert de l'Éducation Nationale française. Pédagogie stricte, respect du B.O.\n\n"
prompt = instruction_edu + prompt
contexte_rag = ""
if thematique_rag != "aucun":
contexte_rag = rechercher_contexte_rag(prompt, categorie=thematique_rag)
urls = re.findall(r"https?://[^\s]+", prompt)
mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "trouve", "site"]
if any(m in prompt.lower() for m in mots_cles):
if any(m in prompt.lower() for m in ["cherche", "recherche", "sur le web"]):
urls.extend(rechercher_sur_google(prompt))
web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "")
if contexte_rag:
full_prompt += f"\n\n[Contexte Documentaire RAG (Base de connaissances interne) :\n{contexte_rag}]"
if web_text:
full_prompt += f"\n\n[Contexte web :\n{web_text}]"
if contexte_rag: full_prompt += f"\n\n[Contexte RAG :\n{contexte_rag}]"
if web_text: full_prompt += f"\n\n[Contexte web :\n{web_text}]"
if "openrouter" in fournisseur:
modele_estime = "anthropic/claude-3.5-sonnet" if mode_payant else "cohere/north-mini-code:free"
@@ -108,19 +130,16 @@ async def handle_chat_request(request, session):
from modules.billing_manager import estimer_cout_facture
_, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
if prix_estime > 0:
return render_popup_paiement(prix_estime, prompt, chat_id, mode_multi_ia)
return render_popup_paiement(prix_estime, prompt, chat_id, mode_multi_ia, mode_education)
# Empêche un utilisateur classique avec un solde à zéro de consommer du Premium
if mode_payant and role_utilisateur.lower() not in ["super admin", "admin", "vip", "famille"]:
from modules.billing_manager import estimer_cout_facture
_, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
if prix_estime > obtenir_solde(utilisateur_nom):
return Div(H3("❌ Solde Insuffisant", style="color:red;"), P("Veuillez recharger votre compte."), style="background:#300; padding:15px; border-radius:10px;")
return Div(H3("❌ Solde Insuffisant", style="color:red;"), P("Rechargez votre compte."), style="background:#300; padding:15px;")
# CRUCIAL : Transmission du chat_id à l'Engine
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia, chat_id=chat_id)
# Le paiement véritable s'exécute APRÈS la réponse de l'IA OpenRouter
if "openrouter" in fournisseur_nom.lower() and mode_payant:
verifier_et_valider_budget(full_prompt, modele, utilisateur_nom=utilisateur_nom, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
@@ -131,20 +150,15 @@ async def handle_chat_request(request, session):
history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []}
execution_time = round(time.time() - start_time, 1)
display_msg = prompt
if file_names:
display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(file_names)}\n> *🗑️ Note : Fichiers traités de manière éphémère en mémoire vive.*"
display_msg = prompt.replace("[SYSTÈME] RÈGLE ÉDUCATION NATIONALE : Agis comme un expert de l'Éducation Nationale française. Pédagogie stricte, respect du B.O.\n\n", "")
if file_names: display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(file_names)}"
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, "execution_time": execution_time})
save_history(history)
log_event("INFO", fournisseur_nom, f"Réponse générée en {execution_time}s.")
return render_user_msg(display_msg), render_ai_msg(reponse, intention, fournisseur_nom, modele, couleur, execution_time), Script("renderMarkdown();"), render_sidebar()
except Exception as e:
if "SOLDE_INSUFFISANT" in str(e):
details = str(e).split("|")
return Div(H3("❌ Solde Insuffisant", style="color:red;"), P(f"Cette requête coûte {details[1]} $, mais il ne vous reste que {details[2]} $."), P("Veuillez recharger votre compte ou désactiver le mode Payant."), style="background:#300; padding:15px; border-radius:10px; border: 1px solid red;")
log_event("CRITICAL", "SYSTEM", f"Erreur dans handle_chat_request : {e}")
log_event("CRITICAL", "SYSTEM", f"Erreur : {e}")
return Div(f"Erreur système : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
+7 -13
View File
@@ -2,29 +2,23 @@ import os
import json
import time
# FORCAGE DU CHEMIN ABSOLU
BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38"
os.makedirs(BASE_DIR, exist_ok=True)
HISTORY_FILE = os.path.join(BASE_DIR, "aethas_history.json")
def load_history():
if not os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
json.dump({}, f)
with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump({}, f)
with open(HISTORY_FILE, 'r', encoding='utf-8') as f: return json.load(f)
def save_history(history):
with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
json.dump(history, f, indent=4)
with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, indent=4)
def nettoyer_vieilles_discussions():
history = load_history()
now = time.time()
un_an_en_secondes = 365 * 24 * 60 * 60
a_supprimer = [cid for cid, data in history.items() if not data.get('pinned', False) and now - data.get('timestamp', now) > un_an_en_secondes]
un_an = 365 * 24 * 60 * 60
a_supprimer = [cid for cid, data in history.items() if not data.get('pinned', False) and now - data.get('timestamp', now) > un_an]
if a_supprimer:
for cid in a_supprimer:
del history[cid]
save_history(history)
print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) supprimée(s).")
for cid in a_supprimer: del history[cid]
save_history(history)
+45
View File
@@ -0,0 +1,45 @@
import os
import requests
import xml.etree.ElementTree as ET
from datetime import datetime
from modules.logger import log_event
RAG_DIR = "/DATA/AppData/MULTI-IA-AETHAS38/RAG/Education_Nationale"
def maj_base_education():
"""Scrape le flux RSS du Bulletin Officiel pour maintenir la base documentaire à jour."""
os.makedirs(RAG_DIR, exist_ok=True)
url_rss = "https://www.education.gouv.fr/bo/rss.xml"
try:
response = requests.get(url_rss, timeout=15)
if response.status_code == 200:
root = ET.fromstring(response.content)
# Formatage pour une ingestion parfaite par le RAG
contenu_md = f"# Veille Pédagogique - Bulletin Officiel (B.O.)\n"
contenu_md += f"*Date de mise à jour RAG : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n"
# On extrait les 20 dernières circulaires/textes de loi
for item in root.findall('.//item')[:20]:
titre = item.find('title').text if item.find('title') is not None else "Document sans titre"
lien = item.find('link').text if item.find('link') is not None else "Aucun lien"
desc = item.find('description').text if item.find('description') is not None else "Pas de résumé disponible."
contenu_md += f"## {titre}\n"
contenu_md += f"**Source officielle :** {lien}\n"
contenu_md += f"> {desc}\n\n---\n"
fichier_bo = os.path.join(RAG_DIR, "bulletin_officiel_recent.md")
with open(fichier_bo, "w", encoding="utf-8") as f:
f.write(contenu_md)
log_event("INFO", "SCRAPER_EDU", "Base RAG Éducation (B.O.) mise à jour avec succès.")
else:
log_event("WARNING", "SCRAPER_EDU", f"Le site du ministère a retourné le code {response.status_code}.")
except Exception as e:
log_event("ERROR", "SCRAPER_EDU", f"Erreur lors de la MAJ de la base Éducation : {e}")
if __name__ == "__main__":
# Permet de tester le script directement en ligne de commande : python modules/eduscol_scraper.py
maj_base_education()
+1 -12
View File
@@ -3,7 +3,6 @@ import logging
from logging.handlers import TimedRotatingFileHandler
from datetime import datetime
# Chemin absolu obligatoire pour correspondre au volume Docker
LOG_DIR = "/DATA/AppData/MULTI-IA-AETHAS38"
os.makedirs(LOG_DIR, exist_ok=True)
LOG_FILE = os.path.join(LOG_DIR, "aethas_logs.txt")
@@ -12,7 +11,6 @@ logger = logging.getLogger("Aethas38")
logger.setLevel(logging.INFO)
if not logger.handlers:
# Conservation 365 jours pour conformité légale
handler = TimedRotatingFileHandler(LOG_FILE, when="midnight", interval=1, backupCount=365, encoding="utf-8")
handler.suffix = "%Y-%m-%d"
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
@@ -21,17 +19,8 @@ if not logger.handlers:
def log_event(niveau, source, message):
msg_formate = f"[{source}] {message}"
if niveau.upper() == "INFO": logger.info(msg_formate)
elif niveau.upper() == "WARNING": logger.warning(msg_formate)
elif niveau.upper() == "ERROR": logger.error(msg_formate)
elif niveau.upper() == "CRITICAL": logger.critical(msg_formate)
print(f"[{datetime.now().strftime('%H:%M:%S')}] [{niveau}] {msg_formate}")
if niveau in ["ERROR", "CRITICAL"]:
try:
from modules.debug_mailer import envoyer_alerte_debug
envoyer_alerte_debug(niveau, source, message)
except Exception:
pass
print(f"[{datetime.now().strftime('%H:%M:%S')}] [{niveau}] {msg_formate}")
+50
View File
@@ -0,0 +1,50 @@
import os
from fasthtml.common import *
def render_paypal_recharge_widget(montant_usd: float = 10.00):
"""
Génère le bouton de paiement PayPal (Smart Button) pour recharger les crédits.
Utilise le Client ID Sandbox défini dans le fichier .env.
"""
client_id = os.getenv("PAYPAL_CLIENT_ID")
if not client_id:
return Div("Clé PayPal (Client ID) manquante dans .env", style="color: red;")
# Le script officiel de PayPal injecte les boutons de paiement
script_paypal = Script(src=f"https://www.paypal.com/sdk/js?client-id={client_id}&currency=USD")
container_id = f"paypal-button-container-{str(montant_usd).replace('.', '')}"
script_logique = Script(f"""
paypal.Buttons({{
createOrder: function(data, actions) {{
return actions.order.create({{
purchase_units: [{{
amount: {{
value: '{montant_usd}'
}}
}}]
}});
}},
onApprove: function(data, actions) {{
return actions.order.capture().then(function(details) {{
alert('Paiement réussi par ' + details.payer.name.given_name + '! Les crédits seront ajoutés.');
// Ici on fera un fetch vers ton backend python pour créditer l'utilisateur
fetch('/api/paypal/success', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ orderID: data.orderID }})
}}).then(res => window.location.reload());
}});
}}
}}).render('#{container_id}');
""")
return Div(
script_paypal,
H3(f"Recharge de {montant_usd}$"),
Div(id=container_id),
script_logique,
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; margin-top: 15px; text-align: center; max-width: 300px;"
)
+37 -28
View File
@@ -13,7 +13,9 @@ def get_theme_hdrs():
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 CLASSIQUE --- */
.sidebar { width: 280px; background-color: #0a0a0c; border-right: 1px solid #333; padding: 20px; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; transition: left 0.3s ease; }
.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; }
@@ -27,8 +29,10 @@ def get_theme_hdrs():
.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); }
/* --- CONTENU CENTRAL --- */
.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; }
.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: 160px; 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; }
@@ -39,38 +43,56 @@ def get_theme_hdrs():
.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; }
/* --- NOUVEAU LAYOUT ZONE DE SAISIE --- */
.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; z-index: 10;}
.input-container-box { width: 100%; max-width: 900px; background: #1e1e24; border-radius: 20px; border: 1px solid #444; padding: 15px; box-sizing: border-box; display: flex; flex-direction: column; gap: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
.textarea-wrapper { display: flex; align-items: flex-end; gap: 10px; width: 100%; }
.options-row { display: flex; gap: 20px; justify-content: center; border-top: 1px solid #333; padding-top: 10px; flex-wrap: wrap; }
#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; }
/* LOADER HTMX SURÉLEVÉ ET CENTRÉ (Flotte au-dessus du texte) */
.htmx-indicator { display: none; opacity: 0; transition: opacity 0.3s ease-in; position: absolute; bottom: 140px; left: 50%; transform: translateX(-50%); z-index: 9999; }
/* LOADER HTMX SURÉLEVÉ */
.htmx-indicator { display: none; opacity: 0; transition: opacity 0.3s ease-in; position: absolute; bottom: 160px; left: 50%; transform: translateX(-50%); z-index: 9999; }
.htmx-request.htmx-indicator { display: flex !important; opacity: 1; }
.htmx-request .htmx-indicator { display: flex !important; opacity: 1; }
.tech-loader-container { display: flex; align-items: center; gap: 15px; color: var(--accent-blue); font-style: italic; background: #1e1e24; padding: 15px 25px; border-radius: 30px; border: 1px dashed var(--accent-blue); box-shadow: 0 5px 20px rgba(0,229,255,0.2); font-size: 0.9em; white-space: nowrap; }
.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); } }
/* --- RESPONSIVE MOBILE & SIDEBAR --- */
.mobile-header { display: none; background: #0a0a0c; padding: 10px 20px; align-items: center; justify-content: space-between; border-bottom: 1px solid #333; width: 100%; box-sizing: border-box; flex-shrink: 0;}
.hamburger-btn { background: none; border: none; color: var(--accent-blue); font-size: 1.8em; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 5px; }
.hamburger-btn:focus { outline: none; }
@media (max-width: 768px) {
body { flex-direction: column; }
.sidebar { width: 100%; height: auto; max-height: 35vh; border-right: none; border-bottom: 1px solid #333; padding: 10px; }
.sidebar-history { max-height: 120px; }
.main-content { flex: 1; height: 65vh; }
.chat-container { padding-bottom: 180px; padding-left: 10px; padding-right: 10px; }
.mobile-header { display: flex; }
.sidebar { position: absolute; left: -100%; top: 0; z-index: 1000; width: 280px; height: 100vh; box-shadow: 5px 0 15px rgba(0,0,0,0.8); }
.sidebar.sidebar-open { left: 0; }
.main-content { flex: 1; height: calc(100vh - 60px); }
.chat-container { padding-bottom: 200px; padding-left: 10px; padding-right: 10px; }
.user-bubble, .ai-bubble { max-width: 95%; }
.input-area { padding: 10px; }
.options-row { justify-content: center; gap: 10px;}
}
"""),
Script(r"""
// Sac à dos virtuel global pour additionner les fichiers sans les écraser
let aethasFiles = new DataTransfer();
// JS pour le bouton Hamburger
window.toggleSidebar = function() {
let sidebar = document.querySelector('.sidebar');
if (sidebar) {
sidebar.classList.toggle('sidebar-open');
}
};
function syncFiles() {
const fileInput = document.getElementById('file-upload');
if(fileInput) fileInput.files = aethasFiles.files;
@@ -139,12 +161,12 @@ def get_theme_hdrs():
pre.parentNode.insertBefore(header, pre);
if (typeof hljs !== 'undefined') hljs.highlightElement(code);
}
} catch(err) { console.warn("Erreur entête code", err); }
} catch(err) {}
});
});
let chat = document.getElementById('chat-history');
if (chat) chat.scrollTop = chat.scrollHeight;
} catch(e) { console.error("Erreur markdown", e); }
} catch(e) {}
}
function getSmartFilename(btn, defaultName = 'Aethas38_Reponse', extension = 'pdf') {
@@ -219,7 +241,6 @@ def get_theme_hdrs():
});
}
// Vider le sac à dos virtel une fois la requête HTMX envoyée !
document.body.addEventListener('htmx:afterRequest', function(evt) {
if(evt.detail.target.id === 'chat-history') {
aethasFiles = new DataTransfer();
@@ -229,13 +250,7 @@ def get_theme_hdrs():
});
});
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);
}
// ... (Fonctions de téléchargement ZIP, PDF, DOC, TXT, JSON et Partage identiques et préservées)
function downloadCodeZip(btn, lang) {
let pre = btn.closest('.code-header').nextElementSibling;
let code = pre.querySelector('code').innerText;
@@ -251,12 +266,6 @@ def get_theme_hdrs():
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();
}
}
+129 -30
View File
@@ -1,10 +1,32 @@
import os
import requests
from modules.paypal_checkout import render_paypal_recharge_widget
from fasthtml.common import *
from modules.database import load_history
from modules.paypal import render_paypal_donation_widget
from modules.billing_manager import obtenir_solde
def get_openrouter_stats():
api_key = os.getenv("OPENROUTER_API_KEY")
if not api_key:
return {}
try:
resp = requests.get(
"https://openrouter.ai/api/v1/auth/key",
headers={"Authorization": f"Bearer {api_key}"},
timeout=3
)
if resp.status_code == 200:
return resp.json().get("data", {})
except:
pass
return {}
def render_user_msg(text):
return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble")
return Div(
P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"),
cls="user-bubble"
)
def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None):
time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else ""
@@ -16,6 +38,7 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi
time_badge,
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"),
@@ -25,12 +48,14 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi
Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par courriel"),
cls="msg-actions"
)
return Div(
encart_info,
Div(text, cls="markdown-raw", style="display:none;"),
Div(cls="markdown-rendered", style="line-height: 1.6;"),
actions,
cls="ai-bubble", style=f"border-left: 4px solid {couleur};"
cls="ai-bubble",
style=f"border-left: 4px solid {couleur};"
)
def render_sidebar():
@@ -45,7 +70,8 @@ def render_sidebar():
Button(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"),
Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"),
Button("🗑️", cls="history-btn", hx_post=f"/delete/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"),
cls="history-item", id=f"chat-item-{chat_id}"
cls="history-item",
id=f"chat-item-{chat_id}"
)
)
return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true")
@@ -53,15 +79,21 @@ def render_sidebar():
def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
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'), msg.get('execution_time', '')
msg['content'],
msg.get('intention', ''),
msg.get('fournisseur', ''),
msg.get('modele', ''),
msg.get('couleur', '#333'),
msg.get('execution_time', '')
))
if messages_ui:
messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
@@ -82,10 +114,22 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
solde_perso = obtenir_solde(utilisateur_nom)
if role.lower() in ["super admin", "admin"]:
stats_or = get_openrouter_stats()
limit_rem = stats_or.get('limit_remaining')
limit_rem = float(limit_rem) if limit_rem is not None else 0.0
usage_daily = stats_or.get('usage_daily')
usage_daily = float(usage_daily) if usage_daily is not None else 0.0
usage_monthly = stats_or.get('usage_monthly')
usage_monthly = float(usage_monthly) if usage_monthly is not None else 0.0
badge_finance = Div(
P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #00e5ff; margin: 0; font-weight: bold; font-size: 0.9em;"),
P(f"💳 Solde personnel : {solde_perso:.2f} $", style="color: #aaa; margin: 2px 0; font-size: 0.8em;"),
P(f"🏦 Cagnotte globale : {obtenir_solde('xavier'):.2f} $", style="color: #4caf50; margin: 0; font-size: 0.8em; font-weight: bold;"),
P(f"🌐 OR Restant : {limit_rem:.4f} $", style="color: #ff9800; margin: 2px 0 0; font-size: 0.8em; font-weight: bold;"),
P(f"📊 OR Conso (Jour/Mois) : {usage_daily:.4f}$ / {usage_monthly:.4f}$", style="color: #8bc34a; margin: 0; font-size: 0.75em;"),
# AJOUT DU BOUTON PAYPAL POUR TESTER L'ACHAT DE 10$
render_paypal_recharge_widget(10.00),
style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;"
)
else:
@@ -97,13 +141,15 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
return Title("Aethas38 - Core"), Body(
auto_refresh_script,
# --- SIDEBAR FIXE (BUREAU) & MASQUABLE (MOBILE) ---
Div(
Div(
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"),
A("🚪 Déconnexion", href="/logout", cls="export-btn", style="background-color: #8b0000; color: white; border-color: #8b0000; margin-top: 5px;"), # NOUVEAU BOUTON ICI
A("🚪 Déconnexion", href="/logout", cls="export-btn", style="background-color: #8b0000; color: white; border-color: #8b0000; margin-top: 5px;"),
Div(
render_paypal_donation_widget(),
@@ -119,38 +165,69 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
Div(badge_finance, cls="sidebar-footer"),
cls="sidebar"
),
# --- CONTENU CENTRAL ---
Div(
# HEADER MOBILE (VISIBLE SEULEMENT SUR SMARTPHONE)
Div(
Button("☰", onclick="toggleSidebar()", cls="hamburger-btn"),
Img(src="/Logo/logo_aethas.svg", style="height: 30px;"),
Div(style="width: 30px;"), # Espaceur pour centrer le logo
cls="mobile-header"
),
Div(*messages_ui, id="chat-history", cls="chat-container"),
Div(Div(Div(cls="tech-spinner"), Span("Analyse en cours... (~5s à 10s)"), 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(
Div(Div(cls="tech-spinner"), Span("Analyse en cours... (~5s à 10s)"), cls="tech-loader-container"),
id="global-loader",
cls="htmx-indicator"
),
# --- LAYOUT ZONE DE SAISIE RESTRUCTURÉ ---
Div(
Form(
Input(type="hidden", name="chat_id", value=chat_id),
Div(
Div(id="file-preview-list"),
# RANGÉE 1 : BOUTON +, TEXTAREA ET BOUTON ENVOI
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="textarea-wrapper"
),
# RANGÉE 2 : LES OPTIONS (SOUS LA TEXTAREA)
Div(
Label(
Input(type="checkbox", name="mode_education", id="mode-education-toggle", style="margin-right: 6px; accent-color: #8bc34a; cursor: pointer;"),
"🎓 Éducation",
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"
),
Label(
Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"),
"👑 Payant",
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; margin-right: 10px; cursor: pointer; white-space: nowrap;"
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"
),
Label(
Input(type="checkbox", name="mode_multi_ia", id="mode-multi-toggle", style="margin-right: 6px; accent-color: #d97757; cursor: pointer;"),
"⚡ Multi-IA",
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; margin-right: 15px; cursor: pointer; white-space: nowrap;"
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"
),
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="options-row"
),
cls="input-container-box"
),
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader",
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;"
enctype="multipart/form-data",
style="width: 100%; display: flex; justify-content: center;"
),
cls="input-area"
),
@@ -169,20 +246,18 @@ def render_admin_moderation_panel(utilisateur_cible):
Select(
Option("Avertissement simple", value="Avertissement"),
Option("Bannissement 1 jour", value="Ban 1J"),
Option("Bannissement 1 semaine", value="Ban 1Sem"),
Option("Bannissement 1 mois", value="Ban 1M"),
Option("Bannissement définitif et signalement PHAROS", value="Ban Definitif + Pharos"),
Option("Faux positif (Levée de sanction)", value="Annulation"),
name="decision", cls="input-field"
name="decision",
cls="input-field"
),
style="margin-bottom: 15px;"
),
Div(
Label("Commentaire libre de l'administrateur (Preuve d'audit) :", style="font-weight: bold;"),
Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison de la décision, contexte juridique..."),
Label("Commentaire (Preuve d'audit) :", style="font-weight: bold;"),
Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison..."),
style="margin-bottom: 15px;"
),
Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white;"),
Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white; width: 100%;"),
hx_post="/admin_decision",
hx_target="#admin-result",
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;"
@@ -217,22 +292,46 @@ def render_login_page(error=None):
style="background-color: #0d0d0d; color: #fff; font-family: monospace; height: 100vh; display: flex; align-items: center; justify-content: center;"
)
def render_popup_paiement(cout_estime, prompt_sauvegarde, chat_id, multi_ia_state=False):
def render_popup_paiement(cout_estime, prompt_sauvegarde, chat_id, multi_ia_state=False, edu_state=False):
multi_ia_val = "on" if multi_ia_state else "off"
edu_val = "on" if edu_state else "off"
return Div(
Div(
H3("⚠️ Validation Financière Requise", style="color: #ff9800; margin-top: 0;"),
P("Cette requête nécessite l'utilisation d'un modèle d'IA payant.", style="color: #ccc;"),
P(f"Coût estimé (TTC) : {cout_estime} $", style="font-size: 1.2em; font-weight: bold; color: #fff; background: #333; padding: 10px; border-radius: 5px; text-align: center;"),
P(f"Coût estimé API (TTC) : {cout_estime} $", style="font-size: 1.2em; font-weight: bold; color: #fff; background: #333; padding: 10px; border-radius: 5px; text-align: center;"),
Div(
Button("Annuler la requête", type="button", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #555; width: 30%; font-size: 0.8em;"),
Button("Annuler la requête", type="button", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #555; width: 30%; font-size: 0.8em; border-radius: 8px;"),
Form(Input(type="hidden", name="prompt", value=prompt_sauvegarde), Input(type="hidden", name="chat_id", value=chat_id), Input(type="hidden", name="mode_payant", value="off"), Input(type="hidden", name="mode_multi_ia", value=multi_ia_val), Input(type="hidden", name="validation_acquise", value="true"), Input(type="hidden", name="choix_utilisateur", value="gratuit_force"), Button("Tenter en Gratuit ⚠️", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #00e5ff; width: 100%; color: black; font-size: 0.8em;"), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;"),
Form(
Input(type="hidden", name="prompt", value=prompt_sauvegarde),
Input(type="hidden", name="chat_id", value=chat_id),
Input(type="hidden", name="mode_payant", value="off"),
Input(type="hidden", name="mode_multi_ia", value=multi_ia_val),
Input(type="hidden", name="mode_education", value=edu_val),
Input(type="hidden", name="validation_acquise", value="true"),
Input(type="hidden", name="choix_utilisateur", value="gratuit_force"),
Button("Tenter en Gratuit ⚠️", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #00e5ff; width: 100%; color: black; font-size: 0.8em; border-radius: 8px;"),
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;"
),
Form(Input(type="hidden", name="prompt", value=prompt_sauvegarde), Input(type="hidden", name="chat_id", value=chat_id), Input(type="hidden", name="mode_payant", value="on"), Input(type="hidden", name="mode_multi_ia", value=multi_ia_val), Input(type="hidden", name="validation_acquise", value="true"), Input(type="hidden", name="choix_utilisateur", value="paiement_accepte"), Button("Payer l'API", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #4caf50; width: 100%; font-size: 0.8em;"), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;"),
Form(
Input(type="hidden", name="prompt", value=prompt_sauvegarde),
Input(type="hidden", name="chat_id", value=chat_id),
Input(type="hidden", name="mode_payant", value="on"),
Input(type="hidden", name="mode_multi_ia", value=multi_ia_val),
Input(type="hidden", name="mode_education", value=edu_val),
Input(type="hidden", name="validation_acquise", value="true"),
Input(type="hidden", name="choix_utilisateur", value="paiement_accepte"),
Button("Payer l'API", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #4caf50; width: 100%; font-size: 0.8em; border-radius: 8px;"),
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;"
),
style="display: flex; justify-content: space-between; margin-top: 20px; gap: 10px;"
), style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #ff9800; width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
), id="popup-paiement", style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 9999;"
),
style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #ff9800; width: 500px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
),
id="popup-paiement",
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 9999;"
)