From d1e08f64c828535c5842c6e2d27024d9651077d8 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Tue, 5 May 2026 21:07:43 +0000 Subject: [PATCH] =?UTF-8?q?feat(ai-note):=20ajouter=20boutons=20G=C3=A9n?= =?UTF-8?q?=C3=A9rer=20slides/diagramme=20dans=20le=20panneau=20IA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouveau endpoint POST /api/agents/run-for-note : crée un agent one-shot (slide-generator ou excalidraw-generator) avec la note courante comme source et l'exécute immédiatement - ContextualAIChat : prop noteId + section "Générer depuis cette note" avec deux boutons gradient (violet=slides, cyan=diagramme), spinner pendant la génération, bouton de téléchargement .pptx ou lien "Ouvrir dans le Lab" après succès - note-editor.tsx : passage de note.id à ContextualAIChat - i18n fr/en : nouvelles clés ai.generate.* Co-authored-by: Cursor --- .../app/api/agents/run-for-note/route.ts | 86 +++++++++++++ .../components/contextual-ai-chat.tsx | 118 ++++++++++++++++++ memento-note/components/note-editor.tsx | 17 +-- memento-note/locales/en.json | 12 ++ memento-note/locales/fr.json | 12 ++ 5 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 memento-note/app/api/agents/run-for-note/route.ts diff --git a/memento-note/app/api/agents/run-for-note/route.ts b/memento-note/app/api/agents/run-for-note/route.ts new file mode 100644 index 0000000..1b8e5e0 --- /dev/null +++ b/memento-note/app/api/agents/run-for-note/route.ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/lib/auth' +import { prisma } from '@/lib/prisma' + +type GenerateType = 'slide-generator' | 'excalidraw-generator' + +const TYPE_DEFAULTS: Record = { + 'slide-generator': { + role: 'Crée une présentation PowerPoint professionnelle et visuelle à partir du contenu de la note fournie.', + tools: JSON.stringify(['note_search', 'note_read', 'generate_pptx']), + maxSteps: 8, + }, + 'excalidraw-generator': { + role: 'Génère un diagramme Excalidraw clair et professionnel à partir du contenu de la note fournie.', + tools: JSON.stringify(['note_search', 'note_read', 'generate_excalidraw']), + maxSteps: 6, + }, +} + +export async function POST(req: NextRequest) { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }) + } + const userId = session.user.id + + const body = await req.json() + const { noteId, type, theme, style } = body as { + noteId: string + type: GenerateType + theme?: string + style?: string + } + + if (!noteId || !type || !TYPE_DEFAULTS[type]) { + return NextResponse.json({ error: 'Paramètres invalides' }, { status: 400 }) + } + + // Verify note belongs to user + const note = await prisma.note.findFirst({ + where: { id: noteId, userId }, + select: { id: true, title: true, notebookId: true }, + }) + if (!note) { + return NextResponse.json({ error: 'Note introuvable' }, { status: 404 }) + } + + const defaults = TYPE_DEFAULTS[type] + const agentName = type === 'slide-generator' + ? `Slides — ${(note.title || 'Note').substring(0, 40)}` + : `Diagramme — ${(note.title || 'Note').substring(0, 40)}` + + // Create a dedicated one-shot agent for this note + const agent = await prisma.agent.create({ + data: { + name: agentName, + type, + role: defaults.role, + tools: defaults.tools, + maxSteps: defaults.maxSteps, + frequency: 'manual', + isEnabled: true, + sourceNoteIds: JSON.stringify([noteId]), + targetNotebookId: note.notebookId ?? undefined, + slideTheme: theme ?? 'vibrant_tech', + slideStyle: style ?? 'soft', + userId, + }, + }) + + try { + const { executeAgent } = await import('@/lib/ai/services/agent-executor.service') + const result = await executeAgent(agent.id, userId) + return NextResponse.json(result) + } catch (err) { + console.error('[run-for-note] executeAgent error:', err) + return NextResponse.json( + { success: false, error: err instanceof Error ? err.message : 'Erreur inconnue' }, + { status: 500 }, + ) + } +} diff --git a/memento-note/components/contextual-ai-chat.tsx b/memento-note/components/contextual-ai-chat.tsx index 44dcf1f..da0c408 100644 --- a/memento-note/components/contextual-ai-chat.tsx +++ b/memento-note/components/contextual-ai-chat.tsx @@ -13,6 +13,7 @@ import { Globe, BookOpen, FileText, RotateCcw, Check, Maximize2, ImageIcon, Link2, Download, ArrowDownToLine, GitMerge, PlusCircle, Eye, Code, Languages, + Presentation, PenTool, ExternalLink, } from 'lucide-react' import { useLanguage } from '@/lib/i18n' import { MarkdownContent } from '@/components/markdown-content' @@ -71,11 +72,18 @@ const ACTION_IDS = [ // ── Types ───────────────────────────────────────────────────────────────────── +interface GenerateResult { + type: 'slides' | 'diagram' + canvasId?: string + noteId?: string +} + interface ContextualAIChatProps { onClose: () => void noteTitle?: string noteContent?: string noteImages?: string[] + noteId?: string /** Called when an action result should be injected into the note */ onApplyToNote?: (newContent: string) => void /** Called when the user wants to undo the last injected action */ @@ -95,6 +103,7 @@ export function ContextualAIChat({ noteTitle, noteContent, noteImages, + noteId, onApplyToNote, onUndoLastAction, lastActionApplied = false, @@ -116,6 +125,10 @@ export function ContextualAIChat({ const [actionPreview, setActionPreview] = useState<{ label: string; text: string } | null>(null) const [showLangPicker, setShowLangPicker] = useState(false) const [translateTarget, setTranslateTarget] = useState('') + + // Generate slides / diagram state + const [generateLoading, setGenerateLoading] = useState<'slides' | 'diagram' | null>(null) + const [generateResult, setGenerateResult] = useState(null) const [customLangInput, setCustomLangInput] = useState('') // Resource tab state @@ -249,6 +262,41 @@ export function ContextualAIChat({ const handleDiscardPreview = () => setActionPreview(null) + // ── Generate slides / diagram ──────────────────────────────────────────────── + + const handleGenerate = async (type: 'slides' | 'diagram') => { + if (!noteId) { + toast.error(t('ai.generate.noNoteId') || 'Note non sauvegardée') + return + } + setGenerateLoading(type) + setGenerateResult(null) + try { + const res = await fetch('/api/agents/run-for-note', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + noteId, + type: type === 'slides' ? 'slide-generator' : 'excalidraw-generator', + }), + }) + const data = await res.json() + if (!res.ok || !data.success) { + toast.error(data.error || t('ai.generate.error') || 'Erreur lors de la génération') + return + } + setGenerateResult({ type, canvasId: data.canvasId, noteId: data.noteId }) + toast.success(type === 'slides' + ? (t('ai.generate.slidesReady') || 'Présentation générée !') + : (t('ai.generate.diagramReady') || 'Diagramme généré !') + ) + } catch { + toast.error(t('ai.generate.error') || 'Erreur lors de la génération') + } finally { + setGenerateLoading(null) + } + } + // ── Resource tab handlers ──────────────────────────────────────────────────── const handleScrapeUrl = async () => { @@ -883,6 +931,76 @@ export function ContextualAIChat({ }) )} + {/* ── Generate slides / diagram ─────────────────────── */} + {noteId && ( +
+

+ {t('ai.generate.sectionLabel') || 'Générer depuis cette note'} +

+ + {/* Slides button */} + + + {/* Slides result */} + {generateResult?.type === 'slides' && generateResult.canvasId && ( + + + {t('ai.generate.downloadPptx') || 'Télécharger le .pptx'} + + + )} + + {/* Diagram button */} + + + {/* Diagram result */} + {generateResult?.type === 'diagram' && generateResult.canvasId && ( + + + {t('ai.generate.openDiagram') || 'Ouvrir dans le Lab'} + + )} +
+ )} + {/* Undo last action shortcut */} {lastActionApplied && onUndoLastAction && ( {/* Add Image */} - {/* Add Link */} - @@ -894,7 +894,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) { setNoteType(newType); if (newType !== 'markdown') setShowMarkdownPreview(false) }} /> {noteType === 'markdown' && ( - @@ -934,7 +934,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) {/* Color Picker */} - @@ -1023,6 +1023,7 @@ export function NoteEditor({ note, readOnly = false, onClose }: NoteEditorProps) noteTitle={title} noteContent={content} noteImages={allImages} + noteId={note.id} onApplyToNote={(newContent) => { setPreviousContentForCopilot(content) setContent(newContent) diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index 282302d..c7106d4 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -436,6 +436,18 @@ "translate": "Translate", "explain": "Explain" }, + "generate": { + "sectionLabel": "Generate from this note", + "slides": "Generate a presentation", + "diagram": "Generate a diagram", + "loading": "Generating…", + "slidesReady": "Presentation generated!", + "diagramReady": "Diagram generated!", + "downloadPptx": "Download .pptx", + "openDiagram": "Open in Lab", + "error": "Error during generation", + "noNoteId": "Save the note first" + }, "openAssistant": "Open AI Assistant", "poweredByMomento": "Powered by Momento AI", "welcomeMsg": "Hello! I'm your AI assistant. How can I help you with your notes today? I can help refine tone, expand messaging, or summarize content.", diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index d866ed5..b6b13eb 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -436,6 +436,18 @@ "translate": "Traduire", "explain": "Expliquer" }, + "generate": { + "sectionLabel": "Générer depuis cette note", + "slides": "Générer une présentation", + "diagram": "Générer un diagramme", + "loading": "Génération en cours…", + "slidesReady": "Présentation générée !", + "diagramReady": "Diagramme généré !", + "downloadPptx": "Télécharger le .pptx", + "openDiagram": "Ouvrir dans le Lab", + "error": "Erreur lors de la génération", + "noNoteId": "Enregistrez d'abord la note" + }, "openAssistant": "Ouvrir IA Note", "poweredByMomento": "Propulsé par Momento AI", "welcomeMsg": "Bonjour ! Je suis votre assistant IA. Comment puis-je vous aider avec vos notes ? Je peux affiner le ton, développer un message ou résumer le contenu.",