Compare commits

...
5 Commits
Author SHA1 Message Date
xavier 83e70eadf6 feat: intégration du découpage en morceaux (chunking), pauses séquentielles anti 429 et plugin de compression de contexte OpenRouter
Build and Push Docker Image / build-and-push (push) Successful in 47s
2026-10-08 11:55:10 +02:00
xavier 60c0699c57 feat: intégration du workflow map-reduce parallèle pour le pré-traitement des fichiers joints, correction de session (7 jours) et redirection auto sur 401
Build and Push Docker Image / build-and-push (push) Successful in 55s
2026-10-08 11:31:42 +02:00
xavier 44bf0b868e feat: intégration du parsing backend (PDF, Excel, VBA) et gestion sécurisée des champs prompteur/concaténeur vides dans les templates
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-08 10:25:02 +02:00
xavier c779c51a03 fix: renforcement drastique de la regex du parseur .md et injection automatique de modèle de secours pour empêcher les cases vides
Build and Push Docker Image / build-and-push (push) Successful in 47s
2026-10-08 09:52:53 +02:00
xavier eaa0ff0564 feat: ajout de l'import de configuration IA via fichier .md et bouton de téléchargement du template
Build and Push Docker Image / build-and-push (push) Successful in 44s
2026-10-08 09:35:13 +02:00
4 changed files with 218 additions and 47 deletions

No files matched your search

+66 -8
View File
@@ -3,7 +3,7 @@ from fastapi.responses import RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from typing import List
import os, json, asyncio, shutil
import os, json, asyncio, shutil, base64, io
from datetime import datetime
import pytz
@@ -73,7 +73,8 @@ def login(login_data: LoginRequest, response: Response, db: Session = Depends(ge
user = db.query(User).filter(User.username == login_data.username).first()
if not user or not verify_password(login_data.password, user.hashed_password): raise HTTPException(status_code=401, detail="Identifiants incorrects.")
if not verify_totp(user.totp_secret, login_data.totp_code): raise HTTPException(status_code=401, detail="2FA invalide.")
response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=3600, samesite="lax")
# Correction : Extension de la durée de session à 7 jours (604800 secondes) pour éviter les erreurs 401 intempestives
response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=604800, samesite="lax")
return {"message": "Connexion réussie"}
@app.get("/dashboard")
@@ -143,21 +144,78 @@ def get_messages(project_id: int, db: Session = Depends(get_db), current_user: U
@app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse])
async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
final_content = message.content
extracted_files_data = []
files_names = []
if message.files:
files_text = ""
for f in message.files:
files_text += f"\n\n[Fichier attaché : {f.name}]\n```\n{f.content}\n```"
final_content = message.content + files_text
files_names.append(f.name)
content = f.content
if content.startswith("data:"):
try:
header, b64data = content.split(",", 1)
file_bytes = base64.b64decode(b64data)
ext = f.name.split('.')[-1].lower()
extracted_text = ""
db.add(Message(role=message.role, content=final_content, project_id=project_id))
if ext == 'pdf':
try:
import PyPDF2
reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
extracted_text = "\n".join([page.extract_text() for page in reader.pages if page.extract_text()])
except ImportError:
extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install PyPDF2' sur le serveur pour lire les PDF.]"
elif ext in ['xls', 'xlsx', 'xlsm', 'xlsb']:
try:
import openpyxl
wb = openpyxl.load_workbook(io.BytesIO(file_bytes), data_only=False)
for sheet_name in wb.sheetnames:
sheet = wb[sheet_name]
extracted_text += f"\n--- Feuille : {sheet_name} ---\n"
for row in sheet.iter_rows(values_only=True):
row_vals = [str(cell) if cell is not None else "" for cell in row]
if any(row_vals):
extracted_text += "\t".join(row_vals) + "\n"
if ext in ['xlsm', 'xlsb', 'xls']:
try:
from oletools.olevba import VBA_Parser
vbaparser = VBA_Parser("filename", data=file_bytes)
if vbaparser.detect_vba_macros():
extracted_text += "\n\n--- MACROS VBA DETECTEES ---\n"
for (filename, stream_path, vba_filename, vba_code) in vbaparser.extract_macros():
extracted_text += f"\n// Module: {vba_filename}\n{vba_code}\n"
except ImportError:
extracted_text += "\n[Extraction VBA impossible: L'administrateur doit exécuter 'pip install oletools' sur le serveur.]\n"
except Exception as e:
extracted_text += f"\n[Erreur de lecture VBA interne: {str(e)}]\n"
except ImportError:
extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install openpyxl' sur le serveur pour lire Excel.]"
else:
extracted_text = file_bytes.decode('utf-8', errors='replace')
extracted_files_data.append({"name": f.name, "content": extracted_text})
except Exception as e:
extracted_files_data.append({"name": f.name, "content": f"[ERREUR DE DECODAGE: {str(e)}]"})
else:
extracted_files_data.append({"name": f.name, "content": content})
# On ne stocke plus le contenu brut des fichiers en DB pour éviter d'exploser le contexte des requêtes suivantes
db_content = message.content
if files_names:
db_content += f"\n\n[Fichiers joints pour analyse : {', '.join(files_names)}]"
db.add(Message(role=message.role, content=db_content, project_id=project_id))
db.commit()
history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
settings = db.query(SystemSettings).first()
conf = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]}
ai_resp = await run_orchestrator(db, history, settings, conf)
# Transmission des données de fichiers en mémoire vive à l'orchestrateur (Map-Reduce)
ai_resp = await run_orchestrator(db, history, settings, conf, extracted_files_data)
db.add(Message(role="assistant", content=ai_resp, project_id=project_id))
db.commit()
+57 -10
View File
@@ -16,10 +16,9 @@ def determine_domain(model_id: str) -> str:
return "Texte Polyvalent"
async def translate_en_to_fr(client: httpx.AsyncClient, text: str) -> str:
"""Traduit automatiquement l'anglais vers le français via l'API publique Google Translate."""
if not text: return "Aucune description fournie."
try:
short_text = text[:300].strip() # Limite stricte pour l'API gratuite
short_text = text[:300].strip()
url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=fr&dt=t&q={urllib.parse.quote(short_text)}"
resp = await client.get(url, timeout=4.0)
if resp.status_code == 200:
@@ -141,7 +140,6 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
models_to_process[m_id] = {"provider": "cloudflare", "name": m_id.split("/")[-1], "desc": "Modèle Serverless Cloudflare Workers AI.", "domain": determine_domain(m_id), "is_free": True, "ctx": 4096, "pp": 0.0, "pc": 0.0}
except Exception: pass
# Phase de traduction asynchrone pour les modèles OpenRouter (les autres sont déjà en français natif)
sem = asyncio.Semaphore(15)
async def process_and_translate(m_id, data, client_session):
async with sem:
@@ -196,20 +194,68 @@ async def ask_agent(client, model_id, messages, provider="openrouter"):
kwargs = {"model": model_id, "messages": messages}
if provider == "openrouter":
kwargs["extra_headers"] = {"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Orchestrator"}
# Activation du plugin natif de compression d'OpenRouter pour éviter le dépassement de contexte
kwargs["extra_body"] = {"plugins": [{"id": "context-compression"}]}
resp = await client.chat.completions.create(**kwargs)
return resp.choices[0].message.content
async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict) -> str:
async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict, extracted_files: list = None) -> str:
workers = config.get("workers", ["gemini-3.5-flash-lite"])
user_prompt = history[-1].content
original_user_text = user_prompt.split("\n\n[Fichiers joints")[0] if "[Fichiers joints" in user_prompt else user_prompt
formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]]
final_response = ""
try:
# Étape 1 : Le prompteur traduit et optimise la requête en ANGLAIS pour les travailleurs
p_mod = config.get("prompter", "gemini-3.5-flash-lite")
p_mod = config.get("prompter")
if not p_mod: p_mod = "gemini-3.5-flash-lite"
p_client, p_prov = get_client_for_model(db, p_mod, settings)
prompt_system = "You are an expert prompt engineer. Translate and optimize the user request into clear, precise English tailored for AI execution."
# --- WORKFLOW MAP-REDUCE : PRÉ-TRAITEMENT SÉQUENTIEL & CHUNKING ---
files_context = ""
if extracted_files:
async def process_single_file(f):
file_sys = "You are an expert data analyst and senior developer. Extract the most important technical information from the file without losing critical code syntax."
content = f['content']
chunk_size = 150000 # Environ 35k à 40k tokens par morceau pour rester très large par rapport aux limites
# CHUNKING : Découpage intelligent si le fichier est massif
if len(content) > chunk_size:
chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
chunk_analyses = []
for idx, chunk in enumerate(chunks):
file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nPartie {idx+1}/{len(chunks)} du fichier '{f['name']}'. Analysez, extrayez et résumez le code, VBA, SQL ou les données pertinentes.\n\nContenu :\n```\n{chunk}\n```"
try:
analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov)
chunk_analyses.append(analysis)
except Exception as e:
chunk_analyses.append(f"[Erreur sur la partie {idx+1}: {str(e)}]")
await asyncio.sleep(1.5) # Pause anti-spam (429) entre les morceaux
return f"\n\n--- Extraction du fichier {f['name']} (en {len(chunks)} parties) ---\n" + "\n".join(chunk_analyses)
else:
file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nAnalysez le fichier ci-dessous. Extrayez, résumez et conservez méticuleusement tout le code, les macros VBA, les requêtes SQL, ou les données métier pertinentes pour répondre à la demande.\n\nFichier : {f['name']}\nContenu :\n```\n{content}\n```"
try:
analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov)
return f"\n\n--- Extraction du fichier {f['name']} ---\n{analysis}"
except Exception as e:
return f"\n\n--- Erreur sur {f['name']} ---\n{str(e)}"
file_analyses = []
for f in extracted_files:
analysis = await process_single_file(f)
file_analyses.append(analysis)
# SÉQUENÇAGE : Pause de 1.5 seconde entre les fichiers pour éviter l'erreur 429
await asyncio.sleep(1.5)
files_context = "".join(file_analyses)
user_prompt = f"{original_user_text}\n\nVoici les données pré-traitées des fichiers joints :\n{files_context}"
# --- OPTIMISATION & TRADUCTION ---
prompt_system = "You are an expert prompt engineer. Translate and optimize the user request and any file context into clear, precise English tailored for AI execution. Keep all code blocks intact."
optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov)
if len(workers) == 1:
@@ -224,8 +270,9 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
w_tasks.append(ask_agent(w_client, w, formatted_history + [{"role": "user", "content": optimized}], w_prov))
responses = await asyncio.gather(*w_tasks, return_exceptions=True)
# Étape 2 : Le concaténeur synthétise et traduit le retour en FRANÇAIS (en préservant le code et les commentaires)
c_mod = config.get("concatenator", "gemini-3.5-flash-lite")
c_mod = config.get("concatenator")
if not c_mod: c_mod = "gemini-3.5-flash-lite"
c_client, c_prov = get_client_for_model(db, c_mod, settings)
concat_system = (
@@ -236,7 +283,7 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
"You may translate code comments into French if appropriate, but leave code syntax strictly intact."
)
synth = f"User Request: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{r}" for i, r in enumerate(responses)])
synth = f"User Request: {original_user_text}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{str(r)}" for i, r in enumerate(responses)])
final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov)
except Exception as e:
+91 -28
View File
@@ -292,11 +292,20 @@
</div>
</div>
<!-- BARRE DE SÉLECTION DE STYLE TABULAIRE (9 COLONNES STRICTES) -->
<div class="p-4 md:p-6 w-full max-w-7xl mx-auto space-y-2 flex-shrink-0">
<div class="grid grid-cols-9 border border-black bg-gray-900 rounded-lg overflow-hidden text-center text-xs font-bold shadow-lg">
<!-- 1. PAYANT / GRATUIT (Filtre par colonne local sans impacter workers) -->
<div class="p-4 md:px-6 md:pt-6 pb-2 w-full max-w-7xl mx-auto flex-shrink-0">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Configuration des IA</span>
<div class="flex gap-2">
<button @click="downloadTemplate" class="text-xs bg-gray-800 hover:bg-gray-700 text-aethas-cyan border border-gray-600 px-2 py-1 rounded flex items-center gap-1 transition-colors" title="Télécharger le modèle .md"><i data-lucide="download" class="w-3 h-3"></i> Template MD</button>
<label class="text-xs bg-gray-800 hover:bg-gray-700 text-aethas-cyan border border-gray-600 px-2 py-1 rounded flex items-center gap-1 transition-colors cursor-pointer" title="Importer une configuration .md">
<i data-lucide="upload" class="w-3 h-3"></i> Importer
<input type="file" accept=".md" @change="handleConfigImport" class="hidden">
</label>
</div>
</div>
<!-- BARRE DE SÉLECTION -->
<div class="grid grid-cols-9 border border-black bg-gray-900 rounded-lg overflow-hidden text-center text-xs font-bold shadow-lg mb-4">
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-gray-400 mb-1">PRIX</label>
<select v-model="colPricing" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none">
@@ -306,7 +315,6 @@
</select>
</div>
<!-- 2. TYPE (DOMAINE) (Filtre par colonne local sans impacter workers) -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-gray-400 mb-1">TYPE</label>
<select v-model="colDomain" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none">
@@ -318,7 +326,6 @@
</select>
</div>
<!-- 3. TRAVAILLEUR 1 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 1</label>
<select v-model="workers[0]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -327,7 +334,6 @@
</select>
</div>
<!-- 4. TRAVAILLEUR 2 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 2</label>
<select v-model="workers[1]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -336,7 +342,6 @@
</select>
</div>
<!-- 5. TRAVAILLEUR 3 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 3</label>
<select v-model="workers[2]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -345,7 +350,6 @@
</select>
</div>
<!-- 6. TRAVAILLEUR 4 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 4</label>
<select v-model="workers[3]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -354,7 +358,6 @@
</select>
</div>
<!-- 7. TRAVAILLEUR 5 -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-cyan mb-1">TRAVAILLEUR 5</label>
<select v-model="workers[4]" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
@@ -363,30 +366,25 @@
</select>
</div>
<!-- 8. PROMPTEUR -->
<div class="border-r border-black p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-gray-400 mb-1">PROMPTEUR</label>
<select v-model="selectedPrompter" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
<option value="gemini-3.5-flash-lite">Gemini Flash</option>
<option value="">(Par défaut)</option>
<option v-for="m in getFilteredModelsForCol(colPricing, colDomain, selectedPrompter)" :key="m.model_id" :value="m.model_id">{{ m.name || m.model_id }}</option>
</select>
</div>
<!-- 9. CONCATENEUR -->
<div class="p-1.5 bg-gray-800 flex flex-col justify-center">
<label class="block text-[9px] text-aethas-purple mb-1">CONCATENEUR</label>
<select v-model="selectedConcatenator" class="w-full bg-aethas-dark border border-gray-700 rounded text-gray-200 text-[10px] p-1 outline-none truncate">
<option value="gemini-3.5-flash-lite">Gemini Flash</option>
<option value="">(Par défaut)</option>
<option v-for="m in getFilteredModelsForCol(colPricing, colDomain, selectedConcatenator)" :key="m.model_id" :value="m.model_id">{{ m.name || m.model_id }}</option>
</select>
</div>
</div>
<!-- Champ de texte et boutons d'envoi / multi-fichiers (jusqu'à 10) -->
<!-- CHAT INPUT -->
<div class="relative flex items-end bg-gray-800 border border-gray-600 rounded-2xl p-2 shadow-lg focus-within:border-aethas-cyan flex-col gap-2">
<!-- Liste des fichiers attachés (max 10) -->
<div v-if="attachedFiles.length > 0" class="w-full flex flex-wrap gap-2 px-2 pt-2">
<div v-for="(file, index) in attachedFiles" :key="index" class="bg-gray-900 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 flex items-center gap-2">
<i data-lucide="file" class="w-3.5 h-3.5 text-aethas-cyan"></i>
@@ -446,9 +444,9 @@
const colPricing = ref("");
const colDomain = ref("");
const workers = ref(["gemini-3.5-flash-lite", "", "", "", ""]);
const selectedPrompter = ref("gemini-3.5-flash-lite");
const selectedConcatenator = ref("gemini-3.5-flash-lite");
const workers = ref(["", "", "", "", ""]);
const selectedPrompter = ref("");
const selectedConcatenator = ref("");
const attachedFiles = ref([]);
@@ -467,14 +465,26 @@
});
const getFilteredModelsForCol = (pricingFilter, domainFilter, currentSelectedId) => {
return modelInfo.models.filter(m => {
if (m.model_id === currentSelectedId) return true;
let filtered = modelInfo.models.filter(m => {
const matchDomain = domainFilter === "" || m.domain === domainFilter;
let matchPrice = true;
if (pricingFilter === 'free') matchPrice = m.is_free;
if (pricingFilter === 'paid') matchPrice = !m.is_free;
return matchDomain && matchPrice;
});
if (currentSelectedId && currentSelectedId.trim() !== "") {
const exists = filtered.find(m => m.model_id === currentSelectedId);
if (!exists) {
const original = modelInfo.models.find(m => m.model_id === currentSelectedId);
if (original) {
filtered.unshift(original);
} else {
filtered.unshift({ model_id: currentSelectedId, name: currentSelectedId, domain: "Importé", pricing_prompt: 0, pricing_completion: 0 });
}
}
}
return filtered;
};
const formatDate = (isoString) => {
@@ -502,7 +512,6 @@
await fetchModelsInfo();
await fetchFinances();
// GENERATION DU FICHIER MARKDOWN A L'EXTRACTION
let mdContent = `# Extraction des Modèles IA - AETHAS38\n\n`;
const d = new Date();
const dateStr = d.getFullYear() + String(d.getMonth()+1).padStart(2, '0') + String(d.getDate()).padStart(2, '0');
@@ -535,6 +544,41 @@
isSyncing.value = false;
};
const downloadTemplate = () => {
const templateContent = `# AETHAS38 - Configuration Multi-Agents\n\n**Rappel important :**\n- **Prompteur :** Le système de l'orchestrateur utilisera ce modèle pour traduire et optimiser votre requête initiale en ANGLAIS (langue native des IA).\n- **Concaténeur :** Le système utilisera ce modèle pour regrouper les réponses des travailleurs et traduire la synthèse finale en FRANÇAIS (en préservant le code brut).\n\n## Sélection des Modèles\nRenseignez l'ID exact du modèle (ex: gemini-3.5-flash-lite, mistralai/mistral-large-4-0). Laissez vide si inutilisé.\n\n- Prompteur : \n- Travailleur 1 : \n- Travailleur 2 : \n- Travailleur 3 : \n- Travailleur 4 : \n- Travailleur 5 : \n- Concaténeur : \n`;
downloadFile(templateContent, 'aethas38-config-agents.md');
};
const handleConfigImport = (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target.result;
const extractModel = (key) => {
const safeKey = key.replace(/[éèe]/gi, '[éèe]');
const regex = new RegExp(`-\\s*${safeKey}\\s*:\\s*([^\\r\\n]*)`, 'i');
const match = content.match(regex);
return match && match[1] ? match[1].trim() : "";
};
const w1 = extractModel('Travailleur 1');
const w2 = extractModel('Travailleur 2');
const w3 = extractModel('Travailleur 3');
const w4 = extractModel('Travailleur 4');
const w5 = extractModel('Travailleur 5');
workers.value = [w1, w2, w3, w4, w5];
selectedPrompter.value = extractModel('Prompteur');
selectedConcatenator.value = extractModel('Concateneur');
alert("Configuration IA importée et appliquée avec succès !");
};
reader.readAsText(file);
event.target.value = null;
};
const uploadAvatar = async (event) => {
const file = event.target.files[0];
if (!file) return;
@@ -580,10 +624,19 @@
const processFile = (file) => {
const reader = new FileReader();
const textExts = ['.txt', '.md', '.csv', '.json', '.py', '.js', '.html', '.css', '.xml', '.yml', '.yaml', '.sh', '.bat', '.ps1', '.ini', '.cfg', '.conf', '.log'];
const isText = textExts.some(ext => file.name.toLowerCase().endsWith(ext)) || file.type.startsWith('text/');
reader.onload = (e) => {
attachedFiles.value.push({ name: file.name, content: e.target.result });
};
reader.readAsText(file);
// Les fichiers binaires sont lus en Base64.
if (isText) {
reader.readAsText(file);
} else {
reader.readAsDataURL(file);
}
};
const removeFile = (index) => { attachedFiles.value.splice(index, 1); };
@@ -634,10 +687,20 @@
const data = await response.json();
if (Array.isArray(data)) messages.value = data;
await fetchFinances();
} else if (response.status === 401) {
alert("Votre session a expiré. Veuillez vous reconnecter.");
window.location.href = '/login';
return;
} else {
const err = await response.json(); alert("Erreur API IA : " + err.detail);
}
} catch (error) {} finally { isAiThinking.value = false; scrollToBottom(); nextTick(()=>lucide.createIcons()); }
} catch (error) {
alert("Erreur locale lors de l'envoi : " + error.message);
} finally {
isAiThinking.value = false;
scrollToBottom();
nextTick(()=>lucide.createIcons());
}
};
const changePassword = async () => {
@@ -660,7 +723,7 @@
createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject,
isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar,
modelInfo, financesList, filterSearch, filterDomain, filterPricing, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing,
workers, selectedPrompter, selectedConcatenator, colPricing, colDomain,
workers, selectedPrompter, selectedConcatenator, colPricing, colDomain, downloadTemplate, handleConfigImport,
attachedFiles, handleFileUpload, handlePaste, removeFile,
exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage
};
+4 -1
View File
@@ -14,4 +14,7 @@ PyJWT
openai
httpx
python-multipart
pytz
pytz
PyPDF2
openpyxl
oletools