Public Access
maj auto 2
This commit is contained in:
1 parent
166c5a5e66
commit
a797816727
2 files changed
+150
-101
No files matched your search
@@ -11,37 +11,44 @@ Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécu
|
|||||||
|
|
||||||
### Phase 3 : Refonte UI & Expérience Utilisateur (En cours) ⏳
|
### Phase 3 : Refonte UI & Expérience Utilisateur (En cours) ⏳
|
||||||
- [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
|
- [x] Historique (Sidebar) avec Renommage, Épinglage, Suppression et Navigation isolée par session
|
||||||
- [ ] Maintien et chargement de l'historique complet des messages dans la zone de chat
|
- [x] Zone de saisie inspirée de Gemini Web (Textarea avec ascenseur, bouton "+" pour import multi-fichiers jusqu'à 10 éléments, et bouton flèche "↑")
|
||||||
- [ ] Export de la conversation complète (PDF, DOC, ou JSON) pour l'utilisateur
|
- [x] Export du code en ZIP et copie rapide
|
||||||
|
- [ ] Export global de la conversation (PDF, DOC, JSON)
|
||||||
- [ ] Script de nettoyage automatique : Suppression des discussions de > 365 jours (avec notification)
|
- [ ] Script de nettoyage automatique : Suppression des discussions de > 365 jours (avec notification)
|
||||||
- [ ] Indicateur d'avancement (Loader dynamique) pendant le travail des IA
|
- [ ] Indicateur d'avancement dynamique (Loader) pendant le traitement des IA
|
||||||
- [ ] Envoi de fichiers (Upload) pour analyse
|
|
||||||
|
|
||||||
### Phase 4 : Gestion des Utilisateurs & Sécurité
|
### Phase 4 : Gestion des Utilisateurs & Sécurité
|
||||||
- [ ] Système de connexion sécurisé (Mots de passe cryptés)
|
- [ ] Système de connexion sécurisé (Mots de passe cryptés)
|
||||||
- [ ] Fail2Ban (Blocage 15 mins après 5 erreurs)
|
- [ ] Fail2Ban (Blocage 15 mins après 5 erreurs)
|
||||||
- [ ] Intégration SMTP (mail.aethas38.duckdns.org) pour mot de passe oublié/notifications
|
- [ ] Intégration SMTP (`mail.aethas38.duckdns.org`) pour mot de passe oublié et notifications de thèmes spécifiques
|
||||||
- [ ] Gestion des rôles : Admin (Xavier, Céline), Famille (Anaëlle, Audric, Renan), et Externes
|
- [ ] Gestion des rôles : Admin (Xavier, Céline), Famille (Anaëlle, Audric, Renan), et Externes
|
||||||
|
|
||||||
### Phase 5 : Sécurité Financière & Facturation Exacte 💰
|
### Phase 5 : Sécurité Financière & Facturation Exacte 💰
|
||||||
- [ ] Simulateur de coût avant requête : L'estimation doit être exacte
|
- [ ] Modèle Excel de calcul des coûts serveur (2x 1000W H24, Écrans 18/24h, Internet 60€/mois)[cite: 3]
|
||||||
- [ ] Gestion des comptes et provisions (CB) avec test du restant financier
|
- [ ] Simulateur de coût avant requête : L'estimation doit être exacte pour éviter les défauts de paiement[cite: 3]
|
||||||
- [ ] Règles d'exceptions Administrateurs & Famille (Pas de provision)
|
- [ ] Gestion des comptes et provisions (CB) avec test du restant financier avant validation[cite: 3]
|
||||||
- [ ] Majoration invisible de 15% pour les comptes Externes
|
- [ ] Règles d'exceptions Administrateurs & Famille (Pas de provision nécessaire, rattachés au master-compte Xavier/Céline)[cite: 3]
|
||||||
- [ ] Compteur en temps réel mis à jour après la requête
|
- [ ] Majoration invisible de 15% appliquée automatiquement pour les comptes Externes[cite: 3]
|
||||||
|
- [ ] Compteur en temps réel mis à jour après la requête basé sur le prix annoncé[cite: 3]
|
||||||
|
- [ ] Routage Conditionnel Payant : Utiliser *toutes* les IA en adéquation avec le thème de la requête[cite: 3]
|
||||||
|
|
||||||
### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖
|
### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖
|
||||||
*Intégration technique des nouvelles API gratuites et payantes.*
|
*La mise en forme finale des retours est assurée par l'abonnement Gemini.*
|
||||||
- [ ] **Mistral, DeepSeek, HuggingFace, Cloudflare** : Implémentation des clients API
|
|
||||||
- [ ] **Multi-Agent (Concaténation)** : L'IA locale synthétise les retours de plusieurs IA pour une réponse optimale.
|
| Domaine de tâche | Modèle / Moteur | Fournisseur / Hébergement | Rôle principal |
|
||||||
- [ ] Code : Claude 3.5 Sonnet / Qwen Coder (Anthropic / OpenRouter)
|
| :--- | :--- | :--- | :--- |
|
||||||
- [ ] Logique & Quêtes : DeepSeek R1
|
| **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 |
|
||||||
- [ ] Multimédia : Gemini 2.0 Flash / FLUX.1 (Image/Vidéo)
|
| **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 & Maintenance
|
### Phase 7 : RAG & Maintenance
|
||||||
- [ ] "RAG" : Base de données vectorielle (ChromaDB)
|
- [ ] "RAG" : Base de données vectorielle (ChromaDB) pour permettre l'apprentissage et la mémorisation de l'IA locale
|
||||||
- [ ] Script de surveillance de l'espace disque du serveur
|
- [ ] Script de surveillance de l'espace disque du serveur
|
||||||
|
- [ ] Création de logs centralisés consultables et téléchargeables par l'Admin
|
||||||
|
|
||||||
### Phase 8 : Déploiement Public & Configuration
|
### Phase 8 : Déploiement Public & Configuration
|
||||||
- [ ] Assistant de configuration (Wizard) au premier lancement pour insérer les clés API, configurer le SMTP, etc.
|
- [ ] Assistant de configuration (Wizard) au premier lancement pour insérer les clés API, configurer le SMTP, etc.
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from google import genai
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# --- GESTION DE L'HISTORIQUE (JSON) ---
|
|
||||||
HISTORY_FILE = "aethas_history.json"
|
HISTORY_FILE = "aethas_history.json"
|
||||||
def load_history():
|
def load_history():
|
||||||
if not os.path.exists(HISTORY_FILE):
|
if not os.path.exists(HISTORY_FILE):
|
||||||
@@ -22,9 +21,7 @@ def load_history():
|
|||||||
def save_history(history):
|
def save_history(history):
|
||||||
with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, indent=4)
|
with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, indent=4)
|
||||||
|
|
||||||
# --- CSS & JAVASCRIPT (Thème, Markdown, ZIP et Copie) ---
|
|
||||||
theme_hdrs = (
|
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://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/highlight.js/11.9.0/highlight.min.js"),
|
||||||
Script(src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"),
|
Script(src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"),
|
||||||
@@ -34,7 +31,6 @@ theme_hdrs = (
|
|||||||
: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; }
|
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; }
|
.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 { 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); }
|
.new-chat-btn:hover { background: rgba(0, 229, 255, 0.1); }
|
||||||
@@ -43,29 +39,30 @@ theme_hdrs = (
|
|||||||
.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); }
|
||||||
|
|
||||||
/* CHAT AREA */
|
|
||||||
.main-content { flex: 1; display: flex; flex-direction: column; height: 100%; position: relative; }
|
.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;}
|
.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: 130px;}
|
||||||
|
|
||||||
/* 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);}
|
.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);}
|
.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 { 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 { 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);}
|
.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;}
|
.markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333;}
|
||||||
|
|
||||||
/* INPUT AREA */
|
/* GEMINI WEB STYLE INPUT BAR */
|
||||||
.input-area { position: absolute; bottom: 0; width: 100%; padding: 20px; background: linear-gradient(0deg, #121212 80%, transparent); display: flex; justify-content: center; }
|
.input-area { position: absolute; bottom: 0; width: 100%; padding: 20px; background: linear-gradient(0deg, #121212 85%, 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-wrapper { display: flex; align-items: flex-end; gap: 12px; width: 100%; max-width: 900px; background: #1e1e24; padding: 12px 15px; border-radius: 24px; border: 1px solid #444; }
|
||||||
.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 { background: transparent; border: none; color: white; resize: none; flex-grow: 1; font-size: 1em; max-height: 150px; overflow-y: auto; padding: 5px; font-family: inherit; }
|
||||||
.input-field:focus { outline: none; border-color: var(--accent-blue); }
|
.input-field:focus { outline: none; }
|
||||||
.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; }
|
|
||||||
|
.file-btn { background: #2a2a35; border: 1px solid #444; color: var(--accent-blue); width: 38px; height: 38px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.4em; transition: 0.2s; flex-shrink: 0; }
|
||||||
|
.file-btn:hover { background: rgba(0, 229, 255, 0.2); border-color: var(--accent-blue); }
|
||||||
|
|
||||||
|
.btn-submit { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; width: 38px; height: 38px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.2em; flex-shrink: 0; transition: transform 0.1s; }
|
||||||
|
.btn-submit:active { transform: scale(0.95); }
|
||||||
"""),
|
"""),
|
||||||
|
|
||||||
# Script Magique : Convertit le Markdown, ajoute les boutons Copier/Télécharger en ZIP
|
|
||||||
Script("""
|
Script("""
|
||||||
function renderMarkdown() {
|
function renderMarkdown() {
|
||||||
document.querySelectorAll('.markdown-raw').forEach(el => {
|
document.querySelectorAll('.markdown-raw').forEach(el => {
|
||||||
@@ -73,7 +70,6 @@ theme_hdrs = (
|
|||||||
container.innerHTML = marked.parse(el.textContent);
|
container.innerHTML = marked.parse(el.textContent);
|
||||||
el.classList.remove('markdown-raw');
|
el.classList.remove('markdown-raw');
|
||||||
|
|
||||||
// Ajout de la barre de code avec boutons
|
|
||||||
container.querySelectorAll('pre').forEach(pre => {
|
container.querySelectorAll('pre').forEach(pre => {
|
||||||
let code = pre.querySelector('code');
|
let code = pre.querySelector('code');
|
||||||
let lang = code.className.replace('language-', '') || 'texte';
|
let lang = code.className.replace('language-', '') || 'texte';
|
||||||
@@ -91,7 +87,6 @@ theme_hdrs = (
|
|||||||
hljs.highlightElement(code);
|
hljs.highlightElement(code);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Auto-scroll vers le bas
|
|
||||||
let chat = document.getElementById('chat-history');
|
let chat = document.getElementById('chat-history');
|
||||||
chat.scrollTop = chat.scrollHeight;
|
chat.scrollTop = chat.scrollHeight;
|
||||||
}
|
}
|
||||||
@@ -107,8 +102,6 @@ theme_hdrs = (
|
|||||||
let code = btn.parentElement.parentElement.nextElementSibling.querySelector('code').innerText;
|
let code = btn.parentElement.parentElement.nextElementSibling.querySelector('code').innerText;
|
||||||
let zip = new JSZip();
|
let zip = new JSZip();
|
||||||
let ext = lang === 'python' ? 'py' : lang === 'javascript' ? 'js' : lang === 'html' ? 'html' : 'txt';
|
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.file(`Aethas38_Code.${ext}`, code);
|
||||||
zip.generateAsync({type:"blob"}).then(function(content) {
|
zip.generateAsync({type:"blob"}).then(function(content) {
|
||||||
let link = document.createElement('a');
|
let link = document.createElement('a');
|
||||||
@@ -120,11 +113,10 @@ theme_hdrs = (
|
|||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Activation des sessions pour se souvenir du chat actuel
|
|
||||||
app, rt = fast_app(hdrs=theme_hdrs, secret_key="super_secret_aethas")
|
app, rt = fast_app(hdrs=theme_hdrs, secret_key="super_secret_aethas")
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# SECTION 2. CONNEXION AUX IA
|
# SECTION 2. CONNEXION AUX IA & CALCUL COÛTS
|
||||||
# ==========================================
|
# ==========================================
|
||||||
local_client = OpenAI(base_url="http://192.168.1.20:11434/v1", api_key="ollama")
|
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"))
|
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
||||||
@@ -144,12 +136,37 @@ def interroger_gemini_cascade(prompt):
|
|||||||
continue
|
continue
|
||||||
raise Exception("Tous les modèles Gemini sont indisponibles actuellement.")
|
raise Exception("Tous les modèles Gemini sont indisponibles actuellement.")
|
||||||
|
|
||||||
|
def calculer_cout_ia(fournisseur, prompt_tokens, completion_tokens):
|
||||||
|
if fournisseur == "GROQ":
|
||||||
|
input_cost_per_m, output_cost_per_m = 0.59, 0.79
|
||||||
|
elif fournisseur == "GOOGLE GEMINI":
|
||||||
|
input_cost_per_m, output_cost_per_m = 0.075, 0.30
|
||||||
|
else:
|
||||||
|
return 0.00
|
||||||
|
return round(((prompt_tokens * input_cost_per_m) + (completion_tokens * output_cost_per_m)) / 1_000_000, 6)
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# SECTION 3. INTERFACE UTILISATEUR & HISTORIQUE
|
# SECTION 3. INTERFACE UTILISATEUR & HISTORIQUE
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
def render_user_msg(text):
|
||||||
|
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):
|
||||||
|
encart_info = Div(
|
||||||
|
Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
||||||
|
Span(f"🏢 {nom_fournisseur}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
||||||
|
Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"),
|
||||||
|
style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;"
|
||||||
|
)
|
||||||
|
return Div(
|
||||||
|
encart_info,
|
||||||
|
Div(text, cls="markdown-raw", style="display:none;"),
|
||||||
|
Div(cls="markdown-rendered", style="line-height: 1.6;"),
|
||||||
|
cls="ai-bubble", style=f"border-left: 4px solid {couleur};"
|
||||||
|
)
|
||||||
|
|
||||||
def render_sidebar():
|
def render_sidebar():
|
||||||
history = load_history()
|
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]))
|
sorted_chats = sorted(history.items(), key=lambda x: (not x[1].get('pinned', False), x[0]))
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
@@ -158,50 +175,61 @@ def render_sidebar():
|
|||||||
items.append(
|
items.append(
|
||||||
Div(
|
Div(
|
||||||
Span(data['title'], hx_post=f"/load_chat/{chat_id}", hx_target="body", style="flex:1; cursor:pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"),
|
Span(data['title'], hx_post=f"/load_chat/{chat_id}", hx_target="body", 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="outerHTML"),
|
||||||
Button(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="innerHTML"),
|
Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"),
|
||||||
# Bouton Renommer
|
Button("🗑️", cls="history-btn", hx_post=f"/delete/{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="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}"
|
cls="history-item", id=f"chat-item-{chat_id}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return items
|
return Div(*items, id="sidebar-content", style="flex:1; overflow-y:auto;", hx_swap_oob="true")
|
||||||
|
|
||||||
@rt('/')
|
@rt('/')
|
||||||
def get(session):
|
def get(session):
|
||||||
# Démarrer une nouvelle session de chat si aucune n'existe
|
|
||||||
if 'current_chat_id' not in session:
|
if 'current_chat_id' not in session:
|
||||||
session['current_chat_id'] = str(uuid.uuid4())
|
session['current_chat_id'] = str(uuid.uuid4())
|
||||||
|
|
||||||
|
chat_id = session['current_chat_id']
|
||||||
|
history = load_history()
|
||||||
|
|
||||||
|
messages_ui = []
|
||||||
|
if chat_id in history:
|
||||||
|
for msg in history[chat_id].get('messages', []):
|
||||||
|
if msg['role'] == 'user':
|
||||||
|
messages_ui.append(render_user_msg(msg['content']))
|
||||||
|
else:
|
||||||
|
messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333')))
|
||||||
|
|
||||||
|
if messages_ui:
|
||||||
|
messages_ui.append(Script("setTimeout(renderMarkdown, 100);"))
|
||||||
|
|
||||||
return Title("Aethas38 - Mine_AI"), Body(
|
return Title("Aethas38 - Mine_AI"), Body(
|
||||||
# --- COLONNE GAUCHE (Historique) ---
|
|
||||||
Div(
|
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"),
|
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"),
|
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;"),
|
render_sidebar(),
|
||||||
Div(P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; text-align:center;"), style="border-top: 1px solid #333; padding-top: 15px;"),
|
Div(P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; text-align:center;"), style="border-top: 1px solid #333; padding-top: 15px;"),
|
||||||
cls="sidebar"
|
cls="sidebar"
|
||||||
),
|
),
|
||||||
|
|
||||||
# --- COLONNE DROITE (Chat) ---
|
|
||||||
Div(
|
Div(
|
||||||
# Zone d'affichage des messages (Append 'beforeend' ici)
|
Div(*messages_ui, id="chat-history", cls="chat-container"),
|
||||||
Div(id="chat-history", cls="chat-container"),
|
|
||||||
|
|
||||||
# Zone de saisie
|
|
||||||
Div(
|
Div(
|
||||||
Form(
|
Form(
|
||||||
Div(
|
Div(
|
||||||
Input(type="text", name="prompt", id="prompt-input", placeholder="Entrez votre requête ou commande...", cls="input-field"),
|
# Bouton + (Label déclenchant l'input file caché supportant le drag & drop et jusqu'à 10 fichiers)
|
||||||
Button("Exécuter", type="submit", cls="btn-submit"),
|
Label("+", _for="file-upload", cls="file-btn", title="Importer des fichiers (max 10)"),
|
||||||
|
Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;"),
|
||||||
|
# Textarea Gemini Web style avec ascenseur automatique
|
||||||
|
Textarea(name="prompt", id="prompt-input", placeholder="Entrez votre requête ou déposez vos fichiers ici...", cls="input-field", rows="1"),
|
||||||
|
# Bouton Flèche de validation
|
||||||
|
Button("↑", type="submit", cls="btn-submit", title="Exécuter"),
|
||||||
cls="input-wrapper"
|
cls="input-wrapper"
|
||||||
),
|
),
|
||||||
hx_post="/chat",
|
hx_post="/chat",
|
||||||
hx_target="#chat-history",
|
hx_target="#chat-history",
|
||||||
hx_swap="beforeend", # AJOUTE à la suite au lieu de remplacer !
|
hx_swap="beforeend",
|
||||||
hx_on__after_request="this.reset()", # Vide le champ de texte
|
hx_on__after_request="this.reset()",
|
||||||
|
enctype="multipart/form-data",
|
||||||
style="width: 100%; display: flex; justify-content: center;"
|
style="width: 100%; display: flex; justify-content: center;"
|
||||||
),
|
),
|
||||||
cls="input-area"
|
cls="input-area"
|
||||||
@@ -211,87 +239,106 @@ def get(session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# SECTION 4. LE MOTEUR DE ROUTAGE (L'Aiguilleur)
|
# SECTION 4. LE MOTEUR DE ROUTAGE & UPLOAD MULTI-FICHIERS
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@rt('/chat')
|
@rt('/chat')
|
||||||
def post(prompt: str, session: dict):
|
async def post(request, session: dict):
|
||||||
try:
|
try:
|
||||||
# 1. Gestion du Titre dans l'historique
|
form = await request.form()
|
||||||
|
prompt = form.get("prompt", "")
|
||||||
|
files = form.getlist("files") # Récupération de la liste des fichiers (jusqu'à 10)
|
||||||
|
|
||||||
chat_id = session.get('current_chat_id')
|
chat_id = session.get('current_chat_id')
|
||||||
history = load_history()
|
history = load_history()
|
||||||
|
|
||||||
if chat_id not in history:
|
if chat_id not in history:
|
||||||
history[chat_id] = {"title": prompt[:30] + "...", "pinned": False, "messages": []}
|
history[chat_id] = {"title": (prompt[:30] if prompt else "Fichiers joints") + "...", "pinned": False, "messages": []}
|
||||||
save_history(history)
|
|
||||||
|
|
||||||
# 2. Création de la bulle VISUELLE Utilisateur (Alignée à droite)
|
file_contents_summary = []
|
||||||
user_bubble = Div(P(prompt, style="margin:0; font-weight:bold;"), cls="user-bubble")
|
file_names = []
|
||||||
|
|
||||||
# 3. Analyse de l'Aiguilleur
|
if files:
|
||||||
|
for file in files[:10]: # Limitation stricte à 10 fichiers
|
||||||
|
if file and file.filename:
|
||||||
|
file_names.append(file.filename)
|
||||||
|
content_bytes = await file.read()
|
||||||
|
try:
|
||||||
|
text_content = content_bytes.decode('utf-8', errors='ignore')
|
||||||
|
except Exception:
|
||||||
|
text_content = "[Fichier binaire ou non textuel]"
|
||||||
|
file_contents_summary.append(f"\n--- Fichier: {file.filename} ---\n{text_content}")
|
||||||
|
|
||||||
|
full_prompt = prompt if prompt else f"Analyse ces fichiers : {', '.join(file_names)}"
|
||||||
|
if file_contents_summary:
|
||||||
|
full_prompt += "\n".join(file_contents_summary)
|
||||||
|
|
||||||
|
# Analyse par l'Aiguilleur
|
||||||
router_prompt = """Tu es le Routeur. Choisis : "groq" (code/maths), "gemini" (création/rédaction), "local" (salutations).
|
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."""
|
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)
|
router_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": router_prompt}, {"role": "user", "content": full_prompt}], temperature=0.1)
|
||||||
|
|
||||||
raw_decision = router_response.choices[0].message.content
|
raw_decision = router_response.choices[0].message.content
|
||||||
start_idx = raw_decision.find('{')
|
start_idx = raw_decision.find('{')
|
||||||
end_idx = raw_decision.rfind('}') + 1
|
end_idx = raw_decision.rfind('}') + 1
|
||||||
|
|
||||||
decision = json.loads(raw_decision[start_idx:end_idx]) if start_idx != -1 and end_idx != 0 else {"intention": "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()
|
fournisseur = decision.get("fournisseur", "local").lower()
|
||||||
|
|
||||||
# 4. Requête IA
|
# Requête IA & Tokens
|
||||||
|
prompt_tokens = len(full_prompt.split()) * 2
|
||||||
|
completion_tokens = 0
|
||||||
|
|
||||||
if fournisseur == "groq":
|
if fournisseur == "groq":
|
||||||
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")
|
ia_response = groq_client.chat.completions.create(messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": full_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"
|
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b"
|
||||||
|
if hasattr(ia_response, 'usage') and ia_response.usage:
|
||||||
|
prompt_tokens = ia_response.usage.prompt_tokens
|
||||||
|
completion_tokens = ia_response.usage.completion_tokens
|
||||||
elif fournisseur == "gemini":
|
elif fournisseur == "gemini":
|
||||||
reponse_finale, nom_modele = interroger_gemini_cascade(prompt)
|
reponse_finale, nom_modele = interroger_gemini_cascade(full_prompt)
|
||||||
couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI"
|
couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI"
|
||||||
|
completion_tokens = len(reponse_finale.split()) * 2
|
||||||
else:
|
else:
|
||||||
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}])
|
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": full_prompt}])
|
||||||
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b"
|
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b"
|
||||||
|
if hasattr(ia_response, 'usage') and ia_response.usage:
|
||||||
|
prompt_tokens = ia_response.usage.prompt_tokens
|
||||||
|
completion_tokens = ia_response.usage.completion_tokens
|
||||||
|
|
||||||
# 5. Création de la bulle VISUELLE IA (Alignée à gauche avec Encart)
|
# Sauvegarde en JSON
|
||||||
encart_info = Div(
|
display_msg = f"{prompt}" + (f"\n📎 [Fichiers: {', '.join(file_names)}]" if file_names else "")
|
||||||
Span(f"🎯 {decision.get('intention', '')}", style="margin-right: 15px; color:#aaa; font-size:0.85em;"),
|
history[chat_id]['messages'].append({"role": "user", "content": display_msg})
|
||||||
Span(f"🏢 {nom_fournisseur}", style="margin-right: 15px; color:#aaa; font-size:0.85em;"),
|
history[chat_id]['messages'].append({"role": "ai", "content": reponse_finale, "intention": decision.get('intention', ''), "fournisseur": nom_fournisseur, "modele": nom_modele, "couleur": couleur})
|
||||||
Span(f"🧠 {nom_modele}", style=f"color:{couleur}; font-weight:bold; font-size:0.85em;"),
|
save_history(history)
|
||||||
style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px;"
|
|
||||||
)
|
|
||||||
|
|
||||||
ai_bubble = Div(
|
user_bubble = render_user_msg(display_msg)
|
||||||
encart_info,
|
ai_bubble = render_ai_msg(reponse_finale, decision.get('intention', ''), nom_fournisseur, nom_modele, couleur)
|
||||||
# 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};"
|
|
||||||
)
|
|
||||||
|
|
||||||
# On retourne l'ensemble (User + IA) et on déclenche le rendu Markdown JS
|
return user_bubble, ai_bubble, Script("renderMarkdown();"), render_sidebar()
|
||||||
return user_bubble, ai_bubble, Script("renderMarkdown();"), Div(*render_sidebar(), id="sidebar-content", hx_swap_oob="true")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
|
return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# SECTION 5. ROUTES DE L'HISTORIQUE (Renommer, Épingler, Supprimer)
|
# SECTION 5. ROUTES DE L'HISTORIQUE & NAVIGATION (RedirectResponse FIX)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@rt('/new_chat')
|
@rt('/new_chat')
|
||||||
def post(session):
|
def post(session):
|
||||||
session['current_chat_id'] = str(uuid.uuid4())
|
session['current_chat_id'] = str(uuid.uuid4())
|
||||||
return Script("window.location.reload();") # Recharge la page à zéro
|
return RedirectResponse('/', status_code=303)
|
||||||
|
|
||||||
|
@rt('/load_chat/{chat_id}')
|
||||||
|
def post(chat_id: str, session):
|
||||||
|
session['current_chat_id'] = chat_id
|
||||||
|
return RedirectResponse('/', status_code=303)
|
||||||
|
|
||||||
@rt('/rename/{chat_id}')
|
@rt('/rename/{chat_id}')
|
||||||
def post(chat_id: str, req):
|
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")
|
nouveau_nom = req.headers.get("HX-Prompt")
|
||||||
|
|
||||||
if nouveau_nom:
|
if nouveau_nom:
|
||||||
history = load_history()
|
history = load_history()
|
||||||
if chat_id in history:
|
if chat_id in history:
|
||||||
history[chat_id]['title'] = nouveau_nom
|
history[chat_id]['title'] = nouveau_nom
|
||||||
save_history(history)
|
save_history(history)
|
||||||
|
|
||||||
return render_sidebar()
|
return render_sidebar()
|
||||||
|
|
||||||
@rt('/pin/{chat_id}')
|
@rt('/pin/{chat_id}')
|
||||||
@@ -312,8 +359,3 @@ def post(chat_id: str):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
serve(port=5001)
|
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
|
|
||||||
Reference in new issue
Block a user