- Implemented multi-selection for notes with Ctrl+click, long-press, and keyboard shortcuts (Ctrl+A, Escape) - Added Gemini API integration with environment configuration and routes - Enhanced code block UI with improved copy feedback animation and visual polish - Added sort order toggle (asc/desc) for note lists with persistent state
569 lines
14 KiB
Markdown
569 lines
14 KiB
Markdown
# 🔧 Interface IA Gemini - Implémentation Technique
|
|
|
|
## 📐 Architecture globale
|
|
|
|
### Flux de données
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ USER INTERACTION │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ GeminiPanelComponent (UI Layer) │
|
|
│ - Affichage des tâches disponibles │
|
|
│ - Gestion des événements utilisateur │
|
|
│ - Feedback visuel (progress, success, error) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ GeminiService (Business Logic) │
|
|
│ - Orchestration des tâches IA │
|
|
│ - Extraction du contenu textuel │
|
|
│ - Génération des résumés │
|
|
│ - Gestion de l'état d'exécution │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
┌──────────┴──────────┐
|
|
▼ ▼
|
|
┌────────────────┐ ┌───────────────┐
|
|
│ VaultService │ │ HttpClient │
|
|
│ (Read Note) │ │ (Update API) │
|
|
└────────────────┘ └───────────────┘
|
|
│ │
|
|
└──────────┬──────────┘
|
|
▼
|
|
┌───────────────────┐
|
|
│ Backend API │
|
|
│ PATCH /api/... │
|
|
└───────────────────┘
|
|
│
|
|
▼
|
|
┌───────────────────┐
|
|
│ Filesystem │
|
|
│ (YAML Updated) │
|
|
└───────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🧩 GeminiService - Service Angular
|
|
|
|
### Responsabilités
|
|
|
|
1. **Gestion des tâches IA**: Catalogue et exécution
|
|
2. **Traitement du contenu**: Extraction et nettoyage
|
|
3. **Communication API**: Mise à jour du frontmatter
|
|
4. **Gestion d'état**: Signals pour la réactivité
|
|
|
|
### Signals Angular
|
|
|
|
```typescript
|
|
// État d'exécution en cours
|
|
readonly currentExecution = signal<GeminiTaskExecution | null>(null);
|
|
|
|
// Disponibilité du service
|
|
readonly isAvailable = signal<boolean>(true);
|
|
|
|
// Compteur de tâches exécutées
|
|
readonly tasksCount = signal<number>(0);
|
|
```
|
|
|
|
### Méthode principale: `generateDescription()`
|
|
|
|
```typescript
|
|
async generateDescription(noteId: string): Promise<GeminiTaskResult> {
|
|
// 1. Récupérer la note via VaultService
|
|
const note = this.vault.getNoteById(noteId);
|
|
|
|
// 2. Extraire le contenu textuel (sans frontmatter, code, etc.)
|
|
const textContent = this.extractTextContent(note);
|
|
|
|
// 3. Générer le résumé (heuristique MVP ou API Gemini future)
|
|
const description = await this.generateSummary(textContent, note.title);
|
|
|
|
// 4. Mettre à jour le frontmatter via l'API
|
|
await this.updateNoteFrontmatter(noteId, {
|
|
...note.frontmatter,
|
|
description
|
|
});
|
|
|
|
// 5. Retourner le résultat
|
|
return { success: true, data: { description, noteId }, duration };
|
|
}
|
|
```
|
|
|
|
### Extraction du contenu textuel
|
|
|
|
La méthode `extractTextContent()` nettoie le markdown:
|
|
|
|
- Retire les blocs de code (` ``` `)
|
|
- Retire les images et liens
|
|
- Retire les titres markdown (`#`)
|
|
- Retire les listes (`-`, `*`, `1.`)
|
|
- Nettoie les espaces multiples
|
|
|
|
### Génération du résumé (MVP)
|
|
|
|
Pour le MVP, approche heuristique simple:
|
|
|
|
1. Découper le texte en phrases
|
|
2. Prendre la première phrase significative (> 20 caractères)
|
|
3. Tronquer à ~120 caractères si nécessaire
|
|
4. Capitaliser et ajouter ponctuation
|
|
|
|
**Future**: Remplacer par un appel à l'API Gemini réelle.
|
|
|
|
### Communication API
|
|
|
|
```typescript
|
|
private async updateNoteFrontmatter(
|
|
noteId: string,
|
|
frontmatter: NoteFrontmatter
|
|
): Promise<void> {
|
|
await firstValueFrom(
|
|
this.http.patch(`/api/vault/notes/${noteId}`, { frontmatter })
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 GeminiPanelComponent - Composant UI
|
|
|
|
### Responsabilités
|
|
|
|
1. **Affichage des tâches**: Grid responsive de cartes
|
|
2. **Feedback utilisateur**: Animations, progress bars, messages
|
|
3. **Gestion des événements**: Clicks, keyboard, backdrop
|
|
4. **Réactivité**: Angular Signals pour updates temps réel
|
|
|
|
### Structure du template
|
|
|
|
```html
|
|
<!-- Backdrop avec blur -->
|
|
<div class="backdrop" (click)="onBackdropClick()">
|
|
|
|
<!-- Panel principal -->
|
|
<div class="panel">
|
|
|
|
<!-- Header avec gradient -->
|
|
<div class="header">
|
|
<icon>🤖</icon>
|
|
<title>IA Gemini</title>
|
|
<close-button></close-button>
|
|
<note-info *ngIf="selectedNote"></note-info>
|
|
</div>
|
|
|
|
<!-- Status/Progress -->
|
|
<div *ngIf="execution()" class="status">
|
|
<progress-bar [value]="execution()!.progress"></progress-bar>
|
|
<success-message *ngIf="success"></success-message>
|
|
<error-message *ngIf="error"></error-message>
|
|
</div>
|
|
|
|
<!-- Tasks Grid -->
|
|
<div class="tasks-grid">
|
|
<task-card
|
|
*ngFor="let task of tasks"
|
|
[task]="task"
|
|
[disabled]="!task.enabled || isRunning()"
|
|
(click)="executeTask(task.id)">
|
|
</task-card>
|
|
</div>
|
|
|
|
<!-- Stats Footer -->
|
|
<div class="footer">
|
|
<status-indicator></status-indicator>
|
|
<tasks-counter></tasks-counter>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### Animations Angular
|
|
|
|
```typescript
|
|
animations: [
|
|
// Fade in/out pour le backdrop
|
|
trigger('fadeInOut', [
|
|
transition(':enter', [
|
|
style({ opacity: 0 }),
|
|
animate('200ms ease-in', style({ opacity: 1 }))
|
|
]),
|
|
transition(':leave', [
|
|
animate('200ms ease-out', style({ opacity: 0 }))
|
|
])
|
|
]),
|
|
|
|
// Scale in pour le panel
|
|
trigger('scaleIn', [
|
|
transition(':enter', [
|
|
style({ opacity: 0, transform: 'scale(0.95)' }),
|
|
animate('200ms ease-out', style({ opacity: 1, transform: 'scale(1)' }))
|
|
])
|
|
]),
|
|
|
|
// Slide in pour les messages
|
|
trigger('slideIn', [
|
|
transition(':enter', [
|
|
style({ opacity: 0, transform: 'translateY(10px)' }),
|
|
animate('300ms ease-out', style({ opacity: 1, transform: 'translateY(0)' }))
|
|
])
|
|
])
|
|
]
|
|
```
|
|
|
|
### Gestion de l'état
|
|
|
|
```typescript
|
|
// Computed signals pour la réactivité
|
|
readonly execution = computed(() => this.gemini.currentExecution());
|
|
readonly isRunning = computed(() => this.execution()?.status === 'running');
|
|
|
|
// Auto-reset après succès
|
|
constructor() {
|
|
effect(() => {
|
|
const exec = this.execution();
|
|
if (exec?.status === 'success') {
|
|
setTimeout(() => this.gemini.resetExecution(), 5000);
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
### Exécution d'une tâche
|
|
|
|
```typescript
|
|
async executeTask(taskId: GeminiTaskType): Promise<void> {
|
|
// Vérifications préalables
|
|
if (!this.selectedNote || this.isRunning()) return;
|
|
|
|
// Routing vers la bonne méthode du service
|
|
switch (taskId) {
|
|
case 'generate-description':
|
|
await this.gemini.generateDescription(this.selectedNote.id);
|
|
break;
|
|
// ... autres tâches
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🔗 Intégration dans AppShellNimbusLayoutComponent
|
|
|
|
### Modifications apportées
|
|
|
|
#### 1. Import du composant
|
|
|
|
```typescript
|
|
import { GeminiPanelComponent } from '../../features/gemini/gemini-panel.component';
|
|
```
|
|
|
|
#### 2. Ajout dans le tableau `imports`
|
|
|
|
```typescript
|
|
imports: [
|
|
// ... autres imports
|
|
GeminiPanelComponent
|
|
]
|
|
```
|
|
|
|
#### 3. Variable d'état
|
|
|
|
```typescript
|
|
showGeminiPanel = false;
|
|
```
|
|
|
|
#### 4. Bouton dans la sidebar
|
|
|
|
```html
|
|
<button
|
|
class="p-2 rounded hover:bg-surface1 dark:hover:bg-card"
|
|
(click)="onGeminiPanelOpen()"
|
|
title="IA Gemini">
|
|
🤖
|
|
</button>
|
|
```
|
|
|
|
#### 5. Méthode d'ouverture
|
|
|
|
```typescript
|
|
onGeminiPanelOpen(): void {
|
|
this.showGeminiPanel = true;
|
|
this.scheduleCloseFlyout(0); // Fermer les flyouts
|
|
}
|
|
```
|
|
|
|
#### 6. Template du panel
|
|
|
|
```html
|
|
<!-- Gemini Panel -->
|
|
<app-gemini-panel
|
|
*ngIf="showGeminiPanel"
|
|
[selectedNote]="selectedNote"
|
|
(close)="showGeminiPanel = false">
|
|
</app-gemini-panel>
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 Styles TailwindCSS
|
|
|
|
### Classes principales
|
|
|
|
```css
|
|
/* Backdrop */
|
|
.backdrop {
|
|
@apply fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4;
|
|
}
|
|
|
|
/* Panel */
|
|
.panel {
|
|
@apply bg-card dark:bg-main border border-border dark:border-gray-700 rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden relative flex flex-col;
|
|
}
|
|
|
|
/* Header gradient */
|
|
.header-gradient {
|
|
@apply absolute inset-0 bg-gradient-to-br from-purple-500/10 via-blue-500/10 to-pink-500/10 dark:from-purple-500/5 dark:via-blue-500/5 dark:to-pink-500/5;
|
|
}
|
|
|
|
/* Task card */
|
|
.task-card {
|
|
@apply p-5 rounded-xl border-2 transition-all duration-200 text-left;
|
|
@apply border-purple-200 dark:border-purple-800;
|
|
@apply hover:border-purple-400 dark:hover:border-purple-600;
|
|
@apply hover:shadow-lg hover:scale-105;
|
|
@apply bg-gradient-to-br from-purple-50/50 to-blue-50/50;
|
|
@apply dark:from-purple-950/20 dark:to-blue-950/20;
|
|
}
|
|
|
|
/* Progress bar */
|
|
.progress-bar {
|
|
@apply w-full h-2 bg-blue-100 dark:bg-blue-900/50 rounded-full overflow-hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
@apply h-full bg-blue-500 transition-all duration-300 ease-out;
|
|
}
|
|
```
|
|
|
|
### Support des thèmes
|
|
|
|
Toutes les classes utilisent les variables CSS des thèmes ObsiViewer:
|
|
|
|
- `bg-card` / `dark:bg-main`
|
|
- `text-main` / `dark:text-white`
|
|
- `border-border` / `dark:border-gray-700`
|
|
- `bg-surface1` / `dark:bg-card`
|
|
- `text-muted`
|
|
|
|
---
|
|
|
|
## 📡 API Backend
|
|
|
|
### Endpoint utilisé
|
|
|
|
```http
|
|
PATCH /api/vault/notes/:id
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"frontmatter": {
|
|
"description": "Generated summary...",
|
|
"tags": ["existing", "tags"],
|
|
// ... autres champs
|
|
}
|
|
}
|
|
```
|
|
|
|
### Réponse
|
|
|
|
```json
|
|
{
|
|
"id": "folder/note",
|
|
"success": true
|
|
}
|
|
```
|
|
|
|
### Gestion d'erreurs
|
|
|
|
- **404**: Note non trouvée
|
|
- **400**: Frontmatter invalide
|
|
- **500**: Erreur serveur
|
|
|
|
---
|
|
|
|
## 🔄 Cycle de vie
|
|
|
|
### 1. Ouverture du panneau
|
|
|
|
```
|
|
User clicks 🤖 button
|
|
↓
|
|
onGeminiPanelOpen() called
|
|
↓
|
|
showGeminiPanel = true
|
|
↓
|
|
GeminiPanelComponent rendered with fadeIn animation
|
|
↓
|
|
Display tasks grid + selected note info
|
|
```
|
|
|
|
### 2. Exécution d'une tâche
|
|
|
|
```
|
|
User clicks task card
|
|
↓
|
|
executeTask(taskId) called
|
|
↓
|
|
GeminiService.generateDescription(noteId)
|
|
↓
|
|
currentExecution signal updated (status: 'running')
|
|
↓
|
|
Progress bar animates (0% → 100%)
|
|
↓
|
|
API call to update frontmatter
|
|
↓
|
|
currentExecution updated (status: 'success')
|
|
↓
|
|
Success message displayed with result
|
|
↓
|
|
Auto-reset after 5 seconds
|
|
```
|
|
|
|
### 3. Fermeture du panneau
|
|
|
|
```
|
|
User clicks close button OR presses ESC OR clicks backdrop
|
|
↓
|
|
close.emit() called
|
|
↓
|
|
showGeminiPanel = false (in parent)
|
|
↓
|
|
GeminiPanelComponent destroyed with fadeOut animation
|
|
```
|
|
|
|
---
|
|
|
|
## ⚡ Optimisations
|
|
|
|
### Angular Signals
|
|
|
|
- Réactivité fine-grain sans zone.js
|
|
- Updates automatiques du DOM
|
|
- Performance optimale
|
|
|
|
### ChangeDetectionStrategy.OnPush
|
|
|
|
- Détection de changements manuelle
|
|
- Reduce angular cycles
|
|
- CPU usage minimal
|
|
|
|
### Lazy loading
|
|
|
|
Le composant n'est chargé que quand nécessaire via `*ngIf`.
|
|
|
|
### Debouncing
|
|
|
|
Auto-reset après 5 secondes évite les memory leaks.
|
|
|
|
---
|
|
|
|
## 🧪 Points de test
|
|
|
|
### Unit tests (GeminiService)
|
|
|
|
- ✅ `generateDescription()` avec note valide
|
|
- ✅ `generateDescription()` avec note vide
|
|
- ✅ `extractTextContent()` retire le code
|
|
- ✅ `extractTextContent()` retire les images
|
|
- ✅ `generateSummary()` tronque correctement
|
|
- ✅ `updateNoteFrontmatter()` appelle l'API
|
|
- ✅ Gestion d'erreurs API
|
|
|
|
### Integration tests (GeminiPanelComponent)
|
|
|
|
- ✅ Ouverture/fermeture du panneau
|
|
- ✅ Affichage des tâches disponibles
|
|
- ✅ Désactivation pendant exécution
|
|
- ✅ Affichage du progress
|
|
- ✅ Affichage du succès
|
|
- ✅ Affichage des erreurs
|
|
- ✅ Support clavier (ESC)
|
|
|
|
### E2E tests (Playwright)
|
|
|
|
- ✅ Workflow complet: open → execute → verify YAML
|
|
- ✅ Responsive (desktop/tablet/mobile)
|
|
- ✅ Thèmes (light/dark/...)
|
|
- ✅ Cas d'erreur (note inexistante, API down)
|
|
|
|
---
|
|
|
|
## 🔐 Sécurité
|
|
|
|
### Validation des entrées
|
|
|
|
```typescript
|
|
// Vérification de l'existence de la note
|
|
const note = this.vault.getNoteById(noteId);
|
|
if (!note) {
|
|
throw new Error('Note introuvable');
|
|
}
|
|
|
|
// Validation du contenu
|
|
if (!textContent || textContent.trim().length === 0) {
|
|
throw new Error('Aucun contenu textuel trouvé');
|
|
}
|
|
```
|
|
|
|
### Sanitization des sorties
|
|
|
|
```typescript
|
|
// S'assurer d'une ponctuation valide
|
|
if (!summary.match(/[.!?]$/)) {
|
|
summary += '.';
|
|
}
|
|
|
|
// Capitalisation correcte
|
|
summary = summary.charAt(0).toUpperCase() + summary.slice(1);
|
|
```
|
|
|
|
### Protection XSS
|
|
|
|
Angular échappe automatiquement les bindings dans le template.
|
|
|
|
---
|
|
|
|
## 📊 Performance Metrics
|
|
|
|
### Temps d'exécution (MVP)
|
|
|
|
| Étape | Durée | % |
|
|
|-------|-------|---|
|
|
| Récupération note | 10-20ms | 2% |
|
|
| Extraction contenu | 50-100ms | 10% |
|
|
| Génération résumé | 800-1000ms | 85% |
|
|
| Mise à jour API | 20-50ms | 3% |
|
|
| **TOTAL** | **~1 seconde** | **100%** |
|
|
|
|
### Avec API Gemini (future)
|
|
|
|
| Étape | Durée | % |
|
|
|-------|-------|---|
|
|
| Récupération note | 10-20ms | 1% |
|
|
| Extraction contenu | 50-100ms | 5% |
|
|
| API Gemini call | 1500-2000ms | 93% |
|
|
| Mise à jour API | 20-50ms | 1% |
|
|
| **TOTAL** | **~2 secondes** | **100%** |
|
|
|
|
---
|
|
|
|
**Dernière mise à jour**: 2025-01-15
|
|
**Version**: 1.0.0
|
|
**Auteur**: Bruno Charest
|