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>
This commit is contained in:
305
keep-notes/lib/ai/services/batch-organization.service.ts
Normal file
305
keep-notes/lib/ai/services/batch-organization.service.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user