298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
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<Notebook | null> {
|
|
// 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<string, string> = {
|
|
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<Map<number, Notebook | null>> {
|
|
const results = new Map<number, Notebook | null>()
|
|
|
|
// 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()
|