import { NextRequest, NextResponse } from 'next/server' import { runLaneWithBillingUser, willUseByokForLane } from '@/lib/ai/provider-for-user' import { getSystemConfig } from '@/lib/config' import { auth } from '@/auth' import { getAISettings } from '@/app/actions/ai-settings' import { reserveUsageOrThrow, QuotaExceededError, QuotaServiceUnavailableError } from '@/lib/entitlements' import { z } from 'zod' import { hasUserAiConsent } from '@/lib/consent/server-consent' const requestSchema = z.object({ content: z.string().min(1, "Le contenu ne peut pas être vide"), }) /** Supprime les balises HTML pour extraire le texte brut */ function stripHtml(html: string): string { return html .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\s+/g, ' ') .trim() } export async function POST(req: NextRequest) { try { // Check authentication and user setting const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } // GDPR AI Consent check if (!(await hasUserAiConsent())) { return NextResponse.json({ error: 'ai_consent_required' }, { status: 403 }) } const settings = await getAISettings(session.user.id) if (settings.titleSuggestions === false) { return NextResponse.json({ suggestions: [] }) } const body = await req.json() const { content: rawContent } = requestSchema.parse(body) // Nettoyer le HTML (l'éditeur TipTap envoie du HTML) const content = stripHtml(rawContent) // Vérifier qu'il y a au moins 10 mots const wordCount = content.split(/\s+/).filter(w => w.length > 0).length if (wordCount < 10) { return NextResponse.json( { error: 'Le contenu doit avoir au moins 10 mots' }, { status: 400 } ) } const config = await getSystemConfig() const { usedByok: willUseByok } = await willUseByokForLane('tags', config, session.user.id) if (!willUseByok) { try { await reserveUsageOrThrow(session.user.id, 'auto_title') } catch (err) { if (err instanceof QuotaExceededError) { return NextResponse.json(err.toJSON(), { status: 402 }) } if (err instanceof QuotaServiceUnavailableError || process.env.NODE_ENV === 'production') { return NextResponse.json({ error: 'QUOTA_SERVICE_UNAVAILABLE' }, { status: 503 }) } console.error('[/api/ai/title-suggestions] Quota check error (fail-open):', err) } } // Détecter la langue du contenu (simple détection basée sur les caractères et mots) const isPersian = /[\u0600-\u06FF]/.test(content) const isChinese = /[\u4E00-\u9FFF]/.test(content) const isRussian = /[\u0400-\u04FF]/.test(content) const isArabic = /[\u0600-\u06FF]/.test(content) // Détection du français par des mots et caractères caractéristiques const frenchWords = /\b(le|la|les|un|une|des|et|ou|mais|donc|pour|dans|sur|avec|sans|très|plus|moins|tout|tous|toute|toutes|ce|cette|ces|mon|ma|mes|ton|ta|tes|son|sa|ses|notre|nos|votre|vos|leur|leurs|je|tu|il|elle|nous|vous|ils|elles|est|sont|été|être|avoir|faire|aller|venir|voir|savoir|pouvoir|vouloir|falloir|comme|que|qui|dont|où|quand|pourquoi|comment|quel|quelle|quels|quelles)\b/i const frenchAccents = /[éèêàâôûùïüç]/i const isFrench = frenchWords.test(content) || frenchAccents.test(content) // Déterminer la langue du prompt système let promptLanguage = 'en' let responseLanguage = 'English' if (isFrench) { promptLanguage = 'fr' // Français responseLanguage = 'French' } else if (isPersian) { promptLanguage = 'fa' // Persan responseLanguage = 'Persian' } else if (isChinese) { promptLanguage = 'zh' // Chinois responseLanguage = 'Chinese' } else if (isRussian) { promptLanguage = 'ru' // Russe responseLanguage = 'Russian' } else if (isArabic) { promptLanguage = 'ar' // Arabe responseLanguage = 'Arabic' } // Générer des titres appropriés basés sur le contenu const titlePrompt = promptLanguage === 'en' ? `You are a title generator. Generate 3 concise, descriptive titles for the following content. IMPORTANT INSTRUCTIONS: - Use ONLY the content provided below between the CONTENT_START and CONTENT_END markers - Do NOT use any external knowledge or training data - Focus on the main topics and themes in THIS SPECIFIC content - Be specific to what is actually discussed CONTENT_START: ${content.substring(0, 3000)} CONTENT_END Respond ONLY with a JSON array: [{"title": "title1", "confidence": 0.95}, {"title": "title2", "confidence": 0.85}, {"title": "title3", "confidence": 0.75}]` : `Tu es un générateur de titres. Génère 3 titres concis et descriptifs pour le contenu suivant en ${responseLanguage}. INSTRUCTIONS IMPORTANTES : - Utilise SEULEMENT le contenu fourni entre les marqueurs CONTENT_START et CONTENT_END - N'utilise AUCUNE connaissance externe ou données d'entraînement - Concentre-toi sur les sujets principaux et thèmes de CE CONTENU SPÉCIFIQUE - Sois spécifique à ce qui est réellement discuté CONTENT_START: ${content.substring(0, 3000)} CONTENT_END Réponds SEULEMENT avec un tableau JSON: [{"title": "titre1", "confidence": 0.95}, {"title": "titre2", "confidence": 0.85}, {"title": "titre3", "confidence": 0.75}]` const { result: titles } = await runLaneWithBillingUser( 'tags', config, session.user.id, (provider) => provider.generateTitles(titlePrompt), ) // Créer les suggestions const suggestions = titles.map((t: any) => ({ title: t.title, confidence: Math.round(t.confidence * 100), reasoning: `Basé sur le contenu` })) return NextResponse.json({ suggestions }) } catch (error: any) { if (error instanceof z.ZodError) { return NextResponse.json({ error: error.issues }, { status: 400 }) } return NextResponse.json( { error: error.message || 'Erreur lors de la génération des titres' }, { status: 500 } ) } }