Files
aethas-free/modules/ui_components.py
T
xavier a2261ab8dd
Build and Push Docker Image / build-and-push (push) Successful in 37s
MAJ Phase 10 : Support KubeJS, bouton RAG UI et Modale d'upload admin
2026-08-29 07:38:55 +02:00

290 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import requests
from fasthtml.common import *
from modules.database import load_history
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):
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, trace=None, prompt_associe="", thematique="general", msg_id=""):
time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else ""
trace_html = ""
if trace and isinstance(trace, list):
badges = [Span(t, style="font-size:0.75em; background:#222; padding:2px 6px; border-radius:4px; border:1px solid #444;") for t in trace]
trace_html = Div(Span("Route : ", style="color:#888; font-size:0.75em;"), *badges, style="display:flex; gap:5px; align-items:center; margin-bottom:8px; flex-wrap:wrap;")
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;"),
time_badge, style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 5px;"
)
bouton_forcer = ""
if msg_id and prompt_associe and thematique != "aucun":
safe_prompt = prompt_associe.replace('"', '\\"').replace('\n', ' ')
safe_reponse = text.replace('"', '\\"').replace('\n', ' ')
valeurs_htmx = f'{{"prompt": "{safe_prompt}", "reponse": "{safe_reponse}", "theme": "{thematique}"}}'
bouton_forcer = Button("💾 Mémoriser",
hx_post="/api/forcer_rag",
hx_vals=valeurs_htmx,
hx_target=f"#badge-rag-{msg_id}",
hx_swap="outerHTML",
id=f"badge-rag-{msg_id}",
cls="msg-action-btn",
style="color: #4caf50; border-color: #4caf50;"
)
actions = Div(
bouton_forcer,
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("📝 TXT", onclick="downloadResponseTXT(this)", cls="msg-action-btn", title="Télécharger en Texte"),
Button("🗄️ JSON", onclick="downloadResponseJSON(this)", cls="msg-action-btn", title="Télécharger en JSON"),
Button("💬 WhatsApp", onclick="shareWhatsApp(this)", cls="msg-action-btn", title="Partager sur WhatsApp"),
Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par courriel"),
cls="msg-actions"
)
return Div(
trace_html,
encart_info,
Div(text, cls="markdown-raw", style="display:none;"),
Div(cls="markdown-rendered", style="line-height: 1.6;"),
actions,
cls="ai-bubble",
style=f"border-left: 4px solid {couleur};"
)
def render_sidebar():
history = load_history()
sorted_chats = sorted(history.items(), key=lambda x: (not x[1].get('pinned', False), -x[1].get('timestamp', 0)))
items = []
for chat_id, data in sorted_chats:
pin_icon = "📌" if data.get('pinned', False) else "📍"
items.append(Div(A(Span(data['title'], style="color:inherit;"), href=f"/load_chat/{chat_id}", cls="history-link"), Button(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"), Button("🗑️", cls="history-btn", hx_post=f"/delete/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), cls="history-item", id=f"chat-item-{chat_id}"))
return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true")
def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
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'),
msg.get('execution_time', ''),
msg.get('trace', []),
msg.get('prompt_associe', ''),
msg.get('thematique', 'general'),
msg.get('msg_id', '')
))
if messages_ui: messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
auto_refresh_script = Script('''
let serverDown = false;
setInterval(() => {
fetch('/ping', {cache: 'no-store'})
.then(response => { if (serverDown && response.ok) { window.location.reload(); } })
.catch(() => { serverDown = true; console.log("Serveur hors ligne..."); });
}, 5000);
function checkMediaPaymentWarning() {
const mediaMode = document.querySelector('input[name="media_mode"]:checked').value;
const isPayant = document.getElementById('mode-payant-toggle').checked;
const warningDiv = document.getElementById('media-warning');
if (mediaMode === 'video' && !isPayant) {
warningDiv.style.display = 'block';
warningDiv.innerHTML = '⚠️ La génération vidéo gratuite est très limitée ou inexistante. Cochez "Payant" pour de meilleurs résultats.';
} else if (mediaMode === 'image' && !isPayant) {
warningDiv.style.display = 'block';
warningDiv.innerHTML = 'ℹ️ Mode image gratuit : Qualité standard (HuggingFace). Pour la HD, cochez "Payant".';
} else {
warningDiv.style.display = 'none';
}
}
''')
solde_perso = obtenir_solde(utilisateur_nom)
if role.lower() in ["super admin", "admin"]:
stats_or = get_openrouter_stats()
limit_rem = float(stats_or.get('limit_remaining') or 0.0)
usage_daily = float(stats_or.get('usage_daily') or 0.0)
usage_monthly = float(stats_or.get('usage_monthly') or 0.0)
badge_finance = Div(
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: #00e5ff; margin: 2px 0; font-size: 0.85em; font-weight: bold;"),
P(f"🏦 Cagnotte globale : {obtenir_solde('xavier'):.2f} $", style="color: #4caf50; margin: 0; font-size: 0.8em; font-weight: bold;"),
P(f"🌐 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;"),
style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;"
)
else:
badge_finance = Div(P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #fff; margin: 0; font-weight: bold; font-size: 0.9em;"), P(f"💳 Solde disponible : {solde_perso:.2f} $", style="color: #00e5ff; margin: 2px 0 0; font-size: 0.85em;"), style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;")
return Title("Aethas38 - Core"), Body(
auto_refresh_script,
Div(
Div(
Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"),
Div(
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn"),
Button("📚 Gestion RAG", hx_get="/admin/rag_modal", hx_target="body", hx_swap="beforeend", cls="export-btn", style="background-color: #2196f3; color: white; border-color: #2196f3; margin-top: 5px; cursor: pointer;") if role.lower() in ["super admin", "admin"] else "",
A("🚪 Déconnexion", href="/logout", cls="export-btn", style="background-color: #8b0000; color: white; border-color: #8b0000; margin-top: 5px;"),
cls="nav-buttons", style="display: flex; flex-direction: column; gap: 10px;"
), cls="sidebar-top"
),
render_sidebar(), Div(badge_finance, cls="sidebar-footer"), cls="sidebar"
),
Div(
Div(Button("☰", onclick="toggleSidebar()", cls="hamburger-btn"), Img(src="/Logo/logo_aethas.svg", style="height: 30px;"), Div(style="width: 30px;"), cls="mobile-header"),
Div(*messages_ui, id="chat-history", cls="chat-container"),
Div(Div(Div(cls="tech-spinner"), Span("Analyse en cours... (~5s à 10s)"), cls="tech-loader-container"), id="global-loader", cls="htmx-indicator"),
Div(
Form(
Input(type="hidden", name="chat_id", value=chat_id),
Div(
Div(
Label(Input(type="radio", name="media_mode", value="texte", checked=True, onchange="checkMediaPaymentWarning()"), "📝 Texte", cls="media-btn"),
Label(Input(type="radio", name="media_mode", value="image", onchange="checkMediaPaymentWarning()"), "🎨 Image", cls="media-btn"),
Label(Input(type="radio", name="media_mode", value="video", onchange="checkMediaPaymentWarning()"), "🎬 Vidéo", cls="media-btn"),
style="display: flex; gap: 15px; justify-content: center; margin-bottom: 10px;"
),
Div(id="media-warning", style="display:none; color: #ff9800; font-size: 0.8em; text-align: center; margin-bottom: 10px;"),
Div(id="file-preview-list"),
Div(
Label("+", _for="file-upload", cls="file-btn"),
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"
),
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(Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;", onchange="checkMediaPaymentWarning()"), "👑 Payant", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"),
Label(Input(type="checkbox", name="mode_multi_ia", id="mode-multi-toggle", style="margin-right: 6px; accent-color: #d97757; cursor: pointer;"), "⚡ Multi-IA", style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; cursor: pointer; white-space: nowrap;"),
cls="options-row"
), cls="input-container-box"
), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='36px'; checkMediaPaymentWarning();", enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;"
), cls="input-area"
), cls="main-content"
)
)
def render_admin_moderation_panel(utilisateur_cible):
return Div(
H3(f"⚖️ Tribunal d'administration - Dossier de : {utilisateur_cible}", style="color: #ff4444;"),
P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la législation en vigueur."),
Form(
Input(type="hidden", name="cible", value=utilisateur_cible),
Div(Label("Action décidée :", style="font-weight: bold;"), Select(Option("Avertissement simple", value="Avertissement"), Option("Bannissement 1 jour", value="Ban 1J"), Option("Bannissement définitif et signalement PHAROS", value="Ban Definitif + Pharos"), name="decision", cls="input-field"), style="margin-bottom: 15px;"),
Div(Label("Commentaire (Preuve d'audit) :", style="font-weight: bold;"), Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison..."), style="margin-bottom: 15px;"),
Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white; width: 100%;"), hx_post="/admin_decision", hx_target="#admin-result", style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;"
), Div(id="admin-result", style="margin-top: 15px; color: #00e5ff;"), cls="admin-panel"
)
def render_login_page(error=None):
error_msg = P(error, style="color: #ff4444; text-align: center; font-weight: bold;") if error else ""
return Title("Connexion - Aethas38"), Body(
Div(
Img(src="/Logo/logo_aethas.svg", style="max-width: 180px; margin: 0 auto 20px; display: block;"),
H2("Accès Restreint", style="text-align: center; color: #00e5ff;"),
error_msg,
Form(
Div(Label("Email :", style="color: #aaa; font-size: 0.9em;"), Input(type="email", name="email", placeholder="admin@mail.aethas38.duckdns.org", cls="input-field", required=True), style="margin-bottom: 15px;"),
Div(Label("Mot de passe :", style="color: #aaa; font-size: 0.9em;"), Input(type="password", name="password", placeholder="••••••••", cls="input-field", required=True), style="margin-bottom: 20px;"),
Button("Déverrouiller le système", type="submit", cls="btn-submit", style="width: 100%;"), method="post", action="/login",
), style="max-width: 400px; margin: 100px auto; padding: 30px; background: #1a1a1a; border-radius: 10px; border: 1px solid #333; box-shadow: 0 4px 15px rgba(0,229,255,0.1);"
), style="background-color: #0d0d0d; color: #fff; font-family: monospace; height: 100vh; display: flex; align-items: center; justify-content: center;"
)
def render_popup_paiement(cout_estime, prompt_sauvegarde, chat_id, multi_ia_state=False, edu_state=False):
edu_val = "on" if edu_state else "off"
return Div(
Div(
H3("⚠️ Crédits insuffisants", style="color: #ff4444; margin-top: 0;"),
P("Cette requête nécessite un modèle d'IA dont le coût dépasse votre solde actuel.", style="color: #ccc;"),
P(f"Coût estimé de la requête : {cout_estime} $", style="font-size: 1.1em; font-weight: bold; color: #fff; background: #333; padding: 10px; border-radius: 5px; text-align: center;"),
Div(
Button("Annuler la requête", type="button", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #555; width: 48%; font-size: 0.85em; border-radius: 8px;"),
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="off"),
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 (Local)", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #00e5ff; width: 100%; color: black; font-size: 0.85em; border-radius: 8px;"),
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 48%;"
),
style="display: flex; justify-content: space-between; margin-top: 20px; gap: 10px;"
),
style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #ff4444; width: 450px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
),
id="popup-paiement",
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 9999;"
)
def render_rag_upload_modal():
"""Génère la fenêtre pop-up pour téléverser des documents dans le RAG."""
themes = [
"Minecraft_KubeJS", "Juridique_Loi_Francaise", "Serveur_Debian",
"Bureautique_Excel", "Regles_Aethas38", "Education_Nationale", "Connaissances_Generales"
]
options = [Option(t.replace("_", " "), value=t) for t in themes]
return Div(
Div(
H3("📚 Administration RAG (Base Vectorielle)", style="color: #2196f3; margin-top: 0;"),
P("Formats supportés : PDF, MD, TXT, JS, JSON, YML, CSV", style="color: #ccc; font-size: 0.9em;"),
Form(
Div(
Label("1. Choisir la thématique :", style="font-weight: bold; color: white;"),
Select(*options, name="theme", cls="input-field", style="margin-bottom: 15px;")
),
Div(
Label("2. Sélectionner les fichiers :", style="font-weight: bold; color: white;"),
Input(type="file", name="fichiers_rag", multiple=True, required=True, cls="input-field", style="margin-bottom: 15px; background: #222; padding: 10px; border: 1px dashed #2196f3; cursor: pointer;")
),
Div(
Button("Annuler", type="button", onclick="document.getElementById('rag-modal').remove();", cls="btn-submit", style="background-color: #555; width: 48%;"),
Button("Téléverser & Indexer", type="submit", cls="btn-submit", style="background-color: #2196f3; color: white; width: 48%;"),
style="display: flex; justify-content: space-between;"
),
hx_post="/api/upload_rag",
hx_target="#rag-upload-result",
enctype="multipart/form-data"
),
Div(id="rag-upload-result", style="margin-top: 15px; text-align: center; font-weight: bold;"),
style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #2196f3; width: 500px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
),
id="rag-modal",
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 9999;"
)