Keep/keep-notes/lib/ai/services/batch-organization.service.ts
sepehr 7fb486c9a4 feat: Complete internationalization and code cleanup
## Translation Files
- Add 11 new language files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl)
- Add 100+ missing translation keys across all 15 languages
- New sections: notebook, pagination, ai.batchOrganization, ai.autoLabels
- Update nav section with workspace, quickAccess, myLibrary keys

## Component Updates
- Update 15+ components to use translation keys instead of hardcoded text
- Components: notebook dialogs, sidebar, header, note-input, ghost-tags, etc.
- Replace 80+ hardcoded English/French strings with t() calls
- Ensure consistent UI across all supported languages

## Code Quality
- Remove 77+ console.log statements from codebase
- Clean up API routes, components, hooks, and services
- Keep only essential error handling (no debugging logs)

## UI/UX Improvements
- Update Keep logo to yellow post-it style (from-yellow-400 to-amber-500)
- Change selection colors to #FEF3C6 (notebooks) and #EFB162 (nav items)
- Make "+" button permanently visible in notebooks section
- Fix grammar and syntax errors in multiple components

## Bug Fixes
- Fix JSON syntax errors in it.json, nl.json, pl.json, zh.json
- Fix syntax errors in notebook-suggestion-toast.tsx
- Fix syntax errors in use-auto-tagging.ts
- Fix syntax errors in paragraph-refactor.service.ts
- Fix duplicate "fusion" section in nl.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Ou une version plus courte si vous préférez :

feat(i18n): Add 15 languages, remove logs, update UI components

- Create 11 new translation files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl)
- Add 100+ translation keys: notebook, pagination, AI features
- Update 15+ components to use translations (80+ strings)
- Remove 77+ console.log statements from codebase
- Fix JSON syntax errors in 4 translation files
- Fix component syntax errors (toast, hooks, services)
- Update logo to yellow post-it style
- Change selection colors (#FEF3C6, #EFB162)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 22:26:13 +01:00

306 lines
7.9 KiB
TypeScript

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<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)
return plan
}
/**
* Use AI to analyze notes and create organization plan
*/
private async aiOrganizeNotes(
notes: NoteForOrganization[],
notebooks: any[]
): Promise<OrganizationPlan> {
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<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()