fix: auto-detect note language for label suggestions (like title suggestions)
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s

Use LanguageDetectionService (tinyld) to detect the language of note
content, same pattern as TitleSuggestionService. No more hardcoded
per-language prompts — single prompt with detected language injected.
Labels now match the note content language (e.g. Persian note → Persian labels).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 20:56:46 +02:00
parent 6e8abc5091
commit c7e654afa6

View File

@@ -7,6 +7,7 @@
import { prisma } from '@/lib/prisma'
import { getTagsProvider } from '@/lib/ai/factory'
import { getSystemConfig } from '@/lib/config'
import { LanguageDetectionService } from './language-detection.service'
export interface LabelSuggestion {
label: string
@@ -16,6 +17,12 @@ export interface LabelSuggestion {
}
export class ContextualAutoTagService {
private languageDetection: LanguageDetectionService
constructor() {
this.languageDetection = new LanguageDetectionService()
}
/**
* Suggest labels for a note
* @param noteContent - Content of the note
@@ -27,13 +34,16 @@ export class ContextualAutoTagService {
noteContent: string,
notebookId: string | null,
userId: string,
language: string = 'en'
_uiLanguage: string = 'en'
): Promise<LabelSuggestion[]> {
// If no notebook, return empty (no context)
if (!notebookId) {
return []
}
// Detect the language of the note content (same as title-suggestion.service.ts)
const { language: contentLanguage } = await this.languageDetection.detectLanguage(noteContent)
// Get notebook with its labels
const notebook = await prisma.notebook.findFirst({
where: {
@@ -54,18 +64,18 @@ export class ContextualAutoTagService {
return []
}
console.log('[ContextualAutoTag] notebook:', notebook.name, 'labels:', notebook.labels.map((l: any) => l.name))
console.log('[ContextualAutoTag] notebook:', notebook.name, 'labels:', notebook.labels.map((l: any) => l.name), 'contentLanguage:', contentLanguage)
// CASE 1: Notebook has existing labels → suggest from them (IA2)
if (notebook.labels.length > 0) {
const existing = await this.suggestFromExistingLabels(noteContent, notebook, language)
const existing = await this.suggestFromExistingLabels(noteContent, notebook, contentLanguage)
console.log('[ContextualAutoTag] existing label suggestions:', existing.length)
if (existing.length > 0) return existing
// Fallback: no existing label matched → suggest new ones too
}
// CASE 2: No labels in notebook (or none matched) → suggest NEW labels
return await this.suggestNewLabels(noteContent, notebook, language)
return await this.suggestNewLabels(noteContent, notebook, contentLanguage)
}
/**
@@ -247,39 +257,10 @@ export class ContextualAutoTagService {
/**
* Build the AI prompt for contextual label suggestion (localized)
*/
private buildPrompt(noteContent: string, notebookName: string, availableLabels: string[], language: string = 'en'): string {
private buildPrompt(noteContent: string, notebookName: string, availableLabels: string[], contentLanguage: string): string {
const labelList = availableLabels.map(l => `- ${l}`).join('\n')
const instructions: Record<string, string> = {
fr: `
Tu es un assistant qui suggère des labels PERTINENTS pour une note.
CONTENU DE LA NOTE :
${noteContent.substring(0, 1000)}
CARNET : ${notebookName}
LABELS DISPONIBLES :
${labelList}
RÈGLES STRICTES :
- Ne suggère un label QUE s'il est DIRECTEMENT en rapport avec le contenu de cette note précise
- Un label "voyage" n'est pertinent QUE si la note parle de voyage
- Un label "projet" n'est pertinent QUE si la note parle d'un projet spécifique
- Ne suggère PAS un label juste parce qu'il existe dans le carnet
- Maximum 2 suggestions
- Confiance < 0.7 = ne pas suggérer
- Si AUCUN label n'est clairement pertinent, retourne un tableau VIDE
Réponds en JSON :
{"suggestions":[{"label":"nom","confidence":0.85,"reasoning":"pourquoi"}]}
Ta réponse :
`.trim(),
en: `
You are an assistant that suggests RELEVANT labels for a note.
NOTE CONTENT:
return `NOTE CONTENT (language: ${contentLanguage}):
${noteContent.substring(0, 1000)}
NOTEBOOK: ${notebookName}
@@ -287,11 +268,10 @@ NOTEBOOK: ${notebookName}
AVAILABLE LABELS:
${labelList}
STRICT RULES:
RULES:
- Only suggest a label if it is DIRECTLY related to the content of THIS specific note
- A "travel" label is relevant ONLY if the note is about travel
- A "project" label is relevant ONLY if the note discusses a specific project
- Do NOT suggest a label just because it exists in the notebook
- If the note content language (${contentLanguage}) differs from the available labels, only suggest a label if it clearly applies regardless of language
- Maximum 2 suggestions
- Confidence < 0.7 = do not suggest
- If NO label is clearly relevant, return an EMPTY array
@@ -299,252 +279,30 @@ STRICT RULES:
Respond in JSON:
{"suggestions":[{"label":"name","confidence":0.85,"reasoning":"why"}]}
Your response:
`.trim(),
fa: `
تو دستیاری هستی که برچسب‌های مرتبط برای یادداشت پیشنهاد می‌دهی.
محتوای یادداشت:
${noteContent.substring(0, 1000)}
دفترچه: ${notebookName}
برچسب‌های موجود:
${labelList}
قوانین سخت‌گیرانه:
- فقط برچسبی را پیشنهاد بده که مستقیماً با محتوای همین یادداشت مرتبط باشد
- برچسب "سفر" فقط وقتی مرتبط است که یادداشت درباره سفر باشد
- حداکثر ۲ پیشنهاد
- اطمینان < 0.7 = پیشنهاد نده
- اگر هیچ برچسبی مرتبط نیست، آرایه خالی برگردان
پاسخ JSON:
{"suggestions":[{"label":"نام","confidence":0.85,"reasoning":"چرا"}]}
پاسخ شما:
`.trim(),
es: `
Eres un asistente que sugiere etiquetas RELEVANTES para una nota.
CONTENIDO DE LA NOTA:
${noteContent.substring(0, 1000)}
CUADERNO: ${notebookName}
ETIQUETAS DISPONIBLES:
${labelList}
REGLAS ESTRICTAS:
- Solo sugiere una etiqueta si está DIRECTAMENTE relacionada con el contenido de ESTA nota
- Máximo 2 sugerencias
- Confianza < 0.7 = no sugerir
- Si NINGUNA etiqueta es relevante, devuelve un array VACÍO
Responde en JSON:
{"suggestions":[{"label":"nombre","confidence":0.85,"reasoning":"por qué"}]}
Tu respuesta:
`.trim(),
de: `
Du bist ein Assistent, der RELEVANTE Labels für eine Notiz vorschlägt.
NOTIZINHALT:
${noteContent.substring(0, 1000)}
NOTIZBUCH: ${notebookName}
VERFÜGBARE LABELS:
${labelList}
STRIKTE REGELN:
- Schlage ein Label nur vor, wenn es DIREKT mit dem Inhalt DIESER Notiz zusammenhängt
- Maximal 2 Vorschläge
- Konfidenz < 0.7 = nicht vorschlagen
- Wenn KEIN Label relevant ist, gib ein LEERES Array zurück
Antworte in JSON:
{"suggestions":[{"label":"name","confidence":0.85,"reasoning":"warum"}]}
Deine Antwort:
`.trim()
}
return instructions[language] || instructions['en'] || instructions['fr']
Your response:`
}
/**
* Build the AI prompt for NEW label suggestions (when notebook is empty) (localized)
*/
private buildNewLabelsPrompt(noteContent: string, notebookName: string, language: string = 'en'): string {
const instructions: Record<string, string> = {
fr: `
Tu es un assistant qui suggère de nouveaux labels pour organiser une note.
CONTENU DE LA NOTE :
private buildNewLabelsPrompt(noteContent: string, notebookName: string, contentLanguage: string): string {
return `NOTE CONTENT (language: ${contentLanguage}):
${noteContent.substring(0, 1000)}
NOTEBOOK ACTUEL :
${notebookName}
CONTEXTE :
Ce notebook n'a pas encore de labels. Tu dois suggérer les PREMIERS labels appropriés pour cette note.
TÂCHE :
Analyse le contenu de la note et suggère 1-3 labels qui seraient pertinents pour organiser cette note.
Considère :
1. Les sujets ou thèmes abordés
2. Le type de contenu (idée, tâche, référence, etc.)
3. Le contexte du notebook "${notebookName}"
RÈGLES :
- Les labels doivent être COURTS (1-2 mots maximum)
- Les labels doivent être en minuscules
- Évite les accents si possible (ex: "idee" au lieu de "idée")
- Retourne au maximum 3 suggestions
- Chaque suggestion doit avoir une confiance > 0.6
IMPORTANT : Réponds UNIQUEMENT avec du JSON valide, sans texte avant ou après. Pas de markdown, pas de code blocks.
FORMAT DE RÉPONSE (JSON brut, sans markdown) :
{"suggestions":[{"label":"nom_du_label","confidence":0.85,"reasoning":"Pourquoi ce label est pertinent"}]}
Ta réponse (JSON brut uniquement) :
`.trim(),
en: `
You are an assistant that suggests new labels to organize a note.
NOTE CONTENT:
${noteContent.substring(0, 1000)}
CURRENT NOTEBOOK:
${notebookName}
CONTEXT:
This notebook has no labels yet. You must suggest the FIRST appropriate labels for this note.
TASK:
Analyze the note content and suggest 1-3 labels that would be relevant to organize this note.
Consider:
1. Topics or themes covered
2. Content type (idea, task, reference, etc.)
3. Context of the notebook "${notebookName}"
NOTEBOOK: ${notebookName}
RULES:
- Suggest 1-3 labels to organize this note
- Labels must be SHORT (max 1-2 words)
- Labels must be in the SAME LANGUAGE as the note content (${contentLanguage})
- Labels must be lowercase
- Avoid accents if possible
- Return maximum 3 suggestions
- Each suggestion must have confidence > 0.6
- Maximum 3 suggestions
- Confidence < 0.6 = do not suggest
IMPORTANT: Respond ONLY with valid JSON, without text before or after. No markdown, no code blocks.
IMPORTANT: Respond ONLY with valid JSON, no markdown, no code blocks.
{"suggestions":[{"label":"label_name","confidence":0.85,"reasoning":"why"}]}
RESPONSE FORMAT (raw JSON, no markdown):
{"suggestions":[{"label":"label_name","confidence":0.85,"reasoning":"Why this label is relevant"}]}
Your response (raw JSON only):
`.trim(),
fa: `
شما یک دستیار هستید که برچسب‌های جدیدی برای سازماندهی یک یادداشت پیشنهاد می‌دهید.
محتوای یادداشت:
${noteContent.substring(0, 1000)}
دفترچه فعلی:
${notebookName}
زمینه:
این دفترچه هنوز هیچ برچسبی ندارد. شما باید اولین برچسب‌های مناسب را برای این یادداشت پیشنهاد دهید.
وظیفه:
محتوای یادداشت را تحلیل کنید و ۱-۳ برچسب پیشنهاد دهید که برای سازماندهی این یادداشت مرتبط باشند.
در نظر بگیرید:
1. موضوعات یا تم‌های پوشش داده شده
2. نوع محتوا (ایده، وظیفه، مرجع و غیره)
3. زمینه دفترچه "${notebookName}"
قوانین:
- برچسب‌ها باید کوتاه باشند (حداکثر ۱-۲ کلمه)
- برچسب‌ها باید با حروف کوچک باشند
- حداکثر ۳ پیشنهاد برگردانید
- هر پیشنهاد باید دارای اطمینان > 0.6 باشد
مهم: فقط با یک JSON معتبر پاسخ دهید، بدون متن قبل یا بعد. بدون مارک‌داون، بدون بلوک کد.
فرمت پاسخ (JSON خام، بدون مارک‌داون):
{"suggestions":[{"label":"نام_برچسب","confidence":0.85,"reasoning":"چرا این برچسب مرتبط است"}]}
پاسخ شما (فقط JSON خام):
`.trim(),
es: `
Eres un asistente que sugiere nuevas etiquetas para organizar una nota.
CONTENIDO DE LA NOTA:
${noteContent.substring(0, 1000)}
CUADERNO ACTUAL:
${notebookName}
CONTEXTO:
Este cuaderno aún no tiene etiquetas. Debes sugerir las PRIMERAS etiquetas apropiadas para esta nota.
TAREA:
Analiza el contenido de la nota y sugiere 1-3 etiquetas que serían relevantes para organizar esta nota.
Considera:
1. Temas o tópicos cubiertos
2. Tipo de contenido (idea, tarea, referencia, etc.)
3. Contexto del cuaderno "${notebookName}"
REGLAS:
- Las etiquetas deben ser CORTAS (máx 1-2 palabras)
- Las etiquetas deben estar en minúsculas
- Evita acentos si es posible
- Devuelve máximo 3 sugerencias
- Cada sugerencia debe tener confianza > 0.6
IMPORTANTE: Responde SOLO con JSON válido, sin texto antes o después. Sin markdown, sin bloques de código.
FORMATO DE RESPUESTA (JSON crudo, sin markdown):
{"suggestions":[{"label":"nombre_etiqueta","confidence":0.85,"reasoning":"Por qué esta etiqueta es relevante"}]}
Tu respuesta (solo JSON crudo):
`.trim(),
de: `
Du bist ein Assistent, der neue Labels vorschlägt, um eine Notiz zu organisieren.
NOTIZINHALT:
${noteContent.substring(0, 1000)}
AKTUELLES NOTIZBUCH:
${notebookName}
KONTEXT:
Dieses Notizbuch hat noch keine Labels. Du musst die ERSTEN passenden Labels für diese Notiz vorschlagen.
AUFGABE:
Analysiere den Notizinhalt und schlage 1-3 Labels vor, die relevant wären, um diese Notiz zu organisieren.
Berücksichtige:
1. Abgedeckte Themen oder Bereiche
2. Inhaltstyp (Idee, Aufgabe, Referenz, usw.)
3. Kontext des Notizbuchs "${notebookName}"
REGELN:
- Labels müssen KURZ sein (max 1-2 Wörter)
- Labels müssen kleingeschrieben sein
- Vermeide Akzente wenn möglich
- Gib maximal 3 Vorschläge zurück
- Jeder Vorschlag muss eine Konfidenz > 0.6 haben
WICHTIG: Antworte NUR mit gültigem JSON, ohne Text davor oder danach. Kein Markdown, keine Code-Blöcke.
ANTWORTFORMAT (rohes JSON, kein Markdown):
{"suggestions":[{"label":"label_name","confidence":0.85,"reasoning":"Warum dieses Label relevant ist"}]}
Deine Antwort (nur rohes JSON):
`.trim()
}
return instructions[language] || instructions['en'] || instructions['fr']
Your response:`
}
}