'use server' import DOMPurify from 'isomorphic-dompurify' import { auth } from '@/auth' import { prisma } from '@/lib/prisma' import { getChatProvider } from '@/lib/ai/factory' import { getSystemConfig } from '@/lib/config' import { getAISettings } from '@/app/actions/ai-settings' import { revalidatePath } from 'next/cache' function extractSvgSnippet(raw: string): string | null { const trimmed = raw.trim() const fenced = trimmed.match(/```(?:svg)?\s*([\s\S]*?)```/i) const candidate = (fenced ? fenced[1] : trimmed).trim() const start = candidate.indexOf('') if (start === -1 || end === -1 || end <= start) return null return candidate.slice(start, end + 6) } function sanitizeSvgMarkup(svg: string): string { return DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true }, ADD_TAGS: ['use'], ADD_ATTR: ['viewBox', 'xmlns', 'preserveAspectRatio'], }) } /** * Génère une miniature SVG abstraite pour le flux éditorial (via modèle chat configuré). * Respecte les préférences utilisateur (assistant IA activé) et nettoie le SVG. */ export async function generateNoteIllustrationSvg(noteId: string): Promise<{ ok: true } | { ok: false; error: string }> { const session = await auth() if (!session?.user?.id) return { ok: false, error: 'Non autorisé' } try { const settings = await getAISettings(session.user.id) if (settings.paragraphRefactor === false) { return { ok: false, error: 'Assistant IA désactivé dans vos paramètres.' } } const note = await prisma.note.findFirst({ where: { id: noteId, userId: session.user.id }, select: { id: true, title: true, content: true }, }) if (!note) return { ok: false, error: 'Note introuvable' } const plainTitle = (note.title || '').slice(0, 200) const plainBody = note.content .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim() .slice(0, 1200) if (!plainBody && !plainTitle) { return { ok: false, error: 'Ajoutez du contenu avant de générer une illustration.' } } const config = await getSystemConfig() const provider = getChatProvider(config) const prompt = `Create a small SVG thumbnail that VISUALLY REPRESENTS this note's topic. OUTPUT: Only raw SVG markup. No markdown, no code fences, no comments. Start with . SPECIFICATIONS: - viewBox="0 0 224 168", NO fixed width/height attributes - Maximum 1000 bytes - Background: soft warm beige (#F5F0E8) or transparent - Color palette (pick 2-3): warm charcoal (#2C2C2C), slate gray (#6B7280), soft sage (#A8B5A0), muted ochre (#C4A882), dusty rose (#C9A9A6), teal (#5F9EA0), burgundy (#8B4513) - NO text, NO scripts, NO foreignObject, NO external links CRITICAL: The illustration MUST be recognizably related to the topic. Think of it like an ICON or PICTOGRAM for the title. Not abstract random shapes. TOPIC: "${plainTitle || 'untitled'}" How to illustrate this topic (pick the BEST match): - If the topic is about CODE/DEV: Show angle brackets <>, curly braces {}, a terminal window shape, or circuit-like lines - If the topic is about MUSIC: Show sound waves, musical notes shapes, or speaker icon - If the topic is about FOOD/COOKING: Show a pot shape, utensils, or plate - If the topic is about TRAVEL: Show a path/road, mountain peaks, or compass - If the topic is about SCIENCE: Show atom orbits, flask/beaker, or molecule bonds - If the topic is about BUSINESS/FINANCE: Show ascending chart lines, coins, or briefcase - If the topic is about HEALTH: Show heart shape, pulse line, or leaf - If the topic is about EDUCATION: Show book shape, graduation cap, or pencil - If the topic is about NATURE: Show tree, mountain, water wave, or sun - If the topic is about DESIGN/ART: Show palette, brush stroke, or frame - If the topic is about PEOPLE/TEAM: Show overlapping circles, handshake, or connected nodes - If the topic is about ARCHITECTURE: Show building outline, blueprint grid, or columns - Otherwise: Extract the KEY CONCEPT from the title and draw its SIMPLEST iconic representation TECHNICAL RULES: - Use simple shapes: , , , , , , - Keep it FLAT and MINIMAL — 2-4 elements max - Use opacity for depth (0.3-0.8) - The icon should be immediately recognizable even at small size Additional context from the note: ${plainBody.slice(0, 200)}` const raw = await provider.generateText(prompt) const extracted = extractSvgSnippet(raw) if (!extracted) { return { ok: false, error: 'Le modèle n’a pas renvoyé un SVG valide. Réessayez.' } } const safe = sanitizeSvgMarkup(extracted) if (!safe.includes('