fix: runAgent fire-and-forget + polling sur la page /agents
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
Le bouton Play des cartes agents appelait runAgent (Server Action) qui
attendait executeAgent (~2-5 min) → spinner bloqué sans résultat.
- runAgent retourne immédiatement { success, agentId }
- agent-card.tsx lance un polling toutes les 3s sur GET
/api/agents/run-for-note?agentId= jusqu'au statut terminal
- Toast persistant Sonner pendant la génération, mis à jour au résultat
- Cleanup automatique du polling au démontage
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,7 @@ export async function createAgent(data: {
|
|||||||
role: string
|
role: string
|
||||||
sourceUrls?: string[]
|
sourceUrls?: string[]
|
||||||
sourceNotebookId?: string
|
sourceNotebookId?: string
|
||||||
|
sourceNoteIds?: string[]
|
||||||
targetNotebookId?: string
|
targetNotebookId?: string
|
||||||
frequency?: string
|
frequency?: string
|
||||||
tools?: string[]
|
tools?: string[]
|
||||||
@@ -28,6 +29,8 @@ export async function createAgent(data: {
|
|||||||
scheduledTime?: string
|
scheduledTime?: string
|
||||||
scheduledDay?: number
|
scheduledDay?: number
|
||||||
timezone?: string
|
timezone?: string
|
||||||
|
slideTheme?: string
|
||||||
|
slideStyle?: string
|
||||||
}) {
|
}) {
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@@ -43,6 +46,7 @@ export async function createAgent(data: {
|
|||||||
role: data.role,
|
role: data.role,
|
||||||
sourceUrls: data.sourceUrls ? JSON.stringify(data.sourceUrls) : null,
|
sourceUrls: data.sourceUrls ? JSON.stringify(data.sourceUrls) : null,
|
||||||
sourceNotebookId: data.sourceNotebookId || null,
|
sourceNotebookId: data.sourceNotebookId || null,
|
||||||
|
sourceNoteIds: data.sourceNoteIds ? JSON.stringify(data.sourceNoteIds) : null,
|
||||||
targetNotebookId: data.targetNotebookId || null,
|
targetNotebookId: data.targetNotebookId || null,
|
||||||
frequency: data.frequency || 'manual',
|
frequency: data.frequency || 'manual',
|
||||||
tools: data.tools ? JSON.stringify(data.tools) : '[]',
|
tools: data.tools ? JSON.stringify(data.tools) : '[]',
|
||||||
@@ -52,6 +56,8 @@ export async function createAgent(data: {
|
|||||||
scheduledTime: data.scheduledTime || '08:00',
|
scheduledTime: data.scheduledTime || '08:00',
|
||||||
scheduledDay: data.scheduledDay ?? null,
|
scheduledDay: data.scheduledDay ?? null,
|
||||||
timezone: data.timezone || null,
|
timezone: data.timezone || null,
|
||||||
|
slideTheme: data.slideTheme || null,
|
||||||
|
slideStyle: data.slideStyle || null,
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -88,6 +94,7 @@ export async function updateAgent(id: string, data: {
|
|||||||
role?: string
|
role?: string
|
||||||
sourceUrls?: string[]
|
sourceUrls?: string[]
|
||||||
sourceNotebookId?: string | null
|
sourceNotebookId?: string | null
|
||||||
|
sourceNoteIds?: string[] | null
|
||||||
targetNotebookId?: string | null
|
targetNotebookId?: string | null
|
||||||
frequency?: string
|
frequency?: string
|
||||||
isEnabled?: boolean
|
isEnabled?: boolean
|
||||||
@@ -98,6 +105,8 @@ export async function updateAgent(id: string, data: {
|
|||||||
scheduledTime?: string
|
scheduledTime?: string
|
||||||
scheduledDay?: number | null
|
scheduledDay?: number | null
|
||||||
timezone?: string
|
timezone?: string
|
||||||
|
slideTheme?: string | null
|
||||||
|
slideStyle?: string | null
|
||||||
}) {
|
}) {
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@@ -117,6 +126,7 @@ export async function updateAgent(id: string, data: {
|
|||||||
if (data.role !== undefined) updateData.role = data.role
|
if (data.role !== undefined) updateData.role = data.role
|
||||||
if (data.sourceUrls !== undefined) updateData.sourceUrls = JSON.stringify(data.sourceUrls)
|
if (data.sourceUrls !== undefined) updateData.sourceUrls = JSON.stringify(data.sourceUrls)
|
||||||
if (data.sourceNotebookId !== undefined) updateData.sourceNotebookId = data.sourceNotebookId
|
if (data.sourceNotebookId !== undefined) updateData.sourceNotebookId = data.sourceNotebookId
|
||||||
|
if (data.sourceNoteIds !== undefined) updateData.sourceNoteIds = data.sourceNoteIds ? JSON.stringify(data.sourceNoteIds) : null
|
||||||
if (data.targetNotebookId !== undefined) updateData.targetNotebookId = data.targetNotebookId
|
if (data.targetNotebookId !== undefined) updateData.targetNotebookId = data.targetNotebookId
|
||||||
if (data.frequency !== undefined) updateData.frequency = data.frequency
|
if (data.frequency !== undefined) updateData.frequency = data.frequency
|
||||||
if (data.isEnabled !== undefined) updateData.isEnabled = data.isEnabled
|
if (data.isEnabled !== undefined) updateData.isEnabled = data.isEnabled
|
||||||
@@ -127,6 +137,8 @@ export async function updateAgent(id: string, data: {
|
|||||||
if (data.scheduledTime !== undefined) updateData.scheduledTime = data.scheduledTime
|
if (data.scheduledTime !== undefined) updateData.scheduledTime = data.scheduledTime
|
||||||
if (data.scheduledDay !== undefined) updateData.scheduledDay = data.scheduledDay
|
if (data.scheduledDay !== undefined) updateData.scheduledDay = data.scheduledDay
|
||||||
if (data.timezone !== undefined) updateData.timezone = data.timezone
|
if (data.timezone !== undefined) updateData.timezone = data.timezone
|
||||||
|
if (data.slideTheme !== undefined) updateData.slideTheme = data.slideTheme
|
||||||
|
if (data.slideStyle !== undefined) updateData.slideStyle = data.slideStyle
|
||||||
|
|
||||||
// Recalculate nextRun when scheduling fields change
|
// Recalculate nextRun when scheduling fields change
|
||||||
const shouldRecalcNextRun =
|
const shouldRecalcNextRun =
|
||||||
@@ -220,21 +232,21 @@ export async function runAgent(id: string) {
|
|||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
throw new Error('Non autorise')
|
throw new Error('Non autorise')
|
||||||
}
|
}
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
try {
|
// Verify ownership
|
||||||
const { executeAgent } = await import('@/lib/ai/services/agent-executor.service')
|
const agent = await prisma.agent.findUnique({ where: { id }, select: { id: true, userId: true } })
|
||||||
const result = await executeAgent(id, session.user.id)
|
if (!agent || agent.userId !== userId) {
|
||||||
revalidatePath('/agents')
|
return { success: false, agentId: id, error: 'Agent introuvable' }
|
||||||
revalidatePath('/')
|
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error running agent:', error)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
actionId: '',
|
|
||||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire and forget — return immediately so the UI doesn't block
|
||||||
|
import('@/lib/ai/services/agent-executor.service')
|
||||||
|
.then(({ executeAgent }) => executeAgent(id, userId))
|
||||||
|
.then(() => { /* revalidation is handled client-side via polling */ })
|
||||||
|
.catch(err => console.error('[runAgent] Background error:', err))
|
||||||
|
|
||||||
|
return { success: true, agentId: id, status: 'running' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- History ---
|
// --- History ---
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* Compact card matching the reference design — with a "Next Run / Status" footer.
|
* Compact card matching the reference design — with a "Next Run / Status" footer.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
import { fr } from 'date-fns/locale/fr'
|
import { fr } from 'date-fns/locale/fr'
|
||||||
import { enUS } from 'date-fns/locale/en-US'
|
import { enUS } from 'date-fns/locale/en-US'
|
||||||
@@ -83,21 +83,51 @@ export function AgentCard({ agent, onEdit, onRefresh, onToggle }: AgentCardProps
|
|||||||
const dateLocale = language === 'fr' ? fr : enUS
|
const dateLocale = language === 'fr' ? fr : enUS
|
||||||
const isNew = Date.now() - new Date(agent.createdAt).getTime() < 5 * 60 * 1000
|
const isNew = Date.now() - new Date(agent.createdAt).getTime() < 5 * 60 * 1000
|
||||||
|
|
||||||
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||||
|
|
||||||
|
// Cleanup polling on unmount
|
||||||
|
useEffect(() => () => { if (pollRef.current) clearInterval(pollRef.current) }, [])
|
||||||
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
setIsRunning(true)
|
setIsRunning(true)
|
||||||
|
const toastId = toast.loading(
|
||||||
|
t('agents.toasts.running') || `Agent "${agent.name}" en cours…`,
|
||||||
|
{ description: t('agents.toasts.runningDesc') || 'La génération peut prendre quelques minutes.', duration: Infinity }
|
||||||
|
)
|
||||||
try {
|
try {
|
||||||
const { runAgent } = await import('@/app/actions/agent-actions')
|
const { runAgent } = await import('@/app/actions/agent-actions')
|
||||||
const result = await runAgent(agent.id)
|
const result = await runAgent(agent.id)
|
||||||
if (result.success) {
|
if (!result.success) {
|
||||||
toast.success(t('agents.toasts.runSuccess', { name: agent.name }))
|
toast.error(t('agents.toasts.runError', { error: result.error || t('agents.toasts.runFailed') }), { id: toastId })
|
||||||
} else {
|
|
||||||
toast.error(t('agents.toasts.runError', { error: result.error || t('agents.toasts.runFailed') }))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
toast.error(t('agents.toasts.runGenericError'))
|
|
||||||
} finally {
|
|
||||||
setIsRunning(false)
|
setIsRunning(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll status every 3 s until terminal state
|
||||||
|
if (pollRef.current) clearInterval(pollRef.current)
|
||||||
|
pollRef.current = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/agents/run-for-note?agentId=${agent.id}`)
|
||||||
|
const data = await res.json()
|
||||||
|
if (data.status === 'success') {
|
||||||
|
clearInterval(pollRef.current!)
|
||||||
|
pollRef.current = null
|
||||||
|
setIsRunning(false)
|
||||||
|
toast.success(t('agents.toasts.runSuccess', { name: agent.name }), { id: toastId, duration: 6000 })
|
||||||
onRefresh()
|
onRefresh()
|
||||||
|
} else if (data.status === 'failure') {
|
||||||
|
clearInterval(pollRef.current!)
|
||||||
|
pollRef.current = null
|
||||||
|
setIsRunning(false)
|
||||||
|
toast.error(t('agents.toasts.runError', { error: data.error || t('agents.toasts.runFailed') }), { id: toastId })
|
||||||
|
onRefresh()
|
||||||
|
}
|
||||||
|
} catch { /* network error — keep polling */ }
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
toast.error(t('agents.toasts.runGenericError'), { id: toastId })
|
||||||
|
setIsRunning(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1579,6 +1579,8 @@
|
|||||||
"updated": "Agent updated",
|
"updated": "Agent updated",
|
||||||
"deleted": "\"{name}\" deleted",
|
"deleted": "\"{name}\" deleted",
|
||||||
"deleteError": "Error deleting",
|
"deleteError": "Error deleting",
|
||||||
|
"running": "Generation in progress…",
|
||||||
|
"runningDesc": "Generation may take a few minutes. You can navigate freely.",
|
||||||
"runSuccess": "\"{name}\" executed successfully",
|
"runSuccess": "\"{name}\" executed successfully",
|
||||||
"runError": "Error: {error}",
|
"runError": "Error: {error}",
|
||||||
"runFailed": "Execution failed",
|
"runFailed": "Execution failed",
|
||||||
|
|||||||
@@ -1657,6 +1657,8 @@
|
|||||||
"updated": "Agent mis à jour",
|
"updated": "Agent mis à jour",
|
||||||
"deleted": "\"{name}\" supprimé",
|
"deleted": "\"{name}\" supprimé",
|
||||||
"deleteError": "Erreur lors de la suppression",
|
"deleteError": "Erreur lors de la suppression",
|
||||||
|
"running": "Génération en cours…",
|
||||||
|
"runningDesc": "La génération peut prendre quelques minutes. Vous pouvez naviguer.",
|
||||||
"runSuccess": "\"{name}\" exécuté avec succès",
|
"runSuccess": "\"{name}\" exécuté avec succès",
|
||||||
"runError": "Erreur : {error}",
|
"runError": "Erreur : {error}",
|
||||||
"runFailed": "Exécution échouée",
|
"runFailed": "Exécution échouée",
|
||||||
|
|||||||
Reference in New Issue
Block a user