Public Access
Mise en veille PayPal, synchro solde OpenRouter et modale de rejet financier
Build and Push Docker Image / build-and-push (push) Successful in 34s
Build and Push Docker Image / build-and-push (push) Successful in 34s
This commit is contained in:
1 parent
2ea18701b5
commit
3e140d7d6f
8 files changed
+380
-293
No files matched your search
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"cagnotte_commune_vip": 34.988855
|
|
||||||
}
|
|
||||||
@@ -11650,5 +11650,179 @@
|
|||||||
"action": "Acc\u00e8s \u00e0 /",
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
"date": "2026-08-23 09:39:21",
|
"date": "2026-08-23 09:39:21",
|
||||||
"timestamp": 1787470761.3066163
|
"timestamp": 1787470761.3066163
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 10:29:03",
|
||||||
|
"timestamp": 1787473743.7729807
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 10:29:07",
|
||||||
|
"timestamp": 1787473747.1458204
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /api/paypal/success",
|
||||||
|
"date": "2026-08-23 10:29:29",
|
||||||
|
"timestamp": 1787473769.173476
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 10:29:29",
|
||||||
|
"timestamp": 1787473769.326424
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 10:38:09",
|
||||||
|
"timestamp": 1787474289.483428
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /api/paypal/success",
|
||||||
|
"date": "2026-08-23 10:38:30",
|
||||||
|
"timestamp": 1787474310.2446322
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 10:38:30",
|
||||||
|
"timestamp": 1787474310.3879251
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 10:40:40",
|
||||||
|
"timestamp": 1787474440.6522577
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /api/paypal/success",
|
||||||
|
"date": "2026-08-23 10:41:17",
|
||||||
|
"timestamp": 1787474477.0745878
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 10:45:53",
|
||||||
|
"timestamp": 1787474753.4862194
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /api/paypal/success",
|
||||||
|
"date": "2026-08-23 10:46:11",
|
||||||
|
"timestamp": 1787474771.704107
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 10:46:11",
|
||||||
|
"timestamp": 1787474771.8661208
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:09:04",
|
||||||
|
"timestamp": 1787476144.4969127
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:09:05",
|
||||||
|
"timestamp": 1787476145.455936
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:09:07",
|
||||||
|
"timestamp": 1787476147.144958
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:09:20",
|
||||||
|
"timestamp": 1787476160.3453426
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:13:18",
|
||||||
|
"timestamp": 1787476398.5733256
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:13:25",
|
||||||
|
"timestamp": 1787476405.3547883
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:16:45",
|
||||||
|
"timestamp": 1787476605.5372877
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:16:54",
|
||||||
|
"timestamp": 1787476614.6163135
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:17:17",
|
||||||
|
"timestamp": 1787476637.6742752
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:17:28",
|
||||||
|
"timestamp": 1787476648.5071766
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:21:45",
|
||||||
|
"timestamp": 1787476905.9197671
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:23:15",
|
||||||
|
"timestamp": 1787476995.713237
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:26:20",
|
||||||
|
"timestamp": 1787477180.2584555
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:27:10",
|
||||||
|
"timestamp": 1787477230.102091
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:27:39",
|
||||||
|
"timestamp": 1787477259.2473674
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:32:08",
|
||||||
|
"timestamp": 1787477528.771442
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-23 11:36:33",
|
||||||
|
"timestamp": 1787477793.7782843
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv() # Force la lecture du fichier .env en local
|
||||||
import uuid
|
import uuid
|
||||||
|
import json
|
||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
from starlette.staticfiles import StaticFiles
|
from starlette.staticfiles import StaticFiles
|
||||||
from starlette.responses import FileResponse, Response, HTMLResponse, RedirectResponse
|
from starlette.responses import FileResponse, Response, HTMLResponse, RedirectResponse
|
||||||
@@ -10,6 +14,8 @@ from modules.ui_components import render_main_page, render_sidebar, render_login
|
|||||||
from modules.chat_handler import handle_chat_request
|
from modules.chat_handler import handle_chat_request
|
||||||
from modules.security import verifier_et_bloquer_vpn, journaliser_ip
|
from modules.security import verifier_et_bloquer_vpn, journaliser_ip
|
||||||
from modules.auth_manager import IDENTIFIANTS_ADMIN, verifier_mot_de_passe
|
from modules.auth_manager import IDENTIFIANTS_ADMIN, verifier_mot_de_passe
|
||||||
|
from modules.logger import log_event
|
||||||
|
from modules.billing_manager import charger_soldes, sauvegarder_soldes
|
||||||
|
|
||||||
# Lancement du module d'auto-mise à jour en arrière-plan
|
# Lancement du module d'auto-mise à jour en arrière-plan
|
||||||
import modules.model_updater
|
import modules.model_updater
|
||||||
|
|||||||
+26
-27
@@ -16,7 +16,6 @@ TARIFS_FILE = os.path.join(BASE_DIR, "tarifs_openrouter.json")
|
|||||||
VIP_ADMIN_USERS = ["xavier", "céline", "celine", "anaëlle", "anaelle", "renan", "audric"]
|
VIP_ADMIN_USERS = ["xavier", "céline", "celine", "anaëlle", "anaelle", "renan", "audric"]
|
||||||
|
|
||||||
def obtenir_tarifs_dynamiques() -> dict:
|
def obtenir_tarifs_dynamiques() -> dict:
|
||||||
"""Récupère les tarifs via API toutes les 2 heures maximum pour éviter le hardcoding."""
|
|
||||||
maintenant = time.time()
|
maintenant = time.time()
|
||||||
|
|
||||||
if os.path.exists(TARIFS_FILE):
|
if os.path.exists(TARIFS_FILE):
|
||||||
@@ -36,7 +35,6 @@ def obtenir_tarifs_dynamiques() -> dict:
|
|||||||
pricing = m.get("pricing", {})
|
pricing = m.get("pricing", {})
|
||||||
p_prompt = float(pricing.get("prompt", 0.0))
|
p_prompt = float(pricing.get("prompt", 0.0))
|
||||||
p_comp = float(pricing.get("completion", 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)
|
tarifs[m["id"]] = max(p_prompt, p_comp)
|
||||||
|
|
||||||
with open(TARIFS_FILE, "w", encoding="utf-8") as f:
|
with open(TARIFS_FILE, "w", encoding="utf-8") as f:
|
||||||
@@ -46,7 +44,6 @@ def obtenir_tarifs_dynamiques() -> dict:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_event("ERROR", "BILLING", f"Échec récupération des tarifs : {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):
|
if os.path.exists(TARIFS_FILE):
|
||||||
try:
|
try:
|
||||||
with open(TARIFS_FILE, "r", encoding="utf-8") as f:
|
with open(TARIFS_FILE, "r", encoding="utf-8") as f:
|
||||||
@@ -150,6 +147,31 @@ def verifier_et_valider_budget(prompt: str, modele_id: str, utilisateur_nom: str
|
|||||||
return True, prix_facture
|
return True, prix_facture
|
||||||
|
|
||||||
def obtenir_solde(utilisateur_nom: str) -> float:
|
def obtenir_solde(utilisateur_nom: str) -> float:
|
||||||
|
# --- SYNCHRONISATION DIRECTE POUR L'ADMINISTRATEUR PRINCIPAL ---
|
||||||
|
if utilisateur_nom.lower() in ["xavier"]:
|
||||||
|
api_key = os.getenv("OPENROUTER_API_KEY")
|
||||||
|
if api_key:
|
||||||
|
try:
|
||||||
|
resp = requests.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {api_key}"}, timeout=3)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
data = resp.json().get("data", {})
|
||||||
|
|
||||||
|
credit_total = data.get('usage', 0.0)
|
||||||
|
limite_restante = data.get('limit_remaining')
|
||||||
|
balance = data.get('balance', 0.0)
|
||||||
|
|
||||||
|
vrai_solde = float(limite_restante) if limite_restante is not None else float(balance)
|
||||||
|
|
||||||
|
if vrai_solde > 0 or limite_restante is not None:
|
||||||
|
soldes = charger_soldes()
|
||||||
|
soldes["cagnotte_commune_vip"] = vrai_solde
|
||||||
|
soldes["xavier"] = vrai_solde
|
||||||
|
sauvegarder_soldes(soldes)
|
||||||
|
return vrai_solde
|
||||||
|
except Exception as e:
|
||||||
|
log_event("WARNING", "BILLING", f"Échec de synchronisation OpenRouter : {e}")
|
||||||
|
|
||||||
|
# --- COMPORTEMENT STANDARD POUR LES AUTRES UTILISATEURS ---
|
||||||
soldes = charger_soldes()
|
soldes = charger_soldes()
|
||||||
if utilisateur_nom.lower() in VIP_ADMIN_USERS:
|
if utilisateur_nom.lower() in VIP_ADMIN_USERS:
|
||||||
return soldes.get("cagnotte_commune_vip", 0.0)
|
return soldes.get("cagnotte_commune_vip", 0.0)
|
||||||
@@ -177,27 +199,4 @@ def generer_dossier_litige(utilisateur_nom: str, est_admin: bool = False) -> lis
|
|||||||
"tokens": item.get("tokens"),
|
"tokens": item.get("tokens"),
|
||||||
"montant_facture_ttc": item.get("prix_facture_client")
|
"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)
|
|
||||||
+21
-12
@@ -3,25 +3,24 @@ import smtplib
|
|||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
||||||
# Option de désactivation depuis l'interface (Phase Debug)
|
|
||||||
DEBUG_MAIL_ACTIVE = True
|
DEBUG_MAIL_ACTIVE = True
|
||||||
|
|
||||||
def envoyer_alerte_debug(niveau, source, message_erreur):
|
def envoyer_alerte_debug(niveau, source, message_erreur):
|
||||||
"""
|
"""
|
||||||
Envoie un e-mail à l'administrateur en cas d'erreur serveur (Phase Debug).
|
Envoie un e-mail à l'administrateur.
|
||||||
Utilise le serveur SMTP défini dans le .env
|
Gère le chiffrement (Port 587) et le relai interne sans mot de passe (Port 25).
|
||||||
"""
|
"""
|
||||||
if not DEBUG_MAIL_ACTIVE:
|
if not DEBUG_MAIL_ACTIVE:
|
||||||
return
|
return
|
||||||
|
|
||||||
email_admin = os.getenv("ADMIN_EMAIL", "votre-email@domaine.com")
|
email_admin = os.getenv("ADMIN_EMAIL", "votre-email@domaine.com")
|
||||||
smtp_serveur = os.getenv("SMTP_SERVER")
|
smtp_serveur = os.getenv("SMTP_SERVER", "192.168.1.20")
|
||||||
smtp_port = int(os.getenv("SMTP_PORT", 587))
|
smtp_port = int(os.getenv("SMTP_PORT", 587))
|
||||||
smtp_user = os.getenv("SMTP_USER")
|
smtp_user = os.getenv("SMTP_USER", "")
|
||||||
smtp_pass = os.getenv("SMTP_PASS")
|
smtp_pass = os.getenv("SMTP_PASS", "")
|
||||||
|
|
||||||
if not all([smtp_serveur, smtp_user, smtp_pass]):
|
if not smtp_serveur:
|
||||||
print(f"[{niveau}] [DEBUG_MAILER] Configuration SMTP absente. Impossible d'envoyer l'alerte pour : {message_erreur}")
|
print(f"[{niveau}] [DEBUG_MAILER] Serveur SMTP absent. Alerte annulée.")
|
||||||
return
|
return
|
||||||
|
|
||||||
sujet = f"[ALERTE {niveau}] Aethas38_Multi_IA - Erreur dans {source}"
|
sujet = f"[ALERTE {niveau}] Aethas38_Multi_IA - Erreur dans {source}"
|
||||||
@@ -36,22 +35,32 @@ def envoyer_alerte_debug(niveau, source, message_erreur):
|
|||||||
{message_erreur}
|
{message_erreur}
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<p><em>Notification automatique de l'Orchestrateur (Phase Debug)</em></p>
|
<p><em>Notification automatique de l'Orchestrateur</em></p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
msg = MIMEMultipart()
|
msg = MIMEMultipart()
|
||||||
msg['From'] = smtp_user
|
# Si pas d'utilisateur défini, on utilise une adresse générique pour le relai interne
|
||||||
|
msg['From'] = smtp_user if smtp_user else "orchestrateur@aethas38.duckdns.org"
|
||||||
msg['To'] = email_admin
|
msg['To'] = email_admin
|
||||||
msg['Subject'] = sujet
|
msg['Subject'] = sujet
|
||||||
msg.attach(MIMEText(corps_html, 'html'))
|
msg.attach(MIMEText(corps_html, 'html'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = smtplib.SMTP(smtp_serveur, smtp_port)
|
server = smtplib.SMTP(smtp_serveur, smtp_port)
|
||||||
server.starttls()
|
|
||||||
server.login(smtp_user, smtp_pass)
|
# Logique de routage intelligent selon le port
|
||||||
|
if smtp_port == 587:
|
||||||
|
server.starttls()
|
||||||
|
if smtp_user and smtp_pass:
|
||||||
|
server.login(smtp_user, smtp_pass)
|
||||||
|
elif smtp_port == 25:
|
||||||
|
# Sur le port 25 en réseau local, Mailu s'attend souvent à un simple relai sans LOGIN
|
||||||
|
pass
|
||||||
|
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
server.quit()
|
server.quit()
|
||||||
|
print(f"[{niveau}] Alerte e-mail envoyée avec succès à l'admin.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ÉCHEC CRITIQUE : Impossible d'envoyer l'alerte e-mail : {e}")
|
print(f"ÉCHEC CRITIQUE : Impossible d'envoyer l'alerte e-mail : {e}")
|
||||||
+49
-14
@@ -1,19 +1,54 @@
|
|||||||
import os
|
import os
|
||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
|
|
||||||
def render_paypal_donation_widget():
|
def render_paypal_recharge_widget(montant_usd: float = 10.00):
|
||||||
"""Génère le bouton de don PayPal pour l'interface sidebar avec le lien configuré."""
|
client_id = os.getenv("PAYPAL_CLIENT_ID")
|
||||||
|
|
||||||
# Récupération depuis l'environnement ou utilisation directe de votre lien officiel
|
if not client_id:
|
||||||
paypal_url = os.environ.get("PAYPAL_DONATION_LINK", "https://www.paypal.com/donate/?hosted_button_id=2B6T9UESCN2W6")
|
return Div("Clé PayPal manquante dans .env", style="color: red;")
|
||||||
|
|
||||||
|
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('.', '')}"
|
||||||
|
|
||||||
return Div(
|
script_logique = Script(f"""
|
||||||
A(
|
function chargerBoutonPayPal() {{
|
||||||
"☕ Soutenir l'hébergeur",
|
if (typeof paypal !== 'undefined') {{
|
||||||
href=paypal_url,
|
paypal.Buttons({{
|
||||||
target="_blank",
|
createOrder: function(data, actions) {{
|
||||||
cls="export-btn",
|
return actions.order.create({{
|
||||||
style="text-align: center; color: #00e5ff; border-color: #00e5ff; display: block; text-decoration: none; margin-top: 10px;"
|
purchase_units: [{{ amount: {{ value: '{montant_usd}' }} }}]
|
||||||
),
|
}});
|
||||||
style="margin-top: 10px;"
|
}},
|
||||||
)
|
onApprove: function(data, actions) {{
|
||||||
|
return actions.order.capture().then(function(details) {{
|
||||||
|
alert('Paiement Sandbox validé par ' + details.payer.name.given_name + ' !');
|
||||||
|
// Fermer la fenêtre après paiement
|
||||||
|
document.getElementById('modal-recharge-paypal').style.display = 'none';
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
}}).render('#{container_id}');
|
||||||
|
}} else {{
|
||||||
|
setTimeout(chargerBoutonPayPal, 100);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
chargerBoutonPayPal();
|
||||||
|
""")
|
||||||
|
|
||||||
|
# La boîte centrale de paiement
|
||||||
|
paypal_box = Div(
|
||||||
|
H3(f"Recharge de {montant_usd}$", style="color: #fff; margin-top: 0; margin-bottom: 15px; font-size: 1.2em; text-align: center;"),
|
||||||
|
Div(id=container_id, style="min-height: 150px; width: 100%;"),
|
||||||
|
Button("Fermer", type="button", onclick="document.getElementById('modal-recharge-paypal').style.display='none'", cls="btn-submit", style="background-color: #555; width: 100%; margin-top: 15px; border-radius: 8px; font-size: 0.9em;"),
|
||||||
|
style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #00e5ff; width: 400px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
|
||||||
|
)
|
||||||
|
|
||||||
|
# L'arrière-plan sombre (overlay)
|
||||||
|
modal_overlay = Div(
|
||||||
|
script_paypal,
|
||||||
|
paypal_box,
|
||||||
|
script_logique,
|
||||||
|
id="modal-recharge-paypal",
|
||||||
|
style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); align-items: center; justify-content: center; z-index: 10000;"
|
||||||
|
)
|
||||||
|
|
||||||
|
return modal_overlay
|
||||||
+50
-33
@@ -2,49 +2,66 @@ import os
|
|||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
|
|
||||||
def render_paypal_recharge_widget(montant_usd: float = 10.00):
|
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")
|
client_id = os.getenv("PAYPAL_CLIENT_ID")
|
||||||
|
|
||||||
if not client_id:
|
if not client_id:
|
||||||
return Div("Clé PayPal (Client ID) manquante dans .env", style="color: red;")
|
return Div("Clé PayPal 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")
|
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('.', '')}"
|
container_id = f"paypal-button-container-{str(montant_usd).replace('.', '')}"
|
||||||
|
|
||||||
script_logique = Script(f"""
|
script_logique = Script(f"""
|
||||||
paypal.Buttons({{
|
function chargerBoutonPayPal() {{
|
||||||
createOrder: function(data, actions) {{
|
if (typeof paypal !== 'undefined') {{
|
||||||
return actions.order.create({{
|
paypal.Buttons({{
|
||||||
purchase_units: [{{
|
createOrder: function(data, actions) {{
|
||||||
amount: {{
|
return actions.order.create({{
|
||||||
value: '{montant_usd}'
|
purchase_units: [{{ amount: {{ value: '{montant_usd}' }} }}]
|
||||||
}}
|
}});
|
||||||
}}]
|
}},
|
||||||
}});
|
onApprove: function(data, actions) {{
|
||||||
}},
|
return actions.order.capture().then(function(details) {{
|
||||||
onApprove: function(data, actions) {{
|
alert('Paiement Sandbox validé par ' + details.payer.name.given_name + ' ! Vos crédits vont être ajoutés.');
|
||||||
return actions.order.capture().then(function(details) {{
|
|
||||||
alert('Paiement réussi par ' + details.payer.name.given_name + '! Les crédits seront ajoutés.');
|
// L'ajout de credentials: 'same-origin' est VITAL pour envoyer le cookie de session !
|
||||||
// Ici on fera un fetch vers ton backend python pour créditer l'utilisateur
|
fetch('/api/paypal/success', {{
|
||||||
fetch('/api/paypal/success', {{
|
method: 'POST',
|
||||||
method: 'POST',
|
headers: {{ 'Content-Type': 'application/json' }},
|
||||||
headers: {{ 'Content-Type': 'application/json' }},
|
credentials: 'same-origin',
|
||||||
body: JSON.stringify({{ orderID: data.orderID }})
|
body: JSON.stringify({{ orderID: data.orderID }})
|
||||||
}}).then(res => window.location.reload());
|
}}).then(res => {{
|
||||||
}});
|
if(res.ok) {{
|
||||||
|
document.getElementById('modal-recharge-paypal').style.display = 'none';
|
||||||
|
window.location.reload();
|
||||||
|
}} else {{
|
||||||
|
alert("Erreur lors de la validation serveur.");
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
}}
|
||||||
|
}}).render('#{container_id}');
|
||||||
|
}} else {{
|
||||||
|
setTimeout(chargerBoutonPayPal, 100);
|
||||||
}}
|
}}
|
||||||
}}).render('#{container_id}');
|
}}
|
||||||
|
chargerBoutonPayPal();
|
||||||
""")
|
""")
|
||||||
|
|
||||||
return Div(
|
# La boîte centrale de paiement
|
||||||
|
paypal_box = Div(
|
||||||
|
H3(f"Recharge de {montant_usd}$", style="color: #fff; margin-top: 0; margin-bottom: 15px; font-size: 1.2em; text-align: center;"),
|
||||||
|
Div(id=container_id, style="min-height: 150px; width: 100%;"),
|
||||||
|
Button("Fermer", type="button", onclick="document.getElementById('modal-recharge-paypal').style.display='none'", cls="btn-submit", style="background-color: #555; width: 100%; margin-top: 15px; border-radius: 8px; font-size: 0.9em;"),
|
||||||
|
style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #00e5ff; width: 400px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
|
||||||
|
)
|
||||||
|
|
||||||
|
# L'arrière-plan sombre (overlay)
|
||||||
|
modal_overlay = Div(
|
||||||
script_paypal,
|
script_paypal,
|
||||||
H3(f"Recharge de {montant_usd}$"),
|
paypal_box,
|
||||||
Div(id=container_id),
|
|
||||||
script_logique,
|
script_logique,
|
||||||
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; margin-top: 15px; text-align: center; max-width: 300px;"
|
id="modal-recharge-paypal",
|
||||||
)
|
style="display: none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); align-items: center; justify-content: center; z-index: 10000;"
|
||||||
|
)
|
||||||
|
|
||||||
|
return modal_overlay
|
||||||
+54
-204
@@ -1,44 +1,30 @@
|
|||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
from modules.paypal_checkout import render_paypal_recharge_widget
|
|
||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
from modules.database import load_history
|
from modules.database import load_history
|
||||||
from modules.paypal import render_paypal_donation_widget
|
|
||||||
from modules.billing_manager import obtenir_solde
|
from modules.billing_manager import obtenir_solde
|
||||||
|
|
||||||
def get_openrouter_stats():
|
def get_openrouter_stats():
|
||||||
api_key = os.getenv("OPENROUTER_API_KEY")
|
api_key = os.getenv("OPENROUTER_API_KEY")
|
||||||
if not api_key:
|
if not api_key: return {}
|
||||||
return {}
|
|
||||||
try:
|
try:
|
||||||
resp = requests.get(
|
resp = requests.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {api_key}"}, timeout=3)
|
||||||
"https://openrouter.ai/api/v1/auth/key",
|
|
||||||
headers={"Authorization": f"Bearer {api_key}"},
|
|
||||||
timeout=3
|
|
||||||
)
|
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
return resp.json().get("data", {})
|
return resp.json().get("data", {})
|
||||||
except:
|
except: pass
|
||||||
pass
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def render_user_msg(text):
|
def render_user_msg(text):
|
||||||
return Div(
|
return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble")
|
||||||
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):
|
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 ""
|
time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else ""
|
||||||
|
|
||||||
encart_info = Div(
|
encart_info = Div(
|
||||||
Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
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_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;"),
|
Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"),
|
||||||
time_badge,
|
time_badge, style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;"
|
||||||
style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
actions = Div(
|
actions = Div(
|
||||||
Button("📄 PDF", onclick="downloadResponsePDF(this)", cls="msg-action-btn", title="Télécharger en PDF"),
|
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"),
|
Button("📘 DOC", onclick="downloadResponseDOC(this)", cls="msg-action-btn", title="Télécharger en Word"),
|
||||||
@@ -48,15 +34,7 @@ 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"),
|
Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par courriel"),
|
||||||
cls="msg-actions"
|
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};")
|
||||||
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};"
|
|
||||||
)
|
|
||||||
|
|
||||||
def render_sidebar():
|
def render_sidebar():
|
||||||
history = load_history()
|
history = load_history()
|
||||||
@@ -64,50 +42,24 @@ def render_sidebar():
|
|||||||
items = []
|
items = []
|
||||||
for chat_id, data in sorted_chats:
|
for chat_id, data in sorted_chats:
|
||||||
pin_icon = "📌" if data.get('pinned', False) else "📍"
|
pin_icon = "📌" if data.get('pinned', False) else "📍"
|
||||||
items.append(
|
items.append(Div(A(Span(data['title'], style="color:inherit;"), href=f"/load_chat/{chat_id}", cls="history-link"), 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}"))
|
||||||
Div(
|
|
||||||
A(Span(data['title'], style="color:inherit;"), href=f"/load_chat/{chat_id}", cls="history-link"),
|
|
||||||
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}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true")
|
return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true")
|
||||||
|
|
||||||
def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
|
def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
|
||||||
history = load_history()
|
history = load_history()
|
||||||
messages_ui = []
|
messages_ui = []
|
||||||
|
|
||||||
if chat_id in history:
|
if chat_id in history:
|
||||||
for msg in history[chat_id].get('messages', []):
|
for msg in history[chat_id].get('messages', []):
|
||||||
if msg['role'] == 'user':
|
if msg['role'] == 'user': messages_ui.append(render_user_msg(msg['content']))
|
||||||
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', '')))
|
||||||
else:
|
if messages_ui: messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
|
||||||
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', '')
|
|
||||||
))
|
|
||||||
|
|
||||||
if messages_ui:
|
|
||||||
messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
|
|
||||||
|
|
||||||
auto_refresh_script = Script('''
|
auto_refresh_script = Script('''
|
||||||
let serverDown = false;
|
let serverDown = false;
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
fetch('/ping', {cache: 'no-store'})
|
fetch('/ping', {cache: 'no-store'})
|
||||||
.then(response => {
|
.then(response => { if (serverDown && response.ok) { window.location.reload(); } })
|
||||||
if (serverDown && response.ok) { window.location.reload(); }
|
.catch(() => { serverDown = true; console.log("Serveur hors ligne..."); });
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
serverDown = true;
|
|
||||||
console.log("Serveur hors ligne (Mise à jour en cours)...");
|
|
||||||
});
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
''')
|
''')
|
||||||
|
|
||||||
@@ -115,34 +67,23 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
|
|||||||
|
|
||||||
if role.lower() in ["super admin", "admin"]:
|
if role.lower() in ["super admin", "admin"]:
|
||||||
stats_or = get_openrouter_stats()
|
stats_or = get_openrouter_stats()
|
||||||
limit_rem = stats_or.get('limit_remaining')
|
limit_rem = float(stats_or.get('limit_remaining') or 0.0)
|
||||||
limit_rem = float(limit_rem) if limit_rem is not None else 0.0
|
usage_daily = float(stats_or.get('usage_daily') or 0.0)
|
||||||
usage_daily = stats_or.get('usage_daily')
|
usage_monthly = float(stats_or.get('usage_monthly') or 0.0)
|
||||||
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(
|
badge_finance = Div(
|
||||||
P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #00e5ff; margin: 0; font-weight: bold; font-size: 0.9em;"),
|
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"💳 Solde personnel : {solde_perso:.2f} $", style="color: #00e5ff; margin: 2px 0; font-size: 0.85em; font-weight: bold;"),
|
||||||
P(f"🏦 Cagnotte globale : {obtenir_solde('xavier'):.2f} $", style="color: #4caf50; margin: 0; font-size: 0.8em; font-weight: bold;"),
|
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 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;"),
|
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;"
|
style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
badge_finance = Div(
|
badge_finance = Div(P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #fff; margin: 0; font-weight: bold; font-size: 0.9em;"), P(f"💳 Solde disponible : {solde_perso:.2f} $", style="color: #00e5ff; margin: 2px 0 0; font-size: 0.85em;"), style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;")
|
||||||
P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #fff; margin: 0; font-weight: bold; font-size: 0.9em;"),
|
|
||||||
P(f"💳 Solde disponible : {solde_perso:.2f} $", style="color: #00e5ff; margin: 2px 0 0; font-size: 0.85em;"),
|
|
||||||
style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;"
|
|
||||||
)
|
|
||||||
|
|
||||||
return Title("Aethas38 - Core"), Body(
|
return Title("Aethas38 - Core"), Body(
|
||||||
auto_refresh_script,
|
auto_refresh_script,
|
||||||
|
|
||||||
# --- SIDEBAR FIXE (BUREAU) & MASQUABLE (MOBILE) ---
|
|
||||||
Div(
|
Div(
|
||||||
Div(
|
Div(
|
||||||
Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"),
|
Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"),
|
||||||
@@ -150,88 +91,35 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
|
|||||||
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
|
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
|
||||||
A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-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;"),
|
A("🚪 Déconnexion", href="/logout", cls="export-btn", style="background-color: #8b0000; color: white; border-color: #8b0000; margin-top: 5px;"),
|
||||||
|
cls="nav-buttons", style="display: flex; flex-direction: column; gap: 10px;"
|
||||||
Div(
|
), cls="sidebar-top"
|
||||||
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(),
|
render_sidebar(), Div(badge_finance, cls="sidebar-footer"), cls="sidebar"
|
||||||
Div(badge_finance, cls="sidebar-footer"),
|
|
||||||
cls="sidebar"
|
|
||||||
),
|
),
|
||||||
|
|
||||||
# --- CONTENU CENTRAL ---
|
|
||||||
Div(
|
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;"), cls="mobile-header"),
|
||||||
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(*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"),
|
||||||
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(
|
Div(
|
||||||
Form(
|
Form(
|
||||||
Input(type="hidden", name="chat_id", value=chat_id),
|
Input(type="hidden", name="chat_id", value=chat_id),
|
||||||
Div(
|
Div(
|
||||||
Div(id="file-preview-list"),
|
Div(id="file-preview-list"),
|
||||||
|
|
||||||
# RANGÉE 1 : BOUTON +, TEXTAREA ET BOUTON ENVOI
|
|
||||||
Div(
|
Div(
|
||||||
Label("+", _for="file-upload", cls="file-btn"),
|
Label("+", _for="file-upload", cls="file-btn"),
|
||||||
Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"),
|
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)"),
|
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"),
|
Button("↑", type="submit", cls="btn-submit"), cls="textarea-wrapper"
|
||||||
cls="textarea-wrapper"
|
|
||||||
),
|
),
|
||||||
|
|
||||||
# RANGÉE 2 : LES OPTIONS (SOUS LA TEXTAREA)
|
|
||||||
Div(
|
Div(
|
||||||
Label(
|
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;"),
|
||||||
Input(type="checkbox", name="mode_education", id="mode-education-toggle", style="margin-right: 6px; accent-color: #8bc34a; cursor: pointer;"),
|
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; cursor: pointer; white-space: nowrap;"),
|
||||||
"🎓 Éducation",
|
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; 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_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; 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; cursor: pointer; white-space: nowrap;"
|
|
||||||
),
|
|
||||||
cls="options-row"
|
cls="options-row"
|
||||||
),
|
), cls="input-container-box"
|
||||||
cls="input-container-box"
|
), 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;"
|
||||||
),
|
), cls="input-area"
|
||||||
hx_post="/chat",
|
), cls="main-content"
|
||||||
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;"
|
|
||||||
),
|
|
||||||
cls="input-area"
|
|
||||||
),
|
|
||||||
cls="main-content"
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -241,29 +129,10 @@ def render_admin_moderation_panel(utilisateur_cible):
|
|||||||
P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la législation en vigueur."),
|
P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la législation en vigueur."),
|
||||||
Form(
|
Form(
|
||||||
Input(type="hidden", name="cible", value=utilisateur_cible),
|
Input(type="hidden", name="cible", value=utilisateur_cible),
|
||||||
Div(
|
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 définitif et signalement PHAROS", value="Ban Definitif + Pharos"), name="decision", cls="input-field"), style="margin-bottom: 15px;"),
|
||||||
Label("Action décidée :", style="font-weight: bold;"),
|
Div(Label("Commentaire (Preuve d'audit) :", style="font-weight: bold;"), Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison..."), style="margin-bottom: 15px;"),
|
||||||
Select(
|
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;"
|
||||||
Option("Avertissement simple", value="Avertissement"),
|
), Div(id="admin-result", style="margin-top: 15px; color: #00e5ff;"), cls="admin-panel"
|
||||||
Option("Bannissement 1 jour", value="Ban 1J"),
|
|
||||||
Option("Bannissement définitif et signalement PHAROS", value="Ban Definitif + Pharos"),
|
|
||||||
name="decision",
|
|
||||||
cls="input-field"
|
|
||||||
),
|
|
||||||
style="margin-bottom: 15px;"
|
|
||||||
),
|
|
||||||
Div(
|
|
||||||
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; width: 100%;"),
|
|
||||||
hx_post="/admin_decision",
|
|
||||||
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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_login_page(error=None):
|
def render_login_page(error=None):
|
||||||
@@ -274,64 +143,45 @@ def render_login_page(error=None):
|
|||||||
H2("Accès Restreint", style="text-align: center; color: #00e5ff;"),
|
H2("Accès Restreint", style="text-align: center; color: #00e5ff;"),
|
||||||
error_msg,
|
error_msg,
|
||||||
Form(
|
Form(
|
||||||
Div(
|
Div(Label("Email :", style="color: #aaa; font-size: 0.9em;"), Input(type="email", name="email", placeholder="admin@mail.aethas38.duckdns.org", cls="input-field", required=True), style="margin-bottom: 15px;"),
|
||||||
Label("Email :", style="color: #aaa; font-size: 0.9em;"),
|
Div(Label("Mot de passe :", style="color: #aaa; font-size: 0.9em;"), Input(type="password", name="password", placeholder="••••••••", cls="input-field", required=True), style="margin-bottom: 20px;"),
|
||||||
Input(type="email", name="email", placeholder="admin@mail.aethas38.duckdns.org", cls="input-field", required=True),
|
Button("Déverrouiller le système", type="submit", cls="btn-submit", style="width: 100%;"), method="post", action="/login",
|
||||||
style="margin-bottom: 15px;"
|
), style="max-width: 400px; margin: 100px auto; padding: 30px; background: #1a1a1a; border-radius: 10px; border: 1px solid #333; box-shadow: 0 4px 15px rgba(0,229,255,0.1);"
|
||||||
),
|
), style="background-color: #0d0d0d; color: #fff; font-family: monospace; height: 100vh; display: flex; align-items: center; justify-content: center;"
|
||||||
Div(
|
|
||||||
Label("Mot de passe :", style="color: #aaa; font-size: 0.9em;"),
|
|
||||||
Input(type="password", name="password", placeholder="••••••••", cls="input-field", required=True),
|
|
||||||
style="margin-bottom: 20px;"
|
|
||||||
),
|
|
||||||
Button("Déverrouiller le système", type="submit", cls="btn-submit", style="width: 100%;"),
|
|
||||||
method="post", action="/login",
|
|
||||||
),
|
|
||||||
style="max-width: 400px; margin: 100px auto; padding: 30px; background: #1a1a1a; border-radius: 10px; border: 1px solid #333; box-shadow: 0 4px 15px rgba(0,229,255,0.1);"
|
|
||||||
),
|
|
||||||
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, edu_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"
|
"""
|
||||||
|
Modale de rejet financier.
|
||||||
|
Version sans système de paiement (en attendant la structure juridique).
|
||||||
|
"""
|
||||||
edu_val = "on" if edu_state else "off"
|
edu_val = "on" if edu_state else "off"
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
Div(
|
Div(
|
||||||
H3("⚠️ Validation Financière Requise", style="color: #ff9800; margin-top: 0;"),
|
H3("⚠️ Crédits insuffisants", style="color: #ff4444; margin-top: 0;"),
|
||||||
P("Cette requête nécessite l'utilisation d'un modèle d'IA payant.", style="color: #ccc;"),
|
P("Cette requête nécessite un modèle d'IA dont le coût dépasse votre solde actuel.", style="color: #ccc;"),
|
||||||
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;"),
|
P(f"Coût estimé de la requête : {cout_estime} $", style="font-size: 1.1em; font-weight: bold; color: #fff; background: #333; padding: 10px; border-radius: 5px; text-align: center;"),
|
||||||
|
|
||||||
Div(
|
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; border-radius: 8px;"),
|
Button("Annuler la requête", type="button", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #555; width: 48%; font-size: 0.85em; border-radius: 8px;"),
|
||||||
|
|
||||||
|
# Bouton de secours pour forcer un modèle gratuit local (Ollama)
|
||||||
Form(
|
Form(
|
||||||
Input(type="hidden", name="prompt", value=prompt_sauvegarde),
|
Input(type="hidden", name="prompt", value=prompt_sauvegarde),
|
||||||
Input(type="hidden", name="chat_id", value=chat_id),
|
Input(type="hidden", name="chat_id", value=chat_id),
|
||||||
Input(type="hidden", name="mode_payant", value="off"),
|
Input(type="hidden", name="mode_payant", value="off"),
|
||||||
Input(type="hidden", name="mode_multi_ia", value=multi_ia_val),
|
Input(type="hidden", name="mode_multi_ia", value="off"),
|
||||||
Input(type="hidden", name="mode_education", value=edu_val),
|
Input(type="hidden", name="mode_education", value=edu_val),
|
||||||
Input(type="hidden", name="validation_acquise", value="true"),
|
Input(type="hidden", name="validation_acquise", value="true"),
|
||||||
Input(type="hidden", name="choix_utilisateur", value="gratuit_force"),
|
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;"),
|
Button("Tenter en Gratuit (Local)", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #00e5ff; width: 100%; color: black; font-size: 0.85em; border-radius: 8px;"),
|
||||||
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;"
|
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 48%;"
|
||||||
),
|
),
|
||||||
|
|
||||||
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="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; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
|
style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #ff4444; width: 450px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
|
||||||
),
|
),
|
||||||
id="popup-paiement",
|
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="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 9999;"
|
||||||
)
|
)
|
||||||
Reference in new issue
Block a user