Compare commits

..
3 Commits
Author SHA1 Message Date
xavier e0f504c8ee fix(ui): restauration de l'accès à la vue complète de configuration (Profil, Admin, Super-Admin) suite au crash de Vue.js
Build and Push Docker Image / build-and-push (push) Successful in 44s
2026-10-08 19:11:17 +02:00
xavier 487da2ed4d feat: auto-renommage des projets, assignation de modèles par clic, nettoyage automatique à 7 jours
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-08 18:55:46 +02:00
xavier 108ba8f167 fix(ui): restauration de l'accès à la vue complète de configuration (Profil, Admin, Super-Admin)
Build and Push Docker Image / build-and-push (push) Successful in 49s
2026-10-08 18:33:46 +02:00
2 changed files with 161 additions and 24 deletions

No files matched your search

+30 -16
View File
@@ -4,7 +4,7 @@ from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List
import os, json, asyncio, shutil, base64, io, csv, zipfile import os, json, asyncio, shutil, base64, io, csv, zipfile
from datetime import datetime from datetime import datetime, timedelta, timezone
import pytz import pytz
from .database import engine, Base, get_db, SessionLocal from .database import engine, Base, get_db, SessionLocal
@@ -28,11 +28,27 @@ async def scheduler_task():
now = datetime.now(tz) now = datetime.now(tz)
if (now.hour == 0 or now.hour == 12) and now.minute == 0: if (now.hour == 0 or now.hour == 12) and now.minute == 0:
db = SessionLocal() db = SessionLocal()
try:
# 1. Sync des modèles
settings = db.query(SystemSettings).first() settings = db.query(SystemSettings).first()
if settings: if settings:
try: await sync_providers_models(db, settings, "Automatique") try: await sync_providers_models(db, settings, "Automatique")
except: pass 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() db.close()
await asyncio.sleep(60) await asyncio.sleep(60)
await asyncio.sleep(30) 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]) @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)):
# 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 = [] extracted_files_data = []
files_names = [] files_names = []
@@ -307,18 +335,4 @@ def export_models(db: Session = Depends(get_db), current_user: User = Depends(ge
desc = m.description_fr.replace('\n', ' ') if m.description_fr else "" 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"- **{m.name or m.model_id}** (`{m.model_id}`)\n"
md_content += f" - *Domaine :* {m.domain}\n" md_content += f" - *Domaine :* {m.domain}\n"
md_content += f" - *Prix (1M tokens) :* {price_info}\n" md_
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"}
)
+131 -8
View File
@@ -23,6 +23,48 @@
<div id="app" class="flex w-full h-full relative bg-aethas-dark"> <div id="app" class="flex w-full h-full relative bg-aethas-dark">
<!-- MODAL ASSIGNATION MODÈLE -->
<div v-if="isAssignModalOpen" class="fixed inset-0 bg-black/80 z-[100] flex items-center justify-center p-4">
<div class="bg-aethas-panel border border-gray-600 rounded-2xl w-full max-w-md overflow-hidden shadow-[0_0_30px_rgba(0,0,0,0.7)]">
<div class="p-4 border-b border-gray-700 flex justify-between items-center bg-gray-800/50">
<h2 class="text-lg font-bold text-white flex items-center gap-2"><i data-lucide="link" class="w-5 h-5 text-aethas-cyan"></i> Assigner un modèle</h2>
<button @click="closeAssignModal" class="text-gray-400 hover:text-white"><i data-lucide="x" class="w-5 h-5"></i></button>
</div>
<div class="p-6 space-y-4">
<div v-if="assignMessage" class="p-3 rounded text-sm text-center bg-green-900/50 text-green-200 border border-green-500">{{ assignMessage }}</div>
<div>
<label class="block text-sm text-gray-300 mb-1">Modèle sélectionné</label>
<input type="text" :value="assignForm.modelId" disabled class="w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-2 text-aethas-cyan font-mono text-xs">
</div>
<div>
<label class="block text-sm text-gray-300 mb-1">Associer à la discussion :</label>
<select v-model="assignForm.projectId" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan focus:outline-none text-gray-200">
<option v-for="p in projects" :key="p.id" :value="p.id">{{ p.title }}</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-300 mb-1">Définir le rôle :</label>
<select v-model="assignForm.role" class="w-full bg-aethas-dark border border-gray-600 rounded-lg px-4 py-2 focus:border-aethas-cyan focus:outline-none text-gray-200">
<option value="prompter">Prompteur</option>
<option value="worker1">Travailleur 1</option>
<option value="worker2">Travailleur 2</option>
<option value="worker3">Travailleur 3</option>
<option value="worker4">Travailleur 4</option>
<option value="worker5">Travailleur 5</option>
<option value="concatenator">Concaténeur</option>
</select>
</div>
<button @click="confirmAssignModel" class="w-full mt-4 bg-aethas-cyan hover:bg-cyan-600 text-black font-bold py-2.5 px-4 rounded-lg transition-colors">
Valider l'assignation
</button>
</div>
</div>
</div>
<!-- MODAL PARAMÈTRES --> <!-- MODAL PARAMÈTRES -->
<div v-if="isSettingsModalOpen" class="fixed inset-0 bg-black/80 z-[100] flex items-center justify-center p-4"> <div v-if="isSettingsModalOpen" class="fixed inset-0 bg-black/80 z-[100] flex items-center justify-center p-4">
<div class="bg-aethas-panel border border-gray-600 rounded-2xl w-full max-w-md overflow-hidden shadow-[0_0_30px_rgba(0,0,0,0.7)]"> <div class="bg-aethas-panel border border-gray-600 rounded-2xl w-full max-w-md overflow-hidden shadow-[0_0_30px_rgba(0,0,0,0.7)]">
@@ -77,7 +119,10 @@
</ul> </ul>
</div> </div>
<div> <div>
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2">Récents</h3> <div class="flex items-center justify-between mb-2">
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Récents</h3>
<span class="text-[9px] text-red-400/80" title="Les discussions non épinglées de plus de 7 jours sont supprimées automatiquement.">Auto-suppression > 7j</span>
</div>
<ul class="space-y-1"> <ul class="space-y-1">
<li v-for="project in recentProjects" :key="project.id" @click="selectProject(project)" :class="['group flex items-center justify-between p-2 rounded cursor-pointer text-sm', activeView === 'chat' && activeProject && activeProject.id === project.id ? 'bg-gray-700 text-white' : 'hover:bg-gray-800 text-gray-300']"> <li v-for="project in recentProjects" :key="project.id" @click="selectProject(project)" :class="['group flex items-center justify-between p-2 rounded cursor-pointer text-sm', activeView === 'chat' && activeProject && activeProject.id === project.id ? 'bg-gray-700 text-white' : 'hover:bg-gray-800 text-gray-300']">
<div class="flex items-center gap-2 truncate"><i data-lucide="message-square" class="w-3 h-3 text-gray-500"></i><span class="truncate">{{ project.title }}</span></div> <div class="flex items-center gap-2 truncate"><i data-lucide="message-square" class="w-3 h-3 text-gray-500"></i><span class="truncate">{{ project.title }}</span></div>
@@ -244,16 +289,29 @@
<option value="paid">Payants</option> <option value="paid">Payants</option>
</select> </select>
<!-- NOUVEAU BOUTON D'EXPORT CSV -->
<button @click="exportCSV" class="bg-aethas-panel border border-gray-600 hover:border-aethas-cyan text-gray-300 hover:text-white px-3 py-1.5 rounded flex items-center gap-2 text-sm transition-colors md:ml-auto"> <button @click="exportCSV" class="bg-aethas-panel border border-gray-600 hover:border-aethas-cyan text-gray-300 hover:text-white px-3 py-1.5 rounded flex items-center gap-2 text-sm transition-colors md:ml-auto">
<i data-lucide="download" class="w-4 h-4 text-green-500"></i> Export CSV <i data-lucide="download" class="w-4 h-4 text-green-500"></i> Export CSV
</button> </button>
</div> </div>
<div class="overflow-x-auto max-h-[500px] custom-scrollbar"> <div class="overflow-x-auto max-h-[500px] custom-scrollbar">
<table class="w-full text-left text-sm text-gray-300"> <table class="w-full text-left text-sm text-gray-300">
<thead class="text-xs uppercase bg-gray-800 text-gray-400 sticky top-0"><tr><th class="px-4 py-3">Modèle</th><th class="px-4 py-3">Fournisseur</th><th class="px-4 py-3">Domaine</th><th class="px-4 py-3">Coût (1M Tokens)</th><th class="px-4 py-3">Contexte</th><th class="px-4 py-3 w-1/3">Description</th></tr></thead> <thead class="text-xs uppercase bg-gray-800 text-gray-400 sticky top-0">
<tr>
<th class="px-4 py-3 text-center">Assigner</th>
<th class="px-4 py-3">Modèle</th>
<th class="px-4 py-3">Fournisseur</th>
<th class="px-4 py-3">Domaine</th>
<th class="px-4 py-3">Coût (1M Tokens)</th>
<th class="px-4 py-3">Contexte</th>
<th class="px-4 py-3 w-1/3">Description</th>
</tr>
</thead>
<tbody> <tbody>
<tr v-for="model in filteredModels" :key="model.model_id" class="border-b border-gray-700 hover:bg-gray-800/50 transition-colors"> <tr v-for="model in filteredModels" :key="model.model_id" class="border-b border-gray-700 hover:bg-gray-800/50 transition-colors">
<!-- PUCE DE SELECTION -->
<td class="px-4 py-3 text-center">
<input type="radio" name="modelSelect" @click.prevent="openAssignModal(model.model_id)" class="w-4 h-4 text-aethas-cyan bg-gray-900 border-gray-600 focus:ring-aethas-cyan cursor-pointer">
</td>
<td class="px-4 py-3 font-mono text-xs text-aethas-cyan break-all">{{ model.model_id }}</td> <td class="px-4 py-3 font-mono text-xs text-aethas-cyan break-all">{{ model.model_id }}</td>
<td class="px-4 py-3 capitalize">{{ model.provider }}</td> <td class="px-4 py-3 capitalize">{{ model.provider }}</td>
<td class="px-4 py-3"><span class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-xs">{{ model.domain }}</span></td> <td class="px-4 py-3"><span class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-xs">{{ model.domain }}</span></td>
@@ -453,9 +511,14 @@
const isSyncing = ref(false); const isSyncing = ref(false);
const isSavingSettings = ref(false); const isSavingSettings = ref(false);
const isSettingsModalOpen = ref(false); const isAssignModalOpen = ref(false);
const settingsMessage = ref(""); const assignForm = reactive({ modelId: '', projectId: '', role: 'worker1' });
const assignMessage = ref("");
const isSettingsModalOpen = ref(false); // Ajout crucial : initialisation de la variable du modal paramètres
const passForm = reactive({ old: '', new: '', confirm: '' }); const passForm = reactive({ old: '', new: '', confirm: '' });
const settingsMessage = ref("");
const modelInfo = reactive({ last_sync_date: null, last_sync_type: null, models: [] }); const modelInfo = reactive({ last_sync_date: null, last_sync_type: null, models: [] });
const financesList = ref([]); const financesList = ref([]);
@@ -528,8 +591,59 @@
const autoResize = () => { const el = promptTextarea.value; if(el){ el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }}; const autoResize = () => { const el = promptTextarea.value; if(el){ el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }};
const scrollToBottom = () => { nextTick(() => { const c = document.getElementById('chat-container'); if (c) c.scrollTop = c.scrollHeight; }); }; const scrollToBottom = () => { nextTick(() => { const c = document.getElementById('chat-container'); if (c) c.scrollTop = c.scrollHeight; }); };
const openSettings = () => { isSettingsModalOpen.value = true; nextTick(() => lucide.createIcons()); }; const openSettings = () => {
const closeSettings = () => { isSettingsModalOpen.value = false; }; activeView.value = 'settings';
if(window.innerWidth < 768) isSidebarOpen.value = false;
nextTick(() => lucide.createIcons());
};
const closeSettings = () => {
isSettingsModalOpen.value = false;
};
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) => { const logToTerminal = async (msg) => {
try { await fetch('/api/logs', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: msg }) }); } catch(e){} try { await fetch('/api/logs', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: msg }) }); } catch(e){}
@@ -863,6 +977,13 @@
isAiThinking.value = false; isAiThinking.value = false;
scrollToBottom(); scrollToBottom();
nextTick(()=>lucide.createIcons()); 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;
}
});
} }
}; };
@@ -893,9 +1014,11 @@
user, activeView, settingsTab, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea, user, activeView, settingsTab, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea,
projects, pinnedProjects, recentProjects, activeProject, messages, isAiThinking, projects, pinnedProjects, recentProjects, activeProject, messages, isAiThinking,
createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject, createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject,
isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar,
modelInfo, financesList, filterSearch, filterDomain, filterPricing, filterProvider, uniqueProviders, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing, modelInfo, financesList, filterSearch, filterDomain, filterPricing, filterProvider, uniqueProviders, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing,
workers, selectedPrompter, selectedConcatenator, colPricing, colDomain, downloadTemplate, exportCSV, handleConfigImport, workers, selectedPrompter, selectedConcatenator, colPricing, colDomain, downloadTemplate, exportCSV, handleConfigImport,
isAssignModalOpen, openAssignModal, closeAssignModal, assignForm, assignMessage, confirmAssignModel,
isSettingsModalOpen, // EXPORTATION OBLIGATOIRE DE LA VARIABLE ICI
attachedFiles, handleFileUpload, handlePaste, removeFile, attachedFiles, handleFileUpload, handlePaste, removeFile,
exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage, systemLogs exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage, systemLogs
}; };