diff --git a/backend/main.py b/backend/main.py index 4a782df..644328b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,7 +4,7 @@ from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session from typing import List import os, json, asyncio, shutil, base64, io, csv, zipfile -from datetime import datetime +from datetime import datetime, timedelta, timezone import pytz from .database import engine, Base, get_db, SessionLocal @@ -28,11 +28,27 @@ async def scheduler_task(): now = datetime.now(tz) if (now.hour == 0 or now.hour == 12) and now.minute == 0: db = SessionLocal() - settings = db.query(SystemSettings).first() - if settings: - try: await sync_providers_models(db, settings, "Automatique") - except: pass - db.close() + try: + # 1. Sync des modèles + settings = db.query(SystemSettings).first() + if settings: + try: await sync_providers_models(db, settings, "Automatique") + except: pass + + # 2. Nettoyage automatique des discussions > 7 jours non épinglées + cutoff = datetime.now(timezone.utc) - timedelta(days=7) + old_projects = db.query(Project).filter(Project.is_pinned == False, Project.created_at < cutoff).all() + if old_projects: + log_activity(f"[Nettoyage] Suppression de {len(old_projects)} discussion(s) de plus de 7 jours.") + for op in old_projects: + db.delete(op) + db.commit() + except Exception as e: + db.rollback() + log_activity(f"[Erreur Nettoyage] {str(e)}") + finally: + db.close() + await asyncio.sleep(60) await asyncio.sleep(30) @@ -189,6 +205,18 @@ 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)): + # 1. Gestion de l'auto-renommage à la 2ème requête + p = db.query(Project).filter(Project.id == project_id).first() + if p and p.title == "Nouvelle discussion": + user_msgs = db.query(Message).filter(Message.project_id == project_id, Message.role == "user").order_by(Message.created_at.asc()).all() + if len(user_msgs) == 1: + first_content = user_msgs[0].content.split("\n\n[Fichiers joints")[0].strip() + new_title = first_content.split('\n')[0][:35].strip() + if not new_title: new_title = "Discussion" + p.title = new_title + ("..." if len(first_content) > 35 else "") + db.commit() + + # 2. Gestion des fichiers extracted_files_data = [] files_names = [] @@ -302,23 +330,9 @@ def export_models(db: Session = Depends(get_db), current_user: User = Depends(ge md_content += f"## Fournisseur : {prov.upper()}\n\n" prov_models = [m for m in models if m.provider == prov] for m in prov_models: - price_info = "**GRATUIT**" if m.is_free else f"In: ${m.pricing_prompt:.2f} / Out: ${m.pricing_completion:.2f}" + price_info = "**GRATUIT**" if m.is_free else f"In: ${m.pricing_prompt:.2f} / Out:${m.pricing_completion:.2f}" ctx_info = f"{int(m.context_length/1000)}k" desc = m.description_fr.replace('\n', ' ') if m.description_fr else "" md_content += f"- **{m.name or m.model_id}** (`{m.model_id}`)\n" md_content += f" - *Domaine :* {m.domain}\n" - md_content += f" - *Prix (1M tokens) :* {price_info}\n" - md_content += f" - *Contexte :* {ctx_info}\n" - md_content += f" - *Description :* {desc}\n\n" - - zip_io = io.BytesIO() - with zipfile.ZipFile(zip_io, mode='w', compression=zipfile.ZIP_DEFLATED) as zf: - zf.writestr("models_export.csv", csv_io.getvalue().encode('utf-8')) - zf.writestr(f"{datetime.now().strftime('%Y%m%d')}-extraction-modeles.md", md_content.encode('utf-8')) - - zip_io.seek(0) - return Response( - content=zip_io.getvalue(), - media_type="application/zip", - headers={"Content-Disposition": f"attachment; filename=aethas38_models_{datetime.now().strftime('%Y%m%d')}.zip"} - ) \ No newline at end of file + md_ \ No newline at end of file diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 6b8de26..3e04411 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -23,6 +23,48 @@
+ +
+
+
+

Assigner un modèle

+ +
+
+
{{ assignMessage }}
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+
-

Récents

+
+

Récents

+ Auto-suppression > 7j +
- + + + + + + + + + + + + + @@ -433,6 +491,10 @@ const isSyncing = ref(false); const isSavingSettings = ref(false); + const isAssignModalOpen = ref(false); + const assignForm = reactive({ modelId: '', projectId: '', role: 'worker1' }); + const assignMessage = ref(""); + const passForm = reactive({ old: '', new: '', confirm: '' }); const settingsMessage = ref(""); @@ -512,6 +574,50 @@ if(window.innerWidth < 768) isSidebarOpen.value = false; nextTick(() => lucide.createIcons()); }; + + const openAssignModal = (modelId) => { + assignForm.modelId = modelId; + assignForm.projectId = activeProject.value ? activeProject.value.id : (projects.value.length > 0 ? projects.value[0].id : ''); + assignForm.role = 'worker1'; + assignMessage.value = ""; + isAssignModalOpen.value = true; + nextTick(() => lucide.createIcons()); + }; + + const closeAssignModal = () => { + isAssignModalOpen.value = false; + }; + + const confirmAssignModel = () => { + if(!assignForm.projectId || !assignForm.role) { + assignMessage.value = "Veuillez sélectionner une discussion et un rôle."; + return; + } + const storageKey = 'aethas_config_' + assignForm.projectId; + let config = { workers: ["", "", "", "", ""], prompter: "", concatenator: "" }; + const saved = localStorage.getItem(storageKey); + if (saved) { + try { config = JSON.parse(saved); } catch(e){} + } + + if(assignForm.role === 'prompter') config.prompter = assignForm.modelId; + else if(assignForm.role === 'concatenator') config.concatenator = assignForm.modelId; + else { + const idx = parseInt(assignForm.role.replace('worker', '')) - 1; + config.workers[idx] = assignForm.modelId; + } + + localStorage.setItem(storageKey, JSON.stringify(config)); + + if (activeProject.value && activeProject.value.id == assignForm.projectId) { + workers.value = config.workers; + selectedPrompter.value = config.prompter; + selectedConcatenator.value = config.concatenator; + } + + assignMessage.value = `Modèle assigné avec succès !`; + setTimeout(() => { closeAssignModal(); }, 1200); + }; const logToTerminal = async (msg) => { try { await fetch('/api/logs', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: msg }) }); } catch(e){} @@ -845,6 +951,13 @@ isAiThinking.value = false; scrollToBottom(); nextTick(()=>lucide.createIcons()); + + fetchProjects().then(() => { + if(activeProject.value) { + const updated = projects.value.find(p => p.id === activeProject.value.id); + if(updated) activeProject.value.title = updated.title; + } + }); } }; @@ -875,9 +988,10 @@ user, activeView, settingsTab, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea, projects, pinnedProjects, recentProjects, activeProject, messages, isAiThinking, createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject, - openSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar, + openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar, modelInfo, financesList, filterSearch, filterDomain, filterPricing, filterProvider, uniqueProviders, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing, workers, selectedPrompter, selectedConcatenator, colPricing, colDomain, downloadTemplate, exportCSV, handleConfigImport, + isAssignModalOpen, openAssignModal, closeAssignModal, assignForm, assignMessage, confirmAssignModel, attachedFiles, handleFileUpload, handlePaste, removeFile, exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage, systemLogs };
ModèleFournisseurDomaineCoût (1M Tokens)ContexteDescription
AssignerModèleFournisseurDomaineCoût (1M Tokens)ContexteDescription
+ + {{ model.model_id }} {{ model.provider }} {{ model.domain }}