import { prisma } from '@/lib/prisma' import { getAIProvider } from '@/lib/ai/factory' import { getSystemConfig } from '@/lib/config' import type { Notebook } from '@/lib/types' export class NotebookSuggestionService { /** * Suggest the most appropriate notebook for a note * @param noteContent - Content of the note * @param userId - User ID (for fetching user's notebooks) * @returns Suggested notebook or null (if no good match) */ async suggestNotebook(noteContent: string, userId: string, language: string = 'en'): Promise { // 1. Get all notebooks for this user const notebooks = await prisma.notebook.findMany({ where: { userId }, include: { labels: true, _count: { select: { notes: true }, }, }, orderBy: { order: 'asc' }, }) if (notebooks.length === 0) { return null // No notebooks to suggest } // 2. Build prompt for AI (always in French - interface language) const prompt = this.buildPrompt(noteContent, notebooks, language) // 3. Call AI try { const config = await getSystemConfig() const provider = getAIProvider(config) const response = await provider.generateText(prompt) const suggestedName = response.trim().toUpperCase() // 5. Find matching notebook const suggestedNotebook = notebooks.find(nb => nb.name.toUpperCase() === suggestedName ) // If AI says "NONE" or no match, return null if (suggestedName === 'NONE' || !suggestedNotebook) { return null } return suggestedNotebook as Notebook } catch (error) { console.error('Failed to suggest notebook:', error) return null } } /** * Build the AI prompt for notebook suggestion (localized) */ private buildPrompt(noteContent: string, notebooks: any[], language: string = 'en'): 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 instructions: Record = { fr: ` Tu es un assistant qui suggère à quel carnet une note devrait appartenir. CONTENU DE LA NOTE : ${noteContent.substring(0, 500)} CARNETS DISPONIBLES : ${notebookList} TÂCHE : Analyse le contenu de la note (peu importe la langue) et suggère le carnet le PLUS approprié pour cette note. Considère : 1. Le sujet/thème de la note (LE PLUS IMPORTANT) 2. Les labels existants dans chaque carnet 3. Le nombre de notes (préfère les carnets avec du contenu connexe) 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 RÈGLES : - Retourne SEULEMENT le nom du carnet, EXACTEMENT comme indiqué ci-dessus (insensible à la casse) - Si aucune bonne correspondance n'existe, retourne "NONE" - Si la note est trop générique/vague, retourne "NONE" - N'inclus pas d'explications ou de texte supplémentaire Exemples : - "Réunion avec Jean sur le planning du projet" → carnet "Travail" - "Liste de courses ou achat de vêtements" → carnet "Personnel" - "Script Python pour analyse de données" → carnet "Code" - "Séance de sport ou fitness" → carnet "Personnel" - "Achat d'une chemise et d'un jean" → carnet "Personnel" Ta suggestion : `.trim(), en: ` You are an assistant that suggests which notebook a note should belong to. NOTE CONTENT: ${noteContent.substring(0, 500)} AVAILABLE NOTEBOOKS: ${notebookList} TASK: Analyze the note content (regardless of language) and suggest the MOST appropriate notebook for this note. Consider: 1. The subject/theme of the note (MOST IMPORTANT) 2. Existing labels in each notebook 3. The number of notes (prefer notebooks with related content) CLASSIFICATION GUIDE: - SPORT/EXERCISE/SHOPPING/GROCERIES → Personal Notebook - HOBBIES/PASSIONS/OUTINGS → Personal Notebook - HEALTH/FITNESS/DOCTOR → Personal Notebook or Health - FAMILY/FRIENDS → Personal Notebook - WORK/MEETINGS/PROJECTS/CLIENTS → Work Notebook - CODING/TECH/DEVELOPMENT → Work Notebook or Code - FINANCE/BILLS/BANKING → Personal Notebook or Finance RULES: - Return ONLY the notebook name, EXACTLY as listed above (case insensitive) - If no good match exists, return "NONE" - If the note is too generic/vague, return "NONE" - Do not include explanations or extra text Examples: - "Meeting with John about project planning" → notebook "Work" - "Grocery list or buying clothes" → notebook "Personal" - "Python script for data analysis" → notebook "Code" - "Gym session or fitness" → notebook "Personal" Your suggestion: `.trim(), fa: ` شما یک دستیار هستید که پیشنهاد می‌دهد یک یادداشت به کدام دفترچه تعلق داشته باشد. محتوای یادداشت: ${noteContent.substring(0, 500)} دفترچه‌های موجود: ${notebookList} وظیفه: محتوای یادداشت را تحلیل کنید (صرف نظر از زبان) و مناسب‌ترین دفترچه را برای این یادداشت پیشنهاد دهید. در نظر بگیرید: 1. موضوع/تم یادداشت (مهم‌ترین) 2. برچسب‌های موجود در هر دفترچه 3. تعداد یادداشت‌ها (دفترچه‌های با محتوای مرتبط را ترجیح دهید) راهنمای طبقه‌بندی: - ورزش/تمرین/خرید → دفترچه شخصی - سرگرمی‌ها/علایق/گردش → دفترچه شخصی - سلامت/تناسب اندام/پزشک → دفترچه شخصی یا سلامت - خانواده/دوستان → دفترچه شخصی - کار/جلسات/پروژه‌ها/مشتریان → دفترچه کار - کدنویسی/تکنولوژی/توسعه → دفترچه کار یا کد - مالی/قبض‌ها/بانک → دفترچه شخصی یا مالی قوانین: - فقط نام دفترچه را برگردانید، دقیقاً همانطور که در بالا ذکر شده است (بدون حساسیت به حروف بزرگ و کوچک) - اگر تطابق خوبی وجود ندارد، "NONE" را برگردانید - اگر یادداشت خیلی کلی/مبهم است، "NONE" را برگردانید - توضیحات یا متن اضافی را شامل نکنید پیشناد شما: `.trim(), es: ` Eres un asistente que sugiere a qué cuaderno debería pertenecer una nota. CONTENIDO DE LA NOTA: ${noteContent.substring(0, 500)} CUADERNOS DISPONIBLES: ${notebookList} TAREA: Analiza el contenido de la nota (independientemente del idioma) y sugiere el cuaderno MÁS apropiado para esta nota. Considera: 1. El tema/asunto de la nota (LO MÁS IMPORTANTE) 2. Etiquetas existentes en cada cuaderno 3. El número de notas (prefiere cuadernos con contenido relacionado) GUÍA DE CLASIFICACIÓN: - DEPORTE/EJERCICIO/COMPRAS → Cuaderno Personal - HOBBIES/PASIONES/SALIDAS → Cuaderno Personal - SALUD/FITNESS/DOCTOR → Cuaderno Personal o Salud - FAMILIA/AMIGOS → Cuaderno Personal - TRABAJO/REUNIONES/PROYECTOS → Cuaderno Trabajo - CODING/TECH/DESARROLLO → Cuaderno Trabajo o Código - FINANZAS/FACTURAS/BANCO → Cuaderno Personal o Finanzas REGLAS: - Devuelve SOLO el nombre del cuaderno, EXACTAMENTE como se lista arriba (insensible a mayúsculas/minúsculas) - Si no existe una buena coincidencia, devuelve "NONE" - Si la nota es demasiado genérica/vaga, devuelve "NONE" - No incluyas explicaciones o texto extra Tu sugerencia: `.trim(), de: ` Du bist ein Assistent, der vorschlägt, zu welchem Notizbuch eine Notiz gehören sollte. NOTIZINHALT: ${noteContent.substring(0, 500)} VERFÜGBARE NOTIZBÜCHER: ${notebookList} AUFGABE: Analysiere den Notizinhalt (unabhängig von der Sprache) und schlage das AM BESTEN geeignete Notizbuch für diese Notiz vor. Berücksichtige: 1. Das Thema/den Inhalt der Notiz (AM WICHTIGSTEN) 2. Vorhandene Labels in jedem Notizbuch 3. Die Anzahl der Notizen (bevorzuge Notizbücher mit verwandtem Inhalt) KLASSIFIZIERUNGSLEITFADEN: - SPORT/ÜBUNG/EINKAUFEN → Persönliches Notizbuch - HOBBYS/LEIDENSCHAFTEN → Persönliches Notizbuch - GESUNDHEIT/FITNESS/ARZT → Persönliches Notizbuch oder Gesundheit - FAMILIE/FREUNDE → Persönliches Notizbuch - ARBEIT/MEETINGS/PROJEKTE → Arbeitsnotizbuch - CODING/TECH/ENTWICKLUNG → Arbeitsnotizbuch oder Code - FINANZEN/RECHNUNGEN/BANK → Persönliches Notizbuch oder Finanzen REGELN: - Gib NUR den Namen des Notizbuchs zurück, GENAU wie oben aufgeführt (Groß-/Kleinschreibung egal) - Wenn keine gute Übereinstimmung existiert, gib "NONE" zurück - Wenn die Notiz zu allgemein/vage ist, gib "NONE" zurück - Füge keine Erklärungen oder zusätzlichen Text hinzu Dein Vorschlag: `.trim() } return instructions[language] || instructions['en'] || instructions['fr'] } /** * Batch suggest notebooks for multiple notes (IA3) * @param noteContents - Array of note contents * @param userId - User ID * @returns Map of note index -> suggested notebook */ async suggestNotebooksBatch( noteContents: string[], userId: string, language: string = 'en' ): Promise> { const results = new Map() // For efficiency, we could batch this into a single AI call // For now, process sequentially (could be parallelized) for (let i = 0; i < noteContents.length; i++) { const suggestion = await this.suggestNotebook(noteContents[i], userId, language) results.set(i, suggestion) } return results } /** * Get notebook suggestion confidence score * (For future UI enhancement: show confidence level) */ async suggestNotebookWithConfidence( noteContent: string, userId: string ): Promise<{ notebook: Notebook | null; confidence: number }> { // This could use logprobs from OpenAI API to calculate confidence // For now, return binary confidence const notebook = await this.suggestNotebook(noteContent, userId) return { notebook, confidence: notebook ? 0.8 : 0, // Fixed confidence for now } } } // Export singleton instance export const notebookSuggestionService = new NotebookSuggestionService()