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:
293
keep-notes/lib/ai/services/auto-label-creation.service.ts
Normal file
293
keep-notes/lib/ai/services/auto-label-creation.service.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getAIProvider } from '@/lib/ai/factory'
|
||||
|
||||
export interface SuggestedLabel {
|
||||
name: string
|
||||
count: number
|
||||
confidence: number
|
||||
noteIds: string[]
|
||||
}
|
||||
|
||||
export interface AutoLabelSuggestion {
|
||||
notebookId: string
|
||||
notebookName: string
|
||||
notebookIcon: string | null
|
||||
suggestedLabels: SuggestedLabel[]
|
||||
totalNotes: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for automatically suggesting new labels based on recurring themes
|
||||
* (Story 5.4 - IA4)
|
||||
*/
|
||||
export class AutoLabelCreationService {
|
||||
/**
|
||||
* Analyze a notebook and suggest new labels based on recurring themes
|
||||
* @param notebookId - Notebook ID to analyze
|
||||
* @param userId - User ID (for authorization)
|
||||
* @returns Suggested labels or null if not enough notes/no patterns found
|
||||
*/
|
||||
async suggestLabels(notebookId: string, userId: string): Promise<AutoLabelSuggestion | null> {
|
||||
// 1. Get notebook with existing labels
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: {
|
||||
id: notebookId,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
labels: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: { notes: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!notebook) {
|
||||
throw new Error('Notebook not found')
|
||||
}
|
||||
|
||||
// Only trigger if notebook has 15+ notes (PRD requirement)
|
||||
if (notebook._count.notes < 15) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get all notes in this notebook
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
notebookId,
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
content: true,
|
||||
labelRelations: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: 'desc',
|
||||
},
|
||||
take: 100, // Limit to 100 most recent notes
|
||||
})
|
||||
|
||||
if (notes.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 2. Use AI to detect recurring themes
|
||||
const suggestions = await this.detectRecurringThemes(notes, notebook)
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
/**
|
||||
* Use AI to detect recurring themes and suggest labels
|
||||
*/
|
||||
private async detectRecurringThemes(
|
||||
notes: any[],
|
||||
notebook: any
|
||||
): Promise<AutoLabelSuggestion | null> {
|
||||
const existingLabelNames = new Set<string>(
|
||||
notebook.labels.map((l: any) => l.name.toLowerCase())
|
||||
)
|
||||
|
||||
const prompt = this.buildPrompt(notes, existingLabelNames)
|
||||
|
||||
try {
|
||||
const provider = getAIProvider()
|
||||
const response = await provider.generateText(prompt)
|
||||
|
||||
// Parse AI response
|
||||
const suggestions = this.parseAIResponse(response, notes)
|
||||
|
||||
if (!suggestions || suggestions.suggestedLabels.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
notebookId: notebook.id,
|
||||
notebookName: notebook.name,
|
||||
notebookIcon: notebook.icon,
|
||||
suggestedLabels: suggestions.suggestedLabels,
|
||||
totalNotes: notebook._count.notes,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to detect recurring themes:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build prompt for AI (always in French - interface language)
|
||||
*/
|
||||
private buildPrompt(notes: any[], existingLabelNames: Set<string>): string {
|
||||
const notesSummary = notes
|
||||
.map((note, index) => {
|
||||
const title = note.title || 'Sans titre'
|
||||
const content = note.content.substring(0, 150)
|
||||
return `[${index}] "${title}": ${content}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const existingLabels = Array.from(existingLabelNames).join(', ')
|
||||
|
||||
return `
|
||||
Tu es un assistant qui détecte les thèmes récurrents dans des notes pour suggérer de nouvelles étiquettes.
|
||||
|
||||
CARNET ANALYSÉ :
|
||||
${notes.length} notes
|
||||
|
||||
ÉTIQUETTES EXISTANTES (ne pas suggérer celles-ci) :
|
||||
${existingLabels || 'Aucune'}
|
||||
|
||||
NOTES DU CARNET :
|
||||
${notesSummary}
|
||||
|
||||
TÂCHE :
|
||||
Analyse les notes et détecte les thèmes récurrents (mots-clés, sujets, lieux, personnes).
|
||||
Un thème doit apparaître dans au moins 5 notes différentes pour être suggéré.
|
||||
|
||||
FORMAT DE RÉPONSE (JSON) :
|
||||
{
|
||||
"labels": [
|
||||
{
|
||||
"nom": "nom_du_label",
|
||||
"note_indices": [0, 5, 12, 23, 45],
|
||||
"confiance": 0.85
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
RÈGLES :
|
||||
- Le nom du label doit être court (1-2 mots max)
|
||||
- Un thème doit apparaître dans 5+ notes pour être suggéré
|
||||
- La confiance doit être > 0.60
|
||||
- Ne pas suggérer des étiquettes qui existent déjà
|
||||
- Priorise les lieux, personnes, catégories claires
|
||||
- Maximum 5 suggestions
|
||||
|
||||
Exemples de bonnes étiquettes :
|
||||
- "tokyo", "kyoto", "osaka" (lieux)
|
||||
- "hôtels", "restos", "vols" (catégories)
|
||||
- "marie", "jean", "équipe" (personnes)
|
||||
|
||||
Ta réponse (JSON seulement) :
|
||||
`.trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse AI response into suggested labels
|
||||
*/
|
||||
private parseAIResponse(response: string, notes: any[]): { suggestedLabels: SuggestedLabel[] } | null {
|
||||
try {
|
||||
const jsonMatch = response.match(/\{[\s\S]*\}/)
|
||||
if (!jsonMatch) {
|
||||
throw new Error('No JSON found in response')
|
||||
}
|
||||
|
||||
const aiData = JSON.parse(jsonMatch[0])
|
||||
|
||||
const suggestedLabels: SuggestedLabel[] = (aiData.labels || [])
|
||||
.map((label: any) => {
|
||||
// Filter by confidence threshold
|
||||
if (label.confiance <= 0.60) return null
|
||||
|
||||
// Get note IDs from indices
|
||||
const noteIds = label.note_indices
|
||||
.map((idx: number) => notes[idx]?.id)
|
||||
.filter(Boolean)
|
||||
|
||||
// Must have at least 5 notes
|
||||
if (noteIds.length < 5) return null
|
||||
|
||||
return {
|
||||
name: label.nom,
|
||||
count: noteIds.length,
|
||||
confidence: label.confiance,
|
||||
noteIds,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (suggestedLabels.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Sort by count (descending) and confidence
|
||||
suggestedLabels.sort((a, b) => {
|
||||
if (b.count !== a.count) {
|
||||
return b.count - a.count // More notes first
|
||||
}
|
||||
return b.confidence - a.confidence // Then higher confidence
|
||||
})
|
||||
|
||||
// Limit to top 5
|
||||
return {
|
||||
suggestedLabels: suggestedLabels.slice(0, 5),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse AI response:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create suggested labels and assign them to notes
|
||||
* @param notebookId - Notebook ID
|
||||
* @param userId - User ID
|
||||
* @param suggestions - Suggested labels to create
|
||||
* @param selectedLabels - Labels user selected to create
|
||||
* @returns Number of labels created
|
||||
*/
|
||||
async createLabels(
|
||||
notebookId: string,
|
||||
userId: string,
|
||||
suggestions: AutoLabelSuggestion,
|
||||
selectedLabels: string[]
|
||||
): Promise<number> {
|
||||
let createdCount = 0
|
||||
|
||||
for (const suggestedLabel of suggestions.suggestedLabels) {
|
||||
if (!selectedLabels.includes(suggestedLabel.name)) continue
|
||||
|
||||
// Create the label
|
||||
const label = await prisma.label.create({
|
||||
data: {
|
||||
name: suggestedLabel.name,
|
||||
color: 'gray', // Default color, user can change later
|
||||
notebookId,
|
||||
userId,
|
||||
},
|
||||
})
|
||||
|
||||
// Assign label to all suggested notes (updateMany doesn't support relations)
|
||||
for (const noteId of suggestedLabel.noteIds) {
|
||||
await prisma.note.update({
|
||||
where: { id: noteId },
|
||||
data: {
|
||||
labelRelations: {
|
||||
connect: {
|
||||
id: label.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
createdCount++
|
||||
}
|
||||
|
||||
return createdCount
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const autoLabelCreationService = new AutoLabelCreationService()
|
||||
Reference in New Issue
Block a user