987 lines
30 KiB
TypeScript
987 lines
30 KiB
TypeScript
import { prisma } from '@/lib/prisma'
|
||
import { getAIProvider } from '@/lib/ai/factory'
|
||
import { getSystemConfig } from '@/lib/config'
|
||
|
||
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
|
||
* @param language - User's preferred language (default: 'en')
|
||
* @returns Organization plan with notebook assignments
|
||
*/
|
||
async createOrganizationPlan(userId: string, language: string = 'en'): Promise<OrganizationPlan> {
|
||
// 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, language)
|
||
|
||
return plan
|
||
}
|
||
|
||
/**
|
||
* Use AI to analyze notes and create organization plan
|
||
*/
|
||
private async aiOrganizeNotes(
|
||
notes: NoteForOrganization[],
|
||
notebooks: any[],
|
||
language: string
|
||
): Promise<OrganizationPlan> {
|
||
const prompt = this.buildPrompt(notes, notebooks, language)
|
||
|
||
try {
|
||
const config = await getSystemConfig()
|
||
const provider = getAIProvider(config)
|
||
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 (localized)
|
||
*/
|
||
private buildPrompt(notes: NoteForOrganization[], 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 notesList = notes
|
||
.map((note, index) => {
|
||
const title = note.title || 'Sans titre'
|
||
const content = note.content.substring(0, 200)
|
||
return `[${index}] "${title}": ${content}`
|
||
})
|
||
.join('\n')
|
||
|
||
// System instructions based on language
|
||
const instructions: Record<string, string> = {
|
||
fr: `
|
||
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 en français"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
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 doit être uniquement un JSON valide
|
||
`.trim(),
|
||
en: `
|
||
You are an assistant that organizes notes by grouping them into notebooks based on their theme.
|
||
|
||
AVAILABLE NOTEBOOKS:
|
||
${notebookList}
|
||
|
||
NOTES TO ORGANIZE (Inbox):
|
||
${notesList}
|
||
|
||
TASK:
|
||
Analyze each note and suggest the MOST appropriate notebook.
|
||
Consider:
|
||
1. The subject/theme of the note (MOST IMPORTANT)
|
||
2. Existing labels in each notebook
|
||
3. Thematic consistency between notes in the same notebook
|
||
|
||
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
|
||
|
||
RESPONSE FORMAT (JSON):
|
||
For each notebook, list the notes that belong to it:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "Notebook Name",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "Short explanation in English"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
RULES:
|
||
- Only assign notes with confidence > 0.60
|
||
- If a note is too generic, do not assign it
|
||
- Be precise in your thematic groupings
|
||
- Your response must be valid JSON only
|
||
`.trim(),
|
||
es: `
|
||
Eres un asistente que organiza notas agrupándolas por temática en cuadernos.
|
||
|
||
CUADERNOS DISPONIBLES:
|
||
${notebookList}
|
||
|
||
NOTAS A ORGANIZAR (Bandeja de entrada):
|
||
${notesList}
|
||
|
||
TAREA:
|
||
Analiza cada nota y sugiere el cuaderno MÁS apropiado.
|
||
Considera:
|
||
1. El tema/asunto de la nota (LO MÁS IMPORTANTE)
|
||
2. Etiquetas existentes en cada cuaderno
|
||
3. Coherencia temática entre notas del mismo cuaderno
|
||
|
||
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
|
||
|
||
FORMATO DE RESPUESTA (JSON):
|
||
Para cada cuaderno, lista las notas que le pertenecen:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "Nombre del cuaderno",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "Breve explicación en español"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
REGLAS:
|
||
- Solo asigna notas con confianza > 0.60
|
||
- Si una nota es demasiado genérica, no la asignes
|
||
- Sé preciso en tus agrupaciones temáticas
|
||
- Tu respuesta debe ser únicamente un JSON válido
|
||
`.trim(),
|
||
de: `
|
||
Du bist ein Assistent, der Notizen organisiert, indem er sie thematisch in Notizbücher gruppiert.
|
||
|
||
VERFÜGBARE NOTIZBÜCHER:
|
||
${notebookList}
|
||
|
||
ZU ORGANISIERENDE NOTIZEN (Eingang):
|
||
${notesList}
|
||
|
||
AUFGABE:
|
||
Analysiere jede Notiz und schlage das AM BESTEN geeignete Notizbuch vor.
|
||
Berücksichtige:
|
||
1. Das Thema/den Inhalt der Notiz (AM WICHTIGSTEN)
|
||
2. Vorhandene Labels in jedem Notizbuch
|
||
3. Thematische Konsistenz zwischen Notizen im selben Notizbuch
|
||
|
||
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
|
||
|
||
ANTWORTFORMAT (JSON):
|
||
Für jedes Notizbuch, liste die zugehörigen Notizen auf:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "Name des Notizbuchs",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "Kurze Erklärung auf Deutsch"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
REGELN:
|
||
- Ordne nur Notizen mit Konfidenz > 0.60
|
||
- Wenn eine Notiz zu allgemein ist, ordne sie nicht zu
|
||
- Sei präzise in deinen thematischen Gruppierungen
|
||
- Deine Antwort muss ein gültiges JSON sein
|
||
`.trim(),
|
||
it: `
|
||
Sei un assistente che organizza le note raggruppandole per tema nei taccuini.
|
||
|
||
TACCUINI DISPONIBILI:
|
||
${notebookList}
|
||
|
||
NOTE DA ORGANIZZARE (In arrivo):
|
||
${notesList}
|
||
|
||
COMPITO:
|
||
Analizza ogni nota e suggerisci il taccuino PIÙ appropriato.
|
||
Considera:
|
||
1. L'argomento/tema della nota (PIÙ IMPORTANTE)
|
||
2. Etichette esistenti in ogni taccuino
|
||
3. Coerenza tematica tra le note dello stesso taccuino
|
||
|
||
GUIDA ALLA CLASSIFICAZIONE:
|
||
- SPORT/ESERCIZIO/SHOPPING → Taccuino Personale
|
||
- HOBBY/PASSIONI/USCITE → Taccuino Personale
|
||
- SALUTE/FITNESS/DOTTORE → Taccuino Personale o Salute
|
||
- FAMIGLIA/AMIGOS → Taccuino Personale
|
||
- LAVORO/RIUNIONI/PROGETTI → Taccuino Lavoro
|
||
- CODING/TECH/SVILUPPO → Taccuino Lavoro o Codice
|
||
- FINANZA/BILLS/BANCA → Taccuino Personale o Finanza
|
||
|
||
FORMATO RISPOSTA (JSON):
|
||
Per ogni taccuino, elenca le note che appartengono ad esso:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "Nome del taccuino",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "Breve spiegazione in italiano"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
REGOLE:
|
||
- Assegna solo note con confidenza > 0.60
|
||
- Se una nota è troppo generica, non assegnarla
|
||
- Sii preciso nei tuoi raggruppamenti tematici
|
||
- La tua risposta deve essere solo un JSON valido
|
||
`.trim(),
|
||
pt: `
|
||
Você é um assistente que organiza notas agrupando-as por tema em cadernos.
|
||
|
||
CADERNOS DISPONÍVEIS:
|
||
${notebookList}
|
||
|
||
NOTAS A ORGANIZAR (Caixa de entrada):
|
||
${notesList}
|
||
|
||
TAREFA:
|
||
Analise cada nota e sugira o caderno MAIS apropriado.
|
||
Considere:
|
||
1. O assunto/tema da nota (MAIS IMPORTANTE)
|
||
2. Etiquetas existentes em cada caderno
|
||
3. Coerência temática entre notas do mesmo caderno
|
||
|
||
GUIA DE CLASSIFICAÇÃO:
|
||
- ESPORTE/EXERCÍCIO/COMPRAS → Caderno Pessoal
|
||
- HOBBIES/PAIXÕES/SAÍDAS → Caderno Pessoal
|
||
- SAÚDE/FITNESS/MÉDICO → Caderno Pessoal ou Saúde
|
||
- FAMÍLIA/AMIGOS → Caderno Pessoal
|
||
- TRABALHO/REUNIÕES/PROJETOS → Caderno Trabalho
|
||
- CODING/TECH/DESENVOLVIMENTO → Caderno Trabalho ou Código
|
||
- FINANÇAS/CONTAS/BANCO → Caderno Pessoal ou Finanças
|
||
|
||
FORMATO DE RESPOSTA (JSON):
|
||
Para cada caderno, liste as notas que pertencem a ele:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "Nome do caderno",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "Breve explicação em português"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
REGRAS:
|
||
- Apenas atribua notas com confiança > 0.60
|
||
- Se uma nota for muito genérica, não a atribua
|
||
- Seja preciso em seus agrupamentos temáticos
|
||
- Sua resposta deve ser apenas um JSON válido
|
||
`.trim(),
|
||
nl: `
|
||
Je bent een assistent die notities organiseert door ze thematisch in notitieboekjes te groeperen.
|
||
|
||
BESCHIKBARE NOTITIEBOEKJES:
|
||
${notebookList}
|
||
|
||
TE ORGANISEREN NOTITIES (Inbox):
|
||
${notesList}
|
||
|
||
TAAK:
|
||
Analyseer elke notitie en stel het MEEST geschikte notitieboek voor.
|
||
Overweeg:
|
||
1. Het onderwerp/thema van de notitie (BELANGRIJKST)
|
||
2. Bestaande labels in elk notitieboekje
|
||
3. Thematische consistentie tussen notities in hetzelfde notitieboekje
|
||
|
||
CLASSIFICATIEGIDS:
|
||
- SPORT/OEFENING/WINKELEN → Persoonlijk Notitieboek
|
||
- HOBBIES/PASSIES/UITJES → Persoonlijk Notitieboek
|
||
- GEZONDHEID/FITNESS/DOKTER → Persoonlijk Notitieboek of Gezondheid
|
||
- FAMILIE/VRIENDEN → Persoonlijk Notitieboek
|
||
- WERK/VERGADERINGEN/PROJECTEN → Werk Notitieboek
|
||
- CODING/TECH/ONTWIKKELING → Werk Notitieboek of Code
|
||
- FINANCIËN/REKENINGEN/BANK → Persoonlijk Notitieboek of Financiën
|
||
|
||
ANTWOORDFORMAAT (JSON):
|
||
Voor elk notitieboekje, lijst de notities op die erbij horen:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "Naam van het notitieboekje",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "Korte uitleg in het Nederlands"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
REGELS:
|
||
- Wijs alleen notities toe met vertrouwen > 0.60
|
||
- Als een notitie te generiek is, wijs deze dan niet toe
|
||
- Wees nauwkeurig in je thematische groeperingen
|
||
- Je antwoord moet alleen een geldige JSON zijn
|
||
`.trim(),
|
||
pl: `
|
||
Jesteś asystentem, który organizuje notatki, grupując je tematycznie w notatnikach.
|
||
|
||
DOSTĘPNE NOTATNIKI:
|
||
${notebookList}
|
||
|
||
NOTATKI DO ZORGANIZOWANIA (Skrzynka odbiorcza):
|
||
${notesList}
|
||
|
||
ZADANIE:
|
||
Przeanalizuj każdą notatkę i zasugeruj NAJBARDZIEJ odpowiedni notatnik.
|
||
Rozważ:
|
||
1. Temat/treść notatki (NAJWAŻNIEJSZE)
|
||
2. Istniejące etykiety w każdym notatniku
|
||
3. Spójność tematyczna między notatkami w tym samym notatniku
|
||
|
||
PRZEWODNIK KLASYFIKACJI:
|
||
- SPORT/ĆWICZENIA/ZAKUPY → Notatnik Osobisty
|
||
- HOBBY/PASJE/WYJŚCIA → Notatnik Osobisty
|
||
- ZDROWIE/FITNESS/LEKARZ → Notatnik Osobisty lub Zdrowie
|
||
- RODZINA/PRZYJACIELE → Notatnik Osobisty
|
||
- PRACA/SPOTKANIA/PROJEKTY → Notatnik Praca
|
||
- KODOWANIE/TECH/ROZWÓJ → Notatnik Praca lub Kod
|
||
- FINANSE/RACHUNKI/BANK → Notatnik Osobisty lub Finanse
|
||
|
||
FORMAT ODPOWIEDZI (JSON):
|
||
Dla każdego notatnika wymień należące do niego notatki:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "Nazwa notatnika",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "Krótkie wyjaśnienie po polsku"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
ZASADY:
|
||
- Przypisuj tylko notatki z pewnością > 0.60
|
||
- Jeśli notatka jest zbyt ogólna, nie przypisuj jej
|
||
- Bądź precyzyjny w swoich grupach tematycznych
|
||
- Twoja odpowiedź musi być tylko prawidłowym JSON
|
||
`.trim(),
|
||
ru: `
|
||
Вы помощник, который организует заметки, группируя их по темам в блокноты.
|
||
|
||
ДОСТУПНЫЕ БЛОКНОТЫ:
|
||
${notebookList}
|
||
|
||
ЗАМЕТКИ ДЛЯ ОРГАНИЗАЦИИ (Входящие):
|
||
${notesList}
|
||
|
||
ЗАДАЧА:
|
||
Проанализируйте каждую заметку и предложите САМЫЙ подходящий блокнот.
|
||
Учитывайте:
|
||
1. Тему/предмет заметки (САМОЕ ВАЖНОЕ)
|
||
2. Существующие метки в каждом блокноте
|
||
3. Тематическую согласованность между заметками в одном блокноте
|
||
|
||
РУКОВОДСТВО ПО КЛАССИФИКАЦИИ:
|
||
- СПОРТ/УПРАЖНЕНИЯ/ПОКУПКИ → Личный блокнот
|
||
- ХОББИ/УВЛЕЧЕНИЯ/ВЫХОДЫ → Личный блокнот
|
||
- ЗДОРОВЬЕ/ФИТНЕС/ВРАЧ → Личный блокнот или Здоровье
|
||
- СЕМЬЯ/ДРУЗЬЯ → Личный блокнот
|
||
- РАБОТА/СОВЕЩАНИЯ/ПРОЕКТЫ → Рабочий блокнот
|
||
- КОДИНГ/ТЕХНОЛОГИИ/РАЗРАБОТКА → Рабочий блокнот или Код
|
||
- ФИНАНСЫ/СЧЕТА/БАНК → Личный блокнот или Финансы
|
||
|
||
ФОРМАТ ОТВЕТА (JSON):
|
||
Для каждого блокнота перечислите заметки, которые к нему относятся:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "Название блокнота",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "Краткое объяснение на русском"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
ПРАВИЛА:
|
||
- Назначайте только заметки с уверенностью > 0.60
|
||
- Если заметка слишком общая, не назначайте ее
|
||
- Будьте точны в своих тематических группировках
|
||
- Ваш ответ должен быть только валидным JSON
|
||
`.trim(),
|
||
ja: `
|
||
あなたは、テーマごとにノートを分類してノートブックに整理するアシスタントです。
|
||
|
||
利用可能なノートブック:
|
||
${notebookList}
|
||
|
||
整理するノート (受信トレイ):
|
||
${notesList}
|
||
|
||
タスク:
|
||
各ノートを分析し、最も適切なノートブックを提案してください。
|
||
考慮事項:
|
||
1. ノートの主題/テーマ (最も重要)
|
||
2. 各ノートブックの既存のラベル
|
||
3. 同じノートブック内のノート間のテーマの一貫性
|
||
|
||
分類ガイド:
|
||
- スポーツ/運動/買い物 → 個人用ノートブック
|
||
- 趣味/情熱/外出 → 個人用ノートブック
|
||
- 健康/フィットネス/医師 → 個人用ノートブックまたは健康
|
||
- 家族/友人 → 個人用ノートブック
|
||
- 仕事/会議/プロジェクト → 仕事用ノートブック
|
||
- コーディング/技術/開発 → 仕事用ノートブックまたはコード
|
||
- 金融/請求書/銀行 → 個人用ノートブックまたは金融
|
||
|
||
回答形式 (JSON):
|
||
各ノートブックについて、それに属するノートをリストアップしてください:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "ノートブック名",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "日本語での短い説明"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
ルール:
|
||
- 信頼度が 0.60 を超えるノートのみを割り当ててください
|
||
- ノートが一般的すぎる場合は、割り当てないでください
|
||
- テーマ別のグループ化において正確であってください
|
||
- 回答は有効な JSON のみにしてください
|
||
`.trim(),
|
||
ko: `
|
||
당신은 주제별로 노트를 분류하여 노트북으로 정리하는 도우미입니다.
|
||
|
||
사용 가능한 노트북:
|
||
${notebookList}
|
||
|
||
정리할 노트 (받은 편지함):
|
||
${notesList}
|
||
|
||
작업:
|
||
각 노트를 분석하고 가장 적절한 노트북을 제안하십시오.
|
||
고려 사항:
|
||
1. 노트의 주제/테마 (가장 중요)
|
||
2. 각 노트북의 기존 라벨
|
||
3. 동일한 노트북 내 노트 간의 주제별 일관성
|
||
|
||
분류 가이드:
|
||
- 스포츠/운동/쇼핑 → 개인 노트북
|
||
- 취미/열정/외출 → 개인 노트북
|
||
- 건강/피트니스/의사 → 개인 노트북 또는 건강
|
||
- 가족/친구 → 개인 노트북
|
||
- 업무/회의/프로젝트 → 업무 노트북
|
||
- 코딩/기술/개발 → 업무 노트북 또는 코드
|
||
- 금융/청구서/은행 → 개인 노트북 또는 금융
|
||
|
||
응답 형식 (JSON):
|
||
각 노트북에 대해 속한 노트를 나열하십시오:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "노트북 이름",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "한국어로 된 짧은 설명"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
규칙:
|
||
- 신뢰도가 0.60을 초과하는 노트만 할당하십시오
|
||
- 노트가 너무 일반적인 경우 할당하지 마십시오
|
||
- 주제별 그룹화에서 정확해야 합니다
|
||
- 응답은 유효한 JSON이어야 합니다
|
||
`.trim(),
|
||
zh: `
|
||
你是一个助手,负责通过按主题将笔记分组到笔记本中来整理笔记。
|
||
|
||
可用笔记本:
|
||
${notebookList}
|
||
|
||
待整理笔记(收件箱):
|
||
${notesList}
|
||
|
||
任务:
|
||
分析每个笔记并建议最合适的笔记本。
|
||
考虑:
|
||
1. 笔记的主题/题材(最重要)
|
||
2. 每个笔记本中的现有标签
|
||
3. 同一笔记本中笔记之间的主题一致性
|
||
|
||
分类指南:
|
||
- 运动/锻炼/购物 → 个人笔记本
|
||
- 爱好/激情/郊游 → 个人笔记本
|
||
- 健康/健身/医生 → 个人笔记本或健康
|
||
- 家庭/朋友 → 个人笔记本
|
||
- 工作/会议/项目 → 工作笔记本
|
||
- 编码/技术/开发 → 工作笔记本或代码
|
||
- 金融/账单/银行 → 个人笔记本或金融
|
||
|
||
响应格式 (JSON):
|
||
对于每个笔记本,列出属于它的笔记:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "笔记本名称",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "中文简短说明"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
规则:
|
||
- 仅分配置信度 > 0.60 的笔记
|
||
- 如果笔记太普通,请勿分配
|
||
- 在主题分组中要精确
|
||
- 您的响应必须仅为有效的 JSON
|
||
`.trim(),
|
||
ar: `
|
||
أنت مساعد يقوم بتنظيم الملاحظات عن طريق تجميعها حسب الموضوع في دفاتر ملاحظات.
|
||
|
||
دفاتر الملاحظات المتاحة:
|
||
${notebookList}
|
||
|
||
ملاحظات للتنظيم (صندوق الوارد):
|
||
${notesList}
|
||
|
||
المهمة:
|
||
حلل كل ملاحظة واقترح دفتر الملاحظات الأكثر ملاءمة.
|
||
اعتبار:
|
||
1. موضوع/مادة الملاحظة (الأهم)
|
||
2. التسميات الموجودة في كل دفتر ملاحظات
|
||
3. الاتساق الموضوعي بين الملاحظات في نفس دفتر الملاحظات
|
||
|
||
دليل التصنيف:
|
||
- الرياضة/التمرين/التسوق → دفتر ملاحظات شخصي
|
||
- الهوايات/الشغف/النزهات → دفتر ملاحظات شخصي
|
||
- الصحة/اللياقة البدنية/الطبيب → دفتر ملاحظات شخصي أو صحة
|
||
- العائلة/الأصدقاء → دفتر ملاحظات شخصي
|
||
- العمل/الاجتماعات/المشاريع → دفتر ملاحظات العمل
|
||
- البرمجة/التقنية/التطوير → دفتر ملاحظات العمل أو الكود
|
||
- المالية/الفواتير/البنك → دفتر ملاحظات شخصي أو مالية
|
||
|
||
تنسيق الاستجابة (JSON):
|
||
لكل دفتر ملاحظات، ضع قائمة بالملاحظات التي تنتمي إليه:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "اسم دفتر الملاحظات",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "شرح قصير باللغة العربية"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
القواعد:
|
||
- عيّن فقط الملاحظات ذات الثقة > 0.60
|
||
- إذا كانت الملاحظة عامة جدًا، فلا تقم بتعيينها
|
||
- كن دقيقًا في مجموعاتك الموضوعية
|
||
- يجب أن تكون إجابتك بتنسيق JSON صالح فقط
|
||
`.trim(),
|
||
hi: `
|
||
आप एक सहायक हैं जो नोटों को विषय के आधार पर नोटबुक में समूहित करके व्यवस्थित करते हैं।
|
||
|
||
उपलब्ध नोटबुक:
|
||
${notebookList}
|
||
|
||
व्यवस्थित करने के लिए नोट्स (इनबॉक्स):
|
||
${notesList}
|
||
|
||
कार्य:
|
||
प्रत्येक नोट का विश्लेषण करें और सबसे उपयुक्त नोटबुक का सुझाव दें।
|
||
विचार करें:
|
||
1. नोट का विषय/थीम (सबसे महत्वपूर्ण)
|
||
2. प्रत्येक नोटबुक में मौजूदा लेबल
|
||
3. एक ही नोटबुक में नोटों के बीच विषयगत स्थिरता
|
||
|
||
वर्गीकरण गाइड:
|
||
- खेल/व्यायाम/खरीदारी → व्यक्तिगत नोटबुक
|
||
- शौक/जुनून/बाहर जाना → व्यक्तिगत नोटबुक
|
||
- स्वास्थ्य/फिटनेस/डॉक्टर → व्यक्तिगत नोटबुक या स्वास्थ्य
|
||
- परिवार/मित्र → व्यक्तिगत नोटबुक
|
||
- कार्य/बैठकें/परियोजनाएं → कार्य नोटबुक
|
||
- कोडिंग/तकनीक/विकास → कार्य नोटबुक या कोड
|
||
- वित्त/बिल/बैंक → व्यक्तिगत नोटबुक या वित्त
|
||
|
||
प्रतिक्रिया प्रारूप (JSON):
|
||
प्रत्येक नोटबुक के लिए, उन नोटों को सूचीबद्ध करें जो उससे संबंधित हैं:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "नोटबुक का नाम",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "हिंदी में संक्षिप्त स्पष्टीकरण"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
नियम:
|
||
- केवल > 0.60 आत्मविश्वास वाले नोट्स असाइन करें
|
||
- यदि कोई नोट बहुत सामान्य है, तो उसे असाइन न करें
|
||
- अपने विषयगत समूहों में सटीक रहें
|
||
- आपकी प्रतिक्रिया केवल वैध JSON होनी चाहिए
|
||
`.trim(),
|
||
fa: `
|
||
شما دستیاری هستید که یادداشتها را با گروهبندی موضوعی در دفترچهها سازماندهی میکنید.
|
||
|
||
دفترچههای موجود:
|
||
${notebookList}
|
||
|
||
یادداشتهای برای سازماندهی (صندوق ورودی):
|
||
${notesList}
|
||
|
||
وظیفه:
|
||
هر یادداشت را تحلیل کنید و مناسبترین دفترچه را پیشنهاد دهید.
|
||
در نظر بگیرید:
|
||
1. موضوع/تم یادداشت (مهمترین)
|
||
2. برچسبهای موجود در هر دفترچه
|
||
3. سازگاری موضوعی بین یادداشتها در همان دفترچه
|
||
|
||
راهنمای طبقهبندی:
|
||
- ورزش/تمرین/خرید → دفترچه شخصی
|
||
- سرگرمیها/علایق/گردش → دفترچه شخصی
|
||
- سلامت/تناسب اندام/پزشک → دفترچه شخصی یا سلامت
|
||
- خانواده/دوستان → دفترچه شخصی
|
||
- کار/جلسات/پروژهها → دفترچه کار
|
||
- کدنویسی/تکنولوژی/توسعه → دفترچه کار یا کد
|
||
- مالی/قبضها/بانک → دفترچه شخصی یا مالی
|
||
|
||
فرمت پاسخ (JSON):
|
||
برای هر دفترچه، یادداشتهایی که به آن تعلق دارند را لیست کنید:
|
||
{
|
||
"carnets": [
|
||
{
|
||
"nom": "نام دفترچه",
|
||
"notes": [
|
||
{
|
||
"index": 0,
|
||
"confiance": 0.95,
|
||
"raison": "توضیح کوتاه به فارسی"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
|
||
قوانین:
|
||
- فقط یادداشتهای با اطمینان > 0.60 را اختصاص دهید
|
||
- اگر یادداشتی خیلی کلی است، آن را اختصاص ندهید
|
||
- در گروهبندیهای موضوعی خود دقیق باشید
|
||
- پاسخ شما باید فقط یک JSON معتبر باشد
|
||
`.trim()
|
||
}
|
||
|
||
// Return instruction for requested language, fallback to English
|
||
return instructions[language] || instructions['en'] || instructions['fr']
|
||
}
|
||
|
||
/**
|
||
* 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<number> {
|
||
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()
|