Files
aethas-free/modules/theme.py
T
xavier 9fdb2bd73e
Build and Push Docker Image / build-and-push (push) Successful in 35s
Amélioration contraste mobile, traçabilité des IA et filtrage catalogue
2026-08-23 15:09:57 +02:00

316 lines
22 KiB
Python

from fasthtml.common import *
def get_theme_hdrs():
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"
return (
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/html2pdf.js/0.10.1/html2pdf.bundle.min.js"),
Script(src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"),
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"),
Link(rel="icon", type="image/svg+xml", href=f"data:image/svg+xml;utf8,{svg_favicon}"),
Style("""
:root { --bg-dark: #121212; --bg-panel: #1e1e24; --accent-blue: #00e5ff; --accent-purple: #b300ff; --text-main: #f8fafc; }
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 CLASSIQUE --- */
.sidebar { width: 280px; background-color: #0a0a0c; border-right: 1px solid #333; padding: 20px; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; transition: left 0.3s ease; }
.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; flex-direction: column; 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; transition: 0.2s; }
.new-chat-btn:hover { background: rgba(0, 229, 255, 0.1); }
.export-btn { background: #2a2a35; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; cursor: pointer; transition: 0.2s; font-size: 1em; text-align:center; text-decoration:none; display:block;}
.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-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); }
/* --- CONTENU CENTRAL & CONTRASTE MOBILE ACCRU --- */
.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: 160px; box-sizing: border-box; }
.user-bubble { background-color: #2a2a35; color: #ffffff; padding: 15px 20px; border-radius: 15px 15px 0 15px; align-self: flex-end; max-width: 80%; border: 1px solid #555; margin-left: auto; word-break: break-word; overflow-wrap: break-word; }
/* BULLE IA : Contraste maximal forcé pour le confort de lecture mobile */
.ai-bubble { background-color: var(--bg-panel); color: #ffffff !important; padding: 15px 20px; border-radius: 15px 15px 15px 0; align-self: flex-start; max-width: 90%; width: 100%; border: 1px solid #444; word-break: break-word; overflow-wrap: break-word; }
.markdown-rendered, .markdown-rendered p, .markdown-rendered li, .markdown-rendered span, .markdown-rendered strong { color: #ffffff !important; }
.msg-actions { display: flex; gap: 10px; margin-top: 15px; border-top: 1px dashed #444; padding-top: 10px; justify-content: flex-end; flex-wrap: wrap; }
.msg-action-btn { background: transparent; border: 1px solid #555; color: #ccc; border-radius: 15px; padding: 5px 12px; cursor: pointer; font-size: 0.85em; transition: 0.2s; }
.msg-action-btn:hover { background: #2a2a35; color: var(--accent-blue); border-color: var(--accent-blue); }
.code-header { background: #16161a; 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-actions-group { display: flex; gap: 8px; }
.code-header button { background: #2a2a35; border: 1px solid #444; color: #e0e0e0; cursor: pointer; padding: 4px 10px; border-radius: 5px; font-size: 0.85em; transition: 0.2s; }
.code-header button:hover { background: var(--accent-blue); color: black; border-color: var(--accent-blue); }
.markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333; padding: 15px; overflow-x: auto; background-color: #0d0d0f !important; }
/* --- NOUVEAU LAYOUT ZONE DE SAISIE --- */
.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; z-index: 10;}
.input-container-box { width: 100%; max-width: 900px; background: #1e1e24; border-radius: 20px; border: 1px solid #444; padding: 15px; box-sizing: border-box; display: flex; flex-direction: column; gap: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
.textarea-wrapper { display: flex; align-items: flex-end; gap: 10px; width: 100%; }
.options-row { display: flex; gap: 20px; justify-content: center; border-top: 1px solid #333; padding-top: 10px; flex-wrap: wrap; }
#file-preview-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 80px; overflow-y: auto; padding-bottom: 5px; }
.file-pill { background: #2a2a35; border: 1px solid #555; color: var(--accent-blue); padding: 5px 12px; border-radius: 12px; font-size: 0.8em; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.input-field { background: transparent; border: none; color: white; resize: none; flex: 1; font-size: 1em; height: 36px; max-height: 140px; overflow-y: hidden; padding: 8px; font-family: inherit; box-sizing: border-box; line-height: 20px; }
.input-field:focus { outline: none; }
.file-btn { background: #2a2a35; border: 1px solid #444; color: var(--accent-blue); width: 36px !important; height: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.3em; text-decoration: none; flex-shrink: 0; }
.btn-submit { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; width: 36px !important; height: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.1em; flex-shrink: 0; }
/* LOADER HTMX SURÉLEVÉ */
.htmx-indicator { display: none; opacity: 0; transition: opacity 0.3s ease-in; position: absolute; bottom: 160px; left: 50%; transform: translateX(-50%); z-index: 9999; }
.htmx-request.htmx-indicator { display: flex !important; opacity: 1; }
.htmx-request .htmx-indicator { display: flex !important; opacity: 1; }
.tech-loader-container { display: flex; align-items: center; gap: 15px; color: var(--accent-blue); font-style: italic; background: #1e1e24; padding: 15px 25px; border-radius: 30px; border: 1px dashed var(--accent-blue); box-shadow: 0 5px 20px rgba(0,229,255,0.2); font-size: 0.9em; white-space: nowrap; }
.tech-spinner { width: 20px; height: 20px; border: 3px solid rgba(0, 229, 255, 0.2); border-top-color: var(--accent-blue); border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* --- RESPONSIVE MOBILE & SIDEBAR --- */
.mobile-header { display: none; background: #0a0a0c; padding: 10px 20px; align-items: center; justify-content: space-between; border-bottom: 1px solid #333; width: 100%; box-sizing: border-box; flex-shrink: 0;}
.hamburger-btn { background: none; border: none; color: var(--accent-blue); font-size: 1.8em; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 5px; }
.hamburger-btn:focus { outline: none; }
@media (max-width: 768px) {
body { flex-direction: column; }
.mobile-header { display: flex; }
.sidebar { position: absolute; left: -100%; top: 0; z-index: 1000; width: 280px; height: 100vh; box-shadow: 5px 0 15px rgba(0,0,0,0.8); }
.sidebar.sidebar-open { left: 0; }
.main-content { flex: 1; height: calc(100vh - 60px); }
.chat-container { padding-bottom: 200px; padding-left: 10px; padding-right: 10px; }
.user-bubble, .ai-bubble { max-width: 95%; }
.input-area { padding: 10px; }
.options-row { justify-content: center; gap: 10px;}
}
"""),
Script(r"""
let aethasFiles = new DataTransfer();
window.toggleSidebar = function() {
let sidebar = document.querySelector('.sidebar');
if (sidebar) {
sidebar.classList.toggle('sidebar-open');
}
};
function syncFiles() {
const fileInput = document.getElementById('file-upload');
if(fileInput) fileInput.files = aethasFiles.files;
renderFilePills();
}
function renderFilePills() {
let previewList = document.getElementById('file-preview-list');
if(!previewList) return;
previewList.innerHTML = '';
for (let i = 0; i < aethasFiles.files.length; i++) {
let file = aethasFiles.files[i];
let pill = document.createElement('div');
pill.className = 'file-pill';
pill.innerHTML = `📎 ${file.name} <span style="cursor:pointer; color:#ff5555; margin-left:4px;" onclick="removeFile(${i})">✕</span>`;
previewList.appendChild(pill);
}
}
function removeFile(index) {
let dt = new DataTransfer();
for (let i = 0; i < aethasFiles.files.length; i++) {
if (i !== index) dt.items.add(aethasFiles.files[i]);
}
aethasFiles = dt;
syncFiles();
}
function handleFileSelection(input) {
if (input.files) {
for (let f of input.files) {
let exists = false;
for (let existing of aethasFiles.files) {
if (existing.name === f.name) exists = true;
}
if (!exists && aethasFiles.files.length < 10) {
aethasFiles.items.add(f);
}
}
}
syncFiles();
}
function renderMarkdown() {
try {
document.querySelectorAll('.markdown-raw').forEach(el => {
if (el.dataset.rendered) return;
let container = el.nextElementSibling;
container.innerHTML = (typeof marked !== 'undefined') ? marked.parse(el.textContent) : el.textContent;
el.dataset.rendered = "true";
el.classList.remove('markdown-raw');
container.querySelectorAll('pre').forEach(pre => {
try {
let code = pre.querySelector('code');
if (code && !pre.previousElementSibling?.classList.contains('code-header')) {
let rawLang = code.className.replace('language-', '') || 'snippet';
let header = document.createElement('div');
header.className = 'code-header';
header.innerHTML = `
<span style="color:#00e5ff; font-weight:bold;">${rawLang.toUpperCase()}</span>
<div class="code-actions-group">
<button onclick="copyCodeBlock(this)">📋 Copier</button>
<button onclick="downloadCodeZip(this, '${rawLang}')">📦 ZIP</button>
</div>`;
pre.parentNode.insertBefore(header, pre);
if (typeof hljs !== 'undefined') hljs.highlightElement(code);
}
} catch(err) {}
});
});
let chat = document.getElementById('chat-history');
if (chat) chat.scrollTop = chat.scrollHeight;
} catch(e) {}
}
function getSmartFilename(btn, defaultName = 'Aethas38_Reponse', extension = 'pdf') {
try {
let bubble = btn.closest('.ai-bubble');
let prevUserMsg = bubble.previousElementSibling;
if (prevUserMsg && prevUserMsg.classList.contains('user-bubble')) {
let text = prevUserMsg.innerText.replace(/📎.*$/, '').trim();
let slug = text.toLowerCase().replace(/[^a-z0-9àâäéèêëîïôöùûüç\s]/g, '').trim().replace(/\s+/g, '_').substring(0, 35);
if (slug.length > 3) return slug + '.' + extension;
}
} catch(e) {}
return defaultName + '.' + extension;
}
function getCleanPlainText(btn) {
let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw');
let source = (rendered && rendered.innerHTML.trim() !== "") ? rendered : raw;
return source.innerText || source.textContent || "";
}
function autoResize(textarea) {
if (!textarea) return;
textarea.style.height = '36px';
let scrollH = textarea.scrollHeight;
if (scrollH > 100) {
textarea.style.height = '100px';
textarea.style.overflowY = 'auto';
} else {
textarea.style.height = scrollH + 'px';
textarea.style.overflowY = 'hidden';
}
}
document.addEventListener('DOMContentLoaded', () => {
const ta = document.getElementById('prompt-input');
const dropZone = document.querySelector('.input-container-box');
if (ta) {
ta.addEventListener('input', () => autoResize(ta));
ta.addEventListener('paste', (e) => {
let items = (e.clipboardData || e.originalEvent.clipboardData).items;
let hasFiles = false;
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
let blob = items[i].getAsFile();
if (blob && aethasFiles.files.length < 10) {
aethasFiles.items.add(blob);
hasFiles = true;
}
}
}
if (hasFiles) {
e.preventDefault();
syncFiles();
} else {
setTimeout(() => autoResize(ta), 20);
}
});
}
if (dropZone) {
['dragenter', 'dragover'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent-blue)'; }));
['dragleave', 'drop'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = '#444'; }));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
for (let f of e.dataTransfer.files) {
if (aethasFiles.files.length < 10) aethasFiles.items.add(f);
}
syncFiles();
});
}
document.body.addEventListener('htmx:afterRequest', function(evt) {
if(evt.detail.target.id === 'chat-history') {
aethasFiles = new DataTransfer();
syncFiles();
if(ta) ta.style.height = '36px';
}
});
});
function downloadCodeZip(btn, lang) {
let pre = btn.closest('.code-header').nextElementSibling;
let code = pre.querySelector('code').innerText;
let ext = lang === 'python' ? 'py' : (lang === 'javascript' ? 'js' : (lang === 'html' ? 'html' : 'txt'));
let filename = getSmartFilename(btn, 'aethas_snippet', ext);
if (typeof JSZip !== 'undefined') {
let zip = new JSZip();
zip.file(filename, code);
zip.generateAsync({type: "blob"}).then(function(content) {
let link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = filename.replace('.'+ext, '_snippet.zip');
link.click();
});
}
}
function downloadResponsePDF(btn) {
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'pdf');
let originalBg = content.style.backgroundColor;
let originalColor = content.style.color;
content.style.backgroundColor = '#ffffff';
content.style.color = '#000000';
content.style.padding = '15px';
html2pdf().set({ margin: 10, filename: filename, html2canvas: { scale: 2, backgroundColor: '#ffffff' }, jsPDF: { unit: 'mm', format: 'a4' } }).from(content).save().then(() => {
content.style.backgroundColor = originalBg;
content.style.color = originalColor;
content.style.padding = '';
});
}
function downloadResponseTXT(btn) {
let text = getCleanPlainText(btn);
let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'txt');
let blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click();
}
function downloadResponseDOC(btn) {
let html = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML;
let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'doc');
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; color: #000000; background-color: #ffffff;'>";
let blob = new Blob(['\\ufeff', preHtml + html + "</body></html>"], { type: 'application/msword' });
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click();
}
function downloadResponseJSON(btn) {
let text = getCleanPlainText(btn);
let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'json');
let blob = new Blob([JSON.stringify({content: text}, null, 2)], {type: 'application/json;charset=utf-8'});
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; link.click();
}
function shareWhatsApp(btn) {
let text = getCleanPlainText(btn);
window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent("🤖 *Aethas38 (IA) :*\\n\\n" + text), '_blank');
}
function shareEmail(btn) {
let text = getCleanPlainText(btn);
window.open('mailto:?subject=Aethas38 - Réponse IA&body=' + encodeURIComponent(text), '_self');
}
""")
)