feat: icon-only toolbar, versioning fixes, history modal, PanelRight repositioning
- Toolbar: remove text labels from all icon buttons (AI, Save, Preview, Convert) all buttons now icon-only with title tooltip for accessibility - Toolbar: reposition PanelRight (info panel toggle) to far right after three-dot menu - Versioning: decouple getNoteHistory/restoreNoteVersion from global userAISettings.noteHistory now checks note.historyEnabled directly — unblocks manual per-note history - Versioning: add 'Sauvegarder cette version' button in Versions tab of info panel calls commitNoteHistory with visual feedback (spinner → success state) - note-document-info-panel: import commitNoteHistory, add isSavingVersion state - notes.ts: fix double guard that silently blocked all history operations
This commit is contained in:
100
memento-note/app/actions/note-illustration.ts
Normal file
100
memento-note/app/actions/note-illustration.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
'use server'
|
||||
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { getAIProvider } 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('<svg')
|
||||
const end = candidate.lastIndexOf('</svg>')
|
||||
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 = getAIProvider(config)
|
||||
|
||||
const prompt = `Tu es un designer minimaliste. Produis UN SEUL document SVG valide pour une vignette de carte note.
|
||||
Contraintes strictes:
|
||||
- viewBox="0 0 224 168" (rapport 4:3), pas de width/height fixes en px sur la racine ou width="100%" height="100%"
|
||||
- Style architectural / papier, 2–4 formes géométriques ou lignes, palette sobre (noir/gris/une couleur douce), pas de texte lisible
|
||||
- AUCUN script, AUCUNE balise foreignObject, AUCUN lien externe, AUCUN attribut on*
|
||||
- Réponds UNIQUEMENT avec le fragment SVG (commence par <svg ...> et finit par </svg>), sans markdown ni commentaire.
|
||||
|
||||
Thème à suggérer visuellement (abstrait, pas littéral):
|
||||
Titre: ${plainTitle || '(sans titre)'}
|
||||
Extrait: ${plainBody.slice(0, 400)}`
|
||||
|
||||
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('<svg')) {
|
||||
return { ok: false, error: 'SVG rejeté après sécurisation.' }
|
||||
}
|
||||
|
||||
await prisma.note.update({
|
||||
where: { id: noteId, userId: session.user.id },
|
||||
data: {
|
||||
illustrationSvg: safe,
|
||||
lastAiAnalysis: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
return { ok: true }
|
||||
} 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 }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user