Public Access
Test : Intégration bouton PayPal Sandbox et activation du Serveur Mail
Build and Push Docker Image / build-and-push (push) Successful in 1m45s
Build and Push Docker Image / build-and-push (push) Successful in 1m45s
This commit is contained in:
1 parent
632c23069a
commit
2ea18701b5
12 files changed
+486
-187
No files matched your search
+15
-10
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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}")
|
||||
@@ -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}¤cy=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
@@ -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
@@ -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;"
|
||||
)
|
||||
Reference in new issue
Block a user