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

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

No files matched your search

+1
View File
@@ -8,3 +8,4 @@ Echange GEMINY.txt
aethas_logs.txt aethas_logs.txt
Important/ Important/
ARCHIVAGE ECHANGE GEMINY.txt ARCHIVAGE ECHANGE GEMINY.txt
test financier/
+33 -44
View File
@@ -1,85 +1,74 @@
# Aethas38 - Multi-IA Orchestrateur (Minecraft-Tech Edition) # Aethas38 - Multi-IA Orchestrateur (Minecraft-Tech Edition)
Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécurisé et facturable. Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécurisé et facturable.
<!-- MAJ 18/08/2026 : URL de production masquée pour raisons de cybersécurité (Information Disclosure) --> <!-- MAJ 23/08/2026 : URL de production masquée pour raisons de cybersécurité (Information Disclosure) -->
**Architecture réseau :** Déploiement via Docker derrière Nginx Proxy Manager (NPM) sur `https://ia.votre-domaine.com`. **Architecture réseau :** Déploiement via Docker derrière Nginx Proxy Manager (NPM) sur `https://ia.votre-domaine.com`. Timeout proxy ajusté à 300s pour le calcul lourd.
## 📜 Licence & Droits d'Utilisation (EULA) ## 📜 Licence & Droits d'Utilisation (EULA)
Ce projet est distribué sous une licence "Source Available" propriétaire (EULA) rédigée par Xavier Fernandez (Aethas38). Ce projet est distribué sous une licence "Source Available" propriétaire (EULA) rédigée par Xavier Fernandez (Aethas38).
* **Modifications & Partage :** Les utilisateurs sont autorisés à modifier et améliorer le code, à condition de partager ces améliorations avec la communauté. * **Modifications & Partage :** Les utilisateurs sont autorisés à modifier et améliorer le code, à condition de partager ces améliorations avec la communauté.
* **Restrictions strictes :** Il est **strictement interdit** de modifier ou supprimer les crédits de l'auteur originel, de retirer le module de dons dédié au créateur, ou de désactiver la télémétrie de sécurité (notification d'installation avec IP/Nom admin envoyée à `admin@mail.aethas38.duckdns.org`). L'installation requiert l'acceptation explicite de ces termes. * **Restrictions strictes :** Il est **strictement interdit** de modifier ou supprimer les crédits de l'auteur originel, de retirer le module de dons dédié au créateur, ou de désactiver la télémétrie de sécurité. L'installation requiert l'acceptation explicite de ces termes.
* **Donations obligatoires :** Le système intègre nativement deux modules de dons. L'un est modifiable par l'hébergeur du projet. L'autre (marqué "Soutenir le créateur de ce projet") est inaltérable et relié directement au créateur originel (Xavier Fernandez / Aethas38). Le retrait de ce module est une violation stricte de cette licence. * **Donations obligatoires :** Le système intègre nativement deux modules de dons. L'un est modifiable par l'hébergeur du projet. L'autre (marqué "Soutenir le créateur de ce projet") est inaltérable et relié directement au créateur originel.
## ⚠️ Pré-requis Matériels (Important) ## ⚠️ Pré-requis Matériels (Important)
Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minimum **une carte graphique avec 4 Go de VRAM dédiée**. Il est possible de configurer l'application pour utiliser le CPU, mais les performances seront considérablement réduites et les temps de réponse allongés. **L'auteur décline toute responsabilité en cas de surcharge matérielle liée à une utilisation CPU intensive.** Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minimum **une carte graphique avec 4 Go de VRAM dédiée** (ex: GTX 1650). Il est possible de configurer l'application pour utiliser le CPU, mais les performances seront considérablement réduites. **L'auteur décline toute responsabilité en cas de surcharge matérielle liée à une utilisation CPU intensive.**
## 📋 To-Do List (Roadmap) ## 📋 To-Do List (Roadmap)
### Phase 1 & 2 : Infrastructure & Backend de Base ✅ ### Phase 1 & 2 : Infrastructure & Backend de Base ✅
- [x] Installation Docker, Nginx (NPM), Pare-feu, Gitea/GitHub - [x] Installation Docker, Nginx (NPM), Pare-feu, Gitea/GitHub
- [x] Serveur Local Ollama (Qwen 3B) + FastHTML (Adapté aux processeurs limités type AMD Phenom) - [x] Serveur Local Ollama (Abliterated 3B) + FastHTML
- [x] Routeur Multi-IA (OpenRouter, Gemini, Local) avec système de cascade et directives d'aiguillage strictes - [x] Routeur Multi-IA (OpenRouter, Gemini, Local) avec système de cascade et directives d'aiguillage strictes
- [x] Continuité de service : IA de secours automatique - [x] Continuité de service : IA de secours automatique et Timeout à 300s.
### Phase 3 : Refonte UI & Expérience Utilisateur ✅ ### Phase 3 : Refonte UI & Expérience Utilisateur ✅
- [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive) - [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive)
- [x] Historique (Sidebar) avec Renommage, Épinglage, Suppression et Navigation isolée par session - [x] Historique (Sidebar) avec Renommage, Épinglage, Suppression et Navigation isolée par session
- [x] Zone de saisie intelligente (Textarea auto-adaptatif, Drag & Drop avec pills/miniatures, Collage multi-fichiers jusqu'à 10) - [x] Zone de saisie intelligente (Drag & Drop, Collage multi-fichiers)
- [x] Export modulaire et propre (PDF, DOC, TXT, JSON, ZIP) - [x] Export modulaire et propre (PDF, DOC, TXT, JSON, ZIP)
- [x] Boutons de partage direct (WhatsApp, Email) - [x] Boutons de partage direct (WhatsApp, Email) et Déconnexion.
- [x] Indicateur d'avancement dynamique (Loader Néon) pendant le traitement des IA - [x] Indicateur d'avancement dynamique (Loader Néon) pendant le traitement des IA.
- [x] Architecture modulaire propre (Dossier `modules/`)
### Phase 4 : Gestion des Utilisateurs & Sécurité Légale ### Phase 4 : Gestion des Utilisateurs & Sécurité Légale
- [ ] Mettre en place une bibliothèque logicielle autonome sur le serveur (création PDF, DOCX, etc.). - [ ] Mettre en place une bibliothèque logicielle autonome sur le serveur (création PDF, DOCX, etc.).
- [x] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur. - [x] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur avec solution de repli.
- [X] Système de connexion sécurisé avec configuration du premier lancement (DOB depuis 1900, Email, Clés API). L'installateur a obligatoirement le rôle "Dieu tout puissant" sur son se (Admin global = Super Admin).
- [x] Système de cryptage de grade militaire (Fernet) et de Hashing (Bcrypt) mis en place. - [x] Système de cryptage de grade militaire (Fernet) et de Hashing (Bcrypt) mis en place.
- [X] Sécurité Légère : Fail2Ban, protection Bruteforce et DDoS. **Blocage total des accès VPN sauf adresses locales (192.168.xxx.xxx & 10.xxx.xxx.xxx)**. - [X] Sécurité Légère : Fail2Ban, protection Bruteforce et DDoS. **Blocage total des accès VPN**. Ping silencieux.
- [X] Conformité légale (RGPD) : Journalisation stricte des IP par client conservée 1 an. - [X] Conformité légale (LCEN/RGPD) : Création de dossiers utilisateurs isolés. Journalisation stricte des IP et de l'activité conservée 365 jours. Purge automatique des comptes inactifs > 1 an.
- [ ] Multilinguisme : Adaptation automatique de la langue du site via la géolocalisation de l'IP. - [ ] Multilinguisme : Adaptation automatique de la langue du site via la géolocalisation de l'IP.
### Phase 5 : Sécurité Financière & Facturation Exacte 💰 ### Phase 5 : Sécurité Financière & Facturation Exacte 💰
- [x] Pré-calcul local du nombre de tokens (via `tiktoken`) pour estimer le coût avant envoi (Module `billing_manager.py`). Bloquage automatique si coût > 0. - [x] Pré-calcul local du nombre de tokens (via `tiktoken`) pour estimer le coût avant envoi.
- [x] Synchronisation dynamique du catalogue OpenRouter et calcul de facturation au token réel (`tokens_in` / `tokens_out` via `response.usage`). - [x] Catalogue tarifaire dynamique (mise à jour auto) supprimant les coûts écrits en dur.
- [x] Interface UI : Affichage du Pop-Up OBLIGATOIRE de validation du coût pour TOUS les utilisateurs. - [x] Affichage du Pop-Up OBLIGATOIRE de validation du coût pour TOUS les utilisateurs.
- [ ] Cryptage absolu de toutes les données sensibles et bancaires. - [x] Télémétrie Admin : Suivi en temps réel de la consommation (Jour/Mois) et du solde restant sur l'API distante.
- [ ] Module de conversion des devises en temps réel (Taux du jour) pour un affichage transparent. - [ ] Module de conversion des devises en temps réel (Taux du jour) pour un affichage transparent.
- [ ] **Utilisateurs Privilégiés (Zéro Marge) :** A définir par Admin/Super Admin. Autorisation de virements manuels pour la cagnotte. - [ ] **Utilisateurs Privilégiés (Zéro Marge) :** A définir par Admin/Super Admin.
- [x] **Marge Commerciale :** Majoration automatique via la formule `Prix_Facture_HT = Cout_API_OpenRouter_HT / x.xx` (soit x% de marge) implémentée dans le gestionnaire financier. - [x] **Marge Commerciale :** Majoration automatique via la formule `Prix_Facture_HT = Cout_API_OpenRouter_HT / x.xx`.
- [ ] Export comptable Excel (TCD) par utilisateur via Pandas/Openpyxl et archivage PDF automatisé. - [ ] Export comptable Excel (TCD) par utilisateur via Pandas/Openpyxl (Optimisé pour les déclarations URSSAF).
### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖 ### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖
*Note : L'infrastructure bascule sur un catalogue dynamique via `modules/model_updater.py` pour pallier les décommissionnements constants des fournisseurs tiers (MAJ toutes les 2h).* *L'infrastructure bascule sur un catalogue dynamique mis à jour toutes les 2h pour pallier les décommissionnements.*
| Domaine de tâche | Modèle / Moteur | Fournisseur / Hébergement | Rôle principal | | Domaine de tâche | Modèle / Moteur | Fournisseur / Hébergement | Rôle principal |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| **Orchestration & Tri** | Llama 3.2 3B Instruct Abliterated | Local (GTX 1650 / Ollama) | Analyse l'intention, juge de sécurité sans censure et route vers la bonne IA | | **Orchestration & Tri** | Llama 3.2 3B Instruct Abliterated | Local (GTX 1650 4Go) | Analyse l'intention, juge de sécurité sans censure et route vers la bonne IA |
| **Bureautique & Code** | à définir par le Super Admin | Programmation, scripts, macros VBA, mathématiques | | **Bureautique & Code** | Catalogue Dynamique (ex: Sonnet 3.5) | OpenRouter | Programmation KubeJS, scripts, mathématiques |
| **Images HD / Fichiers** | à définir par le Super Admin | Analyse d'images/vidéos et documents joints | | **Images HD / Fichiers** | Gemini / Vision | Google API | Analyse d'images/vidéos et documents joints |
| **Génération Multimédia** | à définir par le Super Admin | Création d'images et vidéos (Text-to-Image / Text-to-Video) |
### Phase 7 & 8 : Maintenance & Déploiement ### Phase 7 & 8 : Maintenance & Déploiement
- [ ] Script de surveillance de l'espace disque du serveur (Beszel / Psutil) réservé à l'Admin. - [ ] Script de surveillance de l'espace disque du serveur réservé à l'Admin.
- [ ] Sauvegarde automatique : Compression du site toutes les 6 heures vers un GDrive (dossier à définir lors de l'installation principale et modifiable dans le temps), 4 versions conservées. - [ ] File d'attente asynchrone (Queue List) et Timer en temps réel pour gérer la charge serveur.
- [x] Hébergement sur Gitea/Github des versions Docker. Configuration du fichier `docker-compose.yml` et `Dockerfile` réalisée. - [ ] Sauvegarde automatique : Compression du site toutes les 6 heures vers un GDrive.
- [x] Script Bash de déploiement automatique via `script_perso.sh`. A éprouver - [x] Hébergement Gitea Docker. Injection des clés via `env_file`. Volumes persistants configurés.
- [ ] Création d'un Wiki détaillé multilingue pour l'installation personnelle.
### Phase 9 : Modération Automatisée & Conformité Pénale 🚨 ### Phase 9 : Modération Automatisée & Conformité Pénale 🚨
- [x] Système hiérarchique strict : `Super Admin` (Installateur), `Admin` (Modération/VIP), `VIP` (Grade de filiation sans privilèges d'administration) et Utilisateurs. - [x] Système hiérarchique : `Super Admin`, `Admin`, `VIP` (Famille) et `Standard`. Quotas de ressources (60% CPU/RAM) applicables selon le rang.
- [ ] Validation obligatoire des CGV/CGU incluant une clause de "Ban sans remboursement et signalement aux autorités" en cas de contenu manifestement illicite. - [x] Modération préventive "Anti-Zèle" : Filtrage par IA Locale tolérant la fiction et les jeux vidéo.
- [x] Modération préventive "Anti-Zèle" : Filtrage par IA Locale des intentions illégales bloquant la requête vers OpenRouter tout en tolérant la fiction et les demandes juridiques. - [x] Système d'Avertissements (Strikes) automatisé et preuve d'audit manuscrite.
- [x] Système d'Avertissements (Strikes) automatisé avec alerte E-mail aux Admins.
- [x] Protection anti-abus Admin : Si un Admin enfreint la loi, il est exclu de l'alerte ; seuls les autres Admins statuent.
- [x] Archivage légal sécurisé : Stockage pérenne des logs, preuves d'infractions et **décisions d'audit manuscrites des Admins** dans une arborescence dédiée.
### Phase 10 : Base de Connaissances Intelligente (RAG) 📚 ### Phase 10 : Base de Connaissances Intelligente (RAG) 📚
- [x] Initialisation de l'arborescence dynamique (`modules/rag_manager.py`) sur le RAID (`/DATA/AppData/MULTI-IA-AETHAS38/RAG/`). - [x] Initialisation de l'arborescence dynamique (`RAG/`) sur le stockage RAID.
- [x] Évolution de l'Orchestrateur Local pour la sélection automatique du dossier sémantique pertinent ("minecraft", "droit", etc.).
- [x] Injection asynchrone des documents Markdown et Texte directement dans la fenêtre de contexte des LLMs. - [x] Injection asynchrone des documents Markdown et Texte directement dans la fenêtre de contexte des LLMs.
- [x] Auto-Apprentissage : Le système génère automatiquement des fichiers `.md` de son propre savoir après validation. - [x] Auto-Apprentissage : Le système génère automatiquement des fichiers `.md` de son propre savoir après validation.
- [ ] Interface UI : Ajout d'une vue Administrateur pour téléverser de nouveaux documents RAG directement depuis le navigateur. - [ ] Interface UI : Ajout d'une vue Administrateur pour téléverser de nouveaux documents RAG.
## 📷 Vidéosurveillance & Détection Intelligente
*(Voir configuration Frigate locale et Google Coral TPU existante)*
+48
View File
@@ -11602,5 +11602,53 @@
"action": "Acc\u00e8s \u00e0 /chat", "action": "Acc\u00e8s \u00e0 /chat",
"date": "2026-08-22 01:59:10", "date": "2026-08-22 01:59:10",
"timestamp": 1787356750.8306415 "timestamp": 1787356750.8306415
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 05:50:47",
"timestamp": 1787457047.035862
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 05:50:50",
"timestamp": 1787457050.7357597
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 05:58:37",
"timestamp": 1787457517.9751449
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /chat",
"date": "2026-08-23 06:28:43",
"timestamp": 1787459323.8286097
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 08:26:26",
"timestamp": 1787466386.609821
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 09:28:20",
"timestamp": 1787470100.2071035
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 09:30:55",
"timestamp": 1787470255.7610068
},
{
"ip": "127.0.0.1",
"action": "Acc\u00e8s \u00e0 /",
"date": "2026-08-23 09:39:21",
"timestamp": 1787470761.3066163
} }
] ]
+15 -10
View File
@@ -9,8 +9,6 @@ load_dotenv()
api_key_openrouter = os.getenv("OPENROUTER_API_KEY") or "sk-or-v1-cle-non-definie" 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) 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" CATALOGUE_FILE = "/DATA/AppData/MULTI-IA-AETHAS38/catalogue_dynamique.json"
def verifier_cle_api(): def verifier_cle_api():
@@ -19,8 +17,10 @@ def verifier_cle_api():
raise Exception("Clé OPENROUTER_API_KEY absente dans le fichier .env.") raise Exception("Clé OPENROUTER_API_KEY absente dans le fichier .env.")
return cle return cle
def obtenir_modele_dynamique(premium: bool): def obtenir_modele_dynamique(premium: bool, prompt: str = ""):
"""Charge le catalogue dynamiquement selon l'état Premium ou Gratuit.""" """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: try:
if os.path.exists(CATALOGUE_FILE): if os.path.exists(CATALOGUE_FILE):
with open(CATALOGUE_FILE, "r", encoding="utf-8") as f: 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()] 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") return claude[0] if claude else (paid_models[0] if paid_models else "anthropic/claude-3.5-sonnet")
else: 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", []) free_models = data.get("free_models", [])
priorites = [m for m in free_models if "gemma" in m.lower() or "nemotron" in m.lower()] 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") return priorites[0] if priorites else (free_models[0] if free_models else "google/gemma-2-9b-it:free")
except Exception: except Exception:
pass 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): def interroger_openrouter(prompt: str, type_modele: str = "openrouter_chat", premium: bool = False, max_retries: int = 2, chat_id: str = None):
verifier_cle_api() 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]})" nom_affichage = f"OpenRouter ({nom_modele_api.split('/')[-1]})"
couleur = "#d97757" if premium else "#4a90e2" couleur = "#d97757" if premium else "#4a90e2"
# Injection du Tracker Financier OpenRouter
extra_body_data = {"session_id": chat_id} if chat_id else {} extra_body_data = {"session_id": chat_id} if chat_id else {}
for tentative in range(max_retries): 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}], messages=[{"role": "user", "content": prompt}],
extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"}, extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"},
extra_body=extra_body_data, extra_body=extra_body_data,
timeout=90.0 # TIMEOUT MONTE A 90s timeout=90.0
) )
texte = response.choices[0].message.content texte = response.choices[0].message.content
usage = response.usage usage = response.usage
@@ -78,7 +83,7 @@ def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code
verifier_cle_api() verifier_cle_api()
extra_body_data = {"session_id": chat_id} if chat_id else {} extra_body_data = {"session_id": chat_id} if chat_id else {}
modeles = [obtenir_modele_dynamique(premium)] modeles = [obtenir_modele_dynamique(premium, prompt)]
if premium: if premium:
modeles.append("openai/gpt-4o-mini") modeles.append("openai/gpt-4o-mini")
else: else:
@@ -92,7 +97,7 @@ def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"}, extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"},
extra_body=extra_body_data, extra_body=extra_body_data,
timeout=90.0 # TIMEOUT MONTE A 90s timeout=90.0
) )
contenu = res.choices[0].message.content contenu = res.choices[0].message.content
if contenu: if contenu:
+67 -11
View File
@@ -1,6 +1,7 @@
import os import os
import json import json
import time import time
import requests
from datetime import datetime from datetime import datetime
import tiktoken import tiktoken
from modules.logger import log_event from modules.logger import log_event
@@ -10,19 +11,50 @@ BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38"
os.makedirs(BASE_DIR, exist_ok=True) os.makedirs(BASE_DIR, exist_ok=True)
BALANCES_FILE = os.path.join(BASE_DIR, "aethas_balances.json") BALANCES_FILE = os.path.join(BASE_DIR, "aethas_balances.json")
AUDIT_FILE = os.path.join(BASE_DIR, "aethas_financial_audit.json") AUDIT_FILE = os.path.join(BASE_DIR, "aethas_financial_audit.json")
TARIFS_FILE = os.path.join(BASE_DIR, "tarifs_openrouter.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
}
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:
"""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: def charger_soldes() -> dict:
if os.path.exists(BALANCES_FILE): if os.path.exists(BALANCES_FILE):
try: 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]: 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) 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 cout_brut = tokens * cout_unitaire
if multi_ia and cout_brut > 0: if multi_ia and cout_brut > 0:
@@ -145,3 +178,26 @@ def generer_dossier_litige(utilisateur_nom: str, est_admin: bool = False) -> lis
"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)
+52 -38
View File
@@ -1,6 +1,10 @@
import os
import json
from datetime import date
import uuid import uuid
import time import time
import re import re
import psutil
from fasthtml.common import * from fasthtml.common import *
from modules.database import load_history, save_history from modules.database import load_history, save_history
from modules.scraper import rechercher_sur_google, extraire_contenu_urls 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.rag_manager import rechercher_contexte_rag, auto_apprendre_rag
from modules.moderator import analyser_intention_illicite, appliquer_sanction_et_archiver 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): async def handle_chat_request(request, session):
start_time = time.time() start_time = time.time()
try: try:
# Psutil et la gestion de surcharge retirés comme demandé
form = await request.form() form = await request.form()
prompt = form.get("prompt", "") prompt = form.get("prompt", "")
chat_id = form.get("chat_id", str(uuid.uuid4())) 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_payant = form.get("mode_payant") == "on"
mode_multi_ia = form.get("mode_multi_ia") == "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" validation_acquise = form.get("validation_acquise") == "true"
choix_utilisateur = form.get("choix_utilisateur", "aucun") 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") 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" 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) est_illicite, raison = analyser_intention_illicite(prompt)
if est_illicite: if est_illicite:
sanction = appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client) 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_summary = []
file_names = [] 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')}") 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" 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 (Edu: {mode_education} / Payant: {mode_payant})")
log_event("INFO", "SYSTEM", f"Requête reçue [{log_mode}]. Fichiers joints : {liste_fichiers_str}")
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: try:
texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt) texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt)
decision = nettoyer_decision_router(texte_routeur) decision = nettoyer_decision_router(texte_routeur)
except Exception: except:
decision = {"intention": "Analyse de secours", "fournisseur": "openrouter_code" if "minecraft" in prompt.lower() else "openrouter_chat", "theme_rag": "aucun"} decision = {"intention": "Analyse", "fournisseur": "openrouter_chat", "theme_rag": "aucun"}
intention = decision.get("intention", "Analyse") intention = decision.get("intention", "Analyse")
fournisseur = decision.get("fournisseur", "openrouter_chat").lower() 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: if mode_payant or mode_multi_ia:
fournisseur = "openrouter_code" fournisseur = "openrouter_code"
intention += " (Forcé API Premium/Multi via UI)" intention += " (API Premium/Multi)"
if file_names: if file_names:
fournisseur = "gemini" 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 = "" contexte_rag = ""
if thematique_rag != "aucun": if thematique_rag != "aucun":
contexte_rag = rechercher_contexte_rag(prompt, categorie=thematique_rag) contexte_rag = rechercher_contexte_rag(prompt, categorie=thematique_rag)
urls = re.findall(r"https?://[^\s]+", prompt) 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 ["cherche", "recherche", "sur le web"]):
if any(m in prompt.lower() for m in mots_cles):
urls.extend(rechercher_sur_google(prompt)) urls.extend(rechercher_sur_google(prompt))
web_text = extraire_contenu_urls(list(set(urls))) if urls else "" web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "") full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "")
if contexte_rag: if contexte_rag: full_prompt += f"\n\n[Contexte RAG :\n{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 web_text:
full_prompt += f"\n\n[Contexte web :\n{web_text}]"
if "openrouter" in fournisseur: if "openrouter" in fournisseur:
modele_estime = "anthropic/claude-3.5-sonnet" if mode_payant else "cohere/north-mini-code:free" 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 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) _, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
if prix_estime > 0: 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"]: if mode_payant and role_utilisateur.lower() not in ["super admin", "admin", "vip", "famille"]:
from modules.billing_manager import estimer_cout_facture 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) _, 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): 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) 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: 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) 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": []} history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []}
execution_time = round(time.time() - start_time, 1) execution_time = round(time.time() - start_time, 1)
display_msg = prompt 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: if file_names: display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(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.*"
history[chat_id]["messages"].append({"role": "user", "content": display_msg}) 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}) 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) 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() 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: except Exception as e:
if "SOLDE_INSUFFISANT" in str(e): log_event("CRITICAL", "SYSTEM", f"Erreur : {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}")
return Div(f"Erreur système : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;") return Div(f"Erreur système : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
+6 -12
View File
@@ -2,29 +2,23 @@ import os
import json import json
import time import time
# FORCAGE DU CHEMIN ABSOLU
BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38" BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38"
os.makedirs(BASE_DIR, exist_ok=True) os.makedirs(BASE_DIR, exist_ok=True)
HISTORY_FILE = os.path.join(BASE_DIR, "aethas_history.json") HISTORY_FILE = os.path.join(BASE_DIR, "aethas_history.json")
def load_history(): def load_history():
if not os.path.exists(HISTORY_FILE): if not os.path.exists(HISTORY_FILE):
with open(HISTORY_FILE, 'w', encoding='utf-8') as f: with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump({}, f)
json.dump({}, f) with open(HISTORY_FILE, 'r', encoding='utf-8') as f: return json.load(f)
with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def save_history(history): def save_history(history):
with open(HISTORY_FILE, 'w', encoding='utf-8') as f: with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, indent=4)
json.dump(history, f, indent=4)
def nettoyer_vieilles_discussions(): def nettoyer_vieilles_discussions():
history = load_history() history = load_history()
now = time.time() now = time.time()
un_an_en_secondes = 365 * 24 * 60 * 60 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_en_secondes] 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: if a_supprimer:
for cid in a_supprimer: for cid in a_supprimer: del history[cid]
del history[cid]
save_history(history) save_history(history)
print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) supprimée(s).")
+45
View File
@@ -0,0 +1,45 @@
import os
import requests
import xml.etree.ElementTree as ET
from datetime import datetime
from modules.logger import log_event
RAG_DIR = "/DATA/AppData/MULTI-IA-AETHAS38/RAG/Education_Nationale"
def maj_base_education():
"""Scrape le flux RSS du Bulletin Officiel pour maintenir la base documentaire à jour."""
os.makedirs(RAG_DIR, exist_ok=True)
url_rss = "https://www.education.gouv.fr/bo/rss.xml"
try:
response = requests.get(url_rss, timeout=15)
if response.status_code == 200:
root = ET.fromstring(response.content)
# Formatage pour une ingestion parfaite par le RAG
contenu_md = f"# Veille Pédagogique - Bulletin Officiel (B.O.)\n"
contenu_md += f"*Date de mise à jour RAG : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n"
# On extrait les 20 dernières circulaires/textes de loi
for item in root.findall('.//item')[:20]:
titre = item.find('title').text if item.find('title') is not None else "Document sans titre"
lien = item.find('link').text if item.find('link') is not None else "Aucun lien"
desc = item.find('description').text if item.find('description') is not None else "Pas de résumé disponible."
contenu_md += f"## {titre}\n"
contenu_md += f"**Source officielle :** {lien}\n"
contenu_md += f"> {desc}\n\n---\n"
fichier_bo = os.path.join(RAG_DIR, "bulletin_officiel_recent.md")
with open(fichier_bo, "w", encoding="utf-8") as f:
f.write(contenu_md)
log_event("INFO", "SCRAPER_EDU", "Base RAG Éducation (B.O.) mise à jour avec succès.")
else:
log_event("WARNING", "SCRAPER_EDU", f"Le site du ministère a retourné le code {response.status_code}.")
except Exception as e:
log_event("ERROR", "SCRAPER_EDU", f"Erreur lors de la MAJ de la base Éducation : {e}")
if __name__ == "__main__":
# Permet de tester le script directement en ligne de commande : python modules/eduscol_scraper.py
maj_base_education()
-11
View File
@@ -3,7 +3,6 @@ import logging
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from datetime import datetime from datetime import datetime
# Chemin absolu obligatoire pour correspondre au volume Docker
LOG_DIR = "/DATA/AppData/MULTI-IA-AETHAS38" LOG_DIR = "/DATA/AppData/MULTI-IA-AETHAS38"
os.makedirs(LOG_DIR, exist_ok=True) os.makedirs(LOG_DIR, exist_ok=True)
LOG_FILE = os.path.join(LOG_DIR, "aethas_logs.txt") LOG_FILE = os.path.join(LOG_DIR, "aethas_logs.txt")
@@ -12,7 +11,6 @@ logger = logging.getLogger("Aethas38")
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
if not logger.handlers: 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 = TimedRotatingFileHandler(LOG_FILE, when="midnight", interval=1, backupCount=365, encoding="utf-8")
handler.suffix = "%Y-%m-%d" handler.suffix = "%Y-%m-%d"
formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 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): def log_event(niveau, source, message):
msg_formate = f"[{source}] {message}" msg_formate = f"[{source}] {message}"
if niveau.upper() == "INFO": logger.info(msg_formate) if niveau.upper() == "INFO": logger.info(msg_formate)
elif niveau.upper() == "WARNING": logger.warning(msg_formate) elif niveau.upper() == "WARNING": logger.warning(msg_formate)
elif niveau.upper() == "ERROR": logger.error(msg_formate) elif niveau.upper() == "ERROR": logger.error(msg_formate)
elif niveau.upper() == "CRITICAL": logger.critical(msg_formate) elif niveau.upper() == "CRITICAL": logger.critical(msg_formate)
print(f"[{datetime.now().strftime('%H:%M:%S')}] [{niveau}] {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
+50
View File
@@ -0,0 +1,50 @@
import os
from fasthtml.common import *
def render_paypal_recharge_widget(montant_usd: float = 10.00):
"""
Génère le bouton de paiement PayPal (Smart Button) pour recharger les crédits.
Utilise le Client ID Sandbox défini dans le fichier .env.
"""
client_id = os.getenv("PAYPAL_CLIENT_ID")
if not client_id:
return Div("Clé PayPal (Client ID) manquante dans .env", style="color: red;")
# Le script officiel de PayPal injecte les boutons de paiement
script_paypal = Script(src=f"https://www.paypal.com/sdk/js?client-id={client_id}&currency=USD")
container_id = f"paypal-button-container-{str(montant_usd).replace('.', '')}"
script_logique = Script(f"""
paypal.Buttons({{
createOrder: function(data, actions) {{
return actions.order.create({{
purchase_units: [{{
amount: {{
value: '{montant_usd}'
}}
}}]
}});
}},
onApprove: function(data, actions) {{
return actions.order.capture().then(function(details) {{
alert('Paiement réussi par ' + details.payer.name.given_name + '! Les crédits seront ajoutés.');
// Ici on fera un fetch vers ton backend python pour créditer l'utilisateur
fetch('/api/paypal/success', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{ orderID: data.orderID }})
}}).then(res => window.location.reload());
}});
}}
}}).render('#{container_id}');
""")
return Div(
script_paypal,
H3(f"Recharge de {montant_usd}$"),
Div(id=container_id),
script_logique,
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; margin-top: 15px; text-align: center; max-width: 300px;"
)
+37 -28
View File
@@ -13,7 +13,9 @@ def get_theme_hdrs():
Style(""" Style("""
:root { --bg-dark: #121212; --bg-panel: #1e1e24; --accent-blue: #00e5ff; --accent-purple: #b300ff; --text-main: #e0e0e0; } :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; } 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-top { flex-shrink: 0; }
.sidebar-history { flex: 1; overflow-y: auto; margin-top: 15px; margin-bottom: 15px; } .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; } .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-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 { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em; }
.history-btn:hover { color: var(--accent-blue); } .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; } .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; } .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; } .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; } .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 { 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); } .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; } .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-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); } .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 { 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; } .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; } .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; } .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) */ /* LOADER HTMX SURÉLEVÉ */
.htmx-indicator { display: none; opacity: 0; transition: opacity 0.3s ease-in; position: absolute; bottom: 140px; left: 50%; transform: translateX(-50%); z-index: 9999; } .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; }
.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-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; } .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); } } @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) { @media (max-width: 768px) {
body { flex-direction: column; } body { flex-direction: column; }
.sidebar { width: 100%; height: auto; max-height: 35vh; border-right: none; border-bottom: 1px solid #333; padding: 10px; } .mobile-header { display: flex; }
.sidebar-history { max-height: 120px; } .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); }
.main-content { flex: 1; height: 65vh; } .sidebar.sidebar-open { left: 0; }
.chat-container { padding-bottom: 180px; padding-left: 10px; padding-right: 10px; } .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%; } .user-bubble, .ai-bubble { max-width: 95%; }
.input-area { padding: 10px; } .input-area { padding: 10px; }
.options-row { justify-content: center; gap: 10px;}
} }
"""), """),
Script(r""" Script(r"""
// Sac à dos virtuel global pour additionner les fichiers sans les écraser
let aethasFiles = new DataTransfer(); 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() { function syncFiles() {
const fileInput = document.getElementById('file-upload'); const fileInput = document.getElementById('file-upload');
if(fileInput) fileInput.files = aethasFiles.files; if(fileInput) fileInput.files = aethasFiles.files;
@@ -139,12 +161,12 @@ def get_theme_hdrs():
pre.parentNode.insertBefore(header, pre); pre.parentNode.insertBefore(header, pre);
if (typeof hljs !== 'undefined') hljs.highlightElement(code); if (typeof hljs !== 'undefined') hljs.highlightElement(code);
} }
} catch(err) { console.warn("Erreur entête code", err); } } catch(err) {}
}); });
}); });
let chat = document.getElementById('chat-history'); let chat = document.getElementById('chat-history');
if (chat) chat.scrollTop = chat.scrollHeight; if (chat) chat.scrollTop = chat.scrollHeight;
} catch(e) { console.error("Erreur markdown", e); } } catch(e) {}
} }
function getSmartFilename(btn, defaultName = 'Aethas38_Reponse', extension = 'pdf') { 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) { document.body.addEventListener('htmx:afterRequest', function(evt) {
if(evt.detail.target.id === 'chat-history') { if(evt.detail.target.id === 'chat-history') {
aethasFiles = new DataTransfer(); aethasFiles = new DataTransfer();
@@ -229,13 +250,7 @@ def get_theme_hdrs():
}); });
}); });
function copyCodeBlock(btn) { // ... (Fonctions de téléchargement ZIP, PDF, DOC, TXT, JSON et Partage identiques et préservées)
let pre = btn.closest('.code-header').nextElementSibling;
navigator.clipboard.writeText(pre.querySelector('code').innerText);
btn.innerText = "✅ Copié !";
setTimeout(() => btn.innerText = "📋 Copier", 2000);
}
function downloadCodeZip(btn, lang) { function downloadCodeZip(btn, lang) {
let pre = btn.closest('.code-header').nextElementSibling; let pre = btn.closest('.code-header').nextElementSibling;
let code = pre.querySelector('code').innerText; let code = pre.querySelector('code').innerText;
@@ -251,12 +266,6 @@ def get_theme_hdrs():
link.download = filename.replace('.'+ext, '_snippet.zip'); link.download = filename.replace('.'+ext, '_snippet.zip');
link.click(); 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();
} }
} }
+128 -29
View File
@@ -1,10 +1,32 @@
import os
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.paypal import render_paypal_donation_widget
from modules.billing_manager import obtenir_solde 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): 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): 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 ""
@@ -16,6 +38,7 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi
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"),
@@ -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"), Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par courriel"),
cls="msg-actions" cls="msg-actions"
) )
return Div( return Div(
encart_info, encart_info,
Div(text, cls="markdown-raw", style="display:none;"), Div(text, cls="markdown-raw", style="display:none;"),
Div(cls="markdown-rendered", style="line-height: 1.6;"), Div(cls="markdown-rendered", style="line-height: 1.6;"),
actions, actions,
cls="ai-bubble", style=f"border-left: 4px solid {couleur};" cls="ai-bubble",
style=f"border-left: 4px solid {couleur};"
) )
def render_sidebar(): 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(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"/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"), 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") 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"): 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: else:
messages_ui.append(render_ai_msg( messages_ui.append(render_ai_msg(
msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg['content'],
msg.get('modele', ''), msg.get('couleur', '#333'), msg.get('execution_time', '') msg.get('intention', ''),
msg.get('fournisseur', ''),
msg.get('modele', ''),
msg.get('couleur', '#333'),
msg.get('execution_time', '')
)) ))
if messages_ui: if messages_ui:
messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) 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) solde_perso = obtenir_solde(utilisateur_nom)
if role.lower() in ["super admin", "admin"]: 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( 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: #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"🏦 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;" style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;"
) )
else: else:
@@ -97,13 +141,15 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
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;"),
Div( Div(
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;"), # NOUVEAU BOUTON ICI A("🚪 Déconnexion", href="/logout", cls="export-btn", style="background-color: #8b0000; color: white; border-color: #8b0000; margin-top: 5px;"),
Div( Div(
render_paypal_donation_widget(), 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"), Div(badge_finance, cls="sidebar-footer"),
cls="sidebar" 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;"), # 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", 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( 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)"),
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( Label(
Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"), Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"),
"👑 Payant", "👑 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( Label(
Input(type="checkbox", name="mode_multi_ia", id="mode-multi-toggle", style="margin-right: 6px; accent-color: #d97757; cursor: pointer;"), Input(type="checkbox", name="mode_multi_ia", id="mode-multi-toggle", style="margin-right: 6px; accent-color: #d97757; cursor: pointer;"),
"⚡ Multi-IA", "⚡ 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;"
), ),
cls="options-row"
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="input-container-box" 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';", 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" cls="input-area"
), ),
@@ -169,20 +246,18 @@ def render_admin_moderation_panel(utilisateur_cible):
Select( Select(
Option("Avertissement simple", value="Avertissement"), Option("Avertissement simple", value="Avertissement"),
Option("Bannissement 1 jour", value="Ban 1J"), 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("Bannissement définitif et signalement PHAROS", value="Ban Definitif + Pharos"),
Option("Faux positif (Levée de sanction)", value="Annulation"), name="decision",
name="decision", cls="input-field" cls="input-field"
), ),
style="margin-bottom: 15px;" style="margin-bottom: 15px;"
), ),
Div( Div(
Label("Commentaire libre de l'administrateur (Preuve d'audit) :", style="font-weight: bold;"), Label("Commentaire (Preuve d'audit) :", style="font-weight: bold;"),
Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison de la décision, contexte juridique..."), Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison..."),
style="margin-bottom: 15px;" 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_post="/admin_decision",
hx_target="#admin-result", hx_target="#admin-result",
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;" 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;" 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" multi_ia_val = "on" if multi_ia_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("⚠️ 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("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( 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="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;"
) )