diff --git a/memento-note/app/api/agents/run-for-note/route.ts b/memento-note/app/api/agents/run-for-note/route.ts index 1b8e5e0..efcf208 100644 --- a/memento-note/app/api/agents/run-for-note/route.ts +++ b/memento-note/app/api/agents/run-for-note/route.ts @@ -21,6 +21,7 @@ const TYPE_DEFAULTS: Record executeAgent(agent.id, userId)) + .catch(err => console.error('[run-for-note] Background agent error:', err)) + + return NextResponse.json({ success: true, agentId: agent.id, status: 'running' }) +} + +// ─── GET : poll current agent status ────────────────────────────────────── +export async function GET(req: NextRequest) { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }) + } + const userId = session.user.id + + const agentId = req.nextUrl.searchParams.get('agentId') + if (!agentId) { + return NextResponse.json({ error: 'agentId manquant' }, { status: 400 }) + } + + // Verify ownership + const agent = await prisma.agent.findFirst({ + where: { id: agentId, userId }, + select: { id: true }, + }) + if (!agent) { + return NextResponse.json({ error: 'Agent introuvable' }, { status: 404 }) + } + + // Return latest action for this agent + const action = await prisma.agentAction.findFirst({ + where: { agentId }, + orderBy: { createdAt: 'desc' }, + select: { id: true, status: true, result: true, log: true }, + }) + + if (!action) { + // Action not created yet (race condition in first poll) — still running + return NextResponse.json({ status: 'running' }) + } + + // If success, find canvasId from the related canvas (result stores canvas id) + let canvasId: string | null = null + let noteId: string | null = null + if (action.status === 'success' && action.result) { + // result field: the executor stores canvas.id or note.id + const canvas = await prisma.canvas.findFirst({ + where: { id: action.result }, + select: { id: true }, + }) + if (canvas) { + canvasId = canvas.id + } else { + noteId = action.result + } + } + + return NextResponse.json({ + status: action.status, // 'running' | 'success' | 'failure' + actionId: action.id, + canvasId, + noteId, + error: action.status === 'failure' ? (action.log || 'Erreur inconnue') : undefined, + }) } diff --git a/memento-note/components/contextual-ai-chat.tsx b/memento-note/components/contextual-ai-chat.tsx index d623d3b..33323bb 100644 --- a/memento-note/components/contextual-ai-chat.tsx +++ b/memento-note/components/contextual-ai-chat.tsx @@ -264,6 +264,8 @@ export function ContextualAIChat({ // ── Generate slides / diagram ──────────────────────────────────────────────── + const generatePollRef = useRef | null>(null) + const handleGenerate = async (type: 'slides' | 'diagram') => { if (!noteId) { toast.error(t('ai.generate.noNoteId') || 'Note non sauvegardée') @@ -272,7 +274,7 @@ export function ContextualAIChat({ setGenerateLoading(type) setGenerateResult(null) - // Persistent loading toast — survives navigation (Sonner is layout-level) + // Persistent loading toast — layout-level (Sonner), survives navigation const toastId = toast.loading( type === 'slides' ? (t('ai.generate.toastLoading.slides') || '⏳ Génération de la présentation en cours…') @@ -281,6 +283,7 @@ export function ContextualAIChat({ ) try { + // POST starts the agent immediately and returns agentId (fire-and-forget on server) const res = await fetch('/api/agents/run-for-note', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -292,38 +295,69 @@ export function ContextualAIChat({ 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', { id: toastId }) + setGenerateLoading(null) return } - setGenerateResult({ type, canvasId: data.canvasId, noteId: data.noteId }) - if (type === 'slides' && data.canvasId) { - toast.success(t('ai.generate.slidesReady') || 'Présentation générée !', { - id: toastId, - duration: 10000, - description: t('ai.generate.toastSuccessSlides') || 'Cliquez sur le bouton Télécharger dans le panneau IA.', - action: { label: t('ai.generate.downloadPptx') || 'Télécharger', onClick: () => window.open(`/api/canvas/download?id=${data.canvasId}`, '_blank') }, - }) - } else if (type === 'diagram' && data.canvasId) { - toast.success(t('ai.generate.diagramReady') || 'Diagramme généré !', { - id: toastId, - duration: 10000, - description: t('ai.generate.toastSuccessDiagram') || 'Votre diagramme est disponible dans le Lab.', - action: { label: t('ai.generate.openDiagram') || 'Ouvrir', onClick: () => window.location.href = `/lab?canvas=${data.canvasId}` }, - }) - } else { - toast.success(type === 'slides' - ? (t('ai.generate.slidesReady') || 'Présentation générée !') - : (t('ai.generate.diagramReady') || 'Diagramme généré !'), - { id: toastId } - ) - } + const { agentId } = data as { agentId: string } + + // Poll status every 3 s until terminal state + if (generatePollRef.current) clearInterval(generatePollRef.current) + generatePollRef.current = setInterval(async () => { + try { + const pollRes = await fetch(`/api/agents/run-for-note?agentId=${agentId}`) + const poll = await pollRes.json() + + if (poll.status === 'success') { + clearInterval(generatePollRef.current!) + generatePollRef.current = null + setGenerateLoading(null) + setGenerateResult({ type, canvasId: poll.canvasId, noteId: poll.noteId }) + + if (type === 'slides' && poll.canvasId) { + toast.success(t('ai.generate.slidesReady') || 'Présentation générée !', { + id: toastId, duration: 10000, + description: t('ai.generate.toastSuccessSlides') || 'Cliquez sur Télécharger dans le panneau IA.', + action: { label: t('ai.generate.downloadPptx') || 'Télécharger', onClick: () => window.open(`/api/canvas/download?id=${poll.canvasId}`, '_blank') }, + }) + } else if (type === 'diagram' && poll.canvasId) { + toast.success(t('ai.generate.diagramReady') || 'Diagramme généré !', { + id: toastId, duration: 10000, + description: t('ai.generate.toastSuccessDiagram') || 'Votre diagramme est disponible dans le Lab.', + action: { label: t('ai.generate.openDiagram') || 'Ouvrir', onClick: () => { window.location.href = `/lab?canvas=${poll.canvasId}` } }, + }) + } else { + toast.success(type === 'slides' + ? (t('ai.generate.slidesReady') || 'Présentation générée !') + : (t('ai.generate.diagramReady') || 'Diagramme généré !'), + { id: toastId } + ) + } + } else if (poll.status === 'failure') { + clearInterval(generatePollRef.current!) + generatePollRef.current = null + setGenerateLoading(null) + toast.error(poll.error || t('ai.generate.error') || 'Erreur lors de la génération', { id: toastId }) + } + // If still 'running', do nothing — next poll will check again + } catch { + // Network error during poll — keep polling + } + }, 3000) + } catch { toast.error(t('ai.generate.error') || 'Erreur lors de la génération', { id: toastId }) - } finally { setGenerateLoading(null) } } + // Cleanup polling on unmount + useEffect(() => { + return () => { + if (generatePollRef.current) clearInterval(generatePollRef.current) + } + }, []) + // ── Resource tab handlers ──────────────────────────────────────────────────── const handleScrapeUrl = async () => {