Files
aethas-free/modules/ui_components.py
T
xavier f17ef384f6
Build and Push Docker Image / build-and-push (push) Successful in 36s
Correction fonction RAG auto incrémentation
2026-08-28 20:29:06 +02:00

255 lines
18 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")
# --- LA FONCTION CORRIGÉE AVEC SES 10 ARGUMENTS ---
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 RAG (HTMX) ---
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);
// Script pour gérer l'avertissement de gratuité sur les médias
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"),
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;"
)