import { prisma } from '@/lib/prisma' import { getAIProvider } from '@/lib/ai/factory' export interface NoteForOrganization { id: string title: string | null content: string } export interface NotebookOrganization { notebookId: string notebookName: string notebookIcon: string | null notebookColor: string | null notes: Array<{ noteId: string title: string | null content: string confidence: number reason: string }> } export interface OrganizationPlan { notebooks: NotebookOrganization[] totalNotes: number unorganizedNotes: number // Notes that couldn't be categorized } /** * Service for batch organizing notes from "Notes générales" into notebooks * (Story 5.3 - IA3) */ export class BatchOrganizationService { /** * Analyze all notes in "Notes générales" and create an organization plan * @param userId - User ID * @returns Organization plan with notebook assignments */ async createOrganizationPlan(userId: string): Promise { // 1. Get all notes without notebook (Inbox/Notes générales) const notesWithoutNotebook = await prisma.note.findMany({ where: { userId, notebookId: null, }, select: { id: true, title: true, content: true, }, orderBy: { updatedAt: 'desc', }, take: 50, // Limit to 50 notes for AI processing }) if (notesWithoutNotebook.length === 0) { return { notebooks: [], totalNotes: 0, unorganizedNotes: 0, } } // 2. Get all user's notebooks const notebooks = await prisma.notebook.findMany({ where: { userId }, include: { labels: true, _count: { select: { notes: true }, }, }, orderBy: { order: 'asc' }, }) if (notebooks.length === 0) { // No notebooks to organize into return { notebooks: [], totalNotes: notesWithoutNotebook.length, unorganizedNotes: notesWithoutNotebook.length, } } // 3. Call AI to create organization plan const plan = await this.aiOrganizeNotes(notesWithoutNotebook, notebooks) return plan } /** * Use AI to analyze notes and create organization plan */ private async aiOrganizeNotes( notes: NoteForOrganization[], notebooks: any[] ): Promise { const prompt = this.buildPrompt(notes, notebooks) try { const provider = getAIProvider() const response = await provider.generateText(prompt) // Parse AI response const plan = this.parseAIResponse(response, notes, notebooks) return plan } catch (error) { console.error('Failed to create organization plan:', error) // Return empty plan on error return { notebooks: [], totalNotes: notes.length, unorganizedNotes: notes.length, } } } /** * Build prompt for AI (always in French - interface language) */ private buildPrompt(notes: NoteForOrganization[], notebooks: any[]): string { const notebookList = notebooks .map(nb => { const labels = nb.labels.map((l: any) => l.name).join(', ') const count = nb._count?.notes || 0 return `- ${nb.name} (${count} notes)${labels ? ` [labels: ${labels}]` : ''}` }) .join('\n') const notesList = notes .map((note, index) => { const title = note.title || 'Sans titre' const content = note.content.substring(0, 200) return `[${index}] "${title}": ${content}` }) .join('\n') return ` Tu es un assistant qui organise des notes en les regroupant par thématique dans des carnets. CARNETS DISPONIBLES : ${notebookList} NOTES À ORGANISER (Notes générales) : ${notesList} TÂCHE : Analyse chaque note et propose le carnet le PLUS approprié. Considère : 1. Le sujet/thème de la note (LE PLUS IMPORTANT) 2. Les labels existants dans chaque carnet 3. La cohérence thématique entre notes du même carnet GUIDES DE CLASSIFICATION : - SPORT/EXERCICE/ACHATS/COURSSES → Carnet Personnel - LOISIRS/PASSIONS/SORTIES → Carnet Personnel - SANTÉ/FITNESS/MÉDECIN → Carnet Personnel ou Santé - FAMILLE/AMIS → Carnet Personnel - TRAVAIL/RÉUNIONS/PROJETS/CLIENTS → Carnet Travail - CODING/TECH/DÉVELOPPEMENT → Carnet Travail ou Code - FINANCES/FACTURES/BANQUE → Carnet Personnel ou Finances FORMAT DE RÉPONSE (JSON) : Pour chaque carnet, liste les notes qui lui appartiennent : { "carnets": [ { "nom": "Nom du carnet", "notes": [ { "index": 0, "confiance": 0.95, "raison": "Courte explication" } ] } ] } RÈGLES : - Seules les notes avec confiance > 0.60 doivent être assignées - Si une note est trop générique, ne l'assigne pas - Sois précis dans tes regroupements thématiques Ta réponse (JSON seulement) : `.trim() } /** * Parse AI response into OrganizationPlan */ private parseAIResponse( response: string, notes: NoteForOrganization[], notebooks: any[] ): OrganizationPlan { try { // Try to parse JSON response const jsonMatch = response.match(/\{[\s\S]*\}/) if (!jsonMatch) { throw new Error('No JSON found in response') } const aiData = JSON.parse(jsonMatch[0]) const notebookOrganizations: NotebookOrganization[] = [] // Process each notebook in AI response for (const aiNotebook of aiData.carnets || []) { const notebook = notebooks.find(nb => nb.name === aiNotebook.nom) if (!notebook) continue const noteAssignments = aiNotebook.notes .filter((n: any) => n.confiance > 0.60) // Only high confidence .map((n: any) => { const note = notes[n.index] if (!note) return null return { noteId: note.id, title: note.title, content: note.content, confidence: n.confiance, reason: n.raison || '', } }) .filter(Boolean) if (noteAssignments.length > 0) { notebookOrganizations.push({ notebookId: notebook.id, notebookName: notebook.name, notebookIcon: notebook.icon, notebookColor: notebook.color, notes: noteAssignments, }) } } // Count unorganized notes const organizedNoteIds = new Set( notebookOrganizations.flatMap(nb => nb.notes.map(n => n.noteId)) ) const unorganizedCount = notes.length - organizedNoteIds.size return { notebooks: notebookOrganizations, totalNotes: notes.length, unorganizedNotes: unorganizedCount, } } catch (error) { console.error('Failed to parse AI response:', error) return { notebooks: [], totalNotes: notes.length, unorganizedNotes: notes.length, } } } /** * Apply the organization plan (move notes to notebooks) * @param userId - User ID * @param plan - Organization plan to apply * @param selectedNoteIds - Specific note IDs to organize (user can deselect) * @returns Number of notes moved */ async applyOrganizationPlan( userId: string, plan: OrganizationPlan, selectedNoteIds: string[] ): Promise { let movedCount = 0 for (const notebookOrg of plan.notebooks) { // Filter notes that are selected const notesToMove = notebookOrg.notes.filter(n => selectedNoteIds.includes(n.noteId) ) if (notesToMove.length === 0) continue // Move notes to notebook await prisma.note.updateMany({ where: { id: { in: notesToMove.map(n => n.noteId) }, userId, }, data: { notebookId: notebookOrg.notebookId, }, }) movedCount += notesToMove.length } return movedCount } } // Export singleton instance export const batchOrganizationService = new BatchOrganizationService()