Public Access
maj auto 1
This commit is contained in:
1 parent
a797816727
commit
b9bf7979ba
1 file changed
+217
-81
@@ -1,6 +1,10 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
@@ -21,45 +25,67 @@ 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)
|
||||||
|
|
||||||
|
# SVG Animé (Rotation 45° + Scintillement Pulsé) injecté en Base64
|
||||||
|
svg_favicon = "%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cstyle%3E@keyframes pulse { 0% { opacity: 1; stroke-width: 5; } 50% { opacity: 0.5; stroke-width: 7; } 100% { opacity: 1; stroke-width: 5; } } .neon { animation: pulse 2s infinite; transform-origin: 50px 50px; transform: rotate(45deg); }%3C/style%3E%3Crect width='100' height='100' fill='%23121212'/%3E%3Cg class='neon'%3E%3Crect x='25' y='25' width='50' height='50' fill='none' stroke='%2300e5ff'/%3E%3Cpath d='M25 50 L75 50 M50 25 L50 75' stroke='%23b300ff'/%3E%3Ccircle cx='50' cy='50' r='8' fill='%23e0e0e0'/%3E%3C/g%3E%3C/svg%3E"
|
||||||
|
|
||||||
theme_hdrs = (
|
theme_hdrs = (
|
||||||
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/html2pdf.js/0.10.1/html2pdf.bundle.min.js"),
|
||||||
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"),
|
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"),
|
||||||
|
Link(rel="icon", type="image/svg+xml", href=f"data:image/svg+xml;utf8,{svg_favicon}"),
|
||||||
|
|
||||||
Style("""
|
Style("""
|
||||||
:root { --bg-dark: #121212; --bg-panel: #1e1e24; --accent-blue: #00e5ff; --accent-purple: #b300ff; --text-main: #e0e0e0; }
|
:root { --bg-dark: #121212; --bg-panel: #1e1e24; --accent-blue: #00e5ff; --accent-purple: #b300ff; --text-main: #e0e0e0; }
|
||||||
body { background-color: var(--bg-dark); color: var(--text-main); font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; display: flex; height: 100vh; overflow: hidden; }
|
body { background-color: var(--bg-dark); color: var(--text-main); font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; display: flex; height: 100vh; overflow: hidden; box-sizing: border-box; }
|
||||||
|
|
||||||
.sidebar { width: 280px; background-color: #0a0a0c; border-right: 1px solid #333; padding: 20px; display: flex; flex-direction: column; }
|
.sidebar { width: 280px; background-color: #0a0a0c; border-right: 1px solid #333; padding: 20px; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; }
|
||||||
.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;}
|
.sidebar-top { flex-shrink: 0; }
|
||||||
|
.sidebar-history { flex: 1; overflow-y: auto; margin-top: 15px; margin-bottom: 15px; }
|
||||||
|
.sidebar-footer { flex-shrink: 0; border-top: 1px solid #333; padding-top: 15px; text-align: center; }
|
||||||
|
|
||||||
|
.nav-buttons { display: flex; gap: 10px; margin-bottom: 20px; }
|
||||||
|
.new-chat-btn { display:block; text-align:center; text-decoration:none; background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; flex: 2; transition: 0.2s; }
|
||||||
.new-chat-btn:hover { background: rgba(0, 229, 255, 0.1); }
|
.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;}
|
.export-btn { background: #2a2a35; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; cursor: pointer; flex: 1; transition: 0.2s; font-size: 1.1em;}
|
||||||
|
.export-btn:hover { background: #444; }
|
||||||
|
|
||||||
|
.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-item:hover { background: #1a1a20; }
|
||||||
.history-btn { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em;}
|
.history-link { flex: 1; text-decoration: none; color: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; padding-right: 10px; }
|
||||||
|
.history-btn { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em; }
|
||||||
.history-btn:hover { color: var(--accent-blue); }
|
.history-btn:hover { color: var(--accent-blue); }
|
||||||
|
|
||||||
.main-content { flex: 1; display: flex; flex-direction: column; height: 100%; position: relative; }
|
.main-content { flex: 1; display: flex; flex-direction: column; height: 100vh; position: relative; box-sizing: border-box; }
|
||||||
.chat-container { flex: 1; padding: 20px; max-width: 1000px; margin: 0 auto; width: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; padding-bottom: 130px;}
|
.chat-container { flex: 1; padding: 20px; max-width: 1000px; margin: 0 auto; width: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; padding-bottom: 140px; box-sizing: border-box; }
|
||||||
|
|
||||||
.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); }
|
||||||
|
|
||||||
.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;}
|
.msg-actions { display: flex; gap: 10px; margin-top: 15px; border-top: 1px dashed #333; padding-top: 10px; justify-content: flex-end; }
|
||||||
.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;}
|
.msg-action-btn { background: transparent; border: 1px solid #444; color: #888; border-radius: 15px; padding: 5px 12px; cursor: pointer; font-size: 0.85em; transition: 0.2s; display: flex; align-items: center; gap: 5px;}
|
||||||
.code-header button:hover { background: var(--accent-blue); color: black; border-color: var(--accent-blue);}
|
.msg-action-btn:hover { background: #2a2a35; color: var(--accent-blue); border-color: var(--accent-blue); }
|
||||||
.markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333;}
|
|
||||||
|
|
||||||
/* GEMINI WEB STYLE INPUT BAR */
|
.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; }
|
||||||
.input-area { position: absolute; bottom: 0; width: 100%; padding: 20px; background: linear-gradient(0deg, #121212 85%, transparent); display: flex; justify-content: center; }
|
.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; }
|
||||||
.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; }
|
.code-header button:hover { background: var(--accent-blue); color: black; border-color: var(--accent-blue); }
|
||||||
.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; }
|
.markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333; }
|
||||||
|
|
||||||
|
/* ZONE DE SAISIE GEMINI WEB STYLE */
|
||||||
|
.input-area { position: absolute; bottom: 0; width: 100%; padding: 20px; background: linear-gradient(0deg, #121212 85%, transparent); display: flex; justify-content: center; box-sizing: border-box; }
|
||||||
|
.input-container-box { width: 100%; max-width: 900px; background: #1e1e24; border-radius: 24px; border: 1px solid #444; padding: 10px 15px; box-sizing: border-box; display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|
||||||
|
#file-preview-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 60px; overflow-y: auto; }
|
||||||
|
.file-pill { background: #2a2a35; border: 1px solid #555; color: var(--accent-blue); padding: 4px 10px; border-radius: 12px; font-size: 0.8em; display: flex; align-items: center; gap: 6px; }
|
||||||
|
|
||||||
|
.input-row { display: flex; align-items: flex-end; gap: 10px; width: 100%; }
|
||||||
|
.input-field { background: transparent; border: none; color: white; resize: none; flex: 1; font-size: 1em; max-height: 120px; overflow-y: auto; padding: 8px; font-family: inherit; box-sizing: border-box; }
|
||||||
.input-field:focus { outline: none; }
|
.input-field:focus { outline: none; }
|
||||||
|
|
||||||
.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 { background: #2a2a35; border: 1px solid #444; color: var(--accent-blue); width: 36px !important; height: 36px !important; min-width: 36px !important; max-width: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.3em; flex-shrink: 0 !important; box-sizing: border-box; text-decoration: none; }
|
||||||
.file-btn:hover { background: rgba(0, 229, 255, 0.2); border-color: var(--accent-blue); }
|
.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 { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; width: 36px !important; height: 36px !important; min-width: 36px !important; max-width: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.1em; flex-shrink: 0 !important; box-sizing: border-box; }
|
||||||
.btn-submit:active { transform: scale(0.95); }
|
.btn-submit:active { transform: scale(0.95); }
|
||||||
"""),
|
"""),
|
||||||
|
|
||||||
@@ -73,16 +99,9 @@ theme_hdrs = (
|
|||||||
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';
|
||||||
|
|
||||||
let header = document.createElement('div');
|
let header = document.createElement('div');
|
||||||
header.className = 'code-header';
|
header.className = 'code-header';
|
||||||
header.innerHTML = `
|
header.innerHTML = `<span style="color:#00e5ff; font-weight:bold;">${lang.toUpperCase()}</span><div><button onclick="copyCode(this)">📋 Copier</button></div>`;
|
||||||
<span style="color:#00e5ff; font-weight:bold;">${lang.toUpperCase()}</span>
|
|
||||||
<div>
|
|
||||||
<button onclick="copyCode(this)">📋 Copier</button>
|
|
||||||
<button onclick="downloadCode(this, '${lang}')">⬇️ Télécharger ZIP</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
pre.parentNode.insertBefore(header, pre);
|
pre.parentNode.insertBefore(header, pre);
|
||||||
hljs.highlightElement(code);
|
hljs.highlightElement(code);
|
||||||
});
|
});
|
||||||
@@ -91,24 +110,61 @@ theme_hdrs = (
|
|||||||
chat.scrollTop = chat.scrollHeight;
|
chat.scrollTop = chat.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FONCTIONS DE GÉNÉRATION LOCALE SUR LE PC CLIENT (0 Mo RAM SERVEUR) */
|
||||||
|
function downloadResponsePDF(btn) {
|
||||||
|
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
|
||||||
|
let opt = { margin: 10, filename: 'Aethas38_Reponse.pdf', image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, backgroundColor: '#1e1e24' }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } };
|
||||||
|
let originalColor = content.style.color;
|
||||||
|
content.style.color = '#ffffff';
|
||||||
|
html2pdf().set(opt).from(content).save().then(() => { content.style.color = originalColor; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadResponseTXT(btn) {
|
||||||
|
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
|
||||||
|
let blob = new Blob([content.innerText], {type: 'text/plain;charset=utf-8'});
|
||||||
|
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.txt';
|
||||||
|
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadResponseDOC(btn) {
|
||||||
|
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML;
|
||||||
|
let preHtml = "<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'><head><meta charset='utf-8'><title>Aethas Export</title></head><body style='font-family: Arial, sans-serif;'>";
|
||||||
|
let postHtml = "</body></html>";
|
||||||
|
let blob = new Blob(['\\ufeff', preHtml + content + postHtml], { type: 'application/msword' });
|
||||||
|
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.doc';
|
||||||
|
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
function copyCode(btn) {
|
function copyCode(btn) {
|
||||||
let code = btn.parentElement.parentElement.nextElementSibling.querySelector('code').innerText;
|
let pre = btn.closest('.code-header').nextElementSibling;
|
||||||
|
let code = pre.querySelector('code').innerText;
|
||||||
navigator.clipboard.writeText(code);
|
navigator.clipboard.writeText(code);
|
||||||
btn.innerText = "✅ Copié !";
|
btn.innerText = "✅ Copié !";
|
||||||
setTimeout(() => btn.innerText = "📋 Copier", 2000);
|
setTimeout(() => btn.innerText = "📋 Copier", 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadCode(btn, lang) {
|
function exportChat() {
|
||||||
let code = btn.parentElement.parentElement.nextElementSibling.querySelector('code').innerText;
|
let chat = document.getElementById('chat-history');
|
||||||
let zip = new JSZip();
|
if(!chat) return;
|
||||||
let ext = lang === 'python' ? 'py' : lang === 'javascript' ? 'js' : lang === 'html' ? 'html' : 'txt';
|
let blob = new Blob([chat.innerText], {type: 'text/plain;charset=utf-8'});
|
||||||
zip.file(`Aethas38_Code.${ext}`, code);
|
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Discussion_Globale.txt';
|
||||||
zip.generateAsync({type:"blob"}).then(function(content) {
|
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
||||||
let link = document.createElement('a');
|
}
|
||||||
link.href = URL.createObjectURL(content);
|
|
||||||
link.download = `Aethas38_Export_${lang}.zip`;
|
function handleFileSelection(input) {
|
||||||
link.click();
|
let previewList = document.getElementById('file-preview-list');
|
||||||
});
|
previewList.innerHTML = '';
|
||||||
|
if (input.files) {
|
||||||
|
for (let i = 0; i < input.files.length; i++) {
|
||||||
|
let pill = document.createElement('div'); pill.className = 'file-pill'; pill.innerHTML = `📎 ${input.files[i].name}`;
|
||||||
|
previewList.appendChild(pill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoResize(textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px';
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
@@ -116,7 +172,7 @@ theme_hdrs = (
|
|||||||
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 & CALCUL COÛTS
|
# SECTION 2. CONNEXION AUX IA & RECHERCHE WEB
|
||||||
# ==========================================
|
# ==========================================
|
||||||
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"))
|
||||||
@@ -126,16 +182,49 @@ def interroger_gemini_cascade(prompt):
|
|||||||
modeles_a_tester = ["gemini-3.1-pro-preview", "gemini-3.1-pro", "gemini-2.5-pro", "gemini-3.7-flash", "gemini-3.6-flash"]
|
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:
|
for nom_modele in modeles_a_tester:
|
||||||
try:
|
try:
|
||||||
ia_response = gemini_client.models.generate_content(
|
chat = gemini_client.chats.create(model=nom_modele, config={"system_instruction": "Réponds toujours dans la langue de l'utilisateur."})
|
||||||
model=nom_modele,
|
ia_response = chat.send_message(prompt)
|
||||||
contents=prompt,
|
|
||||||
config={"system_instruction": "Réponds toujours dans la même langue que celle utilisée par l'utilisateur."}
|
|
||||||
)
|
|
||||||
return ia_response.text, nom_modele
|
return ia_response.text, nom_modele
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
raise Exception("Tous les modèles Gemini sont indisponibles actuellement.")
|
raise Exception("Tous les modèles Gemini sont indisponibles actuellement.")
|
||||||
|
|
||||||
|
def rechercher_sur_google(requete):
|
||||||
|
"""Effectue une recherche Google silencieuse si le module est installé."""
|
||||||
|
try:
|
||||||
|
from googlesearch import search
|
||||||
|
urls = []
|
||||||
|
for url in search(requete, num_results=3, lang="fr"):
|
||||||
|
urls.append(url)
|
||||||
|
return " ".join(urls) # Retourne les liens qui seront scrapés par extraire_contenu_urls
|
||||||
|
except ImportError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def extraire_contenu_urls(prompt):
|
||||||
|
urls = re.findall(r'https?://[^\s]+', prompt)
|
||||||
|
scraped_text = ""
|
||||||
|
for url in urls:
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, timeout=5, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
|
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||||
|
for script in soup(["script", "style", "nav", "footer"]): script.extract()
|
||||||
|
scraped_text += f"\n\n--- Contenu extrait de l'URL ({url}) ---\n{soup.get_text(separator=' ', strip=True)[:4000]}"
|
||||||
|
except Exception as e:
|
||||||
|
scraped_text += f"\n\n[Impossible de récupérer le contenu de {url}: {e}]"
|
||||||
|
return scraped_text
|
||||||
|
|
||||||
|
def nettoyer_decision_router(raw_text):
|
||||||
|
try:
|
||||||
|
clean = re.sub(r'```(?:json)?\s*', '', raw_text)
|
||||||
|
clean = clean.replace('```', '').strip()
|
||||||
|
start = clean.find('{')
|
||||||
|
end = clean.rfind('}') + 1
|
||||||
|
if start != -1 and end != 0:
|
||||||
|
return json.loads(clean[start:end])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"intention": "Analyse", "fournisseur": "gemini"}
|
||||||
|
|
||||||
def calculer_cout_ia(fournisseur, prompt_tokens, completion_tokens):
|
def calculer_cout_ia(fournisseur, prompt_tokens, completion_tokens):
|
||||||
if fournisseur == "GROQ":
|
if fournisseur == "GROQ":
|
||||||
input_cost_per_m, output_cost_per_m = 0.59, 0.79
|
input_cost_per_m, output_cost_per_m = 0.59, 0.79
|
||||||
@@ -158,34 +247,45 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur):
|
|||||||
Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; 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;"
|
style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# BOUTONS D'EXPORTATION CLIENT (0 RAM Serveur)
|
||||||
|
actions = Div(
|
||||||
|
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"),
|
||||||
|
cls="msg-actions"
|
||||||
|
)
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
encart_info,
|
encart_info,
|
||||||
Div(text, cls="markdown-raw", style="display:none;"),
|
Div(text, cls="markdown-raw", style="display:none;"),
|
||||||
Div(cls="markdown-rendered", style="line-height: 1.6;"),
|
Div(cls="markdown-rendered", style="line-height: 1.6;"),
|
||||||
|
actions,
|
||||||
cls="ai-bubble", style=f"border-left: 4px solid {couleur};"
|
cls="ai-bubble", style=f"border-left: 4px solid {couleur};"
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_sidebar():
|
def render_sidebar():
|
||||||
history = load_history()
|
history = load_history()
|
||||||
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[1].get('timestamp', 0)))
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for chat_id, data in sorted_chats:
|
for chat_id, data in sorted_chats:
|
||||||
pin_icon = "📌" if data.get('pinned', False) else "📍"
|
pin_icon = "📌" if data.get('pinned', False) else "📍"
|
||||||
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;"),
|
# CORRECTION : Utilisation de vrais liens <A> pour la navigation (Robuste, n'écrase plus rien !)
|
||||||
|
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(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"),
|
||||||
Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"),
|
Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"),
|
||||||
Button("🗑️", cls="history-btn", hx_post=f"/delete/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"),
|
Button("🗑️", cls="history-btn", hx_post=f"/delete/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"),
|
||||||
cls="history-item", id=f"chat-item-{chat_id}"
|
cls="history-item", id=f"chat-item-{chat_id}"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return Div(*items, id="sidebar-content", style="flex:1; overflow-y:auto;", hx_swap_oob="true")
|
return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true")
|
||||||
|
|
||||||
@rt('/')
|
@rt('/')
|
||||||
def get(session):
|
def get(session):
|
||||||
if 'current_chat_id' not in session:
|
if 'current_chat_id' not in session or not session['current_chat_id']:
|
||||||
session['current_chat_id'] = str(uuid.uuid4())
|
session['current_chat_id'] = str(uuid.uuid4())
|
||||||
|
|
||||||
chat_id = session['current_chat_id']
|
chat_id = session['current_chat_id']
|
||||||
@@ -203,11 +303,22 @@ def get(session):
|
|||||||
messages_ui.append(Script("setTimeout(renderMarkdown, 100);"))
|
messages_ui.append(Script("setTimeout(renderMarkdown, 100);"))
|
||||||
|
|
||||||
return Title("Aethas38 - Mine_AI"), Body(
|
return Title("Aethas38 - Mine_AI"), Body(
|
||||||
|
Div(
|
||||||
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"),
|
Div(
|
||||||
|
# CORRECTION NAVIGATION : Vrai lien HTTP pour forcer un nettoyage complet propre
|
||||||
|
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
|
||||||
|
Button("💾 Exporter", cls="export-btn", onclick="exportChat()", title="Télécharger la conversation Globale"),
|
||||||
|
cls="nav-buttons"
|
||||||
|
),
|
||||||
|
cls="sidebar-top"
|
||||||
|
),
|
||||||
render_sidebar(),
|
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; font-size: 0.9em;"),
|
||||||
|
cls="sidebar-footer"
|
||||||
|
),
|
||||||
cls="sidebar"
|
cls="sidebar"
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -215,20 +326,22 @@ def get(session):
|
|||||||
Div(*messages_ui, id="chat-history", cls="chat-container"),
|
Div(*messages_ui, id="chat-history", cls="chat-container"),
|
||||||
Div(
|
Div(
|
||||||
Form(
|
Form(
|
||||||
|
Input(type="hidden", name="chat_id", value=chat_id),
|
||||||
|
Div(
|
||||||
|
Div(id="file-preview-list"),
|
||||||
Div(
|
Div(
|
||||||
# Bouton + (Label déclenchant l'input file caché supportant le drag & drop et jusqu'à 10 fichiers)
|
|
||||||
Label("+", _for="file-upload", cls="file-btn", title="Importer des fichiers (max 10)"),
|
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;"),
|
Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"),
|
||||||
# Textarea Gemini Web style avec ascenseur automatique
|
Textarea(name="prompt", id="prompt-input", placeholder="Entrez votre requête ou déposez vos fichiers / liens ici...", cls="input-field", rows="1", oninput="autoResize(this)"),
|
||||||
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"),
|
Button("↑", type="submit", cls="btn-submit", title="Exécuter"),
|
||||||
cls="input-wrapper"
|
cls="input-row"
|
||||||
|
),
|
||||||
|
cls="input-container-box"
|
||||||
),
|
),
|
||||||
hx_post="/chat",
|
hx_post="/chat",
|
||||||
hx_target="#chat-history",
|
hx_target="#chat-history",
|
||||||
hx_swap="beforeend",
|
hx_swap="beforeend",
|
||||||
hx_on__after_request="this.reset()",
|
hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='auto';",
|
||||||
enctype="multipart/form-data",
|
enctype="multipart/form-data",
|
||||||
style="width: 100%; display: flex; justify-content: center;"
|
style="width: 100%; display: flex; justify-content: center;"
|
||||||
),
|
),
|
||||||
@@ -239,51 +352,66 @@ def get(session):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# SECTION 4. LE MOTEUR DE ROUTAGE & UPLOAD MULTI-FICHIERS
|
# SECTION 4. LE MOTEUR DE ROUTAGE (IA + WEB + FICHIERS)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@rt('/chat')
|
@rt('/chat')
|
||||||
async def post(request, session: dict):
|
async def post(request, session: dict):
|
||||||
try:
|
try:
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
prompt = form.get("prompt", "")
|
prompt = form.get("prompt", "")
|
||||||
files = form.getlist("files") # Récupération de la liste des fichiers (jusqu'à 10)
|
chat_id = form.get("chat_id")
|
||||||
|
files = form.getlist("files")
|
||||||
|
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = str(uuid.uuid4())
|
||||||
|
session['current_chat_id'] = chat_id
|
||||||
|
|
||||||
chat_id = session.get('current_chat_id')
|
|
||||||
history = load_history()
|
history = load_history()
|
||||||
|
|
||||||
if chat_id not in history:
|
# 1. Scraping Moteur Google (Si l'intention est détectée)
|
||||||
history[chat_id] = {"title": (prompt[:30] if prompt else "Fichiers joints") + "...", "pinned": False, "messages": []}
|
mots_cles = ["cherche sur", "recherche sur", "sur internet", "sur le web", "sur le net"]
|
||||||
|
if any(mot in prompt.lower() for mot in mots_cles):
|
||||||
|
liens_google = rechercher_sur_google(prompt)
|
||||||
|
prompt += f"\n {liens_google}" # Ajoute les liens invisibles au prompt pour les scraper juste après
|
||||||
|
|
||||||
|
# 2. Scraping automatique des liens web
|
||||||
|
web_content = extraire_contenu_urls(prompt)
|
||||||
|
|
||||||
|
# 3. Traitement des fichiers joints
|
||||||
file_contents_summary = []
|
file_contents_summary = []
|
||||||
file_names = []
|
file_names = []
|
||||||
|
|
||||||
if files:
|
if files:
|
||||||
for file in files[:10]: # Limitation stricte à 10 fichiers
|
for file in files[:10]:
|
||||||
if file and file.filename:
|
if file and file.filename:
|
||||||
file_names.append(file.filename)
|
file_names.append(file.filename)
|
||||||
content_bytes = await file.read()
|
content_bytes = await file.read()
|
||||||
try:
|
try:
|
||||||
text_content = content_bytes.decode('utf-8', errors='ignore')
|
text_content = content_bytes.decode('utf-8', errors='ignore')
|
||||||
except Exception:
|
except Exception:
|
||||||
text_content = "[Fichier binaire ou non textuel]"
|
text_content = "[Fichier binaire]"
|
||||||
file_contents_summary.append(f"\n--- Fichier: {file.filename} ---\n{text_content}")
|
file_contents_summary.append(f"\n--- Fichier joint: {file.filename} ---\n{text_content}")
|
||||||
|
|
||||||
|
# 4. Construction du prompt complet
|
||||||
full_prompt = prompt if prompt else f"Analyse ces fichiers : {', '.join(file_names)}"
|
full_prompt = prompt if prompt else f"Analyse ces fichiers : {', '.join(file_names)}"
|
||||||
if file_contents_summary:
|
if file_contents_summary: full_prompt += "\n".join(file_contents_summary)
|
||||||
full_prompt += "\n".join(file_contents_summary)
|
if web_content: full_prompt += web_content
|
||||||
|
|
||||||
# Analyse par l'Aiguilleur
|
if chat_id not in history:
|
||||||
|
titre_brut = prompt.replace("cherche sur internet", "").strip()
|
||||||
|
history[chat_id] = {
|
||||||
|
"title": (titre_brut[:30] if titre_brut else "Recherche / Fichiers") + "...",
|
||||||
|
"pinned": False, "timestamp": time.time(), "messages": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5. Analyse par l'Aiguilleur (Qwen)
|
||||||
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 (JSON uniquement) : {"intention": "...", "fournisseur": "..."}"""
|
||||||
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)
|
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
|
decision = nettoyer_decision_router(router_response.choices[0].message.content)
|
||||||
start_idx = raw_decision.find('{')
|
|
||||||
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"}
|
|
||||||
fournisseur = decision.get("fournisseur", "local").lower()
|
fournisseur = decision.get("fournisseur", "local").lower()
|
||||||
|
|
||||||
# Requête IA & Tokens
|
# 6. Requête IA & Tokens
|
||||||
prompt_tokens = len(full_prompt.split()) * 2
|
prompt_tokens = len(full_prompt.split()) * 2
|
||||||
completion_tokens = 0
|
completion_tokens = 0
|
||||||
|
|
||||||
@@ -304,7 +432,7 @@ async def post(request, session: dict):
|
|||||||
prompt_tokens = ia_response.usage.prompt_tokens
|
prompt_tokens = ia_response.usage.prompt_tokens
|
||||||
completion_tokens = ia_response.usage.completion_tokens
|
completion_tokens = ia_response.usage.completion_tokens
|
||||||
|
|
||||||
# Sauvegarde en JSON
|
# 7. Sauvegarde en JSON
|
||||||
display_msg = f"{prompt}" + (f"\n📎 [Fichiers: {', '.join(file_names)}]" if file_names else "")
|
display_msg = f"{prompt}" + (f"\n📎 [Fichiers: {', '.join(file_names)}]" if file_names else "")
|
||||||
history[chat_id]['messages'].append({"role": "user", "content": display_msg})
|
history[chat_id]['messages'].append({"role": "user", "content": display_msg})
|
||||||
history[chat_id]['messages'].append({"role": "ai", "content": reponse_finale, "intention": decision.get('intention', ''), "fournisseur": nom_fournisseur, "modele": nom_modele, "couleur": couleur})
|
history[chat_id]['messages'].append({"role": "ai", "content": reponse_finale, "intention": decision.get('intention', ''), "fournisseur": nom_fournisseur, "modele": nom_modele, "couleur": couleur})
|
||||||
@@ -319,20 +447,26 @@ async def post(request, session: dict):
|
|||||||
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 & NAVIGATION (RedirectResponse FIX)
|
# SECTION 5. ROUTES DE L'HISTORIQUE (Navigation native)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
@rt('/favicon.ico')
|
||||||
|
def get():
|
||||||
|
return HTTPResponse(status_code=204)
|
||||||
|
|
||||||
@rt('/new_chat')
|
@rt('/new_chat')
|
||||||
def post(session):
|
def get(session):
|
||||||
session['current_chat_id'] = str(uuid.uuid4())
|
session['current_chat_id'] = str(uuid.uuid4())
|
||||||
return RedirectResponse('/', status_code=303)
|
return RedirectResponse('/', status_code=303)
|
||||||
|
|
||||||
@rt('/load_chat/{chat_id}')
|
@rt('/load_chat/{chat_id}')
|
||||||
def post(chat_id: str, session):
|
def get(chat_id: str, session):
|
||||||
|
if chat_id and chat_id != "null":
|
||||||
session['current_chat_id'] = chat_id
|
session['current_chat_id'] = chat_id
|
||||||
return RedirectResponse('/', status_code=303)
|
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):
|
||||||
|
if not chat_id or chat_id == "null": return render_sidebar()
|
||||||
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()
|
||||||
@@ -343,6 +477,7 @@ def post(chat_id: str, req):
|
|||||||
|
|
||||||
@rt('/pin/{chat_id}')
|
@rt('/pin/{chat_id}')
|
||||||
def post(chat_id: str):
|
def post(chat_id: str):
|
||||||
|
if not chat_id or chat_id == "null": return render_sidebar()
|
||||||
history = load_history()
|
history = load_history()
|
||||||
if chat_id in history:
|
if chat_id in history:
|
||||||
history[chat_id]['pinned'] = not history[chat_id].get('pinned', False)
|
history[chat_id]['pinned'] = not history[chat_id].get('pinned', False)
|
||||||
@@ -351,6 +486,7 @@ def post(chat_id: str):
|
|||||||
|
|
||||||
@rt('/delete/{chat_id}')
|
@rt('/delete/{chat_id}')
|
||||||
def post(chat_id: str):
|
def post(chat_id: str):
|
||||||
|
if not chat_id or chat_id == "null": return render_sidebar()
|
||||||
history = load_history()
|
history = load_history()
|
||||||
if chat_id in history:
|
if chat_id in history:
|
||||||
del history[chat_id]
|
del history[chat_id]
|
||||||
|
|||||||
Reference in new issue
Block a user