feat(agentflow): add chat panel for real-time flow testing#6316
feat(agentflow): add chat panel for real-time flow testing#6316j-sanaa wants to merge 2 commits intofeat/agentflow-executionfrom
Conversation
Add TestFlowDialog — an SSE-driven chat panel for testing agentflows. Messages stream in real time using @microsoft/fetch-event-source, with node execution status badges updating live as the flow runs. Key additions: - TestFlowDialog: streaming chat, HITL action buttons, history load - TestFlowButton container: FAB with expand/clear controls - StyledFab moved from node-palette to atoms (shared use) - Agentflow.tsx: flowId prop + isChatOpen to hide toolbar while testing - chatflows.ts: abortMessage for stopping in-progress streams - jest mocks for react-markdown, remark-gfm, uuid, and image assets - vite proxy config fixed for cross-origin dev Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a chat-based testing interface for Agentflow, enabling real-time interaction with flows through a new floating action button. Key additions include the TestFlowButton and TestFlowDialog components, which leverage Server-Sent Events (SSE) for streaming responses and include support for chat history retrieval and message abortion. The build and test configurations have been updated with necessary proxies, dependencies (like react-markdown), and Jest mocks. Review feedback highlights the need to reset history state when the chatflowId changes to prevent stale data, the addition of a missing onClick handler for the 'Expand Chat' button, and the inclusion of handleSendCore in several useCallback dependency arrays to avoid stale closures.
| useEffect(() => { | ||
| if (!open || historyLoaded) return | ||
|
|
||
| async function loadHistory() { | ||
| try { | ||
| const page = await executionsApi.getAllExecutions({ agentflowId: chatflowId, limit: HISTORY_LIMIT }) | ||
| const executions = page.data | ||
| if (!executions?.length) return | ||
|
|
||
| // Restore the most recent session's chatId so the user continues it | ||
| const latestSessionId = executions[0]?.sessionId | ||
| if (latestSessionId) chatIdRef.current = latestSessionId | ||
|
|
||
| const historyMessages: ChatMessage[] = [] | ||
| // executions arrive newest-first; reverse to show oldest first | ||
| for (const execution of [...executions].reverse()) { | ||
| let executionData: ExecutionDataItem[] = [] | ||
| try { | ||
| executionData = | ||
| typeof execution.executionData === 'string' ? (JSON.parse(execution.executionData) as ExecutionDataItem[]) : [] | ||
| } catch { | ||
| continue | ||
| } | ||
|
|
||
| const startNode = executionData.find((e) => e.data?.name === 'startAgentflow') | ||
| const question = (startNode?.data?.input?.question ?? startNode?.data?.output?.question) as string | undefined | ||
|
|
||
| const lastWithContent = [...executionData].reverse().find((e) => e.data?.output?.content || e.data?.output?.question) | ||
| const answer = (lastWithContent?.data?.output?.content ?? lastWithContent?.data?.output?.question) as string | undefined | ||
|
|
||
| if (question) { | ||
| historyMessages.push({ id: `${execution.id}-user`, type: 'user', text: question }) | ||
| if (answer && answer !== question) { | ||
| historyMessages.push({ id: `${execution.id}-bot`, type: 'bot', text: answer }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (historyMessages.length > 0) { | ||
| setMessages((prev) => [...prev, ...historyMessages]) | ||
| } | ||
| } catch { | ||
| // history load is best-effort | ||
| } finally { | ||
| setHistoryLoaded(true) | ||
| } | ||
| } | ||
|
|
||
| void loadHistory() | ||
| }, [open, chatflowId, executionsApi, historyLoaded]) |
There was a problem hiding this comment.
The chat history loading logic does not account for changes to chatflowId while the component remains mounted. If the user switches to a different flow, historyLoaded will still be true, preventing the new flow's history from loading. Reset the state in the main useEffect body when chatflowId changes to avoid stale data. Additionally, ensure that any API failures during history retrieval are handled with user-facing feedback rather than just console logging.
useEffect(() => {
setHistoryLoaded(false)
setMessages([{ id: uuidv4(), type: 'bot', text: WELCOME_MESSAGE }])
chatIdRef.current = uuidv4()
}, [chatflowId])
useEffect(() => {
if (!open || historyLoaded) return
References
- Avoid calling setState in a useEffect cleanup function for components that do not unmount. Reset state in the main useEffect body instead.
- Implement user-facing error handling for all API calls to provide feedback to the user when requests fail, rather than just logging errors to the console.
| <StyledFab size='small' color='primary' aria-label='expand' title='Expand Chat'> | ||
| <IconArrowsMaximize /> | ||
| </StyledFab> |
| const handleActionClick = useCallback( | ||
| (elem: ActionElement, action: ActionPayload) => { | ||
| // Clear the action buttons from the last bot message | ||
| setMessages((prev) => { | ||
| const last = prev[prev.length - 1] | ||
| if (!last || last.type !== 'bot') return prev | ||
| return [...prev.slice(0, -1), { ...last, action: undefined }] | ||
| }) | ||
| void handleSendCore(elem.label, action) | ||
| }, | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| [] | ||
| ) |
There was a problem hiding this comment.
The handleActionClick callback is missing handleSendCore in its dependency array. Since handleSendCore is created via useCallback and depends on external state (like token), omitting it can lead to stale closures if those dependencies change. Please include handleSendCore in the dependency array and remove the lint suppression.
const handleActionClick = useCallback(
(elem: ActionElement, action: ActionPayload) => {
// Clear the action buttons from the last bot message
setMessages((prev) => {
const last = prev[prev.length - 1]
if (!last || last.type !== 'bot') return prev
return [...prev.slice(0, -1), { ...last, action: undefined }]
})
void handleSendCore(elem.label, action)
},
[handleSendCore]
)
| const handleSend = useCallback(() => { | ||
| const input = userInput.trim() | ||
| if (!input || loading) return | ||
| setFollowUpPrompts([]) | ||
| void handleSendCore(input) | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [userInput, loading]) |
There was a problem hiding this comment.
The handleSend callback is missing handleSendCore in its dependency array. This can cause the function to use a stale version of the core sending logic if the component re-renders with updated context values. Please add handleSendCore to the dependencies.
const handleSend = useCallback(() => {
const input = userInput.trim()
if (!input || loading) return
setFollowUpPrompts([])
void handleSendCore(input)
}, [userInput, loading, handleSendCore])
Add TestFlowDialog — an SSE-driven chat panel for testing agentflows. Messages stream in real time using @microsoft/fetch-event-source, with node execution status badges updating live as the flow runs.
Key additions: