This commit is contained in:
1 parent
626ec138e4
commit
d6a7d10e3a
8 files changed
+518
-225
No files matched your search
@@ -19,9 +19,9 @@ Le développement suit un versioning strict (format `vX.Y.Z`) synchronisé avec
|
|||||||
## 🚀 ROADMAP (Ordre d'exécution strict)
|
## 🚀 ROADMAP (Ordre d'exécution strict)
|
||||||
|
|
||||||
- [X] **Étape 1 :** Création de la page d'administration (changement des clés, ajout de fournisseurs, configuration serveur mail, etc.).
|
- [X] **Étape 1 :** Création de la page d'administration (changement des clés, ajout de fournisseurs, configuration serveur mail, etc.).
|
||||||
- [ ] **Étape 2 :** Ajout des fournisseurs pour le traitement d'images, de sons (génération de voix, musique, calage sur vidéo) et de vidéos.
|
- [X] **Étape 2 :** Ajout des fournisseurs pour le traitement d'images, de sons (génération de voix, musique, calage sur vidéo) et de vidéos.
|
||||||
- [ ] **Étape 3 :** Possibilité de télécharger les prompts en PDF, TXT, MD et JSON.
|
- [X] **Étape 3 :** Possibilité de télécharger les prompts en PDF, TXT, MD et JSON.
|
||||||
- [ ] **Étape 4 :** Possibilité de télécharger les réponses en PDF, TXT, MD et JSON.
|
- [X] **Étape 4 :** Possibilité de télécharger les réponses en PDF, TXT, MD et JSON.
|
||||||
- [ ] **Étape 5 :** Rédaction du Wiki détaillé pour Gitea et GitHub.
|
- [ ] **Étape 5 :** Rédaction du Wiki détaillé pour Gitea et GitHub.
|
||||||
- [ ] **Étape 6 :** Ne plus créer systématiquement un nouveau projet à l'ouverture de l'application.
|
- [ ] **Étape 6 :** Ne plus créer systématiquement un nouveau projet à l'ouverture de l'application.
|
||||||
- [ ] **Étape 7 :** Refonte de la gestion des projets (séparation Épinglés / Non Épinglés, avec le dernier utilisé en tête de liste).
|
- [ ] **Étape 7 :** Refonte de la gestion des projets (séparation Épinglés / Non Épinglés, avec le dernier utilisé en tête de liste).
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# ai_engine.py
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
os.makedirs("logs", exist_ok=True)
|
||||||
|
APP_LAUNCH_TIME = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
LOG_FILE = f"logs/AETHAS38_Run_{APP_LAUNCH_TIME}.log"
|
||||||
|
|
||||||
|
def log_event(session_id, category, action):
|
||||||
|
sid_short = session_id[:8] if session_id and session_id != "system" else "SYSTEM"
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"[{timestamp}] [Session:{sid_short}] [{category}] {action}\n")[cite: 11]
|
||||||
|
|
||||||
|
def ask_llm(session_id, full_id, msg):
|
||||||
|
parts = full_id.split("|", 1)
|
||||||
|
provider, actual_model = parts[0], parts[1] if len(parts) > 1 else full_id
|
||||||
|
|
||||||
|
log_event(session_id, "API_REQ", f"Provider: {provider} | Modèle: {actual_model}")[cite: 11]
|
||||||
|
try:
|
||||||
|
if provider == "openrouter":
|
||||||
|
res = requests.post("https://openrouter.ai/api/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]})
|
||||||
|
if res.status_code == 200: return res.json()["choices"][0]["message"]["content"]
|
||||||
|
return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11]
|
||||||
|
elif provider == "groq":
|
||||||
|
res = requests.post("https://api.groq.com/openai/v1/chat/completions", headers={"Authorization": f"Bearer {os.getenv('GROQ_API_KEY')}"}, json={"model": actual_model, "messages": [{"role": "user", "content": msg}]})
|
||||||
|
if res.status_code == 200: return res.json()["choices"][0]["message"]["content"]
|
||||||
|
return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11]
|
||||||
|
elif provider == "gemini":
|
||||||
|
res = requests.post(f"https://generativelanguage.googleapis.com/v1beta/models/{actual_model}:generateContent?key={os.getenv('GEMINI_API_KEY')}", json={"contents": [{"parts": [{"text": msg}]}]})
|
||||||
|
if res.status_code == 200: return res.json()["candidates"][0]["content"]["parts"][0]["text"]
|
||||||
|
return f"⚠️ Erreur API ({provider}) : {res.text}"[cite: 11]
|
||||||
|
return "⚠️ Fournisseur API non implémenté ou ID invalide[cite: 11]."
|
||||||
|
except Exception as e: return f"⚠ Erreur de connexion : {str(e)}"[cite: 11]
|
||||||
|
|
||||||
|
async def async_ask_llm(session_id, full_id, msg):
|
||||||
|
return await asyncio.to_thread(ask_llm, session_id, full_id, msg)[cite: 11]
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# auth.py
|
||||||
import re
|
import re
|
||||||
import pyotp
|
import pyotp
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -35,7 +36,6 @@ def send_invite_email(to_email, setup_link):
|
|||||||
smtp_pass = os.getenv("SMTP_PASS")
|
smtp_pass = os.getenv("SMTP_PASS")
|
||||||
|
|
||||||
if not all([smtp_server, smtp_user, smtp_pass]):
|
if not all([smtp_server, smtp_user, smtp_pass]):
|
||||||
print("Erreur: Configuration SMTP incomplète dans .env")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
msg = MIMEMultipart()
|
msg = MIMEMultipart()
|
||||||
@@ -52,6 +52,5 @@ def send_invite_email(to_email, setup_link):
|
|||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
server.quit()
|
server.quit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception:
|
||||||
print(f"Erreur SMTP: {e}")
|
|
||||||
return False
|
return False
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# catalog_manager.py
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
|
||||||
|
CATALOG_FILE = "sessions/catalog.json"
|
||||||
|
MODELS_DATA = []
|
||||||
|
|
||||||
|
def load_catalog():
|
||||||
|
global MODELS_DATA
|
||||||
|
if os.path.exists(CATALOG_FILE):
|
||||||
|
with open(CATALOG_FILE, "r", encoding="utf-8") as f:
|
||||||
|
try: MODELS_DATA = json.load(f)
|
||||||
|
except: MODELS_DATA = []
|
||||||
|
if not MODELS_DATA:
|
||||||
|
MODELS_DATA = [
|
||||||
|
{"id": "groq|llama-3.1-8b-instant", "name": "Groq - Llama 3.1 (8B)", "is_free": True, "provider": "Général / Texte", "description": "Modèle par défaut."},
|
||||||
|
{"id": "gemini|gemini-1.5-flash", "name": "Google - Gemini 1.5 Flash", "is_free": True, "provider": "Vision", "description": "Modèle par défaut."}
|
||||||
|
]
|
||||||
|
load_catalog()
|
||||||
|
|
||||||
|
def save_catalog(data):
|
||||||
|
global MODELS_DATA
|
||||||
|
MODELS_DATA = data
|
||||||
|
with open(CATALOG_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, indent=4, ensure_ascii=False)[cite: 11]
|
||||||
|
|
||||||
|
def get_budget():
|
||||||
|
mgmt_key = os.getenv("OPENROUTER_MANAGEMENT_KEY")
|
||||||
|
if not mgmt_key: return "Clé manquante[cite: 11]"
|
||||||
|
try:
|
||||||
|
data = requests.get("https://openrouter.ai/api/v1/credits", headers={"Authorization": f"Bearer {mgmt_key}"}, timeout=5).json().get("data", {})
|
||||||
|
return f"{data.get('total_credits', 0) - data.get('total_usage', 0):.4f} $"[cite: 11]
|
||||||
|
except: return "Erreur réseau[cite: 11]"
|
||||||
|
|
||||||
|
def parse_txt_catalog(content: str):
|
||||||
|
lines = content.split('\n')
|
||||||
|
models = []
|
||||||
|
current = {}
|
||||||
|
ext_date, ext_time = "Inconnue", ""
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line_trim = line.strip()
|
||||||
|
if "Généré le" in line_trim:
|
||||||
|
match = re.search(r"Généré le (\d{2}/\d{2}/\d{4}) à (\d{2}:\d{2}:\d{2})", line_trim)
|
||||||
|
if match:
|
||||||
|
ext_date, ext_time = match.group(1), match.group(2)
|
||||||
|
if line_trim.startswith("Nom : "): current['name'] = line_trim.replace("Nom : ", "").strip()
|
||||||
|
elif line_trim.startswith("ID : "): current['id'] = line_trim.replace("ID : ", "").strip()
|
||||||
|
elif line_trim.startswith("Tarif : "): current['is_free'] = ("Gratuit" in line_trim)
|
||||||
|
elif line_trim.startswith("Spécialité : "): current['provider'] = line_trim.replace("Spécialité : ", "").strip()
|
||||||
|
elif line_trim.startswith("Description : "): current['description'] = line_trim.replace("Description : ", "").strip()
|
||||||
|
elif line_trim.startswith("---"):
|
||||||
|
if 'id' in current and 'name' in current: models.append(current)
|
||||||
|
current = {}
|
||||||
|
return models, ext_date, ext_time[cite: 11]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# history_manager.py
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
HISTORY_FILE = "sessions/history.json"
|
||||||
|
|
||||||
|
def load_index():
|
||||||
|
if not os.path.exists(HISTORY_FILE): return {}
|
||||||
|
with open(HISTORY_FILE, "r", encoding="utf-8") as f:
|
||||||
|
try: return json.load(f)
|
||||||
|
except: return {}[cite: 11]
|
||||||
|
|
||||||
|
def save_index(idx):
|
||||||
|
with open(HISTORY_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(idx, f, indent=4, ensure_ascii=False)[cite: 11]
|
||||||
|
|
||||||
|
def load_session_data(session_id):
|
||||||
|
path = f"sessions/{session_id}.json"
|
||||||
|
if not os.path.exists(path): return []
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
try: return json.load(f)
|
||||||
|
except: return [][cite: 11]
|
||||||
|
|
||||||
|
def save_session_data(session_id, messages):
|
||||||
|
with open(f"sessions/{session_id}.json", "w", encoding="utf-8") as f:
|
||||||
|
json.dump(messages, f, indent=4, ensure_ascii=False)[cite: 11]
|
||||||
|
|
||||||
|
def create_new_session():
|
||||||
|
session_id = str(uuid.uuid4())
|
||||||
|
idx = load_index()
|
||||||
|
idx[session_id] = {"title": f"Projet du {datetime.now().strftime('%d/%m %H:%M')}", "pinned": False}
|
||||||
|
save_index(idx)
|
||||||
|
save_session_data(session_id, [])
|
||||||
|
return session_id[cite: 11]
|
||||||
+185
-118
@@ -1,140 +1,207 @@
|
|||||||
from fastapi import FastAPI, HTTPException
|
# main.py
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fasthtml.common import *
|
||||||
from fastapi.responses import HTMLResponse, Response
|
|
||||||
from pydantic import BaseModel
|
|
||||||
import requests
|
|
||||||
import asyncio
|
|
||||||
import os
|
import os
|
||||||
import uuid
|
import time
|
||||||
import json
|
import requests
|
||||||
import subprocess
|
from dotenv import load_dotenv
|
||||||
import urllib3
|
|
||||||
import socket
|
|
||||||
import config
|
|
||||||
|
|
||||||
# Forçage IPv4
|
from auth import verify_password, verify_totp
|
||||||
old_getaddrinfo = socket.getaddrinfo
|
from catalog_manager import MODELS_DATA, load_catalog, save_catalog, get_budget, parse_txt_catalog
|
||||||
def force_ipv4_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
|
from history_manager import load_index, save_index, load_session_data, save_session_data, create_new_session
|
||||||
return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
|
from ai_engine import ask_llm, async_ask_llm, log_event
|
||||||
socket.getaddrinfo = force_ipv4_getaddrinfo
|
|
||||||
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
load_dotenv()
|
||||||
|
|
||||||
app = FastAPI(title="AETHAS38 API")
|
theme_hdrs = [
|
||||||
|
Script(src="https://cdn.tailwindcss.com"),
|
||||||
|
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"),
|
||||||
|
Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/tokyo-night-dark.min.css"),
|
||||||
|
Style("""
|
||||||
|
body { background-color: #0f172a; color: #f8fafc; font-family: system-ui, sans-serif; margin: 0;}
|
||||||
|
.sidebar { background-color: #1e293b; border-right: 1px solid #333; height: 100vh; padding: 20px; display: flex; flex-direction: column;}
|
||||||
|
.main-content { padding: 20px; height: 100vh; display: flex; flex-direction: column; position: relative;}
|
||||||
|
.chat-container { flex-grow: 1; overflow-y: auto; margin-bottom: 20px; border: 1px solid #333; padding: 15px; border-radius: 8px; background: #151e2e; }
|
||||||
|
.input-row { display: flex; gap: 10px; align-items: flex-end; width: 100%; background: #1e293b; padding: 10px; border-radius: 10px; border: 1px solid #333;}
|
||||||
|
.input-box { flex-grow: 1; padding: 12px; border-radius: 5px; background: #2a2a35; border: 1px solid #444; color: white; outline: none; }
|
||||||
|
.file-upload-btn { background: #2a2a35; border: 1px solid #444; color: #94a3b8; padding: 12px; border-radius: 5px; cursor: pointer; font-size:14px; display: flex; align-items:center; justify-content:center;}
|
||||||
|
.model-selector { padding: 8px; border-radius: 5px; background: #1e293b; border: 1px solid #444; color: #00e5ff; width: 100%; font-size: 12px; outline:none; }
|
||||||
|
.send-btn { padding: 12px 24px; border-radius: 5px; background: #00e5ff; color: #0f172a; font-weight: bold; cursor: pointer; border: none; }
|
||||||
|
.msg-user { color: #00e5ff; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;}
|
||||||
|
.msg-ia { color: #a855f7; font-size: 12px; font-weight: bold; margin-bottom: 2px; margin-top: 15px;}
|
||||||
|
.bubble-user { background: #1e293b; padding: 10px; border-radius: 5px; display: inline-block; color: white; border: 1px solid #333;}
|
||||||
|
.bubble-ia { background: #1e293b; padding: 15px; border-radius: 8px; display: block; color: #e2e8f0; border: 1px solid #333;}
|
||||||
|
.htmx-indicator { display: none; position: absolute; bottom: 180px; left: 50%; transform: translateX(-50%); background: rgba(15, 23, 42, 0.9); border: 1px solid #00e5ff; padding: 15px 30px; border-radius: 50px; color: #00e5ff; font-weight: bold; z-index: 50;}
|
||||||
|
.htmx-request .htmx-indicator { display: block; }
|
||||||
|
.export-btn { display: block; background: #0f172a; border: 1px solid #00e5ff; color: #00e5ff; font-weight: bold; padding: 10px; border-radius: 5px; text-align: center; cursor: pointer; width: 100%;}
|
||||||
|
"""),
|
||||||
|
Script("""
|
||||||
|
function filterModels() {
|
||||||
|
let filter = document.querySelector('input[name="filter_type"]:checked').value;
|
||||||
|
document.querySelectorAll('.model-selector option').forEach(opt => {
|
||||||
|
if(opt.value === "") return;
|
||||||
|
let isFree = opt.getAttribute('data-free') === 'true';
|
||||||
|
if(filter === 'free' && !isFree) opt.style.display = 'none';
|
||||||
|
else if(filter === 'paid' && isFree) opt.style.display = 'none';
|
||||||
|
else opt.style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function validateMultiIA() {
|
||||||
|
let wCount = 0;
|
||||||
|
for(let i=1; i<=2; i++) { if(document.getElementById('worker'+i).value !== "") wCount++; }
|
||||||
|
if(wCount === 0) { alert("Erreur : Sélectionnez au moins une IA de travail."); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function updateFileCount(input) {
|
||||||
|
let label = document.getElementById('file-label-text');
|
||||||
|
if(input.files && input.files.length > 0) { label.innerText = `📎 ${input.files.length} fichier(s)`; label.style.color = '#00e5ff'; }
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
app, rt = fast_app(secret_key=os.getenv("SESSION_SECRET", "super-secret-key-fallback"), hdrs=theme_hdrs)
|
||||||
FRONTEND_DIR = os.path.join(BASE_DIR, "frontend")
|
|
||||||
SESSIONS_DIR = os.path.join(BASE_DIR, "sessions")
|
|
||||||
MEDIA_DIR = os.path.join(SESSIONS_DIR, "media")
|
|
||||||
TEMP_DIR = os.path.join(SESSIONS_DIR, "temp")
|
|
||||||
|
|
||||||
os.makedirs(MEDIA_DIR, exist_ok=True)
|
os.makedirs("sessions", exist_ok=True)
|
||||||
os.makedirs(TEMP_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
|
@rt('/login')
|
||||||
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
|
def get():
|
||||||
|
return Title("Connexion - AETHAS38"), Body(
|
||||||
|
Div(
|
||||||
|
H2("AETHAS 38 - Accès Sécurisé", style="color:#00e5ff; margin-bottom:20px; text-align:center;"),
|
||||||
|
Form(
|
||||||
|
Input(type="email", name="email", placeholder="E-mail", cls="input-box mb-3 w-full", required=True),
|
||||||
|
Input(type="password", name="password", placeholder="Mot de passe", cls="input-box mb-3 w-full", required=True),
|
||||||
|
Input(type="text", name="totp_code", placeholder="Code 2FA (6 chiffres)", cls="input-box mb-4 w-full", required=True),
|
||||||
|
Button("Se connecter", type="submit", cls="send-btn w-full"),
|
||||||
|
hx_post="/auth/login", hx_target="#auth-response"
|
||||||
|
),
|
||||||
|
Div(id="auth-response", style="color:red; margin-top:10px; text-align:center;"),
|
||||||
|
cls="sidebar", style="margin: auto; width: 400px; height: auto; border-radius: 10px; margin-top: 15vh;"
|
||||||
|
), style="background-color: #0f172a; height: 100vh; display: flex;"
|
||||||
|
)
|
||||||
|
|
||||||
class ExportRequest(BaseModel):
|
@rt('/auth/login')
|
||||||
content: str
|
def post(email: str, password: str, totp_code: str, session):
|
||||||
format: str # pdf, txt, md, json
|
if email == os.getenv("ADMIN_EMAIL") and verify_password(os.getenv("ADMIN_PASSWORD_HASH"), password) and verify_totp(os.getenv("ADMIN_2FA_SECRET"), totp_code):
|
||||||
type: str # prompt ou response
|
session["authenticated"] = True
|
||||||
|
session["role"] = "admin"
|
||||||
|
return Script("window.location.href = '/';")
|
||||||
|
return Div("Identifiants ou code 2FA incorrects.", style="color:#ef4444; font-weight:bold;")
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@rt('/logout')
|
||||||
async def serve_admin():
|
def get(session):
|
||||||
index_path = os.path.join(FRONTEND_DIR, "index.html")
|
session.clear()
|
||||||
with open(index_path, "r", encoding="utf-8") as f:
|
return RedirectResponse('/login')
|
||||||
return f.read()
|
|
||||||
|
|
||||||
@app.get("/api/config")
|
@rt('/check_updates')
|
||||||
async def get_config():
|
def get(session):
|
||||||
return config.load_config()
|
if not session.get("authenticated"): return Span("")
|
||||||
|
try:
|
||||||
|
res = requests.get("https://gitea.aethas38.duckdns.org/api/v1/repos/xavier/MULTI-IA-CODAGE/commits?limit=1", timeout=3)
|
||||||
|
if res.status_code == 200:
|
||||||
|
return Span(f"🟢 Gitea Connecté ({res.json()[0].get('sha', '')[:7]})", style="color:#10b981;")
|
||||||
|
except: pass
|
||||||
|
return Span("🟠 Gitea hors ligne", style="color:#fbbf24;")
|
||||||
|
|
||||||
@app.post("/api/config")
|
def render_history_list():
|
||||||
async def update_config(data: dict):
|
idx = load_index()
|
||||||
config.save_config(data)
|
items = [Button("➕ Nouveau Projet", cls="export-btn mb-3", style="background:#10b981; color:#0f172a;", hx_get="/session/new", hx_target="#chat-history")]
|
||||||
return {"status": "success", "message": "Configuration sauvegardée avec succès."}
|
for sid, data in idx.items():
|
||||||
|
items.append(Div(Div(data['title'], cls="truncate flex-grow cursor-pointer text-sm hover:text-cyan-400", hx_get=f"/session/load/{sid}", hx_target="#chat-history"), cls="flex justify-between items-center w-full mb-2 bg-[#2a2a35] p-2 rounded border border-[#444]"))
|
||||||
|
return Div(*items, id="history-list-container", hx_swap_oob="true")
|
||||||
|
|
||||||
@app.post("/api/export")
|
def render_model_dropdown(field_id, placeholder):
|
||||||
async def export_content(req: ExportRequest):
|
groups = {}
|
||||||
filename_base = f"aethas38_{req.type}_{uuid.uuid4().hex[:8]}"
|
for m in MODELS_DATA:
|
||||||
|
prov = m.get('provider', 'Général')
|
||||||
|
if prov not in groups: groups[prov] = []
|
||||||
|
groups[prov].append(m)
|
||||||
|
opts = [Option("--- Laisser vide ---", value="")]
|
||||||
|
for prov in sorted(groups.keys()):
|
||||||
|
grp_opts = [Option(f"{m['name']} ({'Gratuit' if str(m.get('is_free'))=='True' else 'Payant'})", value=m['id'], **{"data-free": str(m.get('is_free', False)).lower()}) for m in groups[prov]]
|
||||||
|
opts.append(Optgroup(label=prov)(*grp_opts))
|
||||||
|
return Div(P(placeholder, style="color:#94a3b8; font-size:10px; font-weight:bold; margin-bottom:2px;"), Select(*opts, name=field_id, id=field_id, cls="model-selector", onchange="filterModels()"), style="margin-bottom: 8px;")
|
||||||
|
|
||||||
if req.format == "txt":
|
SESSION_PROGRESS = {}
|
||||||
return Response(
|
|
||||||
content=req.content,
|
@rt('/status/{sid}')
|
||||||
media_type="text/plain",
|
def get_status(sid: str):
|
||||||
headers={"Content-Disposition": f"attachment; filename={filename_base}.txt"}
|
return SESSION_PROGRESS.get(sid, "⚙️ Traitement en cours...")
|
||||||
|
|
||||||
|
@rt('/')
|
||||||
|
def get(session):
|
||||||
|
if not session.get("authenticated"): return RedirectResponse('/login')
|
||||||
|
session_id = create_new_session()
|
||||||
|
return Title("AETHAS38 Orchestrateur"), Body(
|
||||||
|
Div(
|
||||||
|
Div(
|
||||||
|
H2("AETHAS 38", style="color:#00e5ff; font-weight:bold; font-size:24px;"),
|
||||||
|
Div("⏳ Vérification GitHub...", hx_get="/check_updates", hx_trigger="load", style="font-size:10px; margin-bottom:15px;"),
|
||||||
|
Div(H3("Historique", style="color:#94a3b8; font-size:12px; font-weight:bold; margin-bottom:10px;"), render_history_list(), style="flex-grow:1; overflow:hidden;"),
|
||||||
|
Button("📥 Exporter Modèles", type="button", cls="export-btn mb-2", onclick="window.location.href='/export_models'"),
|
||||||
|
A("Déconnexion", href="/logout", cls="export-btn text-center", style="color:#ef4444; border-color:#ef4444;"),
|
||||||
|
Div(P("BUDGET", style="color:#94a3b8; font-size:10px; font-weight:bold; margin-top:8px;"), P(get_budget(), id="budget-display", style="color:#10b981; font-size:16px; font-weight:bold;")),
|
||||||
|
cls="sidebar"
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Div(id="chat-history", cls="chat-container"),
|
||||||
|
Div(Span("⚙️ Préparation...", id="loading-text"), id="loading-tracker", cls="htmx-indicator"),
|
||||||
|
Form(
|
||||||
|
Input(type="hidden", name="session_id", value=session_id, id="current-session-id"),
|
||||||
|
Div(
|
||||||
|
Div(render_model_dropdown("writer", "1. Rédacteur"), render_model_dropdown("synthesizer", "7. Concaténeur"), style="border-right: 1px solid #444; padding-right:15px;"),
|
||||||
|
Div(render_model_dropdown("worker1", "2. Travailleur 1"), render_model_dropdown("worker2", "3. Travailleur 2"), style="padding: 0 15px;"),
|
||||||
|
cls="grid grid-cols-2 gap-4 mb-4 bg-[#151e2e] p-4 rounded border border-[#333]"
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Input(type="file", name="fichiers", id="file-upload", multiple=True, style="display:none;", onchange="updateFileCount(this)"),
|
||||||
|
Label(Span("📎 Fichiers", id="file-label-text"), _for="file-upload", cls="file-upload-btn w-40"),
|
||||||
|
Input(type="text", name="msg", placeholder="Votre consigne...", cls="input-box", required=True),
|
||||||
|
Button("Lancer", type="submit", cls="send-btn"),
|
||||||
|
cls="input-row"
|
||||||
|
),
|
||||||
|
hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#loading-tracker", enctype="multipart/form-data",
|
||||||
|
onsubmit="return validateMultiIA();"
|
||||||
|
),
|
||||||
|
cls="main-content"
|
||||||
|
),
|
||||||
|
style="display: grid; grid-template-columns: 280px 1fr;"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
elif req.format == "md":
|
@rt('/chat')
|
||||||
return Response(
|
async def post(msg: str, session_id: str, session, writer: str = "", worker1: str = "", worker2: str = "", synthesizer: str = "", fichiers: list[UploadFile] = None):
|
||||||
content=f"# AETHAS38 Export ({req.type.upper()})\n\n{req.content}",
|
if not session.get("authenticated"): return RedirectResponse('/login')
|
||||||
media_type="text/markdown",
|
workers = [w for w in [worker1, worker2] if w]
|
||||||
headers={"Content-Disposition": f"attachment; filename={filename_base}.md"}
|
|
||||||
)
|
|
||||||
|
|
||||||
elif req.format == "json":
|
SESSION_PROGRESS[session_id] = "⚡ Traitement des IA en cours..."
|
||||||
json_data = json.dumps({"type": req.type, "content": req.content}, indent=4, ensure_ascii=False)
|
tasks = [async_ask_llm(session_id, w, msg) for w in workers]
|
||||||
return Response(
|
results = await asyncio.gather(*tasks)
|
||||||
content=json_data,
|
final_response = results[0] if results else "Aucun travailleur configuré."
|
||||||
media_type="application/json",
|
|
||||||
headers={"Content-Disposition": f"attachment; filename={filename_base}.json"}
|
|
||||||
)
|
|
||||||
|
|
||||||
elif req.format == "pdf":
|
session_data = load_session_data(session_id)
|
||||||
# Utilisation de LibreOffice en mode headless
|
session_data.append({"user": msg, "ia": final_response})
|
||||||
html_filename = os.path.join(TEMP_DIR, f"{filename_base}.html")
|
save_session_data(session_id, session_data)
|
||||||
pdf_filename = os.path.join(TEMP_DIR, f"{filename_base}.pdf")
|
|
||||||
|
|
||||||
# Création d'un HTML propre pour la conversion
|
return Div(P("Vous", cls="msg-user"), Div(msg, cls="bubble-user"), P("AETHAS38", cls="msg-ia"), Div(final_response, cls="bubble-ia")), P(get_budget(), id="budget-display", style="color:#10b981; font-size:16px; font-weight:bold;", hx_swap_oob="true")
|
||||||
html_content = f"""
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
body {{ font-family: Arial, sans-serif; margin: 40px; color: #333; }}
|
|
||||||
h1 {{ color: #00e5ff; border-bottom: 2px solid #ddd; padding-bottom: 10px; }}
|
|
||||||
pre, p {{ white-space: pre-wrap; line-height: 1.6; background: #f4f4f4; padding: 15px; border-radius: 5px; }}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>AETHAS38 - Export {req.type.upper()}</h1>
|
|
||||||
<p>{req.content}</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
with open(html_filename, "w", encoding="utf-8") as f:
|
@rt('/export_models')
|
||||||
f.write(html_content)
|
def get(session):
|
||||||
|
if not session.get("authenticated"): return RedirectResponse('/login')
|
||||||
|
return Response("=== CATALOGUE ===\nID: groq|llama-3.1-8b-instant\nTarif: Gratuit", media_type="text/plain", headers={"Content-Disposition": "attachment; filename=modeles.txt"})
|
||||||
|
|
||||||
try:
|
@rt('/session/new')
|
||||||
# Commande LibreOffice headless pour convertir le HTML en PDF
|
def get(session):
|
||||||
# Note: selon l'OS (Windows vs Linux Docker), la commande peut s'appeler 'soffice' ou 'libreoffice'
|
if not session.get("authenticated"): return RedirectResponse('/login')
|
||||||
cmd = ["soffice", "--headless", "--convert-to", "pdf", "--outdir", TEMP_DIR, html_filename]
|
sid = create_new_session()
|
||||||
|
return Div(), Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true"), render_history_list()
|
||||||
|
|
||||||
# Exécution synchrone dans un thread pour ne pas bloquer l'API
|
@rt('/session/load/{sid}')
|
||||||
def run_libreoffice():
|
def get(sid: str, session):
|
||||||
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
if not session.get("authenticated"): return RedirectResponse('/login')
|
||||||
|
messages = load_session_data(sid)
|
||||||
|
return tuple(Div(P("Vous", cls="msg-user"), Div(m['user'], cls="bubble-user"), P("AETHAS38", cls="msg-ia"), Div(m['ia'], cls="bubble-ia")) for m in messages) + (Input(type="hidden", name="session_id", value=sid, id="current-session-id", hx_swap_oob="true"),)
|
||||||
|
|
||||||
await asyncio.to_thread(run_libreoffice)
|
if __name__ == '__main__':
|
||||||
|
import uvicorn
|
||||||
if not os.path.exists(pdf_filename):
|
uvicorn.run(app, host="0.0.0.0", port=5001)
|
||||||
raise Exception("La génération du PDF par LibreOffice a échoué.")
|
|
||||||
|
|
||||||
with open(pdf_filename, "rb") as pdf_file:
|
|
||||||
pdf_bytes = pdf_file.read()
|
|
||||||
|
|
||||||
# Nettoyage des fichiers temporaires
|
|
||||||
os.remove(html_filename)
|
|
||||||
os.remove(pdf_filename)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
content=pdf_bytes,
|
|
||||||
media_type="application/pdf",
|
|
||||||
headers={"Content-Disposition": f"attachment; filename={filename_base}.pdf"}
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Erreur de génération PDF via LibreOffice : {str(e)}")
|
|
||||||
|
|
||||||
raise HTTPException(status_code=400, detail="Format d'export non supporté.")
|
|
||||||
File renamed without changes.
+196
-101
@@ -14,6 +14,8 @@
|
|||||||
.btn-primary { background-color: #00e5ff; color: #0f172a; font-weight: bold; padding: 10px 20px; border-radius: 5px; cursor: pointer; transition: 0.2s; }
|
.btn-primary { background-color: #00e5ff; color: #0f172a; font-weight: bold; padding: 10px 20px; border-radius: 5px; cursor: pointer; transition: 0.2s; }
|
||||||
.btn-primary:hover { background-color: #38bdf8; }
|
.btn-primary:hover { background-color: #38bdf8; }
|
||||||
.btn-primary:disabled { background-color: #334155; color: #94a3b8; cursor: not-allowed; }
|
.btn-primary:disabled { background-color: #334155; color: #94a3b8; cursor: not-allowed; }
|
||||||
|
.btn-export { background-color: #334155; color: #38bdf8; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 4px; cursor: pointer; border: 1px solid #475569; }
|
||||||
|
.btn-export:hover { background-color: #475569; color: #00e5ff; }
|
||||||
.btn-danger { background-color: #f43f5e; color: white; padding: 5px 10px; border-radius: 5px; cursor: pointer; font-size: 12px; }
|
.btn-danger { background-color: #f43f5e; color: white; padding: 5px 10px; border-radius: 5px; cursor: pointer; font-size: 12px; }
|
||||||
.tab-btn { padding: 10px 20px; font-weight: bold; cursor: pointer; transition: 0.2s; border-bottom: 2px solid transparent; }
|
.tab-btn { padding: 10px 20px; font-weight: bold; cursor: pointer; transition: 0.2s; border-bottom: 2px solid transparent; }
|
||||||
.tab-active { color: #00e5ff; border-bottom-color: #00e5ff; }
|
.tab-active { color: #00e5ff; border-bottom-color: #00e5ff; }
|
||||||
@@ -24,77 +26,30 @@
|
|||||||
<body class="p-8">
|
<body class="p-8">
|
||||||
<div id="app" class="max-w-5xl mx-auto">
|
<div id="app" class="max-w-5xl mx-auto">
|
||||||
|
|
||||||
<!-- En-tête -->
|
<!-- En-tête & Suivi financier OpenRouter -->
|
||||||
<div class="flex justify-between items-center mb-6">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h1 class="text-3xl font-bold text-[#00e5ff]">🤖 AETHAS38</h1>
|
<h1 class="text-3xl font-bold text-[#00e5ff]">🤖 AETHAS38</h1>
|
||||||
<span class="text-xs text-gray-400">v2.1.0</span>
|
<div class="bg-slate-800 border border-slate-700 px-4 py-2 rounded text-xs flex space-x-4">
|
||||||
|
<span>💰 OpenRouter - Crédit : <strong class="text-emerald-400">{{ orCredits.limit }}</strong></span>
|
||||||
|
<span>Consommation : <strong class="text-amber-400">{{ orCredits.usage }}</strong></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Onglets -->
|
<!-- Navigation Onglets -->
|
||||||
<div class="flex space-x-2 mb-8 border-b border-gray-700">
|
<div class="flex space-x-2 mb-8 border-b border-gray-700">
|
||||||
<button @click="currentTab = 'admin'" :class="currentTab === 'admin' ? 'tab-active' : 'tab-inactive'" class="tab-btn">⚙️ Administration</button>
|
<button @click="currentTab = 'admin'" :class="currentTab === 'admin' ? 'tab-active' : 'tab-inactive'" class="tab-btn">⚙️ Administration & Modèles</button>
|
||||||
<button @click="currentTab = 'studio'" :class="currentTab === 'studio' ? 'tab-active' : 'tab-inactive'" class="tab-btn">🎬 Studio Multimédia</button>
|
<button @click="currentTab = 'studio'" :class="currentTab === 'studio' ? 'tab-active' : 'tab-inactive'" class="tab-btn">🎬 Studio Média</button>
|
||||||
|
<button @click="currentTab = 'documents'" :class="currentTab === 'documents' ? 'tab-active' : 'tab-inactive'" class="tab-btn">📂 Documents & Exports</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="notification" class="mb-4 p-4 rounded bg-emerald-900/50 border border-emerald-500 text-emerald-400">
|
<div v-if="notification" class="mb-4 p-4 rounded bg-emerald-900/50 border border-emerald-500 text-emerald-400">
|
||||||
{{ notification }}
|
{{ notification }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ONGLET : STUDIO MULTIMÉDIA (ÉTAPE 2) -->
|
<!-- ONGLET : ADMINISTRATION & CATALOGUE DE MODÈLES -->
|
||||||
<div v-if="currentTab === 'studio'">
|
|
||||||
<div class="card">
|
|
||||||
<h2 class="text-xl font-bold text-purple-400 mb-4">Générateur Média (HuggingFace)</h2>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
|
||||||
<div class="md:col-span-3">
|
|
||||||
<label class="text-xs font-bold text-gray-400">Votre Prompt (Description)</label>
|
|
||||||
<input type="text" v-model="mediaPrompt" class="input-box" placeholder="A futuristic cyberpunk city raining at night..." @keyup.enter="generateMedia">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="text-xs font-bold text-gray-400">Type de Média</label>
|
|
||||||
<select v-model="mediaType" class="input-box">
|
|
||||||
<option value="image">🖼️ Image (Stable Diffusion)</option>
|
|
||||||
<option value="voice">🎙️ Voix (Bark TTS)</option>
|
|
||||||
<option value="music">🎵 Musique (MusicGen)</option>
|
|
||||||
<option value="video">🎥 Vidéo (ZeroScope/Damo)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end mt-4">
|
|
||||||
<button @click="generateMedia" :disabled="isGenerating || !mediaPrompt" class="btn-primary">
|
|
||||||
{{ isGenerating ? '⏳ Génération en cours (Patientez)...' : '✨ Générer le Média' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Affichage du Résultat -->
|
|
||||||
<div v-if="generatedMediaUrl" class="card text-center">
|
|
||||||
<h3 class="font-bold text-gray-300 mb-4">Résultat :</h3>
|
|
||||||
|
|
||||||
<img v-if="generatedMediaType === 'image'" :src="generatedMediaUrl" class="max-w-full h-auto mx-auto rounded border border-gray-600 shadow-lg">
|
|
||||||
|
|
||||||
<audio v-if="['music', 'voice'].includes(generatedMediaType)" controls class="w-full mx-auto">
|
|
||||||
<source :src="generatedMediaUrl" type="audio/wav">
|
|
||||||
Votre navigateur ne supporte pas l'audio.
|
|
||||||
</audio>
|
|
||||||
|
|
||||||
<video v-if="generatedMediaType === 'video'" controls class="max-w-full h-auto mx-auto rounded shadow-lg">
|
|
||||||
<source :src="generatedMediaUrl" type="video/mp4">
|
|
||||||
Votre navigateur ne supporte pas la vidéo.
|
|
||||||
</video>
|
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<a :href="generatedMediaUrl" download class="text-[#00e5ff] hover:underline text-sm font-bold">⬇️ Télécharger le fichier</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ONGLET : ADMINISTRATION (ÉTAPE 1) -->
|
|
||||||
<div v-if="currentTab === 'admin'">
|
<div v-if="currentTab === 'admin'">
|
||||||
<!-- Clés API -->
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="text-xl font-bold text-purple-400 mb-4">🔑 Clés API Principales</h2>
|
<h2 class="text-xl font-bold text-purple-400 mb-4">🔑 Clés API & Suivi</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs font-bold text-gray-400">OpenRouter API Key</label>
|
<label class="text-xs font-bold text-gray-400">OpenRouter API Key</label>
|
||||||
@@ -104,37 +59,43 @@
|
|||||||
<label class="text-xs font-bold text-gray-400">Groq API Key</label>
|
<label class="text-xs font-bold text-gray-400">Groq API Key</label>
|
||||||
<input type="password" v-model="config.api_keys.groq" class="input-box" placeholder="gsk_...">
|
<input type="password" v-model="config.api_keys.groq" class="input-box" placeholder="gsk_...">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="text-xs font-bold text-gray-400">Gemini API Key</label>
|
|
||||||
<input type="password" v-model="config.api_keys.gemini" class="input-box" placeholder="AIzaSy...">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fournisseurs Personnalisés -->
|
<!-- Gestion et Choix des Modèles (Gratuit / Payant & Catégorie) -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-4">
|
||||||
<h2 class="text-xl font-bold text-amber-400">🔌 Fournisseurs Personnalisés</h2>
|
<h2 class="text-xl font-bold text-cyan-400">🎛️ Sélection & Filtrage des Modèles</h2>
|
||||||
<button @click="addProvider" class="btn-primary text-sm py-1 px-3">+ Ajouter</button>
|
<div class="space-x-2">
|
||||||
</div>
|
<select v-model="modelFilterPricing" class="bg-slate-800 text-xs text-gray-300 p-2 rounded border border-gray-700">
|
||||||
<div v-for="(provider, index) in config.custom_providers" :key="index" class="p-4 bg-slate-800 rounded border border-slate-700 mb-4">
|
<option value="all">Tous les tarifs</option>
|
||||||
<div class="flex justify-between mb-2">
|
<option value="free">🟢 Gratuit uniquement</option>
|
||||||
<h3 class="font-bold text-sm">Fournisseur #{{ index + 1 }}</h3>
|
<option value="paid">🟠 Payant</option>
|
||||||
<button @click="removeProvider(index)" class="btn-danger">Supprimer</button>
|
</select>
|
||||||
|
<button @click="exportModelsJson" class="btn-export">Exporter Catalogue JSON</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label class="text-xs text-gray-400">Filtrer par Catégorie / Spécialité</label>
|
||||||
|
<input type="text" v-model="modelSearchCat" class="input-box text-xs" placeholder="Ex: Texte, Vision, Code...">
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label class="text-xs text-gray-400">Modèle Actif Sélectionné</label>
|
||||||
|
<input type="text" v-model="selectedActiveModel" class="input-box text-xs text-emerald-400 font-bold" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-slate-900 p-4 rounded border border-gray-700 max-h-60 overflow-y-auto text-xs space-y-2">
|
||||||
|
<div v-for="m in filteredModels" :key="m.id" @click="selectedActiveModel = m.id" class="p-2 bg-slate-800 rounded hover:bg-slate-700 cursor-pointer flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-400">Nom (Ex: HuggingFace)</label>
|
<strong class="text-cyan-300">{{ m.name }}</strong> <span class="text-gray-500">({{ m.id }})</span>
|
||||||
<input type="text" v-model="provider.name" class="input-box">
|
<p class="text-gray-400">{{ m.description }}</p>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="text-xs text-gray-400">URL de Base</label>
|
|
||||||
<input type="text" v-model="provider.base_url" class="input-box" placeholder="https://api...">
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="text-xs text-gray-400">Clé API</label>
|
|
||||||
<input type="password" v-model="provider.api_key" class="input-box" placeholder="hf_...">
|
|
||||||
</div>
|
</div>
|
||||||
|
<span :class="m.pricing === 'free' ? 'text-emerald-400 bg-emerald-950 px-2 py-1 rounded text-[10px]' : 'text-amber-400 bg-amber-950 px-2 py-1 rounded text-[10px]'">
|
||||||
|
{{ m.pricing === 'free' ? 'GRATUIT' : 'PAYANT' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,26 +105,121 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ONGLET : STUDIO MÉDIA -->
|
||||||
|
<div v-if="currentTab === 'studio'">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="text-xl font-bold text-purple-400 mb-4">🎬 Studio Multimédia</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||||
|
<div class="md:col-span-3">
|
||||||
|
<label class="text-xs font-bold text-gray-400">Votre Prompt</label>
|
||||||
|
<input type="text" v-model="mediaPrompt" class="input-box" placeholder="A futuristic city...">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-xs font-bold text-gray-400">Type</label>
|
||||||
|
<select v-model="mediaType" class="input-box">
|
||||||
|
<option value="image">🖼️ Image</option>
|
||||||
|
<option value="voice">🎙️ Voix</option>
|
||||||
|
<option value="music">🎵 Musique</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button @click="generateMedia" :disabled="isGenerating || !mediaPrompt" class="btn-primary">
|
||||||
|
{{ isGenerating ? '⏳ Génération...' : '✨ Générer' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="generatedMediaUrl" class="card text-center">
|
||||||
|
<h3 class="font-bold text-gray-300 mb-4">Résultat :</h3>
|
||||||
|
<img :src="generatedMediaUrl" class="max-w-full h-auto mx-auto rounded border border-gray-600 shadow-lg">
|
||||||
|
<div class="mt-4"><a :href="generatedMediaUrl" download class="text-[#00e5ff] hover:underline text-sm font-bold">⬇️ Télécharger</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ONGLET : DOCUMENTS & EXPORTS -->
|
||||||
|
<div v-if="currentTab === 'documents'">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="text-xl font-bold text-blue-400 mb-4">📂 Analyse de Documents & Exports Multi-formats</h2>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="text-xs font-bold text-gray-400">Sélectionner un fichier (PDF, Word, TXT)</label>
|
||||||
|
<input type="file" @change="handleFileUpload" class="input-box file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-[#00e5ff] file:text-[#0f172a] hover:file:bg-[#38bdf8] cursor-pointer">
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="text-xs font-bold text-gray-400">Instruction associée</label>
|
||||||
|
<textarea v-model="docPrompt" rows="3" class="input-box" placeholder="Ex: Résume ce document..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button @click="uploadDocument" :disabled="isUploading || !selectedFile" class="btn-primary">
|
||||||
|
{{ isUploading ? '⏳ Traitement...' : '🚀 Envoyer et Analyser' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="docResult" class="card">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h3 class="font-bold text-emerald-400">Rapport : {{ docResult.filename }}</h3>
|
||||||
|
<!-- Boutons d'export directs (PDF, TXT, MD, JSON) -->
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<span class="text-xs text-gray-400 self-center">Exporter en :</span>
|
||||||
|
<button @click="exportData(docResult.full_text, 'document_result', 'pdf')" class="btn-export">PDF</button>
|
||||||
|
<button @click="exportData(docResult.full_text, 'document_result', 'txt')" class="btn-export">TXT</button>
|
||||||
|
<button @click="exportData(docResult.full_text, 'document_result', 'md')" class="btn-export">MD</button>
|
||||||
|
<button @click="exportData(docResult.full_text, 'document_result', 'json')" class="btn-export">JSON</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-900 p-4 rounded border border-gray-700 whitespace-pre-wrap text-sm text-gray-200">
|
||||||
|
{{ docResult.extracted_preview }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const { createApp, ref, onMounted } = Vue;
|
const { createApp, ref, computed, onMounted } = Vue;
|
||||||
createApp({
|
createApp({
|
||||||
setup() {
|
setup() {
|
||||||
const currentTab = ref('studio');
|
const currentTab = ref('admin');
|
||||||
const config = ref({ api_keys: {}, smtp: {}, custom_providers: [] });
|
const config = ref({ api_keys: {}, custom_providers: [] });
|
||||||
const notification = ref("");
|
const notification = ref("");
|
||||||
|
const orCredits = ref({ limit: "Chargement...", usage: "..." });
|
||||||
|
|
||||||
|
// Modèles et filtres
|
||||||
|
const modelFilterPricing = ref("free");
|
||||||
|
const modelSearchCat = ref("");
|
||||||
|
const selectedActiveModel = ref("groq/llama-3.1-8b-instant");
|
||||||
|
const availableModels = ref([
|
||||||
|
{ id: "groq/llama-3.1-8b-instant", name: "Groq Llama 3.1 8B", pricing: "free", category: "Texte", description: "Modèle ultra-rapide sur Groq" },
|
||||||
|
{ id: "google/gemini-1.5-flash", name: "Gemini 1.5 Flash", pricing: "free", category: "Vision / Multimodal", description: "Modèle léger Google" },
|
||||||
|
{ id: "anthropic/claude-3.5-sonnet", name: "Claude 3.5 Sonnet", pricing: "paid", category: "Texte / Code", description: "Modèle de pointe Anthropic" },
|
||||||
|
{ id: "black-forest-labs/flux-schnell", name: "Flux Schnell", pricing: "free", category: "Image", description: "Génération d'images rapide" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filteredModels = computed(() => {
|
||||||
|
return availableModels.value.filter(m => {
|
||||||
|
const matchPricing = modelFilterPricing.value === 'all' || m.pricing === modelFilterPricing.value;
|
||||||
|
const matchCat = !modelSearchCat.value || m.category.toLowerCase().includes(modelSearchCat.value.toLowerCase()) || m.name.toLowerCase().includes(modelSearchCat.value.toLowerCase());
|
||||||
|
return matchPricing && matchCat;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Variables Studio
|
|
||||||
const mediaPrompt = ref("");
|
const mediaPrompt = ref("");
|
||||||
const mediaType = ref("image");
|
const mediaType = ref("image");
|
||||||
const isGenerating = ref(false);
|
const isGenerating = ref(false);
|
||||||
const generatedMediaUrl = ref(null);
|
const generatedMediaUrl = ref(null);
|
||||||
const generatedMediaType = ref(null);
|
|
||||||
|
const selectedFile = ref(null);
|
||||||
|
const docPrompt = ref("");
|
||||||
|
const isUploading = ref(false);
|
||||||
|
const docResult = ref(null);
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
const res = await fetch('/api/config');
|
const res = await fetch('/api/config');
|
||||||
config.value = await res.json();
|
config.value = await res.json();
|
||||||
|
|
||||||
|
const credRes = await fetch('/api/openrouter/credits');
|
||||||
|
orCredits.value = await credRes.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveConfig = async () => {
|
const saveConfig = async () => {
|
||||||
@@ -177,13 +233,19 @@
|
|||||||
setTimeout(() => notification.value = "", 3000);
|
setTimeout(() => notification.value = "", 3000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addProvider = () => { config.value.custom_providers.push({ name: "", base_url: "", api_key: "" }); };
|
const exportModelsJson = () => {
|
||||||
const removeProvider = (index) => { config.value.custom_providers.splice(index, 1); };
|
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(availableModels.value, null, 4));
|
||||||
|
const downloadAnchor = document.createElement('a');
|
||||||
|
downloadAnchor.setAttribute("href", dataStr);
|
||||||
|
downloadAnchor.setAttribute("download", "aethas38_models_catalog.json");
|
||||||
|
document.body.appendChild(downloadAnchor);
|
||||||
|
downloadAnchor.click();
|
||||||
|
downloadAnchor.remove();
|
||||||
|
};
|
||||||
|
|
||||||
const generateMedia = async () => {
|
const generateMedia = async () => {
|
||||||
if (!mediaPrompt.value) return;
|
if (!mediaPrompt.value) return;
|
||||||
isGenerating.value = true;
|
isGenerating.value = true;
|
||||||
generatedMediaUrl.value = null;
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/media/generate', {
|
const res = await fetch('/api/media/generate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -191,23 +253,56 @@
|
|||||||
body: JSON.stringify({ prompt: mediaPrompt.value, media_type: mediaType.value })
|
body: JSON.stringify({ prompt: mediaPrompt.value, media_type: mediaType.value })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) generatedMediaUrl.value = data.url;
|
||||||
generatedMediaUrl.value = data.url;
|
else alert(data.detail);
|
||||||
generatedMediaType.value = data.type;
|
} catch (e) { alert("Erreur réseau"); }
|
||||||
} else {
|
finally { isGenerating.value = false; }
|
||||||
alert(data.detail);
|
};
|
||||||
}
|
|
||||||
} catch (e) {
|
const handleFileUpload = (event) => { selectedFile.value = event.target.files[0]; };
|
||||||
alert("Erreur de connexion au serveur.");
|
|
||||||
} finally {
|
const uploadDocument = async () => {
|
||||||
isGenerating.value = false;
|
if (!selectedFile.value) return;
|
||||||
}
|
isUploading.value = true;
|
||||||
|
docResult.value = null;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", selectedFile.value);
|
||||||
|
formData.append("prompt", docPrompt.value);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/documents/process', { method: 'POST', body: formData });
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) docResult.value = data;
|
||||||
|
else alert(data.detail || "Erreur document");
|
||||||
|
} catch (e) { alert("Erreur connexion"); }
|
||||||
|
finally { isUploading.value = false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportData = async (content, type, format) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/export', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ content, format, type })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Erreur export");
|
||||||
|
const blob = await res.blob();
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `aethas38_${type}.${format}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (e) { alert("Erreur téléchargement export."); }
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(loadConfig);
|
onMounted(loadConfig);
|
||||||
return {
|
return {
|
||||||
currentTab, config, notification, saveConfig, addProvider, removeProvider,
|
currentTab, config, notification, orCredits, saveConfig,
|
||||||
mediaPrompt, mediaType, isGenerating, generatedMediaUrl, generatedMediaType, generateMedia
|
modelFilterPricing, modelSearchCat, selectedActiveModel, availableModels, filteredModels, exportModelsJson,
|
||||||
|
mediaPrompt, mediaType, isGenerating, generatedMediaUrl, generateMedia,
|
||||||
|
selectedFile, docPrompt, isUploading, docResult, handleFileUpload, uploadDocument, exportData
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
|
|||||||
Reference in new issue
Block a user