Compare commits

..
3 Commits
Author SHA1 Message Date
xavier 71e80f93c6 feat: intégration complète de l'API de messagerie dans main.py
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-07 08:42:16 +02:00
xavier c8a38f3eb8 feat: ajout de l'épinglage et de la suppression des projets avec mise à jour UI
Build and Push Docker Image / build-and-push (push) Successful in 43s
2026-10-07 08:33:52 +02:00
xavier cac70a5aaa ci: intégration de docker/metadata-action pour générer dynamiquement les tags SemVer et latest
Build and Push Docker Image / build-and-push (push) Failing after 27s
2026-10-07 08:24:14 +02:00
5 changed files with 238 additions and 60 deletions

No files matched your search

+14 -1
View File
@@ -4,6 +4,8 @@ on:
push: push:
branches: branches:
- main - main
tags:
- 'v*' # Déclenche aussi le pipeline lors de la création d'un tag
jobs: jobs:
build-and-push: build-and-push:
@@ -16,6 +18,16 @@ jobs:
- name: Checkout du code - name: Checkout du code
uses: actions/checkout@v3 uses: actions/checkout@v3
# NOUVEAU : Génération dynamique des tags (SemVer + Latest)
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: gitea.aethas38.duckdns.org/xavier/multi-ia-codage
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
- name: Connexion au registre Gitea - name: Connexion au registre Gitea
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
@@ -28,4 +40,5 @@ jobs:
with: with:
context: . context: .
push: true push: true
tags: gitea.aethas38.duckdns.org/xavier/multi-ia-codage:latest tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+15 -3
View File
@@ -2,7 +2,8 @@ name: Build and Publish Docker Image
on: on:
push: push:
branches: [ "main" ] # Modifiez si votre branche s'appelle "master" branches: [ "main" ]
tags: [ 'v*' ] # Déclencheur sur les tags ajouté ici aussi
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
@@ -13,12 +14,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
packages: write # Indispensable pour publier le paquet sur le dépôt packages: write
steps: steps:
- name: Checkout du code - name: Checkout du code
uses: actions/checkout@v4 uses: actions/checkout@v4
# NOUVEAU : Identique à Gitea, gère ghcr.io
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=semver,pattern={{version}}
- name: Connexion au registre GitHub (ghcr.io) - name: Connexion au registre GitHub (ghcr.io)
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -31,4 +42,5 @@ jobs:
with: with:
context: . context: .
push: true push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+50 -8
View File
@@ -2,18 +2,14 @@ from fastapi import FastAPI, Depends, HTTPException, status, Request, Response
from fastapi.responses import RedirectResponse, FileResponse from fastapi.responses import RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List
import os import os
# Importation de nos modules locaux # Importation de nos modules locaux
from .database import engine, Base, get_db from .database import engine, Base, get_db
from .models import User 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 from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, MessageCreate, MessageResponse
# MAJ BDD
from typing import List
from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse
from .models import User, Project
# 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)
@@ -173,4 +169,50 @@ def create_project(project: ProjectCreate, db: Session = Depends(get_db), curren
db.add(new_project) db.add(new_project)
db.commit() db.commit()
db.refresh(new_project) db.refresh(new_project)
return new_project return new_project
@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)):
"""Bascule le statut épinglé (is_pinned) d'un projet."""
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.is_pinned = not project.is_pinned
db.commit()
db.refresh(project)
return project
@app.delete("/api/projects/{project_id}")
def delete_project(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Supprime définitivement un projet et tous ses messages associés."""
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")
db.delete(project)
db.commit()
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)):
"""Récupère tous les messages d'un projet spécifique."""
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")
return db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
@app.post("/api/projects/{project_id}/messages", response_model=MessageResponse)
def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Ajoute un message à un projet."""
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")
new_message = Message(role=message.role, content=message.content, project_id=project_id)
db.add(new_message)
db.commit()
db.refresh(new_message)
return new_message
+16
View File
@@ -24,5 +24,21 @@ class ProjectResponse(ProjectBase):
created_at: datetime created_at: datetime
is_pinned: bool is_pinned: bool
class Config:
from_attributes = True
# --- NOUVEAU : Schémas pour les Messages ---
class MessageBase(BaseModel):
role: str
content: str
class MessageCreate(MessageBase):
pass
class MessageResponse(MessageBase):
id: int
created_at: datetime
project_id: int
class Config: class Config:
from_attributes = True from_attributes = True
+143 -48
View File
@@ -8,9 +8,7 @@
<script> <script>
tailwind.config = { tailwind.config = {
darkMode: 'class', darkMode: 'class',
theme: { theme: { extend: { colors: { aethas: { dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7' } } } }
extend: { colors: { aethas: { dark: '#0f172a', panel: '#1e293b', cyan: '#06b6d4', purple: '#a855f7' } } }
}
} }
</script> </script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
@@ -26,9 +24,9 @@
<body class="h-screen flex overflow-hidden"> <body class="h-screen flex overflow-hidden">
<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">
<div v-if="isSidebarOpen" @click="toggleSidebar" class="md:hidden fixed inset-0 bg-black/60 z-40 transition-opacity"></div> <div v-if="isSidebarOpen" @click="toggleSidebar" class="md:hidden fixed inset-0 bg-black/60 z-40 transition-opacity"></div>
<!-- BARRE LATÉRALE -->
<div :class="['transition-all duration-300 flex-shrink-0 relative z-50', isSidebarOpen ? 'w-72' : 'w-0']"> <div :class="['transition-all duration-300 flex-shrink-0 relative z-50', isSidebarOpen ? 'w-72' : 'w-0']">
<aside :class="['absolute top-0 left-0 h-full w-72 bg-aethas-panel border-r border-gray-700 flex flex-col transition-transform duration-300', isSidebarOpen ? 'translate-x-0' : '-translate-x-full']"> <aside :class="['absolute top-0 left-0 h-full w-72 bg-aethas-panel border-r border-gray-700 flex flex-col transition-transform duration-300', isSidebarOpen ? 'translate-x-0' : '-translate-x-full']">
<div class="p-4 border-b border-gray-700 flex items-center justify-between"> <div class="p-4 border-b border-gray-700 flex items-center justify-between">
@@ -36,77 +34,107 @@
<img src="/static/logo.png" alt="Logo" class="w-10 h-10 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0"> <img src="/static/logo.png" alt="Logo" class="w-10 h-10 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0">
<span class="font-bold text-lg tracking-wider neon-text">AETHAS38</span> <span class="font-bold text-lg tracking-wider neon-text">AETHAS38</span>
</div> </div>
<button @click="toggleSidebar" class="md:hidden text-gray-400 hover:text-white"> <button @click="toggleSidebar" class="md:hidden text-gray-400 hover:text-white"><i data-lucide="x" class="w-5 h-5"></i></button>
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div> </div>
<div class="p-4"> <div class="p-4">
<button @click="createNewProject" :disabled="isLoading" class="w-full bg-gray-800 hover:bg-gray-700 text-white border border-gray-600 rounded-lg py-2.5 px-4 flex items-center justify-center gap-2 transition-colors disabled:opacity-50"> <button @click="createNewProject" :disabled="isLoading" class="w-full bg-gray-800 hover:bg-gray-700 text-white border border-gray-600 rounded-lg py-2.5 px-4 flex items-center justify-center gap-2 transition-colors disabled:opacity-50 shadow-md">
<i data-lucide="plus" class="w-5 h-5 text-aethas-cyan"></i> <i data-lucide="plus" class="w-5 h-5 text-aethas-cyan"></i> Nouveau Projet IA
Nouveau Projet IA
</button> </button>
</div> </div>
<div class="flex-1 overflow-y-auto p-4 space-y-6"> <div class="flex-1 overflow-y-auto p-4 space-y-6">
<!-- Projets Épinglés -->
<div v-if="pinnedProjects.length > 0">
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Épinglés</h3>
<ul class="space-y-1">
<li v-for="project in pinnedProjects" :key="project.id" @click="selectProject(project)" :class="['group flex items-center justify-between p-2 rounded cursor-pointer text-sm transition-colors', 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="pin" class="w-4 h-4 text-aethas-cyan fill-aethas-cyan flex-shrink-0"></i>
<span class="truncate">{{ project.title }}</span>
</div>
<div class="hidden group-hover:flex items-center gap-1 flex-shrink-0">
<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="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>
</div>
</li>
</ul>
</div>
<!-- Projets Récents -->
<div> <div>
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Récents</h3> <h3 class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Récents</h3>
<ul class="space-y-1"> <ul class="space-y-1">
<li v-if="projects.length === 0" class="text-sm text-gray-500 italic p-2">Aucun projet pour le moment.</li> <li v-if="recentProjects.length === 0 && pinnedProjects.length === 0" class="text-sm text-gray-500 italic p-2">Aucun projet.</li>
<li v-for="project in projects" :key="project.id" class="p-2 hover:bg-gray-800 rounded cursor-pointer text-sm text-gray-300 truncate transition-colors flex items-center gap-2"> <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 transition-colors', activeProject && activeProject.id === project.id ? 'bg-gray-700 text-white' : 'hover:bg-gray-800 text-gray-300']">
<i data-lucide="message-square" class="w-4 h-4 text-gray-500"></i> <div class="flex items-center gap-2 truncate">
{{ project.title }} <i data-lucide="message-square" class="w-4 h-4 text-gray-500 flex-shrink-0"></i>
<span class="truncate">{{ project.title }}</span>
</div>
<div class="hidden group-hover:flex items-center gap-1 flex-shrink-0">
<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="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>
</div>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="p-4 border-t border-gray-700 flex justify-between items-center"> <div class="p-4 border-t border-gray-700 flex items-center gap-2">
<div class="flex items-center gap-2"> <div class="w-8 h-8 rounded-full bg-gradient-to-tr from-aethas-cyan to-aethas-purple flex items-center justify-center text-sm font-bold shadow-md">{{ username.charAt(0).toUpperCase() }}</div>
<div class="w-8 h-8 rounded-full bg-gradient-to-tr from-aethas-cyan to-aethas-purple flex items-center justify-center text-sm font-bold shadow-md"> <span class="text-sm font-medium">{{ username }}</span>
{{ username.charAt(0).toUpperCase() }}
</div>
<span class="text-sm font-medium">{{ username }}</span>
</div>
<button class="text-gray-400 hover:text-white transition-colors"><i data-lucide="settings" class="w-5 h-5"></i></button>
</div> </div>
</aside> </aside>
</div> </div>
<!-- ZONE PRINCIPALE -->
<main class="flex-1 flex flex-col min-w-0 z-10 transition-all duration-300"> <main class="flex-1 flex flex-col min-w-0 z-10 transition-all duration-300">
<header class="flex items-center justify-between p-4 border-b border-gray-700 bg-aethas-panel h-[73px]"> <header class="flex items-center justify-between p-4 border-b border-gray-700 bg-aethas-panel h-[73px]">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button @click="toggleSidebar" class="text-gray-400 hover:text-white transition-colors p-1.5 rounded hover:bg-gray-700" title="Basculer le menu"> <button @click="toggleSidebar" class="text-gray-400 hover:text-white transition-colors p-1.5 rounded hover:bg-gray-700"><i data-lucide="menu" class="w-6 h-6"></i></button>
<i data-lucide="menu" class="w-6 h-6"></i>
</button>
<div v-show="!isSidebarOpen" class="flex items-center gap-2 transition-opacity duration-300"> <div v-show="!isSidebarOpen" class="flex items-center gap-2 transition-opacity duration-300">
<img src="/static/logo.png" alt="Logo" class="w-8 h-8 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0"> <img src="/static/logo.png" alt="Logo" class="w-8 h-8 rounded-full border border-aethas-cyan object-cover bg-gray-800 flex-shrink-0">
<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>
</header> </header>
<div class="flex-1 overflow-y-auto p-4 md:p-8 flex flex-col items-center justify-center"> <!-- Affichage par défaut (Aucun projet actif) -->
<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">L'orchestrateur Multi-IA est prêt.</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) -->
<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-for="msg in messages" :key="msg.id" :class="['max-w-3xl rounded-2xl p-4', msg.role === 'user' ? 'bg-gray-800 ml-auto border border-gray-700' : 'bg-aethas-panel mr-auto border border-aethas-cyan/30']">
<div class="flex items-center gap-2 mb-2">
<i v-if="msg.role === 'user'" data-lucide="user" class="w-4 h-4 text-gray-400"></i>
<i v-else data-lucide="bot" class="w-4 h-4 text-aethas-cyan"></i>
<span class="text-xs font-semibold uppercase tracking-wider text-gray-400">{{ msg.role === 'user' ? 'Vous' : 'AETHAS38' }}</span>
</div>
<div class="text-gray-200 whitespace-pre-wrap text-sm leading-relaxed">{{ msg.content }}</div>
</div>
</div>
<!-- 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" title="Joindre un fichier"> <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>
<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"
ref="promptTextarea" ref="promptTextarea"
:disabled="!activeProject"
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" 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 class="p-3 text-aethas-cyan hover:text-white transition-colors flex-shrink-0" title="Envoyer"> <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>
<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>
@@ -114,7 +142,7 @@
</div> </div>
<script> <script>
const { createApp, ref, onMounted, nextTick } = Vue; const { createApp, ref, onMounted, nextTick, computed } = Vue;
createApp({ createApp({
setup() { setup() {
@@ -123,8 +151,13 @@
const prompt = ref(""); const prompt = ref("");
const promptTextarea = ref(null); const promptTextarea = ref(null);
const projects = ref([]); const projects = ref([]);
const activeProject = ref(null);
const messages = ref([]);
const isLoading = ref(false); const isLoading = ref(false);
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 toggleSidebar = () => { isSidebarOpen.value = !isSidebarOpen.value; };
const autoResize = () => { const autoResize = () => {
@@ -134,6 +167,13 @@
el.style.height = el.scrollHeight + 'px'; el.style.height = el.scrollHeight + 'px';
}; };
const scrollToBottom = () => {
nextTick(() => {
const container = document.getElementById('chat-container');
if (container) container.scrollTop = container.scrollHeight;
});
};
const fetchProjects = async () => { const fetchProjects = async () => {
try { try {
const response = await fetch('/api/projects'); const response = await fetch('/api/projects');
@@ -141,9 +181,20 @@
projects.value = await response.json(); projects.value = await response.json();
nextTick(() => lucide.createIcons()); nextTick(() => lucide.createIcons());
} }
} catch (error) { } catch (error) { console.error("Erreur", error); }
console.error("Erreur lors de la récupération des projets", 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("Erreur chargement messages", error); }
}; };
const createNewProject = async () => { const createNewProject = async () => {
@@ -154,17 +205,57 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: "Nouvelle discussion" }) body: JSON.stringify({ title: "Nouvelle discussion" })
}); });
if (response.ok) { if (response.ok) {
const newProject = await response.json(); const newProject = await response.json();
projects.value.unshift(newProject); // Ajoute le nouveau projet en haut de la liste projects.value.unshift(newProject);
nextTick(() => lucide.createIcons()); selectProject(newProject);
} }
} catch (error) { } catch (error) { console.error("Erreur", error); }
console.error("Erreur lors de la création du projet", error); finally { isLoading.value = false; }
} finally { };
isLoading.value = false;
} const sendMessage = async () => {
if (!prompt.value.trim() || !activeProject.value) return;
const userText = prompt.value.trim();
prompt.value = "";
autoResize();
// Ajout optimiste à l'UI
messages.value.push({ id: Date.now(), role: 'user', content: userText });
scrollToBottom();
nextTick(() => lucide.createIcons());
// Sauvegarde en DB
try {
await fetch(`/api/projects/${activeProject.value.id}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
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); }
};
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("Erreur d'épinglage", 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("Erreur de suppression", error); }
}; };
onMounted(() => { onMounted(() => {
@@ -172,7 +263,11 @@
fetchProjects(); fetchProjects();
}); });
return { username, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea, projects, createNewProject, isLoading }; return {
username, isSidebarOpen, toggleSidebar, prompt, autoResize, promptTextarea,
projects, pinnedProjects, recentProjects, activeProject, messages,
createNewProject, selectProject, sendMessage, isLoading, togglePin, deleteProject
};
} }
}).mount('#app'); }).mount('#app');
</script> </script>