perf: memo GridCard, fuse save fns, fix slash tab active color
This commit is contained in:
@@ -8,27 +8,198 @@ 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 {
|
||||
const trimmed = raw.trim()
|
||||
const fenced = trimmed.match(/```(?:svg)?\s*([\s\S]*?)```/i)
|
||||
const candidate = (fenced ? fenced[1] : trimmed).trim()
|
||||
const start = candidate.indexOf('<svg')
|
||||
const end = candidate.lastIndexOf('</svg>')
|
||||
let text = raw
|
||||
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
||||
.replace(/```(?:svg|xml)?\s*/gi, '')
|
||||
.replace(/```/g, '')
|
||||
.trim()
|
||||
const start = text.indexOf('<svg')
|
||||
const end = text.lastIndexOf('</svg>')
|
||||
if (start === -1 || end === -1 || end <= start) return null
|
||||
return candidate.slice(start, end + 6)
|
||||
return text.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'],
|
||||
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. <svg viewBox="0 0 400 300"> ... </svg>. 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: <rect width="400" height="300" fill="#E5E2D9" opacity="0.3"/>
|
||||
- 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. <svg viewBox="0 0 400 300"> ... </svg>. 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 <defs> 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: <rect fill="#E5E2D9" opacity="0.2" width="400" height="300"/>
|
||||
- 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. <svg viewBox="0 0 400 300"> ... </svg>. 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 <rect rx="8"> with #A47148 fill for main node, #ACB995 for secondary, #C9B8A1 for tertiary
|
||||
- Text inside nodes: <text fill="#FAF8F4" font-family="Georgia, serif" font-size="11" font-weight="bold">
|
||||
- Connection lines: <line stroke="#A47148" stroke-width="1.5" opacity="0.5">
|
||||
- Arrow markers: <defs><marker> with copper fill
|
||||
- Drop shadows on nodes: <filter><feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#00000022">
|
||||
|
||||
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 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.
|
||||
* Génère ou regénère une illustration SVG pour une note.
|
||||
*/
|
||||
export async function generateNoteIllustrationSvg(
|
||||
noteId: string,
|
||||
@@ -43,6 +214,9 @@ export async function generateNoteIllustrationSvg(
|
||||
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 },
|
||||
@@ -54,7 +228,6 @@ export async function generateNoteIllustrationSvg(
|
||||
.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.' }
|
||||
@@ -63,50 +236,11 @@ export async function generateNoteIllustrationSvg(
|
||||
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 <svg and end with </svg>.
|
||||
|
||||
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: <circle>, <rect>, <line>, <path>, <ellipse>, <polygon>, <g>
|
||||
- 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 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.' }
|
||||
return { ok: false, error: 'Le modèle n\'a pas renvoyé un SVG valide. Réessayez.' }
|
||||
}
|
||||
|
||||
const safe = sanitizeSvgMarkup(extracted)
|
||||
@@ -129,6 +263,11 @@ ${plainBody.slice(0, 200)}`
|
||||
} catch (e) {
|
||||
console.error('[note-illustration]', e)
|
||||
const msg = e instanceof Error ? e.message : 'Erreur inconnue'
|
||||
return { ok: false, error: msg.includes('required') ? 'Configurez un fournisseur IA (admin ou paramètres système).' : msg }
|
||||
return {
|
||||
ok: false,
|
||||
error: msg.includes('required')
|
||||
? 'Configurez un fournisseur IA (admin ou paramètres système).'
|
||||
: msg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user