feat: support de 10 fichiers max avec destruction post-envoi, traduction automatique en anglais par le prompteur et traduction en français par le concaténeur en préservant le code
Build and Push Docker Image / build-and-push (push) Successful in 41s

This commit is contained in:
xavier committed 2026-10-08 09:06:12 +02:00
1 parent 48f929a8f3
commit f840561123
4 files changed
+100 -93

No files matched your search

+5 -2
View File
@@ -144,8 +144,11 @@ 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]) @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)): 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 final_content = message.content
if message.file_content and message.file_name: if message.files:
final_content = f"[Fichier attaché : {message.file_name}]\n```\n{message.file_content}\n```\n\n{message.content}" 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
db.add(Message(role=message.role, content=final_content, project_id=project_id)) db.add(Message(role=message.role, content=final_content, project_id=project_id))
db.commit() db.commit()
+40 -63
View File
@@ -15,9 +15,7 @@ def determine_domain(model_id: str) -> str:
return "Texte Polyvalent" return "Texte Polyvalent"
async def sync_finances(db: Session, settings: SystemSettings): async def sync_finances(db: Session, settings: SystemSettings):
"""Interroge les fournisseurs pour récupérer le solde financier exact."""
async with httpx.AsyncClient(timeout=30.0) as client: async with httpx.AsyncClient(timeout=30.0) as client:
# OpenRouter
if settings.openrouter_management_key or settings.openrouter_api_key: if settings.openrouter_management_key or settings.openrouter_api_key:
try: try:
key = settings.openrouter_management_key or settings.openrouter_api_key key = settings.openrouter_management_key or settings.openrouter_api_key
@@ -38,10 +36,7 @@ async def sync_finances(db: Session, settings: SystemSettings):
update_finance_db(db, "OpenRouter", balance, usage) update_finance_db(db, "OpenRouter", balance, usage)
except Exception as e: print(f"Erreur Finance OR: {e}") except Exception as e: print(f"Erreur Finance OR: {e}")
# Groq
if settings.groq_api_key: update_finance_db(db, "Groq", 999.0, 0.0) if settings.groq_api_key: update_finance_db(db, "Groq", 999.0, 0.0)
# DeepSeek
if settings.deepseek_api_key: if settings.deepseek_api_key:
try: try:
resp = await client.get("https://api.deepseek.com/user/balance", headers={"Authorization": f"Bearer {settings.deepseek_api_key}"}) resp = await client.get("https://api.deepseek.com/user/balance", headers={"Authorization": f"Bearer {settings.deepseek_api_key}"})
@@ -49,11 +44,7 @@ async def sync_finances(db: Session, settings: SystemSettings):
infos = resp.json().get("balance_infos", [{}])[0] infos = resp.json().get("balance_infos", [{}])[0]
update_finance_db(db, "DeepSeek", float(infos.get("total_balance", 0)), 0.0) update_finance_db(db, "DeepSeek", float(infos.get("total_balance", 0)), 0.0)
except Exception: pass except Exception: pass
# Mistral AI
if settings.mistral_api_key: update_finance_db(db, "Mistral", 0.0, 0.0) if settings.mistral_api_key: update_finance_db(db, "Mistral", 0.0, 0.0)
# Gemini
if settings.gemini_api_key: update_finance_db(db, "Gemini", 0.0, 0.0) if settings.gemini_api_key: update_finance_db(db, "Gemini", 0.0, 0.0)
try: db.commit() try: db.commit()
@@ -70,11 +61,8 @@ def update_finance_db(db, provider, balance, usage):
async def sync_providers_models(db: Session, settings: SystemSettings, sync_type: str = "Automatique"): async def sync_providers_models(db: Session, settings: SystemSettings, sync_type: str = "Automatique"):
added = 0 added = 0
# On stocke les modèles en mémoire avant de les envoyer en base pour éviter les doublons
models_to_process = {} models_to_process = {}
async with httpx.AsyncClient(timeout=90.0) as client: async with httpx.AsyncClient(timeout=90.0) as client:
# 1. OpenRouter
if settings.openrouter_api_key: if settings.openrouter_api_key:
try: try:
resp = await client.get("https://openrouter.ai/api/v1/models") resp = await client.get("https://openrouter.ai/api/v1/models")
@@ -84,80 +72,62 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
m_id = item.get("id") m_id = item.get("id")
if not m_id: continue if not m_id: continue
pricing = item.get("pricing") or {} pricing = item.get("pricing") or {}
try: pp = float(pricing.get("prompt") or 0.0) * 1000000 pp = float(pricing.get("prompt") or 0.0) * 1000000
except: pp = 0.0 pc = float(pricing.get("completion") or 0.0) * 1000000
try: pc = float(pricing.get("completion") or 0.0) * 1000000
except: pc = 0.0
is_free = (pp == 0.0 and pc == 0.0) is_free = (pp == 0.0 and pc == 0.0)
desc = item.get("description", "Modèle OpenRouter.")[:200] + "..." desc = item.get("description", "Modèle OpenRouter.")[:200] + "..."
models_to_process[m_id] = {"provider": "openrouter", "name": item.get("name", "Inconnu"), "desc": desc, "domain": determine_domain(m_id), "is_free": is_free, "ctx": item.get("context_length", 0), "pp": pp, "pc": pc} models_to_process[m_id] = {"provider": "openrouter", "name": item.get("name", "Inconnu"), "desc": desc, "domain": determine_domain(m_id), "is_free": is_free, "ctx": item.get("context_length", 0), "pp": pp, "pc": pc}
except: pass except: pass
except Exception as e: print(f"Erreur OR Models: {e}") except Exception as e: print(f"Erreur OR Models: {e}")
# 2. Groq
if settings.groq_api_key: if settings.groq_api_key:
try: try:
resp = await client.get("https://api.groq.com/openai/v1/models", headers={"Authorization": f"Bearer {settings.groq_api_key}"}) resp = await client.get("https://api.groq.com/openai/v1/models", headers={"Authorization": f"Bearer {settings.groq_api_key}"})
if resp.status_code == 200: if resp.status_code == 200:
for item in resp.json().get("data", []): for item in resp.json().get("data", []):
try: m_id = item["id"]
m_id = item["id"] models_to_process[m_id] = {"provider": "groq", "name": m_id.capitalize(), "desc": "Modèle rapide LPU Groq.", "domain": determine_domain(m_id), "is_free": True, "ctx": 8192, "pp": 0.0, "pc": 0.0}
models_to_process[m_id] = {"provider": "groq", "name": m_id.capitalize(), "desc": "Modèle rapide LPU Groq.", "domain": determine_domain(m_id), "is_free": True, "ctx": 8192, "pp": 0.0, "pc": 0.0}
except: pass
except Exception: pass except Exception: pass
# 3. DeepSeek
if settings.deepseek_api_key: if settings.deepseek_api_key:
try: try:
resp = await client.get("https://api.deepseek.com/models", headers={"Authorization": f"Bearer {settings.deepseek_api_key}"}) resp = await client.get("https://api.deepseek.com/models", headers={"Authorization": f"Bearer {settings.deepseek_api_key}"})
if resp.status_code == 200: if resp.status_code == 200:
for item in resp.json().get("data", []): for item in resp.json().get("data", []):
try: m_id = item["id"]
m_id = item["id"] models_to_process[m_id] = {"provider": "deepseek", "name": m_id.capitalize(), "desc": "Modèle officiel DeepSeek.", "domain": determine_domain(m_id), "is_free": False, "ctx": 64000, "pp": 0.14, "pc": 0.28}
models_to_process[m_id] = {"provider": "deepseek", "name": m_id.capitalize(), "desc": "Modèle officiel DeepSeek.", "domain": determine_domain(m_id), "is_free": False, "ctx": 64000, "pp": 0.14, "pc": 0.28}
except: pass
except Exception: pass except Exception: pass
# 4. Mistral
if settings.mistral_api_key: if settings.mistral_api_key:
try: try:
resp = await client.get("https://api.mistral.ai/v1/models", headers={"Authorization": f"Bearer {settings.mistral_api_key}"}) resp = await client.get("https://api.mistral.ai/v1/models", headers={"Authorization": f"Bearer {settings.mistral_api_key}"})
if resp.status_code == 200: if resp.status_code == 200:
for item in resp.json().get("data", []): for item in resp.json().get("data", []):
try: m_id = item["id"]
m_id = item["id"] models_to_process[m_id] = {"provider": "mistral", "name": m_id.capitalize(), "desc": "Modèle officiel Mistral AI.", "domain": determine_domain(m_id), "is_free": False, "ctx": 32000, "pp": 0.2, "pc": 0.6}
models_to_process[m_id] = {"provider": "mistral", "name": m_id.capitalize(), "desc": "Modèle officiel Mistral AI.", "domain": determine_domain(m_id), "is_free": False, "ctx": 32000, "pp": 0.2, "pc": 0.6}
except: pass
except Exception: pass except Exception: pass
# 5. Gemini (Google)
if settings.gemini_api_key: if settings.gemini_api_key:
try: try:
resp = await client.get(f"https://generativelanguage.googleapis.com/v1beta/models?key={settings.gemini_api_key}") resp = await client.get(f"https://generativelanguage.googleapis.com/v1beta/models?key={settings.gemini_api_key}")
if resp.status_code == 200: if resp.status_code == 200:
for item in resp.json().get("models", []): for item in resp.json().get("models", []):
try: m_id = item["name"].replace("models/", "")
m_id = item["name"].replace("models/", "") desc = item.get("description", "Modèle Google Gemini.")[:200] + "..."
desc = item.get("description", "Modèle Google Gemini.")[:200] + "..." models_to_process[m_id] = {"provider": "gemini", "name": item.get("displayName", m_id), "desc": desc, "domain": determine_domain(m_id), "is_free": True, "ctx": item.get("inputTokenLimit", 32000), "pp": 0.0, "pc": 0.0}
models_to_process[m_id] = {"provider": "gemini", "name": item.get("displayName", m_id), "desc": desc, "domain": determine_domain(m_id), "is_free": True, "ctx": item.get("inputTokenLimit", 32000), "pp": 0.0, "pc": 0.0}
except: pass
except Exception: pass except Exception: pass
# 6. Cloudflare
if settings.cloudflare_account_id and settings.cloudflare_api_token: if settings.cloudflare_account_id and settings.cloudflare_api_token:
try: try:
url = f"https://api.cloudflare.com/client/v4/accounts/{settings.cloudflare_account_id}/ai/models/search" url = f"https://api.cloudflare.com/client/v4/accounts/{settings.cloudflare_account_id}/ai/models/search"
resp = await client.get(url, headers={"Authorization": f"Bearer {settings.cloudflare_api_token}"}) resp = await client.get(url, headers={"Authorization": f"Bearer {settings.cloudflare_api_token}"})
if resp.status_code == 200: if resp.status_code == 200:
for item in resp.json().get("result", []): for item in resp.json().get("result", []):
try: m_id = item.get("name")
m_id = item.get("name") desc = item.get("description", "Modèle Cloudflare Workers AI.")[:200] + "..."
desc = item.get("description", "Modèle Cloudflare Workers AI.")[:200] + "..." models_to_process[m_id] = {"provider": "cloudflare", "name": m_id.split("/")[-1], "desc": desc, "domain": determine_domain(m_id), "is_free": True, "ctx": 4096, "pp": 0.0, "pc": 0.0}
models_to_process[m_id] = {"provider": "cloudflare", "name": m_id.split("/")[-1], "desc": desc, "domain": determine_domain(m_id), "is_free": True, "ctx": 4096, "pp": 0.0, "pc": 0.0}
except: pass
except Exception: pass except Exception: pass
# --- Phase d'enregistrement sécurisée ---
for m_id, data in models_to_process.items(): for m_id, data in models_to_process.items():
try: try:
existing = db.query(AIModel).filter(AIModel.model_id == m_id).first() existing = db.query(AIModel).filter(AIModel.model_id == m_id).first()
@@ -166,13 +136,9 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
else: else:
db.add(AIModel(provider=data["provider"], model_id=m_id, name=data["name"], description_fr=data["desc"], domain=data["domain"], is_free=data["is_free"], context_length=data["ctx"], pricing_prompt=data["pp"], pricing_completion=data["pc"])) db.add(AIModel(provider=data["provider"], model_id=m_id, name=data["name"], description_fr=data["desc"], domain=data["domain"], is_free=data["is_free"], context_length=data["ctx"], pricing_prompt=data["pp"], pricing_completion=data["pc"]))
added += 1 added += 1
# Commit très fréquent pour éviter les gros blocs qui plantent if added % 50 == 0: db.commit()
if added % 50 == 0: except IntegrityError: db.rollback()
db.commit() except Exception: db.rollback()
except IntegrityError:
db.rollback() # Si conflit, on annule cette insertion et on continue
except Exception:
db.rollback()
try: try:
settings.last_sync_date = datetime.now(timezone.utc) settings.last_sync_date = datetime.now(timezone.utc)
@@ -180,13 +146,11 @@ async def sync_providers_models(db: Session, settings: SystemSettings, sync_type
db.commit() db.commit()
except Exception as e: except Exception as e:
db.rollback() db.rollback()
print(f"Erreur DB Commit Sync: {e}")
await sync_finances(db, settings) await sync_finances(db, settings)
return {"status": "success", "models_processed": added} return {"status": "success", "models_processed": added}
def get_client_for_model(db: Session, model_id: str, settings: SystemSettings): def get_client_for_model(db: Session, model_id: str, settings: SystemSettings):
"""Récupère dynamiquement le bon client OpenAI en fonction du fournisseur du modèle."""
model_db = db.query(AIModel).filter(AIModel.model_id == model_id).first() model_db = db.query(AIModel).filter(AIModel.model_id == model_id).first()
provider = model_db.provider if model_db else "openrouter" provider = model_db.provider if model_db else "openrouter"
@@ -217,26 +181,39 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
final_response = "" final_response = ""
try: 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_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."
optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov)
if len(workers) == 1: if len(workers) == 1:
w_mod = workers[0] w_mod = workers[0]
client, provider = get_client_for_model(db, w_mod, settings) client, provider = get_client_for_model(db, w_mod, settings)
final_response = await ask_agent(client, w_mod, formatted_history + [{"role": "user", "content": user_prompt}], provider) worker_response = await ask_agent(client, w_mod, formatted_history + [{"role": "user", "content": optimized}], provider)
responses = [worker_response]
else: else:
p_mod = config.get("prompter", "gemini-3.5-flash-lite")
p_client, p_prov = get_client_for_model(db, p_mod, settings)
optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": "Optimise cette requête."}, {"role": "user", "content": user_prompt}], p_prov)
w_tasks = [] w_tasks = []
for w in workers: for w in workers:
w_client, w_prov = get_client_for_model(db, w, settings) w_client, w_prov = get_client_for_model(db, w, settings)
w_tasks.append(ask_agent(w_client, w, formatted_history + [{"role": "user", "content": optimized}], w_prov)) 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) responses = await asyncio.gather(*w_tasks, return_exceptions=True)
c_mod = config.get("concatenator", "gemini-3.5-flash-lite") # Étape 2 : Le concaténeur synthétise et traduit le retour en FRANÇAIS (en préservant le code et les commentaires)
c_client, c_prov = get_client_for_model(db, c_mod, settings) c_mod = config.get("concatenator", "gemini-3.5-flash-lite")
synth = f"Requête: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{r}" for i, r in enumerate(responses)]) + "\n\nFais une synthèse finale." c_client, c_prov = get_client_for_model(db, c_mod, settings)
final_response = await ask_agent(c_client, c_mod, [{"role": "user", "content": synth}], c_prov)
concat_system = (
"You are a master lead developer and technical synthesizer. "
"Synthesize the provided expert responses into a single cohesive response. "
"Translate all explanatory text, descriptions, and user-facing prose into natural French. "
"CRITICAL: Do NOT translate code blocks, programming keywords, or source code contents. "
"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)])
final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov)
except Exception as e: except Exception as e:
final_response = f"L'IA a rencontré une erreur critique: {str(e)}" final_response = f"L'IA a rencontré une erreur critique: {str(e)}"
+5 -2
View File
@@ -44,14 +44,17 @@ class OrchestratorConfig(BaseModel):
prompter: Optional[str] = "gemini-3.5-flash-lite" prompter: Optional[str] = "gemini-3.5-flash-lite"
concatenator: Optional[str] = "gemini-3.5-flash-lite" concatenator: Optional[str] = "gemini-3.5-flash-lite"
class AttachedFile(BaseModel):
name: str
content: str
class MessageBase(BaseModel): class MessageBase(BaseModel):
role: str role: str
content: str content: str
class MessageCreate(MessageBase): class MessageCreate(MessageBase):
config: Optional[OrchestratorConfig] = None config: Optional[OrchestratorConfig] = None
file_content: Optional[str] = None files: Optional[List[AttachedFile]] = None
file_name: Optional[str] = None
class MessageResponse(MessageBase): class MessageResponse(MessageBase):
id: int id: int
+50 -26
View File
@@ -383,19 +383,28 @@
</div> </div>
<!-- Champ de texte et boutons d'envoi / fichiers --> <!-- Champ de texte et boutons d'envoi / multi-fichiers (jusqu'à 10) -->
<div class="relative flex items-end bg-gray-800 border border-gray-600 rounded-2xl p-2 shadow-lg focus-within:border-aethas-cyan"> <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">
<!-- Bouton d'import de fichiers -->
<label class="p-3 text-gray-400 hover:text-aethas-cyan cursor-pointer transition-colors" title="Joindre un fichier (code, texte, script, etc.)">
<i data-lucide="paperclip" class="w-5 h-5"></i>
<input type="file" @change="handleFileUpload" class="hidden">
</label>
<textarea v-model="prompt" @input="autoResize" @paste="handlePaste" @keydown.enter.exact.prevent="sendMessage" ref="promptTextarea" :disabled="isAiThinking" rows="1" class="w-full bg-transparent text-gray-200 p-3 max-h-[200px] overflow-y-auto focus:outline-none resize-none disabled:opacity-50 custom-scrollbar" :placeholder="attachedFile ? 'Fichier attaché : ' + attachedFile.name : 'Posez votre question ou glissez un fichier ici...'"></textarea>
<button v-if="attachedFile" @click="removeFile" class="p-2 text-red-400 hover:text-red-300 text-xs flex items-center gap-1 mr-2" title="Retirer le fichier"><i data-lucide="x-circle" class="w-4 h-4"></i></button> <!-- 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>
<span class="truncate max-w-[150px]">{{ file.name }}</span>
<button @click="removeFile(index)" class="text-red-400 hover:text-red-300"><i data-lucide="x" class="w-3 h-3"></i></button>
</div>
</div>
<button @click="sendMessage" :disabled="(!prompt.trim() && !attachedFile) || isAiThinking" class="p-3 text-aethas-cyan hover:text-white disabled:opacity-50"><i data-lucide="send" class="w-5 h-5"></i></button> <div class="w-full flex items-end">
<label class="p-3 text-gray-400 hover:text-aethas-cyan cursor-pointer transition-colors" title="Joindre des fichiers (max 10)">
<i data-lucide="paperclip" class="w-5 h-5"></i>
<input type="file" multiple @change="handleFileUpload" class="hidden">
</label>
<textarea v-model="prompt" @input="autoResize" @paste="handlePaste" @keydown.enter.exact.prevent="sendMessage" ref="promptTextarea" :disabled="isAiThinking" rows="1" class="w-full bg-transparent text-gray-200 p-3 max-h-[200px] overflow-y-auto focus:outline-none resize-none disabled:opacity-50 custom-scrollbar" :placeholder="attachedFiles.length > 0 ? attachedFiles.length + ' fichier(s) attaché(s)' : 'Posez votre question ou glissez jusqu\'à 10 fichiers ici...'"></textarea>
<button @click="sendMessage" :disabled="(!prompt.trim() && attachedFiles.length === 0) || isAiThinking" class="p-3 text-aethas-cyan hover:text-white disabled:opacity-50"><i data-lucide="send" class="w-5 h-5"></i></button>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -441,7 +450,7 @@
const selectedPrompter = ref("gemini-3.5-flash-lite"); const selectedPrompter = ref("gemini-3.5-flash-lite");
const selectedConcatenator = ref("gemini-3.5-flash-lite"); const selectedConcatenator = ref("gemini-3.5-flash-lite");
const attachedFile = ref(null); const attachedFiles = ref([]); // Liste de max 10 fichiers
const pinnedProjects = computed(() => projects.value.filter(p => p.is_pinned)); const pinnedProjects = computed(() => projects.value.filter(p => p.is_pinned));
const recentProjects = computed(() => projects.value.filter(p => !p.is_pinned)); const recentProjects = computed(() => projects.value.filter(p => !p.is_pinned));
@@ -516,9 +525,15 @@
}; };
const handleFileUpload = (event) => { const handleFileUpload = (event) => {
const file = event.target.files[0]; const files = event.target.files;
if (!file) return; if (!files) return;
processFile(file); for (let i = 0; i < files.length; i++) {
if (attachedFiles.value.length >= 10) {
alert("Maximum 10 fichiers autorisés par message.");
break;
}
processFile(files[i]);
}
event.target.value = null; event.target.value = null;
}; };
@@ -528,7 +543,13 @@
let item = items[index]; let item = items[index];
if (item.kind === 'file') { if (item.kind === 'file') {
let file = item.getAsFile(); let file = item.getAsFile();
if (file) processFile(file); if (file) {
if (attachedFiles.value.length >= 10) {
alert("Maximum 10 fichiers autorisés par message.");
break;
}
processFile(file);
}
} }
} }
}; };
@@ -536,12 +557,12 @@
const processFile = (file) => { const processFile = (file) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
attachedFile.value = { name: file.name, content: e.target.result }; attachedFiles.value.push({ name: file.name, content: e.target.result });
}; };
reader.readAsText(file); reader.readAsText(file);
}; };
const removeFile = () => { attachedFile.value = null; }; const removeFile = (index) => { attachedFiles.value.splice(index, 1); };
const exportProject = (format) => { if(activeProject.value) window.open(`/api/projects/${activeProject.value.id}/export?format=${format}`, '_blank'); }; const exportProject = (format) => { if(activeProject.value) window.open(`/api/projects/${activeProject.value.id}/export?format=${format}`, '_blank'); };
const copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); alert("Copié !"); } catch (e) {} }; const copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); alert("Copié !"); } catch (e) {} };
@@ -555,7 +576,7 @@
const sendMessage = async () => { const sendMessage = async () => {
const activeWorkers = workers.value.filter(w => w && w.trim() !== ""); const activeWorkers = workers.value.filter(w => w && w.trim() !== "");
if ((!prompt.value.trim() && !attachedFile.value) || !activeProject.value || activeWorkers.length === 0) { if ((!prompt.value.trim() && attachedFiles.value.length === 0) || !activeProject.value || activeWorkers.length === 0) {
alert("Veuillez sélectionner au moins un travailleur et rédiger ou joindre un contenu."); alert("Veuillez sélectionner au moins un travailleur et rédiger ou joindre un contenu.");
return; return;
} }
@@ -563,24 +584,27 @@
prompt.value = ""; prompt.value = "";
autoResize(); autoResize();
messages.value.push({ id: Date.now(), role: 'user', content: userText + (attachedFile.value ? `\n[Fichier joint: ${attachedFile.value.name}]` : "") }); const fileNames = attachedFiles.value.map(f => f.name).join(", ");
messages.value.push({ id: Date.now(), role: 'user', content: userText + (attachedFiles.value.length > 0 ? `\n[Fichiers joints: ${fileNames}]` : "") });
isAiThinking.value = true; isAiThinking.value = true;
scrollToBottom(); scrollToBottom();
// Préparation du payload avec copie des fichiers avant destruction
const payloadFiles = [...attachedFiles.value];
const payload = { const payload = {
role: 'user', role: 'user',
content: userText || "Analyse ce fichier joint.", content: userText || "Analyse ces fichiers joints.",
config: { config: {
workers: activeWorkers, workers: activeWorkers,
prompter: selectedPrompter.value, prompter: selectedPrompter.value,
concatenator: selectedConcatenator.value concatenator: selectedConcatenator.value
}, },
file_content: attachedFile.value ? attachedFile.value.content : null, files: payloadFiles
file_name: attachedFile.value ? attachedFile.value.name : null
}; };
// DESTRUCTION IMMÉDIATE DU FICHIER CÔTÉ FRONTEND APRÈS ENVOI // DESTRUCTION AUTOMATIQUE DES FICHIERS JOINTS APRÈS ENVOI
attachedFile.value = null; attachedFiles.value = [];
try { try {
const response = await fetch(`/api/projects/${activeProject.value.id}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const response = await fetch(`/api/projects/${activeProject.value.id}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
@@ -615,7 +639,7 @@
isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar, isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar,
modelInfo, financesList, filterSearch, filterDomain, filterPricing, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing, modelInfo, financesList, filterSearch, filterDomain, filterPricing, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing,
workers, selectedPrompter, selectedConcatenator, colPricing, colDomain, workers, selectedPrompter, selectedConcatenator, colPricing, colDomain,
attachedFile, handleFileUpload, handlePaste, removeFile, attachedFiles, handleFileUpload, handlePaste, removeFile,
exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage
}; };
} }