'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' type SvgComplexity = 'simple' | 'illustrated' | 'rich' // Palette de l'application Momento — à utiliser dans TOUS les SVGs const APP_PALETTE = ` APP COLOR PALETTE (use ONLY these colors — no other palettes): - Background warm beige: #F2F0E9 - Desk warm grey: #E5E2D9 - Brand copper: #A47148 (main accent — use for key shapes) - Sage green: #ACB995 (secondary accent) - Ink charcoal: #2C2A26 (for dark shapes, text) - Warm tan: #C9B8A1 (mid tones) - Soft cream: #FAF8F4 (light elements) - Dusty rose: #C4998B (warm pink accent) - Muted slate: #8D8D8D (subtle elements) - Deep bark: #6B4C35 (dark brown for depth) FORBIDDEN colors: cold blue, navy, electric cyan, neon, pure white (#fff), pure black (#000) The overall mood must be WARM, EDITORIAL, like aged paper with copper accents. ` function extractSvgSnippet(raw: string): string | null { let text = raw .replace(/[\s\S]*?<\/think>/gi, '') .replace(/```(?:svg|xml)?\s*/gi, '') .replace(/```/g, '') .trim() const start = text.indexOf('') if (start === -1 || end === -1 || end <= start) return null return text.slice(start, end + 6) } function sanitizeSvgMarkup(svg: string): string { return DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true }, ADD_TAGS: [ 'use', 'defs', 'linearGradient', 'radialGradient', 'stop', 'filter', 'feDropShadow', 'feGaussianBlur', 'feBlend', 'feComposite', 'feMerge', 'feMergeNode', 'feColorMatrix', 'feOffset', 'feTurbulence', 'feDisplacementMap', 'clipPath', 'mask', 'pattern', 'symbol', 'marker', ], ADD_ATTR: [ 'viewBox', 'xmlns', 'preserveAspectRatio', 'gradientUnits', 'gradientTransform', 'spreadMethod', 'offset', 'stop-color', 'stop-opacity', 'stdDeviation', 'dx', 'dy', 'flood-color', 'flood-opacity', 'in', 'in2', 'result', 'type', 'values', 'markerWidth', 'markerHeight', 'refX', 'refY', 'orient', 'font-family', 'font-size', 'font-weight', 'text-anchor', 'dominant-baseline', 'letter-spacing', 'word-spacing', 'baseFrequency', 'numOctaves', 'seed', 'scale', ], }) } function buildPrompt( complexity: SvgComplexity, plainTitle: string, plainBody: string, ): string { const title = (plainTitle || 'Note').slice(0, 200) const bodyContext = plainBody.slice(0, 2000) const sharedRules = ` MULTILINGUAL: The title/content may be in French, Persian, Arabic, English or any language. Ignore the language — produce VISUAL output only. ${APP_PALETTE} ABSOLUTELY FORBIDDEN: - Generic/meaningless shapes: plain eye shapes, nested concentric circles with a triangle, abstract orbits, random geometric blobs - Cold colors: blue, navy, cyan, neon green, purple, electric tones - Shapes that have NO visual connection to the topic - Gradients that go dark-to-dark with cold tones - Dashed circles or crosshair-style patterns unless the topic is literally about targeting/aiming WHAT MAKES A GOOD ILLUSTRATION: - A viewer who sees the SVG and reads the title should say "yes, that makes sense" - Concrete objects > abstract shapes (airplane > circle, book > rectangle, brain > circle with bumps) - The most recognizable iconic form of the concept - Warmth and editorial feel matching the app palette ` if (complexity === 'simple') { return `You are a precision SVG icon designer creating note card thumbnails for a warm editorial note-taking app. Your task: create ONE clear, iconic pictogram that represents THIS SPECIFIC note topic. ${sharedRules} OUTPUT: Raw SVG ONLY. ... . No markdown, no comments. CANVAS: viewBox="0 0 400 300" — NO width/height attributes. SIZE TARGET: 800-1400 bytes of SVG markup. DESIGN APPROACH — "Warm Stamp" style: - Background: fill entire canvas with warm beige #F2F0E9 - Add a subtle texture rectangle: - Main pictogram: centered, using brand copper #A47148 as primary color - Secondary element: sage green #ACB995 for accents or depth - The pictogram fills roughly 40-55% of the canvas height — large and readable SPECIFIC TOPIC → SPECIFIC SHAPE rules (think like a professional icon designer): - AI / Machine Learning → neural network nodes connected by lines, OR a stylized brain with circuit paths - Tech analysis / research → magnifying glass over a document or graph bars - Travel / flights → an airplane silhouette (side view, classic shape with wings) - Governance / regulation → scales of justice, OR a shield with checkmark - Innovation / startup → rocket ship, OR a lightbulb with circuit inside - Cooking / food → specific food item mentioned, OR bowl with utensils - Finance / money → ascending bar chart, OR coin stack - Nature / environment → tree with roots showing, OR mountain peaks - Music → treble clef, OR speaker with sound waves - Health / medical → heartbeat line, OR medical cross - Education / learning → open book with pages, OR graduation cap - Writing / literature → quill pen, OR typewriter keys - Philosophy / thinking → head silhouette with thought bubble, OR question mark with gears - Calendar / date / event → calendar grid with highlighted date - Sport → the specific sport equipment mentioned - Data / statistics → bar chart or scatter plot dots - Social / community → group of overlapping person silhouettes DO NOT use a generic shape if the topic is specific. Read CAREFULLY: - "Tensions IA" → NOT a circle+triangle → USE: two opposing arrows pulling apart, OR a scale with AI chip on one side - "Veille IA & Tech" → NOT an eye → USE: a magnifying glass over circuit board, OR stacked tech layers with a search icon - "Gouvernance" → USE: shield with checkmark, OR gavel/hammer of judge - "Innovation" → USE: rocket or lightbulb TITLE: "${title}" CONTENT (for context): ${bodyContext.slice(0, 500)}` } if (complexity === 'illustrated') { return `You are a professional SVG illustrator creating rich editorial card illustrations for a warm note-taking app. ${sharedRules} OUTPUT: Raw SVG ONLY. ... . No markdown, no comments. CANVAS: viewBox="0 0 400 300" — NO width/height attributes. SIZE TARGET: 2000-3500 bytes. DESIGN APPROACH — "Editorial Print" style: - Layered composition: background fill → texture layer → mid elements → focal element → foreground details - Use with 1-2 gradients (warm tones only — from the app palette) - 12-20 SVG elements - The illustration should tell a visual story about the note content - Add subtle paper texture: - Focal element is specific and recognizable (not abstract) - Depth through opacity stacking (0.2, 0.4, 0.7, 1.0 layers) Example structure for "AI Tech Analysis": - Background: warm beige rect - Mid: faint grid lines in #C9B8A1 suggesting a document or data surface - Focal: a magnifying glass (#A47148) with a neural net pattern visible inside its lens - Foreground: small data points or chip-like squares in #ACB995 TITLE: "${title}" CONTENT: ${bodyContext.slice(0, 1000)}` } // 'rich' — carte conceptuelle return `You are an expert information designer creating SVG concept maps for a warm editorial note-taking app. ${sharedRules} OUTPUT: Raw SVG ONLY. ... . No markdown, no comments. CANVAS: viewBox="0 0 400 300" — NO width/height attributes. SIZE TARGET: 2500-4000 bytes. DESIGN APPROACH — "Warm Knowledge Map": - Background: warm beige #F2F0E9 (NOT dark — this is a LIGHT warm background) - Node style: rounded with #A47148 fill for main node, #ACB995 for secondary, #C9B8A1 for tertiary - Text inside nodes: - Connection lines: - Arrow markers: with copper fill - Drop shadows on nodes: STRUCTURE: 1. Central node (title concept) — large, centered or slightly left, #A47148 2. 3-5 satellite nodes for key sub-concepts from the content body 3. Labels: SHORT (2-3 words), in the app's serif style 4. Spread across the canvas — use the full 400×300 space TITLE: "${title}" CONTENT (extract 3-5 key concepts): ${bodyContext.slice(0, 1500)}` } /** * Génère ou regénère une illustration SVG pour une note. */ export async function generateNoteIllustrationSvg( noteId: string, options?: { skipRevalidation?: boolean }, ): 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 complexity: SvgComplexity = (settings.svgComplexity as SvgComplexity) ?? 'simple' 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() 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 = buildPrompt(complexity, plainTitle, plainBody) 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('