import { prisma } from '@/lib/prisma' import { getAIProvider } from '@/lib/ai/factory' export interface SuggestedLabel { name: string count: number confidence: number noteIds: string[] } export interface AutoLabelSuggestion { notebookId: string notebookName: string notebookIcon: string | null suggestedLabels: SuggestedLabel[] totalNotes: number } /** * Service for automatically suggesting new labels based on recurring themes * (Story 5.4 - IA4) */ export class AutoLabelCreationService { /** * Analyze a notebook and suggest new labels based on recurring themes * @param notebookId - Notebook ID to analyze * @param userId - User ID (for authorization) * @returns Suggested labels or null if not enough notes/no patterns found */ async suggestLabels(notebookId: string, userId: string): Promise { // 1. Get notebook with existing labels const notebook = await prisma.notebook.findFirst({ where: { id: notebookId, userId, }, include: { labels: { select: { id: true, name: true, }, }, _count: { select: { notes: true }, }, }, }) if (!notebook) { throw new Error('Notebook not found') } // Only trigger if notebook has 15+ notes (PRD requirement) if (notebook._count.notes < 15) { return null } // Get all notes in this notebook const notes = await prisma.note.findMany({ where: { notebookId, userId, }, select: { id: true, title: true, content: true, labelRelations: { select: { name: true, }, }, }, orderBy: { updatedAt: 'desc', }, take: 100, // Limit to 100 most recent notes }) if (notes.length === 0) { return null } // 2. Use AI to detect recurring themes const suggestions = await this.detectRecurringThemes(notes, notebook) return suggestions } /** * Use AI to detect recurring themes and suggest labels */ private async detectRecurringThemes( notes: any[], notebook: any ): Promise { const existingLabelNames = new Set( notebook.labels.map((l: any) => l.name.toLowerCase()) ) const prompt = this.buildPrompt(notes, existingLabelNames) try { const provider = getAIProvider() const response = await provider.generateText(prompt) // Parse AI response const suggestions = this.parseAIResponse(response, notes) if (!suggestions || suggestions.suggestedLabels.length === 0) { return null } return { notebookId: notebook.id, notebookName: notebook.name, notebookIcon: notebook.icon, suggestedLabels: suggestions.suggestedLabels, totalNotes: notebook._count.notes, } } catch (error) { console.error('Failed to detect recurring themes:', error) return null } } /** * Build prompt for AI (always in French - interface language) */ private buildPrompt(notes: any[], existingLabelNames: Set): string { const notesSummary = notes .map((note, index) => { const title = note.title || 'Sans titre' const content = note.content.substring(0, 150) return `[${index}] "${title}": ${content}` }) .join('\n') const existingLabels = Array.from(existingLabelNames).join(', ') return ` Tu es un assistant qui détecte les thèmes récurrents dans des notes pour suggérer de nouvelles étiquettes. CARNET ANALYSÉ : ${notes.length} notes ÉTIQUETTES EXISTANTES (ne pas suggérer celles-ci) : ${existingLabels || 'Aucune'} NOTES DU CARNET : ${notesSummary} TÂCHE : Analyse les notes et détecte les thèmes récurrents (mots-clés, sujets, lieux, personnes). Un thème doit apparaître dans au moins 5 notes différentes pour être suggéré. FORMAT DE RÉPONSE (JSON) : { "labels": [ { "nom": "nom_du_label", "note_indices": [0, 5, 12, 23, 45], "confiance": 0.85 } ] } RÈGLES : - Le nom du label doit être court (1-2 mots max) - Un thème doit apparaître dans 5+ notes pour être suggéré - La confiance doit être > 0.60 - Ne pas suggérer des étiquettes qui existent déjà - Priorise les lieux, personnes, catégories claires - Maximum 5 suggestions Exemples de bonnes étiquettes : - "tokyo", "kyoto", "osaka" (lieux) - "hôtels", "restos", "vols" (catégories) - "marie", "jean", "équipe" (personnes) Ta réponse (JSON seulement) : `.trim() } /** * Parse AI response into suggested labels */ private parseAIResponse(response: string, notes: any[]): { suggestedLabels: SuggestedLabel[] } | null { try { const jsonMatch = response.match(/\{[\s\S]*\}/) if (!jsonMatch) { throw new Error('No JSON found in response') } const aiData = JSON.parse(jsonMatch[0]) const suggestedLabels: SuggestedLabel[] = (aiData.labels || []) .map((label: any) => { // Filter by confidence threshold if (label.confiance <= 0.60) return null // Get note IDs from indices const noteIds = label.note_indices .map((idx: number) => notes[idx]?.id) .filter(Boolean) // Must have at least 5 notes if (noteIds.length < 5) return null return { name: label.nom, count: noteIds.length, confidence: label.confiance, noteIds, } }) .filter(Boolean) if (suggestedLabels.length === 0) { return null } // Sort by count (descending) and confidence suggestedLabels.sort((a, b) => { if (b.count !== a.count) { return b.count - a.count // More notes first } return b.confidence - a.confidence // Then higher confidence }) // Limit to top 5 return { suggestedLabels: suggestedLabels.slice(0, 5), } } catch (error) { console.error('Failed to parse AI response:', error) return null } } /** * Create suggested labels and assign them to notes * @param notebookId - Notebook ID * @param userId - User ID * @param suggestions - Suggested labels to create * @param selectedLabels - Labels user selected to create * @returns Number of labels created */ async createLabels( notebookId: string, userId: string, suggestions: AutoLabelSuggestion, selectedLabels: string[] ): Promise { let createdCount = 0 for (const suggestedLabel of suggestions.suggestedLabels) { if (!selectedLabels.includes(suggestedLabel.name)) continue // Create the label const label = await prisma.label.create({ data: { name: suggestedLabel.name, color: 'gray', // Default color, user can change later notebookId, userId, }, }) // Assign label to all suggested notes (updateMany doesn't support relations) for (const noteId of suggestedLabel.noteIds) { await prisma.note.update({ where: { id: noteId }, data: { labelRelations: { connect: { id: label.id, }, }, }, }) } createdCount++ } return createdCount } } // Export singleton instance export const autoLabelCreationService = new AutoLabelCreationService()