diff --git a/.gitignore b/.gitignore
index 864e746..1f2b607 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
__pycache__/
venv/
.env
-.sesskey
\ No newline at end of file
+.sesskey
+aethas_history.json
diff --git a/README.md b/README.md
index e8e8724..afeeacc 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,55 @@
-# Aethas38 - Multi-IA Orchestrator
+# Aethas38 - Multi-IA Orchestrateur (Minecraft-Tech Edition)
-Système d'IA modulaire avec orchestrateur local et routeur intelligent.
+Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécurisé et facturable.
## 📋 To-Do List (Roadmap)
-### Phase 1 : Infrastructure & Environnement ✅
-- [x] Désactiver interface graphique serveur (mode headless)
-- [x] Installation Docker, Compose et Nginx Proxy Manager
-- [x] Configuration réseau (Pare-feu / Ports)
-- [x] Initialisation dépôts Gitea/GitHub
+### Phase 1 & 2 : Infrastructure & Backend de Base ✅
+- [x] Installation Docker, Nginx, Pare-feu, Gitea/GitHub
+- [x] Serveur Local Ollama (Qwen 3B) + FastHTML
+- [x] Routeur Multi-IA (Groq, Gemini, Local) avec système de cascade
-### Phase 2 : Orchestrateur Local (Backend) ✅
-- [x] Installation Ollama + Qwen 2.5 3B (Serveur)
-- [x] Développement de l'interface de base FastHTML
-- [x] Connexion réussie entre PC de dev et Serveur IA
-- [x] Développement du script de Routage (Gateway avec Qwen)
-- [x] Intégration des APIs tierces (Groq, Gemini avec système de cascade)
-- [ ] Système de gestion de file d'attente (Queue List)
-- [ ] Création du système d'interdiction automatique (Fichiers refusés)
+### Phase 3 : Refonte UI & Expérience Utilisateur (En cours) ⏳
+- [ ] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive)
+- [ ] Historique des conversations (Sidebar type ChatGPT/Copilot/Gemini)
+- [ ] Envoi de fichiers (Upload) pour analyse
+- [ ] Demande de thème personnalisé par l'utilisateur (Notification admin par mail)
-### Phase 3 : Front-Office & Sécurité Financière ⏳
-- [ ] Création UI légère (Thème Aethas avec encarts dynamiques)
-- [ ] Dashboard Finance (Compteur de frais et analyse consommation tokens)
-- [ ] **Simulateur de coût avant requête** (pour éviter les défauts de paiement)
-- [ ] Passage des clés API en mode "Pay-as-you-go" (Une fois le simulateur testé et validé)
-- [ ] Implémentation du switch Gratuit/Payant pour les utilisateurs
-- [ ] Système d'inscription et validation Admin
+### Phase 4 : Gestion des Utilisateurs & Sécurité
+- [ ] Système de connexion sécurisé (Mots de passe cryptés)
+- [ ] Fail2Ban (Blocage 15 mins après 5 erreurs)
+- [ ] Intégration SMTP (mail.aethas38.duckdns.org) pour mot de passe oublié/notifications
+- [ ] Gestion des rôles : Admin (Xavier, Céline), Famille (Anaëlle, Audric, Renan), et Externes
-### Phase 4 : Déploiement & Sécurité
-- [ ] Mise en conteneur Docker
-- [ ] Mise en production sur aethas38.duckdns.org
-- [ ] Tests de charge et vérification des sécurités de facturation
+### Phase 5 : Sécurité Financière & Facturation Exacte 💰
+- [ ] Création d'un fichier Excel pour le calcul du coût serveur (2x 1000W H24, Écrans 18/24h, Internet 60€/mois)
+- [ ] Simulateur de coût avant requête : L'estimation doit être exacte pour éviter les défauts de paiement
+- [ ] Gestion des comptes et provisions (Carte Bancaire) :
+ - Le compte doit être approvisionné avant requête
+ - Un test du restant financier est effectué avant d'autoriser le lancement
+- [ ] Règles d'exceptions Administrateurs & Famille :
+ - Xavier, Céline, Audric, Renan et Anaëlle sont rattachés au master-compte (Xavier/Céline)
+ - Ces 5 comptes n'ont pas besoin de provisionner leurs fonds
+- [ ] Règles d'exceptions Externes :
+ - Les utilisateurs externes reçoivent une demande de validation pour la partie payante
+ - Application d'une majoration invisible de 15% sur leurs requêtes
+- [ ] Compteur en temps réel mis à jour pour tout le monde après le retour de l'IA (Basé sur le prix annoncé)
+- [ ] Routage Conditionnel Payant : Utiliser *toutes* les IA en adéquation avec le thème de la requête (payantes et gratuites)
----
-## 💡 Informations Techniques
-* **Orchestrateur :** Local (Ollama / Qwen 2.5 3B)
-* **Framework Interface :** FastHTML
-* **Langage :** Python 3.13 (Environnement Virtuel strict)
-* **Serveur :** Debian (Headless, GTX 1650 4Go VRAM)
\ No newline at end of file
+### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖
+*La mise en forme finale des retours est assurée par l'abonnement Gemini.*
+
+| Domaine de tâche | Modèle / Moteur | Fournisseur / Hébergement | Rôle principal |
+| :--- | :--- | :--- | :--- |
+| **Orchestration & Tri** | Qwen 2.5 (3B/7B quantizé) | Local (GTX 1650 / Ollama) | Analyse l'intention, gère le chat rapide et route vers la bonne IA |
+| **Prog. & Java 21** | Claude 3.5 Sonnet / Qwen Coder | Anthropic / OpenRouter | Correction bugs, KubeJS, modernisation de mods (Pay-as-you-go) |
+| **Serveur / Logs** | Llama 3.3 (8B / 70B) | Groq API | Analyse crashs Prism Launcher, Bash, Docker (Gratuit / Rapide) |
+| **Logique & Quêtes** | DeepSeek R1 (Distillé) | DeepSeek API / Gratuit | Structuration logique des modpacks et FTB Quests |
+| **Images HD / Vidéo** | Gemini 2.0 Flash / FLUX.1 | Google AI Studio & Fal.ai | Analyse d'images/vidéos et génération d'assets 2K |
+| **Bureautique** | À définir | À définir | Excel (Macros, formules complexes) |
+
+### Phase 7 : RAG (Mémoire Locale) & Maintenance
+- [ ] "RAG" (Retrieval-Augmented Generation) : Permettre au modèle local d'apprendre, chercher et mémoriser (Base de données vectorielle type ChromaDB)
+- [ ] Script de surveillance de l'espace disque du serveur
+- [ ] Création de logs centralisés consultables/téléchargeables par l'Admin
+- [ ] Étude de marché (Tarifs concurrents type "Mammouth" etc.) pour la viabilité de la version Pro
\ No newline at end of file
diff --git a/auto_git.ps1 b/auto_git.ps1
new file mode 100644
index 0000000..b9c27a5
--- /dev/null
+++ b/auto_git.ps1
@@ -0,0 +1,21 @@
+$counter = 1
+
+Write-Host "Démarrage de la sauvegarde automatique Git (Toutes les heures)..." -ForegroundColor Green
+
+while ($true) {
+ # Attend 1 heure (3600 secondes)
+ Start-Sleep -Seconds 3600
+
+ # Prépare le message
+ $commitMessage = "maj auto $counter"
+
+ # Exécute les commandes Git
+ git add .
+ git commit -m $commitMessage
+ git push
+
+ Write-Host "[$(Get-Date -Format 'HH:mm:ss')] Sauvegarde '$commitMessage' effectuée avec succès !" -ForegroundColor Cyan
+
+ # Incrémente le compteur
+ $counter++
+}
\ No newline at end of file
diff --git a/main.py b/main.py
index cc5ed8a..bf5a8cf 100644
--- a/main.py
+++ b/main.py
@@ -1,5 +1,6 @@
import os
import json
+import uuid
from dotenv import load_dotenv
from fasthtml.common import *
from openai import OpenAI
@@ -7,33 +8,130 @@ from groq import Groq
from google import genai
# ==========================================
-# SECTION 1. INITIALISATION & SÉCURITÉ
+# SECTION 1. INITIALISATION, SÉCURITÉ & HISTORIQUE
# ==========================================
load_dotenv()
-app, rt = fast_app()
+
+# --- GESTION DE L'HISTORIQUE (JSON) ---
+HISTORY_FILE = "aethas_history.json"
+def load_history():
+ if not os.path.exists(HISTORY_FILE):
+ with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump({}, f)
+ with open(HISTORY_FILE, 'r', encoding='utf-8') as f: return json.load(f)
+
+def save_history(history):
+ with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, indent=4)
+
+# --- CSS & JAVASCRIPT (Thème, Markdown, ZIP et Copie) ---
+theme_hdrs = (
+ # Bibliothèques externes pour le Markdown et la création de ZIP
+ Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"),
+ Script(src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"),
+ Script(src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"),
+ Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"),
+
+ Style("""
+ :root { --bg-dark: #121212; --bg-panel: #1e1e24; --accent-blue: #00e5ff; --accent-purple: #b300ff; --text-main: #e0e0e0; }
+ body { background-color: var(--bg-dark); color: var(--text-main); font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; display: flex; height: 100vh; overflow: hidden; }
+
+ /* SIDEBAR */
+ .sidebar { width: 280px; background-color: #0a0a0c; border-right: 1px solid #333; padding: 20px; display: flex; flex-direction: column; }
+ .new-chat-btn { background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; width: 100%; margin-bottom: 20px;}
+ .new-chat-btn:hover { background: rgba(0, 229, 255, 0.1); }
+ .history-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #222; font-size: 0.9em; transition: 0.2s;}
+ .history-item:hover { background: #1a1a20; }
+ .history-btn { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em;}
+ .history-btn:hover { color: var(--accent-blue); }
+
+ /* CHAT AREA */
+ .main-content { flex: 1; display: flex; flex-direction: column; height: 100%; position: relative; }
+ .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: 120px;}
+
+ /* BULLES DE CHAT */
+ .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; box-shadow: 0 4px 6px rgba(0,0,0,0.3);}
+ .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; box-shadow: 0 4px 6px rgba(0,0,0,0.3);}
+
+ /* BLOCS DE CODE (Copie & ZIP) */
+ .code-header { background: #111; padding: 8px 15px; display: flex; justify-content: space-between; align-items: center; border-radius: 8px 8px 0 0; font-family: monospace; font-size: 0.85em; border: 1px solid #333; border-bottom: none;}
+ .code-header button { background: #333; border: 1px solid #444; color: white; cursor: pointer; padding: 4px 10px; border-radius: 5px; margin-left: 8px; transition: 0.2s;}
+ .code-header button:hover { background: var(--accent-blue); color: black; border-color: var(--accent-blue);}
+ .markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333;}
+
+ /* INPUT AREA */
+ .input-area { position: absolute; bottom: 0; width: 100%; padding: 20px; background: linear-gradient(0deg, #121212 80%, transparent); display: flex; justify-content: center; }
+ .input-wrapper { display: flex; flex-direction: row; gap: 15px; width: 100%; max-width: 900px; background: #1e1e24; padding: 15px; border-radius: 15px; border: 1px solid #444; align-items: center;}
+ .input-field { background: #121212; border: 1px solid #333; color: white; padding: 15px; flex-grow: 1; min-width: 50%; font-size: 1em; border-radius: 10px; }
+ .input-field:focus { outline: none; border-color: var(--accent-blue); }
+ .btn-submit { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; padding: 15px 30px; border-radius: 10px; font-weight: bold; cursor: pointer; white-space: nowrap; }
+ """),
+
+ # Script Magique : Convertit le Markdown, ajoute les boutons Copier/Télécharger en ZIP
+ Script("""
+ function renderMarkdown() {
+ document.querySelectorAll('.markdown-raw').forEach(el => {
+ let container = el.nextElementSibling;
+ container.innerHTML = marked.parse(el.textContent);
+ el.classList.remove('markdown-raw');
+
+ // Ajout de la barre de code avec boutons
+ container.querySelectorAll('pre').forEach(pre => {
+ let code = pre.querySelector('code');
+ let lang = code.className.replace('language-', '') || 'texte';
+
+ let header = document.createElement('div');
+ header.className = 'code-header';
+ header.innerHTML = `
+ ${lang.toUpperCase()}
+
+
+
+
+ `;
+ pre.parentNode.insertBefore(header, pre);
+ hljs.highlightElement(code);
+ });
+ });
+ // Auto-scroll vers le bas
+ let chat = document.getElementById('chat-history');
+ chat.scrollTop = chat.scrollHeight;
+ }
+
+ function copyCode(btn) {
+ let code = btn.parentElement.parentElement.nextElementSibling.querySelector('code').innerText;
+ navigator.clipboard.writeText(code);
+ btn.innerText = "✅ Copié !";
+ setTimeout(() => btn.innerText = "📋 Copier", 2000);
+ }
+
+ function downloadCode(btn, lang) {
+ let code = btn.parentElement.parentElement.nextElementSibling.querySelector('code').innerText;
+ let zip = new JSZip();
+ let ext = lang === 'python' ? 'py' : lang === 'javascript' ? 'js' : lang === 'html' ? 'html' : 'txt';
+
+ // Création du fichier dans le ZIP
+ zip.file(`Aethas38_Code.${ext}`, code);
+ zip.generateAsync({type:"blob"}).then(function(content) {
+ let link = document.createElement('a');
+ link.href = URL.createObjectURL(content);
+ link.download = `Aethas38_Export_${lang}.zip`;
+ link.click();
+ });
+ }
+ """)
+)
+
+# Activation des sessions pour se souvenir du chat actuel
+app, rt = fast_app(hdrs=theme_hdrs, secret_key="super_secret_aethas")
# ==========================================
# SECTION 2. CONNEXION AUX IA
# ==========================================
-# --- SOUS-SECTION 2A : Initialisation des clients ---
local_client = OpenAI(base_url="http://192.168.1.20:11434/v1", api_key="ollama")
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
gemini_client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
-# --- SOUS-SECTION 2B : Cascade Google Gemini ---
def interroger_gemini_cascade(prompt):
- """
- Tente systématiquement la version MAXIMUM (Pro).
- Si le compte n'a pas les droits ou si le serveur est plein, descend en gamme.
- """
- modeles_a_tester = [
- "gemini-3.1-pro-preview", # Max absolu
- "gemini-3.1-pro", # Max stable
- "gemini-2.5-pro", # Max ancienne génération
- "gemini-3.7-flash", # Meilleur modèle rapide et gratuit
- "gemini-3.6-flash" # Secours
- ]
-
+ modeles_a_tester = ["gemini-3.1-pro-preview", "gemini-3.1-pro", "gemini-2.5-pro", "gemini-3.7-flash", "gemini-3.6-flash"]
for nom_modele in modeles_a_tester:
try:
ia_response = gemini_client.models.generate_content(
@@ -43,29 +141,72 @@ def interroger_gemini_cascade(prompt):
)
return ia_response.text, nom_modele
except Exception:
- # On ignore l'erreur silencieusement et on passe au modèle suivant
continue
-
raise Exception("Tous les modèles Gemini sont indisponibles actuellement.")
# ==========================================
-# SECTION 3. INTERFACE UTILISATEUR (UI FastHTML)
+# SECTION 3. INTERFACE UTILISATEUR & HISTORIQUE
# ==========================================
+def render_sidebar():
+ history = load_history()
+ # Trier par épinglé d'abord, puis par ID (ordre de création)
+ sorted_chats = sorted(history.items(), key=lambda x: (not x[1].get('pinned', False), x[0]))
+
+ items = []
+ for chat_id, data in sorted_chats:
+ pin_icon = "📌" if data.get('pinned', False) else "📍"
+ items.append(
+ Div(
+ Span(data['title'], style="flex:1; cursor:pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"),
+ # Bouton Épingler
+ Button(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="innerHTML"),
+ # Bouton Renommer
+ Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="innerHTML"),
+ # Bouton Supprimer
+ Button("🗑️", cls="history-btn", hx_post=f"/delete/{chat_id}", hx_target="#sidebar-content", hx_swap="innerHTML"),
+ cls="history-item", id=f"chat-item-{chat_id}"
+ )
+ )
+ return items
+
@rt('/')
-def get():
- return Titled("Aethas38 - Orchestrateur",
- Main(
- H1("Multi-IA : Demandez, l'Orchestrateur choisit !"),
- Form(
- Input(type="text", name="prompt", placeholder="Posez votre question...", style="width: 100%; padding: 10px; margin-bottom: 10px;"),
- Button("Envoyer la requête", type="submit", style="padding: 10px 20px; cursor: pointer;"),
- hx_post="/chat",
- hx_target="#result",
- hx_indicator="#loading"
+def get(session):
+ # Démarrer une nouvelle session de chat si aucune n'existe
+ if 'current_chat_id' not in session:
+ session['current_chat_id'] = str(uuid.uuid4())
+
+ return Title("Aethas38 - Mine_AI"), Body(
+ # --- COLONNE GAUCHE (Historique) ---
+ Div(
+ Div(H2("AETHAS 38", style="color:#00e5ff; margin:0; text-align:center;"), P("Tech Core", style="font-size:0.7em; color:#888; text-align:center;"), margin_bottom="20px"),
+ Form(Button("+ Nouvelle discussion", cls="new-chat-btn"), hx_post="/new_chat", hx_target="body"),
+ Div(*render_sidebar(), id="sidebar-content", style="flex:1; overflow-y:auto;"),
+ Div(P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; text-align:center;"), style="border-top: 1px solid #333; padding-top: 15px;"),
+ cls="sidebar"
+ ),
+
+ # --- COLONNE DROITE (Chat) ---
+ Div(
+ # Zone d'affichage des messages (Append 'beforeend' ici)
+ Div(id="chat-history", cls="chat-container"),
+
+ # Zone de saisie
+ Div(
+ Form(
+ Div(
+ Input(type="text", name="prompt", id="prompt-input", placeholder="Entrez votre requête ou commande...", cls="input-field"),
+ Button("Exécuter", type="submit", cls="btn-submit"),
+ cls="input-wrapper"
+ ),
+ hx_post="/chat",
+ hx_target="#chat-history",
+ hx_swap="beforeend", # AJOUTE à la suite au lieu de remplacer !
+ hx_on__after_request="this.reset()", # Vide le champ de texte
+ style="width: 100%; display: flex; justify-content: center;"
+ ),
+ cls="input-area"
),
- Div("L'Orchestrateur analyse et route la requête...", id="loading", cls="htmx-indicator", style="color: gray; font-style: italic; margin-top: 10px;"),
- Div(id="result", style="margin-top: 20px;"), # Zone vide où le résultat s'injectera
- cls="container"
+ cls="main-content"
)
)
@@ -73,100 +214,106 @@ def get():
# SECTION 4. LE MOTEUR DE ROUTAGE (L'Aiguilleur)
# ==========================================
@rt('/chat')
-def post(prompt: str):
+def post(prompt: str, session: dict):
try:
- # --- SOUS-SECTION 4A : Analyse par Qwen (Le Cerveau) ---
- router_prompt = """Tu es l'Aiguilleur (Routeur) du projet Aethas38.
- Choisis le fournisseur d'IA le plus adapté :
- - "groq" : programmation, code, scripts, mathématiques, logique.
- - "gemini" : rédaction, littérature, recettes, culture générale, résumés, art.
- - "local" : UNIQUEMENT pour les salutations (bonjour, test, coucou).
- Format strict : {"intention": "description courte", "fournisseur": "nom"}
- Ne dis absolument RIEN d'autre.
- """
+ # 1. Gestion du Titre dans l'historique
+ chat_id = session.get('current_chat_id')
+ history = load_history()
+ if chat_id not in history:
+ history[chat_id] = {"title": prompt[:30] + "...", "pinned": False, "messages": []}
+ save_history(history)
- router_response = local_client.chat.completions.create(
- model="qwen2.5:3b",
- messages=[{"role": "system", "content": router_prompt}, {"role": "user", "content": prompt}],
- temperature=0.1
- )
+ # 2. Création de la bulle VISUELLE Utilisateur (Alignée à droite)
+ user_bubble = Div(P(prompt, style="margin:0; font-weight:bold;"), cls="user-bubble")
+
+ # 3. Analyse de l'Aiguilleur
+ router_prompt = """Tu es le Routeur. Choisis : "groq" (code/maths), "gemini" (création/rédaction), "local" (salutations).
+ Format strict : {"intention": "...", "fournisseur": "..."}. Ne dis rien d'autre."""
+ router_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": router_prompt}, {"role": "user", "content": prompt}], temperature=0.1)
raw_decision = router_response.choices[0].message.content
-
- # --- NOUVEAU : Nettoyage blindé (Anticrash) ---
- # On cherche manuellement les accolades pour extraire le JSON pur
start_idx = raw_decision.find('{')
end_idx = raw_decision.rfind('}') + 1
- if start_idx != -1 and end_idx != 0:
- clean_json = raw_decision[start_idx:end_idx]
- decision = json.loads(clean_json)
- else:
- # Filet de sécurité si Qwen ne renvoie pas du tout de JSON
- decision = {"intention": "Analyse échouée (Secours)", "fournisseur": "gemini"}
-
+ decision = json.loads(raw_decision[start_idx:end_idx]) if start_idx != -1 and end_idx != 0 else {"intention": "Secours", "fournisseur": "gemini"}
fournisseur = decision.get("fournisseur", "local").lower()
- intention = decision.get("intention", "Non définie")
- # --- SOUS-SECTION 4B : Exécution de la Requête (avec respect de la langue) ---
- nom_modele_utilise = ""
-
+ # 4. Requête IA
if fournisseur == "groq":
- nom_modele_utilise = "llama3-70b-8192"
- ia_response = groq_client.chat.completions.create(
- messages=[
- {"role": "system", "content": "Réponds systématiquement dans la même langue que celle utilisée par l'utilisateur."},
- {"role": "user", "content": prompt}
- ],
- model=nom_modele_utilise,
- )
- reponse_finale = ia_response.choices[0].message.content
- couleur = "#f97316" # Orange Groq
- nom_fournisseur = "GROQ (Llama 3)"
-
+ ia_response = groq_client.chat.completions.create(messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": prompt}], model="llama-3.3-70b-versatile")
+ reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b"
elif fournisseur == "gemini":
- reponse_finale, nom_modele_utilise = interroger_gemini_cascade(prompt)
- couleur = "#3b82f6" # Bleu Gemini
- nom_fournisseur = "GOOGLE GEMINI"
-
+ reponse_finale, nom_modele = interroger_gemini_cascade(prompt)
+ couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI"
else:
- nom_modele_utilise = "qwen2.5:3b"
- ia_response = local_client.chat.completions.create(
- model=nom_modele_utilise,
- messages=[
- {"role": "system", "content": "Réponds systématiquement dans la langue utilisée par l'utilisateur."},
- {"role": "user", "content": prompt}
- ]
- )
- reponse_finale = ia_response.choices[0].message.content
- couleur = "#10b981" # Vert Local
- nom_fournisseur = "SERVEUR LOCAL"
+ ia_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": prompt}])
+ reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b"
- # --- SOUS-SECTION 4C : Formatage Visuel (L'encart spécifique) ---
-
- # 1. Le bloc d'informations (Encart visuel distinct)
+ # 5. Création de la bulle VISUELLE IA (Alignée à gauche avec Encart)
encart_info = Div(
- H4("Aperçu IA", style="margin: 0 0 10px 0; color: #fff;"),
- P(f"🎯 Intention : {intention}", style="margin: 0 0 5px 0; font-size: 0.9em;"),
- P(f"🏢 Fournisseur : {nom_fournisseur}", style="margin: 0 0 5px 0; font-size: 0.9em;"),
- P(f"🧠 Modèle utilisé : ", Span(nom_modele_utilise, style="font-weight: bold; color: #fff;"), style="margin: 0; font-size: 0.9em;"),
- style=f"background-color: {couleur}22; border: 1px solid {couleur}; border-left: 5px solid {couleur}; padding: 15px; border-radius: 5px; margin-bottom: 20px;"
+ Span(f"🎯 {decision.get('intention', '')}", style="margin-right: 15px; color:#aaa; font-size:0.85em;"),
+ Span(f"🏢 {nom_fournisseur}", style="margin-right: 15px; color:#aaa; font-size:0.85em;"),
+ Span(f"🧠 {nom_modele}", style=f"color:{couleur}; font-weight:bold; font-size:0.85em;"),
+ style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px;"
+ )
+
+ ai_bubble = Div(
+ encart_info,
+ # Le texte brut caché
+ Div(reponse_finale, cls="markdown-raw", style="display:none;"),
+ # Le conteneur final rendu par JS
+ Div(cls="markdown-rendered", style="line-height: 1.6;"),
+ cls="ai-bubble", style=f"border-left: 4px solid {couleur};"
)
- # 2. Le bloc de réponse de l'IA
- bloc_reponse = Div(
- P(reponse_finale, style="white-space: pre-wrap; margin: 0; line-height: 1.6;"),
- style="background-color: #222; border: 1px solid #444; padding: 20px; border-radius: 5px; color: #eee;"
- )
-
- # On retourne les deux blocs empilés
- return Div(encart_info, bloc_reponse)
+ # On retourne l'ensemble (User + IA) et on déclenche le rendu Markdown JS
+ return user_bubble, ai_bubble, Script("renderMarkdown();"), Div(*render_sidebar(), id="sidebar-content", hx_swap_oob="true")
except Exception as e:
- return P(f"Erreur du système : {e}", style="color: red; font-weight: bold; padding: 15px; background: #330000; border: 1px solid red;")
+ return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
+
# ==========================================
-# SECTION 5. DÉMARRAGE DU SERVEUR
+# SECTION 5. ROUTES DE L'HISTORIQUE (Renommer, Épingler, Supprimer)
# ==========================================
+@rt('/new_chat')
+def post(session):
+ session['current_chat_id'] = str(uuid.uuid4())
+ return Script("window.location.reload();") # Recharge la page à zéro
+
+@rt('/rename/{chat_id}')
+def post(chat_id: str, req):
+ # htmx envoie la valeur tapée dans la popup via l'en-tête 'HX-Prompt'
+ nouveau_nom = req.headers.get("HX-Prompt")
+
+ if nouveau_nom:
+ history = load_history()
+ if chat_id in history:
+ history[chat_id]['title'] = nouveau_nom
+ save_history(history)
+
+ return render_sidebar()
+
+@rt('/pin/{chat_id}')
+def post(chat_id: str):
+ history = load_history()
+ if chat_id in history:
+ history[chat_id]['pinned'] = not history[chat_id].get('pinned', False)
+ save_history(history)
+ return render_sidebar()
+
+@rt('/delete/{chat_id}')
+def post(chat_id: str):
+ history = load_history()
+ if chat_id in history:
+ del history[chat_id]
+ save_history(history)
+ return render_sidebar()
+
if __name__ == '__main__':
- serve(port=5001)
\ No newline at end of file
+ serve(port=5001)
+
+@rt('/load_chat/{chat_id}')
+def post(chat_id: str, session):
+ session['current_chat_id'] = chat_id
+ return Script("window.location.reload();") # Recharge la page avec la bonne session
\ No newline at end of file