diff --git a/backend/main.py b/backend/main.py index 3c6da93..209738a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,18 +1,17 @@ -from fastapi import FastAPI, Depends, HTTPException, status, Request, Response +from fastapi import FastAPI, Depends, HTTPException, status, Request, Response, UploadFile, File from fastapi.responses import RedirectResponse, FileResponse from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session from typing import List import os +import json # Importation de nos modules locaux from .database import engine, Base, get_db 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 -from .models import User, Project, Message, SystemSettings -from .orchestrator import run_orchestrator, sync_providers_models +from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse, PasswordChange from .models import User, Project, Message, SystemSettings, AIModel -from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse, PasswordChangeS +from .orchestrator import run_orchestrator, sync_providers_models # Création des tables dans la base de données Base.metadata.create_all(bind=engine) @@ -108,8 +107,6 @@ def change_password(passwords: PasswordChange, db: Session = Depends(get_db), cu db.commit() return {"message": "Mot de passe mis à jour avec succès."} -# --- GESTION DES PROJETS --- - def get_current_user(request: Request, db: Session = Depends(get_db)): token = request.cookies.get("session_token") if not token: raise HTTPException(status_code=401, detail="Non authentifié") @@ -119,6 +116,7 @@ def get_current_user(request: Request, db: Session = Depends(get_db)): if not user: raise HTTPException(status_code=401, detail="Utilisateur introuvable") return user +# --- GESTION DES PROJETS --- @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() @@ -158,7 +156,6 @@ def delete_project(project_id: int, db: Session = Depends(get_db), current_user: return {"message": "Projet supprimé"} # --- GESTION DES MESSAGES --- - @app.get("/api/projects/{project_id}/messages", response_model=List[MessageResponse]) def get_messages(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first() @@ -167,7 +164,6 @@ 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)): - """Ajoute un message, lance l'Orchestrateur asynchrone et retourne l'historique.""" project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first() if not project: raise HTTPException(status_code=404, detail="Projet introuvable") @@ -178,9 +174,7 @@ async def create_message(project_id: int, message: MessageCreate, db: Session = history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all() settings = db.query(SystemSettings).first() - # Configuration temporaire (sera remplacée par les choix du Frontend à la Phase 2) - # Ex pour tester le pipeline complet : {"workers": ["gemini-3.5-flash-lite", "google/gemini-1.5-pro"]} - orchestrator_config = {"workers": ["gemini-3.5-flash-lite"]} + orchestrator_config = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]} ai_response_text = await run_orchestrator(history, settings, orchestrator_config) @@ -190,9 +184,9 @@ async def create_message(project_id: int, message: MessageCreate, db: Session = return db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all() +# --- GESTION DES MODELES --- @app.post("/api/models/sync") async def trigger_model_sync(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - """Déclenche manuellement l'extraction et la mise à jour des modèles depuis les API.""" if not current_user.is_admin: raise HTTPException(status_code=403, detail="Accès réservé aux administrateurs.") settings = db.query(SystemSettings).first() @@ -200,5 +194,28 @@ async def trigger_model_sync(db: Session = Depends(get_db), current_user: User = @app.get("/api/models") def get_models(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - """Renvoie la liste complète des modèles stockés en base de données.""" - return db.query(AIModel).order_by(AIModel.name.asc()).all() \ No newline at end of file + return db.query(AIModel).order_by(AIModel.name.asc()).all() + +@app.get("/api/models/export") +def export_models(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + if not current_user.is_admin: raise HTTPException(status_code=403, detail="Accès admin requis.") + models = db.query(AIModel).all() + models_data = [{"provider": m.provider, "model_id": m.model_id, "name": m.name, "context_length": m.context_length, "pricing_prompt": m.pricing_prompt, "pricing_completion": m.pricing_completion} for m in models] + return Response(content=json.dumps(models_data), media_type="application/json", headers={"Content-Disposition": "attachment; filename=aethas38_models.json"}) + +@app.post("/api/models/import") +async def import_models(file: UploadFile = File(...), db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + if not current_user.is_admin: raise HTTPException(status_code=403, detail="Accès admin requis.") + content = await file.read() + try: + data = json.loads(content) + imported_count = 0 + for item in data: + existing = db.query(AIModel).filter(AIModel.model_id == item["model_id"]).first() + if not existing: + db.add(AIModel(**item)) + imported_count += 1 + db.commit() + return {"message": f"Import réussi. {imported_count} nouveaux modèles ajoutés."} + except Exception as e: + raise HTTPException(status_code=400, detail="Fichier JSON invalide ou mal formaté.") \ No newline at end of file diff --git a/backend/schemas.py b/backend/schemas.py index 849066c..3f812fc 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, EmailStr from datetime import datetime -from typing import Optional +from typing import Optional, List class AdminCreate(BaseModel): email: EmailStr @@ -27,7 +27,7 @@ class LoginRequest(BaseModel): password: str totp_code: str -# --- NOUVEAU : Schémas pour les Projets --- +# --- GESTION DES PROJETS --- class ProjectBase(BaseModel): title: str @@ -39,25 +39,34 @@ class ProjectResponse(ProjectBase): created_at: datetime is_pinned: bool -class Config: + class Config: from_attributes = True -# --- NOUVEAU : Schémas pour les Messages --- +class ProjectRename(BaseModel): + title: str + +# --- GESTION DES MESSAGES ET ORCHESTRATION --- +class OrchestratorConfig(BaseModel): + workers: List[str] + prompter: Optional[str] = "gemini-3.5-flash-lite" + concatenator: Optional[str] = "gemini-3.5-flash-lite" + class MessageBase(BaseModel): role: str content: str class MessageCreate(MessageBase): - pass + config: Optional[OrchestratorConfig] = None class MessageResponse(MessageBase): id: int created_at: datetime project_id: int -class ProjectRename(BaseModel): - title: str + class Config: + from_attributes = True +# --- UTILISATEUR --- class PasswordChange(BaseModel): old_password: str new_password: str \ No newline at end of file diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 759e948..b67bb9f 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -6,10 +6,7 @@ AETHAS38 - Dashboard @@ -17,7 +14,6 @@ body { background-color: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; } .neon-text { background: linear-gradient(to right, #06b6d4, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } ::-webkit-scrollbar { width: 6px; } - ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; } @@ -35,30 +31,63 @@

Sécurité du compte

{{ settingsMessage }}
-
- +
- +
- +
-
-
+ +
+
+
+

Base de données Modèles IA

+ +
+
+ + + Exporter (JSON) + + +
+
+

Aucun modèle en base. Cliquez sur Synchroniser.

+ + + + + + + + + + + +
ID ModèleFournisseurContexte
{{ model.model_id }}{{ model.provider }}{{ model.context_length }}
+
+
+
+
+
- -
- Logo + +
+ AETHAS38
@@ -139,29 +163,19 @@
-
- AETHAS38 Logo -

Comment puis-je vous aider aujourd'hui ?

-

Sélectionnez ou créez un projet pour commencer.

+
+ +

Comment puis-je vous aider aujourd'hui ?

-
Démarrez la conversation ci-dessous.
- -
+
- - {{ msg.role === 'user' ? 'Vous' : 'AETHAS38' }}
{{ msg.content }}
-
-
- - AETHAS38 -
@@ -170,21 +184,40 @@
-
-
- - - + +
+
+
+ Travailleurs: + + {{ selectedWorkers.length }}/5 +
+ +
+
+ Prompteur: + +
+
+ Concaténeur: + +
+
+
+ +
+ +
-

AETHAS38 peut faire des erreurs. Vérifiez toujours les informations importantes.

@@ -205,203 +238,150 @@ const isAiThinking = ref(false); const isSettingsModalOpen = ref(false); + const isModelsModalOpen = ref(false); const isSavingSettings = ref(false); + const isSyncing = ref(false); const settingsMessage = ref(""); const passForm = reactive({ old: '', new: '', confirm: '' }); + + const modelsList = ref([]); + const selectedWorkers = ref(["gemini-3.5-flash-lite"]); + const selectedPrompter = ref("gemini-3.5-flash-lite"); + const selectedConcatenator = ref("gemini-3.5-flash-lite"); const pinnedProjects = computed(() => projects.value.filter(p => p.is_pinned)); const recentProjects = computed(() => projects.value.filter(p => !p.is_pinned)); const toggleSidebar = () => { isSidebarOpen.value = !isSidebarOpen.value; }; - const autoResize = () => { - const el = promptTextarea.value; - if (!el) return; - el.style.height = 'auto'; - el.style.height = el.scrollHeight + 'px'; - }; - const scrollToBottom = () => { - nextTick(() => { - const container = document.getElementById('chat-container'); - if (container) container.scrollTop = container.scrollHeight; - }); - }; + 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; - settingsMessage.value = ""; - passForm.old = ''; passForm.new = ''; passForm.confirm = ''; - nextTick(() => lucide.createIcons()); - }; + const openSettings = () => { isSettingsModalOpen.value = true; nextTick(() => lucide.createIcons()); }; + const closeSettings = () => { isSettingsModalOpen.value = false; }; - const closeSettings = () => { - isSettingsModalOpen.value = false; + const openModelsModal = () => { fetchModels(); isModelsModalOpen.value = true; nextTick(() => lucide.createIcons()); }; + + const fetchModels = async () => { + try { + const res = await fetch('/api/models'); + if(res.ok) modelsList.value = await res.json(); + } catch(e) { console.error(e); } }; - const changePassword = async () => { - settingsMessage.value = ""; - if (passForm.new !== passForm.confirm) { - settingsMessage.value = "Les nouveaux mots de passe ne correspondent pas."; - return; - } - if (passForm.new.length < 8) { - settingsMessage.value = "Le mot de passe doit faire au moins 8 caractères."; - return; - } - - isSavingSettings.value = true; + const syncModels = async () => { + isSyncing.value = true; try { - const response = await fetch('/api/users/me/password', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ old_password: passForm.old, new_password: passForm.new }) - }); - const data = await response.json(); - - if (response.ok) { - settingsMessage.value = data.message; - passForm.old = ''; passForm.new = ''; passForm.confirm = ''; - setTimeout(() => closeSettings(), 2000); - } else { - settingsMessage.value = data.detail; - } - } catch (error) { - settingsMessage.value = "Erreur de connexion au serveur."; - } finally { - isSavingSettings.value = false; + const res = await fetch('/api/models/sync', { method: 'POST' }); + if(res.ok) { alert("Synchronisation terminée !"); await fetchModels(); } + } catch(e) { console.error(e); } + isSyncing.value = false; + }; + + const importModels = async (event) => { + const file = event.target.files[0]; + if (!file) return; + const formData = new FormData(); + formData.append("file", file); + try { + const res = await fetch('/api/models/import', { method: 'POST', body: formData }); + const data = await res.json(); + alert(data.message || data.detail); + await fetchModels(); + } catch (e) { alert("Erreur d'import"); } + event.target.value = null; + }; + + const limitWorkers = () => { + if(selectedWorkers.value.length > 5) { + selectedWorkers.value.pop(); + alert("Maximum 5 travailleurs autorisés en parallèle."); } }; - const fetchProjects = async () => { - try { - const response = await fetch('/api/projects'); - if (response.ok) { - projects.value = await response.json(); - nextTick(() => lucide.createIcons()); - } - } catch (error) { console.error(error); } - }; - - const selectProject = async (project) => { - activeProject.value = project; - messages.value = []; - try { - const response = await fetch(`/api/projects/${project.id}/messages`); - if (response.ok) { - messages.value = await response.json(); - scrollToBottom(); - nextTick(() => lucide.createIcons()); - } - } catch (error) { console.error(error); } - }; - - const createNewProject = async () => { - isLoading.value = true; - try { - const response = await fetch('/api/projects', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title: "Nouvelle discussion" }) - }); - if (response.ok) { - const newProject = await response.json(); - projects.value.unshift(newProject); - selectProject(newProject); - } - } catch (error) { console.error(error); } - finally { isLoading.value = false; } - }; - - const renameProject = async (project) => { - const newTitle = window.prompt("Nouveau nom du projet :", project.title); - if (!newTitle || newTitle.trim() === "" || newTitle === project.title) return; - - try { - const response = await fetch(`/api/projects/${project.id}/rename`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title: newTitle.trim() }) - }); - if (response.ok) { - const updatedProject = await response.json(); - const index = projects.value.findIndex(p => p.id === project.id); - if (index !== -1) projects.value[index] = updatedProject; - if (activeProject.value && activeProject.value.id === project.id) { - activeProject.value.title = updatedProject.title; - } - } - } catch (error) { console.error("Erreur de renommage", error); } - }; + const fetchProjects = async () => { try { const res = await fetch('/api/projects'); if(res.ok) projects.value = await res.json(); nextTick(()=>lucide.createIcons()); } catch(e){} }; + const selectProject = async (project) => { activeProject.value = project; messages.value = []; try { const res = await fetch(`/api/projects/${project.id}/messages`); if(res.ok) { messages.value = await res.json(); scrollToBottom(); nextTick(()=>lucide.createIcons()); } } catch(e){} }; + const createNewProject = async () => { isLoading.value = true; try { const res = await fetch('/api/projects', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title: "Nouvelle discussion"})}); if(res.ok){ const np = await res.json(); projects.value.unshift(np); selectProject(np); } } catch(e){} finally{isLoading.value = false;} }; const sendMessage = async () => { - if (!prompt.value.trim() || !activeProject.value) return; + if (!prompt.value.trim() || !activeProject.value || selectedWorkers.value.length === 0) return; const userText = prompt.value.trim(); - prompt.value = ""; - autoResize(); + prompt.value = ""; autoResize(); messages.value.push({ id: Date.now(), role: 'user', content: userText }); - isAiThinking.value = true; - scrollToBottom(); - nextTick(() => lucide.createIcons()); + isAiThinking.value = true; scrollToBottom(); + + const payload = { + role: 'user', + content: userText, + config: { + workers: selectedWorkers.value, + prompter: selectedPrompter.value, + concatenator: selectedConcatenator.value + } + }; try { const response = await fetch(`/api/projects/${activeProject.value.id}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ role: 'user', content: userText }) + body: JSON.stringify(payload) }); if (response.ok) { const data = await response.json(); - if (Array.isArray(data)) { - messages.value = data; - } else { - selectProject(activeProject.value); - } + if (Array.isArray(data)) messages.value = data; } else { const err = await response.json(); - alert("Erreur de l'IA: " + err.detail); + alert("Erreur: " + err.detail); } - } catch (error) { - console.error("Erreur envoi message", error); - } finally { - isAiThinking.value = false; - scrollToBottom(); - nextTick(() => lucide.createIcons()); - } + } catch (error) { console.error(error); } + finally { isAiThinking.value = false; scrollToBottom(); nextTick(()=>lucide.createIcons()); } }; - const togglePin = async (projectId) => { + const changePassword = async () => { + settingsMessage.value = ""; + if (passForm.new !== passForm.confirm) { settingsMessage.value = "Les nouveaux mots de passe ne correspondent pas."; return; } + if (passForm.new.length < 8) { settingsMessage.value = "Le mot de passe doit faire au moins 8 caractères."; return; } + + isSavingSettings.value = true; try { - const response = await fetch(`/api/projects/${projectId}/pin`, { method: 'PUT' }); + const response = await fetch('/api/users/me/password', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ old_password: passForm.old, new_password: passForm.new }) }); + const data = await response.json(); + if (response.ok) { + settingsMessage.value = data.message; + passForm.old = ''; passForm.new = ''; passForm.confirm = ''; + setTimeout(() => closeSettings(), 2000); + } else { settingsMessage.value = data.detail; } + } catch (error) { settingsMessage.value = "Erreur de connexion au serveur."; } + finally { isSavingSettings.value = false; } + }; + + const renameProject = async (project) => { + const newTitle = window.prompt("Nouveau nom du projet :", project.title); + if (!newTitle || newTitle.trim() === "" || newTitle === project.title) return; + try { + const response = await fetch(`/api/projects/${project.id}/rename`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: newTitle.trim() }) }); if (response.ok) { const updatedProject = await response.json(); - const index = projects.value.findIndex(p => p.id === projectId); + const index = projects.value.findIndex(p => p.id === project.id); if (index !== -1) projects.value[index] = updatedProject; + if (activeProject.value && activeProject.value.id === project.id) activeProject.value.title = updatedProject.title; } } catch (error) { console.error(error); } }; - const deleteProject = async (projectId) => { - if (!confirm("Voulez-vous vraiment supprimer ce projet ?")) return; - try { - const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' }); - if (response.ok) { - projects.value = projects.value.filter(p => p.id !== projectId); - if (activeProject.value && activeProject.value.id === projectId) activeProject.value = null; - } - } catch (error) { console.error(error); } - }; + const togglePin = async (projectId) => { try { const response = await fetch(`/api/projects/${projectId}/pin`, { method: 'PUT' }); if (response.ok) { const updatedProject = await response.json(); const index = projects.value.findIndex(p => p.id === projectId); if (index !== -1) projects.value[index] = updatedProject; } } catch (error) { console.error(error); } }; + const deleteProject = async (projectId) => { if (!confirm("Voulez-vous vraiment supprimer ce projet ?")) return; try { const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' }); if (response.ok) { projects.value = projects.value.filter(p => p.id !== projectId); if (activeProject.value && activeProject.value.id === projectId) activeProject.value = null; } } catch (error) { console.error(error); } }; - onMounted(() => { - lucide.createIcons(); - fetchProjects(); - }); + onMounted(() => { lucide.createIcons(); fetchProjects(); fetchModels(); }); return { username, 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 + isSettingsModalOpen, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, + isModelsModalOpen, openModelsModal, modelsList, syncModels, importModels, + selectedWorkers, selectedPrompter, selectedConcatenator, limitWorkers }; } }).mount('#app');