Compare commits

..
4 Commits
Author SHA1 Message Date
xavier e8288731d8 feat: intégration du panneau super-admin (clés API/SMTP), recherche web native OpenRouter, cookie étendu (8h), toast notification et reset textarea
Build and Push Docker Image / build-and-push (push) Successful in 46s
2026-10-09 06:45:10 +02:00
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
4 changed files with 301 additions and 25 deletions

No files matched your search

+46 -3
View File
@@ -4,12 +4,12 @@ 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
from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token
from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse, PasswordChange, ModelReplacementRequest, LogRequest
from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse, PasswordChange, ModelReplacementRequest, LogRequest, SystemSettingsUpdate, SystemSettingsResponse
from .models import User, Project, Message, SystemSettings, AIModel, FinancialLog
from .orchestrator import run_orchestrator, sync_providers_models, sync_finances, activity_logs, log_activity
@@ -28,11 +28,25 @@ async def scheduler_task():
now = datetime.now(tz)
if (now.hour == 0 or now.hour == 12) and now.minute == 0:
db = SessionLocal()
try:
settings = db.query(SystemSettings).first()
if settings:
try: await sync_providers_models(db, settings, "Automatique")
except: pass
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)
@@ -73,7 +87,9 @@ 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=604800, samesite="lax")
# Session valide 8 heures (28800 secondes)
response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=28800, samesite="lax")
return {"message": "Connexion réussie"}
@app.get("/dashboard")
@@ -148,6 +164,23 @@ async def upload_avatar(file: UploadFile = File(...), db: Session = Depends(get_
db.commit()
return {"message": "Avatar mis à jour", "avatar_path": current_user.avatar_path}
# --- ROUTES SUPER-ADMIN ---
@app.get("/api/settings", response_model=SystemSettingsResponse)
def get_settings(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
if not current_user.is_superadmin: raise HTTPException(status_code=403, detail="Super-Admin requis.")
return db.query(SystemSettings).first()
@app.put("/api/settings")
def update_settings(settings_data: SystemSettingsUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
if not current_user.is_superadmin: raise HTTPException(status_code=403, detail="Super-Admin requis.")
s = db.query(SystemSettings).first()
if not s: raise HTTPException(status_code=404)
for k, v in settings_data.dict(exclude_unset=True).items():
setattr(s, k, v)
db.commit()
log_activity("Configuration système mise à jour par le Super-Admin.")
return {"message": "Paramètres mis à jour avec succès."}
@app.get("/api/projects", response_model=List[ProjectResponse])
def get_projects(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): return db.query(Project).filter(Project.user_id == current_user.id).order_by(Project.created_at.desc()).all()
@@ -189,6 +222,16 @@ 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)):
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()
extracted_files_data = []
files_names = []
+2 -3
View File
@@ -206,7 +206,8 @@ 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"}
kwargs["extra_body"] = {"plugins": [{"id": "context-compression"}]}
# Activation native du plugin web-search et de compression
kwargs["extra_body"] = {"plugins": [{"id": "context-compression"}, {"id": "web-search"}]}
resp = await client.chat.completions.create(**kwargs)
return resp.choices[0].message.content
@@ -225,7 +226,6 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
log_activity(f"Démarrage de l'orchestration. Modèle Prompteur: {p_mod}")
p_client, p_prov = get_client_for_model(db, p_mod, settings)
# --- WORKFLOW MAP-REDUCE : PRÉ-TRAITEMENT SÉQUENTIEL & CHUNKING ---
files_context = ""
if extracted_files:
log_activity(f"Traitement Map-Reduce de {len(extracted_files)} fichier(s) joint(s).")
@@ -270,7 +270,6 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings,
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 ---
log_activity(f"Optimisation/Traduction de la requête via Prompteur...")
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)
+20
View File
@@ -20,6 +20,26 @@ class AdminCreate(BaseModel):
cloudflare_api_token: Optional[str] = None
huggingface_api_key: Optional[str] = None
class SystemSettingsBase(BaseModel):
smtp_host: Optional[str] = None
smtp_port: Optional[int] = None
smtp_user: Optional[str] = None
smtp_password: Optional[str] = None
openrouter_api_key: Optional[str] = None
openrouter_management_key: Optional[str] = None
groq_api_key: Optional[str] = None
gemini_api_key: Optional[str] = None
deepseek_api_key: Optional[str] = None
mistral_api_key: Optional[str] = None
cloudflare_account_id: Optional[str] = None
cloudflare_api_token: Optional[str] = None
huggingface_api_key: Optional[str] = None
class SystemSettingsUpdate(SystemSettingsBase): pass
class SystemSettingsResponse(SystemSettingsBase):
class Config: from_attributes = True
class LoginRequest(BaseModel):
username: str
password: str
+233 -19
View File
@@ -23,7 +23,55 @@
<div id="app" class="flex w-full h-full relative bg-aethas-dark">
<!-- MODAL PARAMÈTRES -->
<!-- TOAST NOTIFICATION TEMPORAIRE -->
<div v-if="toastVisible" class="fixed bottom-6 right-6 bg-gray-800 border-l-4 border-aethas-cyan text-gray-200 p-4 rounded shadow-[0_0_15px_rgba(6,182,212,0.3)] z-[200] flex items-center gap-3 transition-opacity duration-300">
<i data-lucide="check-circle" class="w-5 h-5 text-aethas-cyan"></i>
<span class="text-sm font-medium">{{ toastMessage }}</span>
</div>
<!-- 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 (PROFIL) -->
<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="p-4 border-b border-gray-700 flex justify-between items-center bg-gray-800/50">
@@ -77,7 +125,10 @@
</ul>
</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">
<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>
@@ -197,8 +248,33 @@
<div v-if="settingsTab === 'super' && user.is_superadmin" class="space-y-6">
<div class="bg-gray-800 p-6 rounded-xl border border-gray-700 border-l-4 border-l-aethas-purple">
<h3 class="text-lg font-semibold text-purple-400 mb-4">Espace Réservé Super-Admin</h3>
<p class="text-sm text-gray-400">Gestion globale du système.</p>
<h3 class="text-lg font-semibold text-purple-400 mb-4">Espace Réservé Super-Admin - Clés API & SMTP</h3>
<div v-if="adminSettingsMessage" class="p-3 rounded text-sm mb-4 bg-gray-900 border border-aethas-purple text-purple-300">{{ adminSettingsMessage }}</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Clés API -->
<div><label class="block text-xs text-gray-400 mb-1">OpenRouter API Key</label><input v-model="adminSettings.openrouter_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">OpenRouter Management Key</label><input v-model="adminSettings.openrouter_management_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">Gemini API Key</label><input v-model="adminSettings.gemini_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">Groq API Key</label><input v-model="adminSettings.groq_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">DeepSeek API Key</label><input v-model="adminSettings.deepseek_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">Mistral API Key</label><input v-model="adminSettings.mistral_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">HuggingFace API Key</label><input v-model="adminSettings.huggingface_api_key" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">Cloudflare Account ID</label><input v-model="adminSettings.cloudflare_account_id" type="text" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">Cloudflare API Token</label><input v-model="adminSettings.cloudflare_api_token" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<!-- SMTP -->
<div class="col-span-1 md:col-span-2 mt-2 border-t border-gray-700 pt-4"><h4 class="text-sm font-semibold text-gray-300 mb-2">Configuration SMTP</h4></div>
<div><label class="block text-xs text-gray-400 mb-1">SMTP Host</label><input v-model="adminSettings.smtp_host" type="text" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">SMTP Port</label><input v-model.number="adminSettings.smtp_port" type="number" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">SMTP User</label><input v-model="adminSettings.smtp_user" type="text" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
<div><label class="block text-xs text-gray-400 mb-1">SMTP Password</label><input v-model="adminSettings.smtp_password" type="password" class="w-full bg-aethas-dark border border-gray-600 rounded p-2 text-sm text-gray-200 focus:border-aethas-purple outline-none"></div>
</div>
<button @click="saveAdminSettings" :disabled="isSavingAdminSettings" class="mt-6 w-full bg-aethas-purple hover:bg-purple-600 text-white font-bold py-2 rounded transition-colors disabled:opacity-50">
Sauvegarder la configuration
</button>
</div>
</div>
</div>
@@ -244,16 +320,28 @@
<option value="paid">Payants</option>
</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">
<i data-lucide="download" class="w-4 h-4 text-green-500"></i> Export CSV
</button>
</div>
<div class="overflow-x-auto max-h-[500px] custom-scrollbar">
<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>
<tr v-for="model in filteredModels" :key="model.model_id" class="border-b border-gray-700 hover:bg-gray-800/50 transition-colors">
<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 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>
@@ -291,17 +379,21 @@
<div v-if="part.type === 'code'" class="my-4 bg-gray-900 rounded-lg border border-gray-700 overflow-hidden shadow-inner">
<div class="flex justify-between items-center px-4 py-2 bg-gray-800 border-b border-gray-700">
<span class="text-xs text-gray-400 font-mono flex items-center gap-2"><i data-lucide="file-code-2" class="w-4 h-4"></i> {{ part.language || 'code' }}</span>
<div class="flex items-center">
<!-- BOUTON COPIER CODE (NOUVEAU) -->
<button @click="copyToClipboard(part.content)" class="text-aethas-cyan hover:text-white flex items-center gap-1 text-xs transition-colors bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded mr-2"><i data-lucide="copy" class="w-3 h-3"></i> Copier Code</button>
<button @click="downloadFile(part.content, part.language)" class="text-aethas-cyan hover:text-white flex items-center gap-1 text-xs transition-colors bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded"><i data-lucide="download" class="w-3 h-3"></i> Fichier</button>
</div>
</div>
<pre class="p-4 text-xs font-mono text-gray-300 overflow-x-auto custom-scrollbar"><code>{{ part.content }}</code></pre>
</div>
</template>
</div>
<!-- Boutons de copie et téléchargement individuel -->
<!-- Boutons de copie et téléchargement individuel du message -->
<div class="mt-3 flex items-center gap-3 border-t border-gray-700/50 pt-2 opacity-50 hover:opacity-100 transition-opacity">
<button @click="copyToClipboard(msg.content)" class="text-xs text-gray-400 hover:text-aethas-cyan flex items-center gap-1"><i data-lucide="copy" class="w-3 h-3"></i> Copier</button>
<button @click="downloadMessage(msg.content, msg.created_at || new Date().toISOString())" class="text-xs text-gray-400 hover:text-aethas-cyan flex items-center gap-1"><i data-lucide="file-down" class="w-3 h-3"></i> Extraire</button>
<button @click="copyToClipboard(msg.content)" class="text-xs text-gray-400 hover:text-aethas-cyan flex items-center gap-1"><i data-lucide="copy" class="w-3 h-3"></i> Copier Message</button>
<button @click="downloadMessage(msg.content, msg.created_at || new Date().toISOString())" class="text-xs text-gray-400 hover:text-aethas-cyan flex items-center gap-1"><i data-lucide="file-down" class="w-3 h-3"></i> Extraire Message</button>
</div>
</div>
@@ -451,11 +543,30 @@
const isLoading = ref(false);
const isAiThinking = ref(false);
const isSyncing = ref(false);
const isSavingSettings = ref(false);
// TOAST (Non bloquant)
const toastVisible = ref(false);
const toastMessage = ref("");
const showToast = (msg) => {
toastMessage.value = msg;
toastVisible.value = true;
setTimeout(() => { toastVisible.value = false; }, 5000); // Disparaît après 5 secondes
};
const isAssignModalOpen = ref(false);
const assignForm = reactive({ modelId: '', projectId: '', role: 'worker1' });
const assignMessage = ref("");
const isSettingsModalOpen = ref(false);
const settingsMessage = ref("");
// FORMULAIRES SETTINGS
const passForm = reactive({ old: '', new: '', confirm: '' });
const settingsMessage = ref("");
const isSavingSettings = ref(false);
const adminSettings = reactive({});
const adminSettingsMessage = ref("");
const isSavingAdminSettings = ref(false);
const modelInfo = reactive({ last_sync_date: null, last_sync_type: null, models: [] });
const financesList = ref([]);
@@ -528,8 +639,89 @@
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 openSettings = () => { isSettingsModalOpen.value = true; nextTick(() => lucide.createIcons()); };
const closeSettings = () => { isSettingsModalOpen.value = false; };
const fetchAdminSettings = async () => {
try {
const res = await fetch('/api/settings');
if (res.ok) {
const data = await res.json();
Object.assign(adminSettings, data);
}
} catch(e) {}
};
const saveAdminSettings = async () => {
isSavingAdminSettings.value = true;
adminSettingsMessage.value = "";
try {
const res = await fetch('/api/settings', {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(adminSettings)
});
if (res.ok) {
adminSettingsMessage.value = "Paramètres sauvegardés avec succès.";
setTimeout(() => adminSettingsMessage.value="", 3000);
} else {
adminSettingsMessage.value = "Erreur lors de la sauvegarde.";
}
} catch (e) {
adminSettingsMessage.value = "Erreur réseau.";
}
isSavingAdminSettings.value = false;
};
const openSettings = () => {
activeView.value = 'settings';
if(window.innerWidth < 768) isSidebarOpen.value = false;
if(user.value.is_superadmin) fetchAdminSettings();
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) => {
try { await fetch('/api/logs', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ message: msg }) }); } catch(e){}
@@ -612,7 +804,7 @@
if(res.ok) {
await fetchModelsInfo();
await fetchFinances();
alert("Synchronisation réussie !");
showToast("Synchronisation réussie !");
} else if (res.status === 401) {
alert("Votre session a expiré."); window.location.href = '/login';
} else {
@@ -685,7 +877,7 @@
selectedConcatenator.value = extractModel('Concateneur');
await validateConfigAndReplace();
alert("Configuration IA importée et appliquée avec succès !");
showToast("Configuration IA importée et appliquée avec succès !");
};
reader.readAsText(file);
event.target.value = null;
@@ -754,7 +946,14 @@
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 copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); alert("Copié !"); } catch (e) {} };
// COPIE REMPLACEE PAR UN TOAST (NON BLOQUANT)
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
showToast("Texte copié dans le presse-papier !");
} catch (e) {}
};
const downloadFile = (content, hint, mimeType = 'text/plain;charset=utf-8') => {
let fname = hint ? hint.trim() : 'code.txt';
@@ -825,8 +1024,12 @@
return;
}
const userText = prompt.value.trim();
// REINITIALISATION DE LA HAUTEUR DU TEXTAREA
prompt.value = "";
autoResize();
nextTick(() => {
if (promptTextarea.value) promptTextarea.value.style.height = 'auto';
});
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}]` : "") });
@@ -863,6 +1066,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;
}
});
}
};
@@ -893,11 +1103,15 @@
user, activeView, settingsTab, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea,
projects, pinnedProjects, recentProjects, activeProject, messages, isAiThinking,
createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject,
isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar,
openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar,
adminSettings, adminSettingsMessage, isSavingAdminSettings, saveAdminSettings,
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,
isSettingsModalOpen,
attachedFiles, handleFileUpload, handlePaste, removeFile,
exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage, systemLogs
exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage, systemLogs,
toastVisible, toastMessage, showToast
};
}
}).mount('#app');