Compare commits

..
2 Commits
Author SHA1 Message Date
xavier 8dd8d65b42 fix: intégration de la route API de renommage des projets dans main.py
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-07 09:54:06 +02:00
xavier 3226886190 docs: mise à jour de la roadmap avec les nouvelles exigences métier (V1)
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-07 09:17:36 +02:00
4 changed files with 119 additions and 47 deletions

No files matched your search

+29 -19
View File
@@ -22,29 +22,39 @@ Le développement suit un versioning strict (format `vX.Y.Z`) synchronisé avec
### Sécurité & Infrastructure ### Sécurité & Infrastructure
- [x] Installation stricte via Dockge et Gitea (création automatique des dossiers inclus). - [x] Installation stricte via Dockge et Gitea (création automatique des dossiers inclus).
- [x] Sécurisation de la totalité du projet (fichiers, BDD PostgreSQL) sur le stockage RAID. - [x] Sécurisation de la totalité du projet (fichiers, BDD PostgreSQL) sur le stockage RAID.
- [x] Mots de passe cryptés (bcrypt), protection anti-brute force et anti-DDOS (à compléter pour le DDOS). - [x] Mots de passe cryptés (bcrypt), protection anti-brute force.
- [x] Authentification 2FA obligatoire (Keepassium, Authenticator, etc.) sans accès SSH pour les utilisateurs. - [x] Authentification 2FA obligatoire (Keepassium, Authenticator, etc.).
- [x] Déconnexion automatique (timer invisible / session JWT) après 60 minutes d'inactivité. - [x] Déconnexion automatique (timer invisible / session JWT) après 60 minutes d'inactivité.
- [x] Configuration de GitHub Actions / Gitea Actions pour la création automatique des paquets Docker (Tags SemVer + latest).
- [x] Horodatage UTC en base de données pour immunité au changement d'heure français (Prévention crash 2FA).
- [ ] Procédure de réinitialisation/modification du mot de passe (Admin et Utilisateurs). - [ ] Procédure de réinitialisation/modification du mot de passe (Admin et Utilisateurs).
- [x] Configuration de GitHub Actions / Gitea Actions pour la création automatique des paquets Docker.
### Administration & Gestion des Utilisateurs ### Administration & Gestion des Utilisateurs
- [ ] Page d'administration complète : changement des clés, ajout de fournisseurs, configuration et test du serveur mail. - [x] Assistant de première installation (création du compte admin complet).
- [x] Assistant de première installation (création du compte admin complet), puis auto-destruction / blocage du script. - [ ] Refonte du setup initial (Super Admin) : Obligation de configurer le serveur mail (aide opérateurs FR/Gmail) et proposition de saisie des clés API.
- [ ] Système d'invitation par mail : création de compte avec génération de QR Code 2FA à la première connexion. - [ ] Intégration de la roue crantée (Paramètres) en bas de la barre latérale.
- [ ] Vérification du budget API toutes les 15 minutes et actualisation après chaque requête. - [ ] Page de configuration Utilisateur : changement d'email (avec validation via lien envoyé) et de mot de passe.
- [ ] Suivi financier détaillé par fournisseur d'IA. - [ ] Page de configuration Admin : ajout/modification de fournisseurs, clés API, et serveur mail en permanence.
- [ ] Hiérarchie des rôles : Un Admin peut proposer l'élévation d'un User en Admin (formulaire soumis à la validation exclusive du Super Admin).
- [ ] Export de l'activité Admin : Fichier Excel avec KPI, graphiques "camembert", et suivi financier par utilisateur.
- [ ] Suivi financier en temps réel et vérification du budget API.
- [ ] Envoi de rapport d'activité automatisé chaque jeudi à 04h00.
### Expérience Utilisateur (UI/UX) ### Expérience Utilisateur (UI/UX)
- [~] Thème moderne (Gemini-like) et 100% compatible smartphones (Setup et Login terminés). - [x] Thème moderne (Gemini-like) et 100% compatible smartphones.
- [ ] Refonte de la gestion des projets : séparation Épinglés (en haut) / Non Épinglés, avec le dernier utilisé en tête de liste. - [x] Refonte de la gestion des projets : séparation Épinglés (en haut) / Non Épinglés (Récents).
- [ ] Plus de création automatique de nouveau projet à l'ouverture. - [x] Arrêt de la création automatique de nouveau projet à l'ouverture.
- [ ] Pré-filtre et classement automatique des modèles par catégorie (Texte/Code, Photo/Image, Vidéo, Son) avec l'aide de l'API Gemini lors de l'import. - [x] Intégration permanente du logo AETHAS38 en local (Setup / Login / Dashboard).
- [x] Intégration permanente du logo AETHAS38 dans l'interface (Setup / Login). - [ ] Indicateur visuel (Loader/Animation) signalant que l'IA AETHAS38 "réfléchit" ou travaille.
- [ ] Fonctionnalité de renommage manuel des discussions/projets.
### Moteur IA & Fonctionnalités d'Export ### Moteur IA, Modèles & Fonctionnalités
- [ ] Récupération dynamique de la liste des modèles directement auprès des fournisseurs par défaut (OpenRouter, Groq, Gemini, DeepSeek, Mistral, Cloudflare, HuggingFace, etc.). - [ ] Pipeline multi-agents : 1 Rédacteur de prompt, jusqu'à 5 Travailleurs, 1 Concaténeur final.
- [ ] Pipeline multi-agents : 1 Rédacteur de prompt, jusqu'à 5 Travailleurs en parallèle, 1 Concaténeur final. - [ ] Extraction automatisée des modèles disponibles chez les fournisseurs (sauvegarde txt horodatée ex: "07/10/2026 à 10h00").
- [ ] Prise en charge de modèles IA pour le traitement de texte, la génération d'images, la synthèse vocale, la musique (calage sur vidéo) et la vidéo. - [ ] Tâche planifiée (CRON) pour la mise à jour des modèles toutes les 12h (le timer se reset si mise à jour manuelle).
- [ ] Téléchargement des prompts, des réponses, et de l'historique complet en PDF, TXT, MD ou JSON. - [ ] Classement, filtrage (Gratuit/Payant) et catégorisation des modèles (Texte, Code, Image, Audio, Vidéo).
- [ ] Bouton dédié pour télécharger les fichiers générés (Images, Vidéos, Excel mis en forme via LibreOffice). - [ ] Upload manuel du fichier d'extraction des modèles avec intégration dans l'interface filtrable.
- [ ] Traduction automatisée des descriptions des modèles via Ollama (local) ou Gemini.
- [ ] Interface d'aide à la décision pour le choix des modèles selon la tâche souhaitée.
- [ ] Mécanisme de fallback / termes génériques pour les appels API (protection contre le dépassement des quotas Gemini).
- [ ] Téléchargement des requêtes, réponses et fichiers générés (Excel, PDF, TXT, Images).
+13 -1
View File
@@ -9,7 +9,7 @@ import os
from .database import engine, Base, get_db from .database import engine, Base, get_db
from .models import User, Project, Message from .models import User, Project, Message
from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token 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, MessageCreate, MessageResponse from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse
# Création des tables dans la base de données # Création des tables dans la base de données
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@@ -171,6 +171,18 @@ def create_project(project: ProjectCreate, db: Session = Depends(get_db), curren
db.refresh(new_project) db.refresh(new_project)
return new_project return new_project
@app.put("/api/projects/{project_id}/rename", response_model=ProjectResponse)
def rename_project(project_id: int, project_data: ProjectRename, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Renomme un projet existant."""
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")
project.title = project_data.title
db.commit()
db.refresh(project)
return project
@app.put("/api/projects/{project_id}/pin", response_model=ProjectResponse) @app.put("/api/projects/{project_id}/pin", response_model=ProjectResponse)
def toggle_pin_project(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): def toggle_pin_project(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Bascule le statut épinglé (is_pinned) d'un projet.""" """Bascule le statut épinglé (is_pinned) d'un projet."""
+3
View File
@@ -42,3 +42,6 @@ class MessageResponse(MessageBase):
class Config: class Config:
from_attributes = True from_attributes = True
class ProjectRename(BaseModel):
title: str
+74 -27
View File
@@ -53,9 +53,10 @@
<i data-lucide="pin" class="w-4 h-4 text-aethas-cyan fill-aethas-cyan flex-shrink-0"></i> <i data-lucide="pin" class="w-4 h-4 text-aethas-cyan fill-aethas-cyan flex-shrink-0"></i>
<span class="truncate">{{ project.title }}</span> <span class="truncate">{{ project.title }}</span>
</div> </div>
<div class="hidden group-hover:flex items-center gap-1 flex-shrink-0"> <div class="hidden group-hover:flex items-center gap-1 flex-shrink-0 bg-gray-800 pl-2 rounded">
<button @click.stop="togglePin(project.id)" class="text-gray-400 hover:text-white p-1 rounded hover:bg-gray-700"><i data-lucide="pin-off" class="w-3.5 h-3.5"></i></button> <button @click.stop="renameProject(project)" class="text-gray-400 hover:text-aethas-cyan p-1 rounded hover:bg-gray-700" title="Renommer"><i data-lucide="edit-2" class="w-3.5 h-3.5"></i></button>
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-400 p-1 rounded hover:bg-gray-700"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button> <button @click.stop="togglePin(project.id)" class="text-gray-400 hover:text-white p-1 rounded hover:bg-gray-700" title="Désépingler"><i data-lucide="pin-off" class="w-3.5 h-3.5"></i></button>
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-400 p-1 rounded hover:bg-gray-700" title="Supprimer"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button>
</div> </div>
</li> </li>
</ul> </ul>
@@ -71,9 +72,10 @@
<i data-lucide="message-square" class="w-4 h-4 text-gray-500 flex-shrink-0"></i> <i data-lucide="message-square" class="w-4 h-4 text-gray-500 flex-shrink-0"></i>
<span class="truncate">{{ project.title }}</span> <span class="truncate">{{ project.title }}</span>
</div> </div>
<div class="hidden group-hover:flex items-center gap-1 flex-shrink-0"> <div class="hidden group-hover:flex items-center gap-1 flex-shrink-0 bg-gray-800 pl-2 rounded">
<button @click.stop="togglePin(project.id)" class="text-gray-400 hover:text-white p-1 rounded hover:bg-gray-700"><i data-lucide="pin" class="w-3.5 h-3.5"></i></button> <button @click.stop="renameProject(project)" class="text-gray-400 hover:text-aethas-cyan p-1 rounded hover:bg-gray-700" title="Renommer"><i data-lucide="edit-2" class="w-3.5 h-3.5"></i></button>
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-400 p-1 rounded hover:bg-gray-700"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button> <button @click.stop="togglePin(project.id)" class="text-gray-400 hover:text-white p-1 rounded hover:bg-gray-700" title="Épingler"><i data-lucide="pin" class="w-3.5 h-3.5"></i></button>
<button @click.stop="deleteProject(project.id)" class="text-gray-400 hover:text-red-400 p-1 rounded hover:bg-gray-700" title="Supprimer"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button>
</div> </div>
</li> </li>
</ul> </ul>
@@ -97,17 +99,20 @@
<span class="font-bold text-lg neon-text">AETHAS38</span> <span class="font-bold text-lg neon-text">AETHAS38</span>
</div> </div>
</div> </div>
<div v-if="activeProject" class="text-sm font-medium text-gray-300">{{ activeProject.title }}</div> <div v-if="activeProject" class="text-sm font-medium text-gray-300 flex items-center gap-2">
{{ activeProject.title }}
<button @click="renameProject(activeProject)" class="text-gray-500 hover:text-aethas-cyan"><i data-lucide="edit-2" class="w-4 h-4"></i></button>
</div>
</header> </header>
<!-- Affichage par défaut (Aucun projet actif) --> <!-- Affichage par défaut -->
<div v-if="!activeProject" class="flex-1 overflow-y-auto p-4 md:p-8 flex flex-col items-center justify-center"> <div v-if="!activeProject" class="flex-1 overflow-y-auto p-4 md:p-8 flex flex-col items-center justify-center">
<img src="/static/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mb-6 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800 flex-shrink-0"> <img src="/static/logo.png" alt="AETHAS38 Logo" class="w-24 h-24 mb-6 rounded-full border-2 border-aethas-cyan shadow-[0_0_15px_rgba(6,182,212,0.5)] object-cover bg-gray-800 flex-shrink-0">
<h2 class="text-2xl font-bold text-gray-200 mb-2 text-center">Comment puis-je vous aider aujourd'hui ?</h2> <h2 class="text-2xl font-bold text-gray-200 mb-2 text-center">Comment puis-je vous aider aujourd'hui ?</h2>
<p class="text-gray-500 text-sm text-center">Sélectionnez ou créez un projet pour commencer.</p> <p class="text-gray-500 text-sm text-center">Sélectionnez ou créez un projet pour commencer.</p>
</div> </div>
<!-- Zone de Chat (Projet Actif) --> <!-- Zone de Chat -->
<div v-else class="flex-1 overflow-y-auto p-4 md:p-8 space-y-6 flex flex-col" id="chat-container"> <div v-else class="flex-1 overflow-y-auto p-4 md:p-8 space-y-6 flex flex-col" id="chat-container">
<div v-if="messages.length === 0" class="flex-1 flex items-center justify-center text-gray-500 italic">Démarrez la conversation ci-dessous.</div> <div v-if="messages.length === 0" class="flex-1 flex items-center justify-center text-gray-500 italic">Démarrez la conversation ci-dessous.</div>
@@ -119,22 +124,35 @@
</div> </div>
<div class="text-gray-200 whitespace-pre-wrap text-sm leading-relaxed">{{ msg.content }}</div> <div class="text-gray-200 whitespace-pre-wrap text-sm leading-relaxed">{{ msg.content }}</div>
</div> </div>
<!-- Indicateur de chargement IA -->
<div v-if="isAiThinking" class="max-w-3xl rounded-2xl p-4 bg-aethas-panel mr-auto border border-aethas-cyan/30 w-24">
<div class="flex items-center gap-2 mb-2">
<i data-lucide="bot" class="w-4 h-4 text-aethas-cyan"></i>
<span class="text-xs font-semibold uppercase tracking-wider text-gray-400">AETHAS38</span>
</div>
<div class="flex space-x-1.5 h-4 items-center">
<div class="w-2 h-2 bg-aethas-cyan rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-aethas-cyan rounded-full animate-bounce" style="animation-delay: 0.15s"></div>
<div class="w-2 h-2 bg-aethas-cyan rounded-full animate-bounce" style="animation-delay: 0.3s"></div>
</div>
</div>
</div> </div>
<!-- Saisie du Prompt --> <!-- Saisie du Prompt -->
<div class="p-4 md:p-6 w-full max-w-4xl mx-auto"> <div class="p-4 md:p-6 w-full max-w-4xl mx-auto">
<div class="relative flex items-end bg-gray-800 border border-gray-600 rounded-2xl p-2 shadow-lg focus-within:border-aethas-cyan focus-within:ring-1 focus-within:ring-aethas-cyan transition-all"> <div class="relative flex items-end bg-gray-800 border border-gray-600 rounded-2xl p-2 shadow-lg focus-within:border-aethas-cyan focus-within:ring-1 focus-within:ring-aethas-cyan transition-all">
<button class="p-3 text-gray-400 hover:text-aethas-cyan transition-colors flex-shrink-0" :disabled="!activeProject"><i data-lucide="paperclip" class="w-5 h-5"></i></button> <button class="p-3 text-gray-400 hover:text-aethas-cyan transition-colors flex-shrink-0" :disabled="!activeProject || isAiThinking"><i data-lucide="paperclip" class="w-5 h-5"></i></button>
<textarea <textarea
v-model="prompt" v-model="prompt"
@input="autoResize" @input="autoResize"
@keydown.enter.exact.prevent="sendMessage" @keydown.enter.exact.prevent="sendMessage"
ref="promptTextarea" ref="promptTextarea"
:disabled="!activeProject" :disabled="!activeProject || isAiThinking"
rows="1" rows="1"
class="w-full bg-transparent text-gray-200 p-3 max-h-[200px] overflow-y-auto focus:outline-none resize-none leading-relaxed disabled:opacity-50" class="w-full bg-transparent text-gray-200 p-3 max-h-[200px] overflow-y-auto focus:outline-none resize-none leading-relaxed disabled:opacity-50"
placeholder="Posez votre question à l'Orchestrateur..."></textarea> placeholder="Posez votre question à l'Orchestrateur..."></textarea>
<button @click="sendMessage" :disabled="!activeProject || !prompt.trim()" class="p-3 text-aethas-cyan hover:text-white transition-colors flex-shrink-0 disabled:opacity-50 disabled:text-gray-500"><i data-lucide="send" class="w-5 h-5"></i></button> <button @click="sendMessage" :disabled="!activeProject || !prompt.trim() || isAiThinking" class="p-3 text-aethas-cyan hover:text-white transition-colors flex-shrink-0 disabled:opacity-50 disabled:text-gray-500"><i data-lucide="send" class="w-5 h-5"></i></button>
</div> </div>
<p class="text-center text-xs text-gray-500 mt-3">AETHAS38 peut faire des erreurs. Vérifiez toujours les informations importantes.</p> <p class="text-center text-xs text-gray-500 mt-3">AETHAS38 peut faire des erreurs. Vérifiez toujours les informations importantes.</p>
</div> </div>
@@ -154,19 +172,18 @@
const activeProject = ref(null); const activeProject = ref(null);
const messages = ref([]); const messages = ref([]);
const isLoading = ref(false); const isLoading = ref(false);
const isAiThinking = ref(false);
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));
const toggleSidebar = () => { isSidebarOpen.value = !isSidebarOpen.value; }; const toggleSidebar = () => { isSidebarOpen.value = !isSidebarOpen.value; };
const autoResize = () => { const autoResize = () => {
const el = promptTextarea.value; const el = promptTextarea.value;
if (!el) return; if (!el) return;
el.style.height = 'auto'; el.style.height = 'auto';
el.style.height = el.scrollHeight + 'px'; el.style.height = el.scrollHeight + 'px';
}; };
const scrollToBottom = () => { const scrollToBottom = () => {
nextTick(() => { nextTick(() => {
const container = document.getElementById('chat-container'); const container = document.getElementById('chat-container');
@@ -181,7 +198,7 @@
projects.value = await response.json(); projects.value = await response.json();
nextTick(() => lucide.createIcons()); nextTick(() => lucide.createIcons());
} }
} catch (error) { console.error("Erreur", error); } } catch (error) { console.error(error); }
}; };
const selectProject = async (project) => { const selectProject = async (project) => {
@@ -194,7 +211,7 @@
scrollToBottom(); scrollToBottom();
nextTick(() => lucide.createIcons()); nextTick(() => lucide.createIcons());
} }
} catch (error) { console.error("Erreur chargement messages", error); } } catch (error) { console.error(error); }
}; };
const createNewProject = async () => { const createNewProject = async () => {
@@ -210,33 +227,63 @@
projects.value.unshift(newProject); projects.value.unshift(newProject);
selectProject(newProject); selectProject(newProject);
} }
} catch (error) { console.error("Erreur", error); } } catch (error) { console.error(error); }
finally { isLoading.value = false; } 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 sendMessage = async () => { const sendMessage = async () => {
if (!prompt.value.trim() || !activeProject.value) return; if (!prompt.value.trim() || !activeProject.value) return;
const userText = prompt.value.trim(); const userText = prompt.value.trim();
prompt.value = ""; prompt.value = "";
autoResize(); autoResize();
// Ajout optimiste à l'UI
messages.value.push({ id: Date.now(), role: 'user', content: userText }); messages.value.push({ id: Date.now(), role: 'user', content: userText });
isAiThinking.value = true;
scrollToBottom(); scrollToBottom();
nextTick(() => lucide.createIcons()); nextTick(() => lucide.createIcons());
// Sauvegarde en DB
try { try {
await fetch(`/api/projects/${activeProject.value.id}/messages`, { await fetch(`/api/projects/${activeProject.value.id}/messages`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: 'user', content: userText }) body: JSON.stringify({ role: 'user', content: userText })
}); });
// Bientôt : C'est ici que nous déclencherons le workflow IA !
} catch (error) { console.error("Erreur envoi message", error); } // SIMULATION TEMPORAIRE : Attente de l'IA (2 secondes)
setTimeout(() => {
isAiThinking.value = false;
scrollToBottom();
// La prochaine étape remplacera ce setTimeout par le véritable appel au moteur IA !
}, 2000);
} catch (error) {
console.error("Erreur envoi message", error);
isAiThinking.value = false;
}
}; };
const togglePin = async (projectId) => { const togglePin = async (projectId) => { /* ... code existant identique ... */
try { try {
const response = await fetch(`/api/projects/${projectId}/pin`, { method: 'PUT' }); const response = await fetch(`/api/projects/${projectId}/pin`, { method: 'PUT' });
if (response.ok) { if (response.ok) {
@@ -244,10 +291,10 @@
const index = projects.value.findIndex(p => p.id === projectId); const index = projects.value.findIndex(p => p.id === projectId);
if (index !== -1) projects.value[index] = updatedProject; if (index !== -1) projects.value[index] = updatedProject;
} }
} catch (error) { console.error("Erreur d'épinglage", error); } } catch (error) { console.error(error); }
}; };
const deleteProject = async (projectId) => { const deleteProject = async (projectId) => { /* ... code existant identique ... */
if (!confirm("Voulez-vous vraiment supprimer ce projet ?")) return; if (!confirm("Voulez-vous vraiment supprimer ce projet ?")) return;
try { try {
const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' }); const response = await fetch(`/api/projects/${projectId}`, { method: 'DELETE' });
@@ -255,7 +302,7 @@
projects.value = projects.value.filter(p => p.id !== projectId); projects.value = projects.value.filter(p => p.id !== projectId);
if (activeProject.value && activeProject.value.id === projectId) activeProject.value = null; if (activeProject.value && activeProject.value.id === projectId) activeProject.value = null;
} }
} catch (error) { console.error("Erreur de suppression", error); } } catch (error) { console.error(error); }
}; };
onMounted(() => { onMounted(() => {
@@ -265,8 +312,8 @@
return { return {
username, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea, username, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea,
projects, pinnedProjects, recentProjects, activeProject, messages, projects, pinnedProjects, recentProjects, activeProject, messages, isAiThinking,
createNewProject, selectProject, sendMessage, isLoading, togglePin, deleteProject createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject
}; };
} }
}).mount('#app'); }).mount('#app');