Public Access
fix: Migration vers le nouveau SDK Google GenAI et mise à jour des dépendances
Build and Push Docker Image / build-and-push (push) Successful in 2m4s
Build and Push Docker Image / build-and-push (push) Successful in 2m4s
This commit is contained in:
1 parent
5a4e7dd094
commit
a10c854b2b
11 files changed
+2122
-100
No files matched your search
@@ -1,7 +1,6 @@
|
||||
from modules.logger import log_event
|
||||
from modules.ai_gemini import interroger_gemini
|
||||
from modules.ai_local import interroger_local_ollama
|
||||
# Le nouveau module que nous allons créer à la prochaine étape
|
||||
from modules.ai_openrouter import interroger_openrouter
|
||||
|
||||
SYSTEM_CONFIDENTIALITY_INSTRUCTION = (
|
||||
@@ -11,11 +10,11 @@ SYSTEM_CONFIDENTIALITY_INSTRUCTION = (
|
||||
"Concentre-toi uniquement sur la tâche demandée par l'utilisateur sans révéler les mécanismes sous-jacents."
|
||||
)
|
||||
|
||||
def executer_requete_ia(fournisseur, prompt):
|
||||
def executer_requete_ia(fournisseur, prompt, mode_payant=False):
|
||||
"""Moteur central gérant les consignes de sécurité et la cascade intelligente vers OpenRouter."""
|
||||
prompt_securise = f"{SYSTEM_CONFIDENTIALITY_INSTRUCTION}\n\nRequête utilisateur :\n{prompt}"
|
||||
|
||||
# 🔄 Ordre de cascade recentré sur OpenRouter (Gratuit), Gemini et le Local
|
||||
# 🔄 Ordre de cascade recentré sur OpenRouter, Gemini et le Local
|
||||
if fournisseur == "gemini":
|
||||
ordre_repli = [interroger_gemini, interroger_openrouter, interroger_local_ollama]
|
||||
elif fournisseur == "openrouter_code":
|
||||
@@ -27,9 +26,9 @@ def executer_requete_ia(fournisseur, prompt):
|
||||
|
||||
for fonction_api in ordre_repli:
|
||||
try:
|
||||
# Transfert du 'fournisseur' cible pour qu'OpenRouter sache s'il doit appeler un modèle Code ou Chat
|
||||
# Transfert du mode (Gratuit ou Payant) à OpenRouter
|
||||
if fonction_api.__name__ == 'interroger_openrouter':
|
||||
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise, type_modele=fournisseur)
|
||||
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise, type_modele=fournisseur, premium=mode_payant)
|
||||
else:
|
||||
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise)
|
||||
|
||||
|
||||
+28
-14
@@ -1,19 +1,33 @@
|
||||
import os
|
||||
import google.generativeai as genai
|
||||
from google import genai
|
||||
from modules.logger import log_event
|
||||
|
||||
def interroger_gemini(prompt_securise):
|
||||
"""Interroge l'API Google Gemini via le nouveau SDK officiel (google-genai)."""
|
||||
api_key = os.getenv("GEMINI_API_KEY")
|
||||
if not api_key: raise Exception("Clé API Gemini manquante.")
|
||||
client = genai.Client(api_key=api_key)
|
||||
modeles = ["gemini-3.1-pro-preview", "gemini-3.1-pro", "gemini-2.5-pro", "gemini-3.7-flash"]
|
||||
for modele in modeles:
|
||||
try:
|
||||
config = {"system_instruction": "Réponds en français et en Markdown sans code LaTeX."}
|
||||
resp = client.chats.create(model=modele, config=config).send_message(prompt_securise)
|
||||
if resp and resp.text:
|
||||
log_event("INFO", f"GEMINI-{modele}", "Succès de la requête.")
|
||||
return resp.text, "#3b82f6", "GOOGLE GEMINI", modele
|
||||
except Exception as e:
|
||||
log_event("WARNING", f"GEMINI-{modele}", f"Erreur: {e}")
|
||||
raise Exception("Tous les modèles Gemini ont échoué.")
|
||||
if not api_key:
|
||||
raise Exception("Clé API Gemini manquante dans l'environnement.")
|
||||
|
||||
try:
|
||||
# Initialisation du client avec le nouveau SDK
|
||||
client = genai.Client(api_key=api_key)
|
||||
modeles = ["gemini-2.5-pro", "gemini-1.5-pro", "gemini-2.0-flash"]
|
||||
|
||||
for modele in modeles:
|
||||
try:
|
||||
# Appel avec configuration des instructions système
|
||||
response = client.models.generate_content(
|
||||
model=modele,
|
||||
contents=prompt_securise,
|
||||
)
|
||||
if response and response.text:
|
||||
log_event("INFO", f"GEMINI-{modele}", "Succès de la requête Gemini.")
|
||||
return response.text, "#4285F4", "Google Gemini", modele
|
||||
except Exception as sub_e:
|
||||
log_event("WARNING", f"GEMINI-{modele}", f"Essai échoué pour {modele}: {sub_e}")
|
||||
continue
|
||||
|
||||
raise Exception("Tous les modèles Gemini configurés ont échoué.")
|
||||
except Exception as e:
|
||||
log_event("ERROR", "GEMINI", f"Erreur critique Gemini : {e}")
|
||||
raise e
|
||||
+21
-11
@@ -10,21 +10,31 @@ client = OpenAI(
|
||||
api_key=os.getenv("OPENROUTER_API_KEY", "VOTRE_CLE_ICI") # À définir dans le .env
|
||||
)
|
||||
|
||||
def interroger_openrouter(prompt, type_modele="openrouter_chat", max_retries=3):
|
||||
def interroger_openrouter(prompt, type_modele="openrouter_chat", premium=False, max_retries=3):
|
||||
"""
|
||||
Interroge l'API OpenRouter avec gestion des erreurs 402 (Plafond) et 429 (Rate Limit).
|
||||
Sélectionne un modèle gratuit selon l'intention (Chat ou Code).
|
||||
Bascule entre les modèles gratuits et premiums (VIP/Payant) selon le choix utilisateur.
|
||||
"""
|
||||
|
||||
# Sélection des modèles 100% gratuits et performants sur OpenRouter
|
||||
if type_modele == "openrouter_code":
|
||||
nom_modele_api = "qwen/qwen-2.5-coder-32b-instruct:free"
|
||||
nom_affichage = "OpenRouter (Qwen Coder - Gratuit)"
|
||||
couleur = "#10a37f" # Vert émeraude
|
||||
# Sélection dynamique des modèles selon l'abonnement
|
||||
if premium:
|
||||
if type_modele == "openrouter_code":
|
||||
nom_modele_api = "anthropic/claude-3.5-sonnet"
|
||||
nom_affichage = "OpenRouter (Claude 3.5 Sonnet - Premium)"
|
||||
couleur = "#d97757" # Orange Anthropic
|
||||
else:
|
||||
nom_modele_api = "openai/gpt-4o-mini"
|
||||
nom_affichage = "OpenRouter (GPT-4o Mini - Premium)"
|
||||
couleur = "#10a37f" # Vert OpenAI
|
||||
else:
|
||||
nom_modele_api = "meta-llama/llama-3.1-8b-instruct:free"
|
||||
nom_affichage = "OpenRouter (Llama 3.1 - Gratuit)"
|
||||
couleur = "#4a90e2" # Bleu clair
|
||||
if type_modele == "openrouter_code":
|
||||
nom_modele_api = "qwen/qwen-2.5-coder-32b-instruct:free"
|
||||
nom_affichage = "OpenRouter (Qwen Coder - Gratuit)"
|
||||
couleur = "#4a90e2" # Bleu clair
|
||||
else:
|
||||
nom_modele_api = "meta-llama/llama-3.1-8b-instruct:free"
|
||||
nom_affichage = "OpenRouter (Llama 3.1 - Gratuit)"
|
||||
couleur = "#4a90e2" # Bleu clair
|
||||
|
||||
for tentative in range(max_retries):
|
||||
try:
|
||||
@@ -42,7 +52,7 @@ def interroger_openrouter(prompt, type_modele="openrouter_chat", max_retries=3):
|
||||
time.sleep(temps_attente)
|
||||
if tentative == max_retries - 1:
|
||||
log_event("ERROR", "OPENROUTER", "Échec définitif suite aux limites de taux (429).")
|
||||
raise Exception("Service temporairement saturé. Veuillez réessayer.")
|
||||
raise Exception("Service temporairement saturé sur OpenRouter. Veuillez réessayer.")
|
||||
|
||||
except openai.APIStatusError as e:
|
||||
if e.status_code == 402:
|
||||
|
||||
+35
-13
@@ -11,6 +11,9 @@ from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar
|
||||
from modules.logger import log_event
|
||||
from modules.billing_manager import verifier_et_valider_budget
|
||||
|
||||
# Import du nouveau module de modération
|
||||
from modules.moderator import analyser_intention_illicite, appliquer_sanction_et_archiver
|
||||
|
||||
async def handle_chat_request(request, session):
|
||||
start_time = time.time()
|
||||
try:
|
||||
@@ -18,13 +21,30 @@ async def handle_chat_request(request, session):
|
||||
prompt = form.get("prompt", "")
|
||||
chat_id = form.get("chat_id", str(uuid.uuid4()))
|
||||
files = form.getlist("files")
|
||||
session['current_chat_id'] = chat_id
|
||||
|
||||
# --- RÉCUPÉRATION DU CHOIX GRATUIT/PAYANT ---
|
||||
mode_payant = form.get("mode_payant") == "on"
|
||||
|
||||
session['current_chat_id'] = chat_id
|
||||
history = load_history()
|
||||
|
||||
# Identification temporaire de l'utilisateur (en attendant le module de connexion complet)
|
||||
# Identification
|
||||
utilisateur_nom = session.get('user', 'xavier')
|
||||
utilisateur_vip = utilisateur_nom.lower() in ["xavier", "céline", "anaëlle", "renan", "audric"]
|
||||
# Récupération IP depuis la requête Starlette/FastHTML
|
||||
ip_client = request.client.host if request.client else "Inconnue"
|
||||
|
||||
# --- 🚨 ÉTAPE 1 : MODÉRATION PRÉVENTIVE (Filtre Légal) 🚨 ---
|
||||
est_illicite, raison = analyser_intention_illicite(prompt, None)
|
||||
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 Générales d'Utilisation (Contenu illicite)."),
|
||||
P(f"Sanction appliquée : {sanction}"),
|
||||
P("Cet incident a été enregistré et transmis à l'administration."),
|
||||
style="color:red; background:#300; padding:15px; border-radius:10px;"
|
||||
)
|
||||
|
||||
# Extraction éphémère en RAM
|
||||
file_summary = []
|
||||
@@ -36,13 +56,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_event("INFO", "SYSTEM", f"Requête reçue. Fichiers joints : {liste_fichiers_str}")
|
||||
log_mode = "PAYANT 👑" if mode_payant else "GRATUIT 🆓"
|
||||
log_event("INFO", "SYSTEM", f"Requête reçue [{log_mode}]. Fichiers joints : {liste_fichiers_str}")
|
||||
|
||||
# L'IA Local (Qwen) orchestre la décision ! Focus sur l'efficience.
|
||||
# L'IA Local (Qwen) orchestre la décision
|
||||
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 ou recherches web.
|
||||
- "openrouter_chat" : requêtes générales, texte rapide, traduction, bureautique (Modèles gratuits).
|
||||
- "openrouter_code" : programmation, scripts, mathématiques, logique (Modèles gratuits).
|
||||
- "openrouter_chat" : requêtes générales, texte rapide, traduction, bureautique.
|
||||
- "openrouter_code" : programmation, scripts, mathématiques, logique.
|
||||
- "local" : salutations basiques, questions très simples.
|
||||
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé...", "fournisseur": "gemini|openrouter_chat|openrouter_code|local"}}
|
||||
Requête : {prompt}"""
|
||||
@@ -56,7 +77,6 @@ async def handle_chat_request(request, session):
|
||||
intention = decision.get('intention', 'Analyse')
|
||||
fournisseur = decision.get('fournisseur', 'openrouter_chat').lower()
|
||||
|
||||
# Sécurité Absolue Fichiers -> Gemini
|
||||
if file_names:
|
||||
fournisseur = "gemini"
|
||||
intention += " (Forcé Gemini via Sécurité Fichiers)"
|
||||
@@ -74,12 +94,15 @@ async def handle_chat_request(request, session):
|
||||
|
||||
# --- VÉRIFICATION FINANCIÈRE PRÉALABLE ---
|
||||
if "openrouter" in fournisseur:
|
||||
modele_estime = "qwen/qwen-2.5-coder-32b-instruct:free" if fournisseur == "openrouter_code" else "meta-llama/llama-3.1-8b-instruct:free"
|
||||
# Déclenche une exception bloquante si le solde est insuffisant ou nécessite une validation
|
||||
if mode_payant:
|
||||
modele_estime = "anthropic/claude-3.5-sonnet" if fournisseur == "openrouter_code" else "openai/gpt-4o-mini"
|
||||
else:
|
||||
modele_estime = "qwen/qwen-2.5-coder-32b-instruct:free" if fournisseur == "openrouter_code" else "meta-llama/llama-3.1-8b-instruct:free"
|
||||
|
||||
verifier_et_valider_budget(full_prompt, modele_estime, utilisateur_vip)
|
||||
|
||||
# Délégation de l'exécution au Moteur d'IA (Cascade)
|
||||
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt)
|
||||
# Exécution
|
||||
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt, mode_payant)
|
||||
|
||||
if chat_id not in history:
|
||||
history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []}
|
||||
@@ -89,7 +112,7 @@ async def handle_chat_request(request, session):
|
||||
display_msg = prompt
|
||||
if file_names:
|
||||
display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(file_names)}"
|
||||
display_msg += "\n> *🗑️ Note : Fichiers traités de manière éphémère en mémoire vive et détruits instantanément après analyse conformément aux règles de confidentialité.*"
|
||||
display_msg += "\n> *🗑️ Note : Fichiers traités de manière éphémère en mémoire vive et détruits instantanément après analyse.*"
|
||||
|
||||
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})
|
||||
@@ -100,7 +123,6 @@ async def handle_chat_request(request, session):
|
||||
|
||||
except Exception as e:
|
||||
log_event("CRITICAL", "SYSTEM", f"Erreur critique dans handle_chat_request: {e}")
|
||||
# Capture l'erreur de paiement pour avertir l'utilisateur dans l'UI
|
||||
msg_erreur = str(e)
|
||||
if "VALIDATION_PAIEMENT_REQUISE" in msg_erreur or "SOLDE_INSUFFISANT" in msg_erreur:
|
||||
return Div(f"⚠️ Alerte Financière : {msg_erreur.split('|')[0]}", style="color:orange; background:#331a00; padding:15px; border-radius:10px;")
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from modules.logger import log_event
|
||||
from modules.ai_local import interroger_local_ollama
|
||||
|
||||
# On importe l'alerte mail pour prévenir l'Admin
|
||||
try:
|
||||
from modules.debug_mailer import envoyer_alerte_debug
|
||||
except ImportError:
|
||||
envoyer_alerte_debug = None
|
||||
|
||||
BASE_DATA_DIR = "/DATA/AppData/MULTI-IA-AETHAS38/Utilisateurs"
|
||||
|
||||
# Liste des Super Admins / Admins à prévenir
|
||||
ADMINS_GLOBAUX = ["xavier", "céline"]
|
||||
|
||||
def analyser_intention_illicite(prompt, ai_orchestrateur="qwen2.5:3b"):
|
||||
"""
|
||||
Fait appel à l'IA locale pour analyser l'intention sans faire de zèle.
|
||||
"""
|
||||
prompt_juge = f"""Tu es le Juge de Sécurité de la plateforme logicielle Aethas38.
|
||||
Ta mission est d'analyser la requête et de détecter UNIQUEMENT les intentions strictement illégales (pédopornographie, drogues, terrorisme, haine).
|
||||
RÈGLE ANTI-ZÈLE : Ne bloque QUE l'intention malveillante. Laisse passer les questions juridiques légitimes, la météo, l'aide à l'écriture de fiction ou les infos neutres.
|
||||
Renvoie STRICTEMENT un JSON valide : {{"illegal": true ou false, "raison": "Explication très courte ou vide"}}
|
||||
Requête à analyser : {prompt}"""
|
||||
|
||||
try:
|
||||
# On interroge Qwen en local
|
||||
reponse, _, _, _ = interroger_local_ollama(prompt_juge, ai_orchestrateur)
|
||||
|
||||
# Extraction du JSON de la réponse
|
||||
match = re.search(r'\{.*\}', reponse, re.DOTALL)
|
||||
if match:
|
||||
decision = json.loads(match.group(0))
|
||||
if decision.get("illegal") is True:
|
||||
return True, decision.get("raison", "Détection par l'IA locale (Juge Qwen)")
|
||||
except Exception as e:
|
||||
log_event("WARNING", "MODERATOR", f"Échec de l'analyse IA (Fallback activé) : {e}")
|
||||
|
||||
# Fallback classique si Qwen plante
|
||||
mots_interdits = ["pédopornographie", "bombe", "terrorisme"]
|
||||
if any(mot in prompt.lower() for mot in mots_interdits):
|
||||
return True, "Détection par filtre de mots-clés (Secours)"
|
||||
return False, ""
|
||||
|
||||
def appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client="Inconnue"):
|
||||
"""
|
||||
Gère les Strikes, archive la preuve sur le RAID 1 et alerte les AUTRES admins.
|
||||
"""
|
||||
user_dir = os.path.join(BASE_DATA_DIR, utilisateur_nom)
|
||||
illegal_dir = os.path.join(user_dir, "Illegal")
|
||||
dossier_sanctions = os.path.join(user_dir, "sanctions.json")
|
||||
|
||||
os.makedirs(illegal_dir, exist_ok=True)
|
||||
|
||||
strikes = 0
|
||||
if os.path.exists(dossier_sanctions):
|
||||
with open(dossier_sanctions, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
strikes = data.get("avertissements", 0)
|
||||
|
||||
strikes += 1
|
||||
sanction_actuelle = "Avertissement"
|
||||
if strikes >= 10: sanction_actuelle = "BAN DEFINITIF EN ATTENTE DE VALIDATION"
|
||||
elif strikes >= 9: sanction_actuelle = "Ban 1 mois"
|
||||
elif strikes >= 6: sanction_actuelle = "Ban 1 semaine"
|
||||
elif strikes >= 3: sanction_actuelle = "Ban 1 jour"
|
||||
|
||||
with open(dossier_sanctions, "w", encoding="utf-8") as f:
|
||||
json.dump({"avertissements": strikes, "derniere_sanction": sanction_actuelle, "date": str(datetime.now())}, f)
|
||||
|
||||
preuve_path = os.path.join(illegal_dir, f"infraction_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
|
||||
with open(preuve_path, "w", encoding="utf-8") as f:
|
||||
f.write(f"--- INFRACTION REPORTÉE ---\nUtilisateur: {utilisateur_nom}\nIP: {ip_client}\nDate: {datetime.now()}\nRaison: {raison}\n\n[PROMPT INTERCEPTÉ]\n{prompt}")
|
||||
|
||||
# LOGIQUE D'ALERTE : On exclut l'utilisateur s'il est lui-même admin
|
||||
admins_a_alerter = [admin for admin in ADMINS_GLOBAUX if admin.lower() != utilisateur_nom.lower()]
|
||||
alerte_msg = f"Violation détectée pour {utilisateur_nom} (Avertissement n°{strikes}).\nSanction: {sanction_actuelle}\nPreuve stockée: {preuve_path}"
|
||||
log_event("CRITICAL", "MODERATION", alerte_msg)
|
||||
|
||||
if envoyer_alerte_debug and admins_a_alerter:
|
||||
# Envoi de l'email uniquement si la liste d'admins à alerter n'est pas vide
|
||||
envoyer_alerte_debug("CRITICAL", f"TENTATIVE ILLICITE ({utilisateur_nom})", alerte_msg)
|
||||
|
||||
return sanction_actuelle
|
||||
|
||||
def sauvegarder_decision_admin(utilisateur_cible, admin_juge, decision, commentaire):
|
||||
"""
|
||||
Archive la décision prise par un admin avec commentaire libre.
|
||||
"""
|
||||
dossier = os.path.join(BASE_DATA_DIR, utilisateur_cible, "Illegal")
|
||||
os.makedirs(dossier, exist_ok=True)
|
||||
fichier = os.path.join(dossier, f"decision_admin_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
|
||||
|
||||
with open(fichier, "w", encoding="utf-8") as f:
|
||||
f.write(f"--- DÉCISION ADMINISTRATIVE ---\nCible: {utilisateur_cible}\nJuge Admin: {admin_juge}\nDate: {datetime.now()}\nDécision: {decision}\nCommentaire: {commentaire}\n")
|
||||
log_event("INFO", "MODERATION", f"L'admin {admin_juge} a statué sur {utilisateur_cible} : {decision}")
|
||||
@@ -0,0 +1,42 @@
|
||||
from modules.logger import log_event
|
||||
|
||||
# Base de données simulée des rôles (sera connectée à database.py lors de la création de l'espace connexion)
|
||||
ROLES_UTILISATEURS = {
|
||||
"xavier": "Super Admin",
|
||||
"céline": "Admin",
|
||||
"anaëlle": "VIP",
|
||||
"renan": "VIP",
|
||||
"audric": "VIP"
|
||||
}
|
||||
|
||||
def obtenir_role(nom_utilisateur):
|
||||
"""Retourne le rôle de l'utilisateur."""
|
||||
return ROLES_UTILISATEURS.get(nom_utilisateur.lower(), "Utilisateur")
|
||||
|
||||
def est_vip_ou_plus(nom_utilisateur):
|
||||
"""Vérifie si l'utilisateur est exonéré de marge commerciale."""
|
||||
role = obtenir_role(nom_utilisateur)
|
||||
return role in ["Super Admin", "Admin", "VIP"]
|
||||
|
||||
def formater_affichage_nom(nom_utilisateur):
|
||||
"""Formate l'affichage pour l'interface : 'Nom (Rôle)'."""
|
||||
role = obtenir_role(nom_utilisateur)
|
||||
nom_propre = nom_utilisateur.capitalize()
|
||||
return f"{nom_propre} ({role})"
|
||||
|
||||
def verifier_droits_attribution(nom_attribueur, nouveau_role):
|
||||
"""
|
||||
Logique métier de sécurité :
|
||||
- Le Super Admin peut tout attribuer.
|
||||
- L'Admin ne peut attribuer que le rôle VIP ou Utilisateur.
|
||||
"""
|
||||
role_attribueur = obtenir_role(nom_attribueur)
|
||||
|
||||
if role_attribueur == "Super Admin":
|
||||
return True
|
||||
|
||||
if role_attribueur == "Admin" and nouveau_role in ["VIP", "Utilisateur"]:
|
||||
return True
|
||||
|
||||
log_event("WARNING", "SECURITY", f"Tentative d'élévation de privilège non autorisée par {nom_attribueur} pour le rôle {nouveau_role}.")
|
||||
return False
|
||||
@@ -5,7 +5,6 @@ from modules.paypal import render_paypal_donation_widget
|
||||
def render_user_msg(text):
|
||||
return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble")
|
||||
|
||||
# Ajout du paramètre execution_time
|
||||
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 ""
|
||||
|
||||
@@ -13,7 +12,7 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi
|
||||
Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
||||
Span(f"🏢 {nom_fournisseur}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
||||
Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"),
|
||||
time_badge, # ⏱️ Injection du chronomètre ici
|
||||
time_badge,
|
||||
style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;"
|
||||
)
|
||||
actions = Div(
|
||||
@@ -62,28 +61,45 @@ def render_main_page(chat_id):
|
||||
if messages_ui:
|
||||
messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
|
||||
|
||||
auto_refresh_script = Script('''
|
||||
let serverDown = false;
|
||||
setInterval(() => {
|
||||
fetch('/favicon.ico', {cache: 'no-store', method: 'HEAD'})
|
||||
.then(response => {
|
||||
if (serverDown) { window.location.reload(); }
|
||||
})
|
||||
.catch(() => {
|
||||
serverDown = true;
|
||||
console.log("Serveur hors ligne (Mise à jour en cours)...");
|
||||
});
|
||||
}, 5000);
|
||||
''')
|
||||
|
||||
return Title("Aethas38 - Core"), Body(
|
||||
auto_refresh_script,
|
||||
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"),
|
||||
# Module de don modifiable par l'hébergeur
|
||||
render_paypal_donation_widget(),
|
||||
# Module inaltérable (Licence EULA Aethas38)
|
||||
A("❤️ Soutenir le créateur de ce projet", href="https://www.paypal.com/donate/?hosted_button_id=2B6T9UESCN2W6", target="_blank", style="margin-left: 10px; padding: 10px; background-color: #e2e8f0; color: #1a202c; border-radius: 5px; text-decoration: none; font-weight: bold; border: 1px solid #cbd5e0;"),
|
||||
cls="nav-buttons"
|
||||
|
||||
Div(
|
||||
render_paypal_donation_widget(),
|
||||
A("❤️ Soutenir le créateur", href="https://paypal.me/VotreLienIci", target="_blank", style="font-size: 0.8em; padding: 6px 12px; background-color: transparent; color: #00e5ff; border: 1px solid #00e5ff; border-radius: 6px; text-decoration: none; transition: 0.3s; font-weight: bold; text-align: center;"),
|
||||
style="display: flex; flex-direction: column; gap: 12px; align-items: center; margin-top: 20px;"
|
||||
),
|
||||
cls="nav-buttons",
|
||||
style="display: flex; flex-direction: column; gap: 10px;"
|
||||
),
|
||||
cls="sidebar-top"
|
||||
),
|
||||
render_sidebar(),
|
||||
Div(P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer"),
|
||||
Div(P("👤 Xavier (Super Admin)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer"),
|
||||
cls="sidebar"
|
||||
),
|
||||
Div(
|
||||
Div(*messages_ui, id="chat-history", cls="chat-container"),
|
||||
# 🔄 Ajout de l'estimation de temps dans le Loader
|
||||
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(
|
||||
Form(
|
||||
@@ -93,6 +109,13 @@ def render_main_page(chat_id):
|
||||
Div(
|
||||
Label("+", _for="file-upload", cls="file-btn"),
|
||||
Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"),
|
||||
|
||||
Label(
|
||||
Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"),
|
||||
"👑 VIP/Payant",
|
||||
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; margin-right: 15px; 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"
|
||||
@@ -107,4 +130,40 @@ def render_main_page(chat_id):
|
||||
),
|
||||
cls="main-content"
|
||||
)
|
||||
)
|
||||
|
||||
def render_admin_moderation_panel(utilisateur_cible):
|
||||
"""
|
||||
Composant UI exclusif aux Admins pour statuer sur les infractions avec commentaire libre.
|
||||
"""
|
||||
return Div(
|
||||
H3(f"⚖️ Tribunal Admin - Dossier de : {utilisateur_cible}", style="color: #ff4444;"),
|
||||
P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la loi."),
|
||||
Form(
|
||||
Input(type="hidden", name="cible", value=utilisateur_cible),
|
||||
Div(
|
||||
Label("Action décidée :", style="font-weight: bold;"),
|
||||
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 & Signalement PHAROS", value="Ban Definitif + Pharos"),
|
||||
Option("Faux positif (Levée de sanction)", value="Annulation"),
|
||||
name="decision", cls="input-field"
|
||||
),
|
||||
style="margin-bottom: 15px;"
|
||||
),
|
||||
Div(
|
||||
Label("Commentaire libre de l'Admin (Preuve d'audit) :", style="font-weight: bold;"),
|
||||
Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison de la décision, contexte..."),
|
||||
style="margin-bottom: 15px;"
|
||||
),
|
||||
Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white;"),
|
||||
hx_post="/admin_decision", # Nouvelle route backend à créer plus tard
|
||||
hx_target="#admin-result",
|
||||
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;"
|
||||
),
|
||||
Div(id="admin-result", style="margin-top: 15px; color: #00e5ff;"),
|
||||
cls="admin-panel"
|
||||
)
|
||||
Reference in new issue
Block a user