fix(web): stream support abort

This commit is contained in:
zhaoying
2026-04-21 15:00:28 +08:00
parent c50969dea4
commit a2df14f658
12 changed files with 109 additions and 54 deletions

View File

@@ -53,12 +53,12 @@ export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => {
return request.put(`/apps/${app_id}/workflow`, values) return request.put(`/apps/${app_id}/workflow`, values)
} }
// Model comparison test run // Model comparison test run
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => { export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void, onAbort?: (abort: () => void) => void) => {
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage) return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage, undefined, onAbort)
} }
// Test run // Test run
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => { export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void, onAbort?: (abort: () => void) => void) => {
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage) return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage, undefined, onAbort)
} }
// Delete application // Delete application
export const deleteApplication = (app_id: string) => { export const deleteApplication = (app_id: string) => {
@@ -93,12 +93,12 @@ export const getConversationHistory = (share_token: string, data: { page: number
}) })
} }
// Send conversation // Send conversation
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => { export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string, onAbort?: (abort: () => void) => void) => {
return handleSSE(`/public/share/chat`, values, onMessage, { return handleSSE(`/public/share/chat`, values, onMessage, {
headers: { headers: {
'Authorization': `Bearer ${shareToken}` 'Authorization': `Bearer ${shareToken}`
} }
}) }, onAbort)
} }
// Get conversation details // Get conversation details
export const getConversationDetail = (share_token: string, conversation_id: string) => { export const getConversationDetail = (share_token: string, conversation_id: string) => {

View File

@@ -274,8 +274,8 @@ export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => {
return request.post('/memory-storage/update_config_extracted', values) return request.post('/memory-storage/update_config_extracted', values)
} }
// Memory Extraction Engine - Pilot run // Memory Extraction Engine - Pilot run
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; custom_text?: string; }, onMessage?: (data: SSEMessage[]) => void) => { export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; custom_text?: string; }, onMessage?: (data: SSEMessage[]) => void, onAbort?: (abort: () => void) => void) => {
return handleSSE('/memory-storage/pilot_run', values, onMessage) return handleSSE('/memory-storage/pilot_run', values, onMessage, undefined, onAbort)
} }
// Emotion Engine - Get configuration // Emotion Engine - Get configuration
export const getMemoryEmotionConfig = (config_id: number | string) => { export const getMemoryEmotionConfig = (config_id: number | string) => {

View File

@@ -14,8 +14,8 @@ export const createPromptSessions = () => {
return request.post(`/prompt/sessions`) return request.post(`/prompt/sessions`)
} }
// Get prompt optimization // Get prompt optimization
export const updatePromptMessages = (session_id: string, data: AiPromptForm, onMessage?: (data: SSEMessage[]) => void) => { export const updatePromptMessages = (session_id: string, data: AiPromptForm, onMessage?: (data: SSEMessage[]) => void, config?: any, onAbort?: (abort: () => void) => void) => {
return handleSSE(`/prompt/sessions/${session_id}/messages`, data, onMessage) return handleSSE(`/prompt/sessions/${session_id}/messages`, data, onMessage, config, onAbort)
} }
// Prompt release list // Prompt release list
export const getPromptReleaseListUrl = '/prompt/releases/list' export const getPromptReleaseListUrl = '/prompt/releases/list'

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-02 16:35:43 * @Date: 2026-02-02 16:35:43
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-18 14:32:40 * @Last Modified time: 2026-04-21 14:20:39
*/ */
/** /**
* Server-Sent Events (SSE) Stream Utility Module * Server-Sent Events (SSE) Stream Utility Module
@@ -148,7 +148,7 @@ function parseDataContent(dataContent: string): string | object {
* @param config - Additional request configuration * @param config - Additional request configuration
* @returns Fetch response * @returns Fetch response
*/ */
const makeSSERequest = async (url: string, data: any, token: string, config = { headers: {} }) => { const makeSSERequest = async (url: string, data: any, token: string, config = { headers: {} }, signal?: AbortSignal) => {
return fetch(`${API_PREFIX}${url}`, { return fetch(`${API_PREFIX}${url}`, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -156,7 +156,8 @@ const makeSSERequest = async (url: string, data: any, token: string, config = {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
...config.headers, ...config.headers,
}, },
body: JSON.stringify(data) body: JSON.stringify(data),
signal,
}); });
}; };
@@ -167,10 +168,14 @@ const makeSSERequest = async (url: string, data: any, token: string, config = {
* @param onMessage - Callback for each parsed message * @param onMessage - Callback for each parsed message
* @param config - Additional request configuration * @param config - Additional request configuration
*/ */
export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }) => { export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }, onAbort?: (abort: () => void) => void) => {
const controller = new AbortController();
const abort = () => controller.abort();
onAbort?.(abort);
try { try {
let token = cookieUtils.get('authToken'); let token = cookieUtils.get('authToken');
let response = await makeSSERequest(url, data, token || '', config); let response = await makeSSERequest(url, data, token || '', config, controller.signal);
switch (response.status) { switch (response.status) {
case 500: case 500:
@@ -199,7 +204,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
} }
try { try {
const newToken = await refreshTokenForSSE(); const newToken = await refreshTokenForSSE();
response = await makeSSERequest(url, data, newToken, config); response = await makeSSERequest(url, data, newToken, config, controller.signal);
} catch (refreshError) { } catch (refreshError) {
return; return;
} }
@@ -211,30 +216,37 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMe
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ''; // Buffer for handling incomplete messages let buffer = ''; // Buffer for handling incomplete messages
while (true) { try {
const { done, value } = await reader.read(); while (true) {
if (done) break; const { done, value } = await reader.read();
if (done || controller.signal.aborted) break;
const chunk = decoder.decode(value, { stream: true }); const chunk = decoder.decode(value, { stream: true });
buffer += chunk; buffer += chunk;
// Process complete events // Process complete events
const events = buffer.split('\n\n'); const events = buffer.split('\n\n');
buffer = events.pop() || ''; // Keep last potentially incomplete event buffer = events.pop() || ''; // Keep last potentially incomplete event
for (const event of events) { for (const event of events) {
if (event.trim() && onMessage) { if (event.trim() && onMessage) {
onMessage(parseSSEToJSON(event) ?? {}); onMessage(parseSSEToJSON(event) ?? {});
}
} }
} }
}
// Process remaining buffer content // Process remaining buffer content
if (buffer.trim() && onMessage) { if (!controller.signal.aborted && buffer.trim() && onMessage) {
onMessage(parseSSEToJSON(buffer) ?? {}); onMessage(parseSSEToJSON(buffer) ?? {});
}
} finally {
reader.cancel();
}
} catch (error: any) {
if (error?.name !== 'AbortError') {
console.error('Request failed:', error);
throw error;
} }
} catch (error) {
console.error('Request failed:', error);
throw error;
} }
}; };

View File

@@ -92,6 +92,7 @@ const TestChat: FC<TestChatProps> = ({
const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map()) const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
const streamLoadingRef = useRef(false) const streamLoadingRef = useRef(false)
const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({}) const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({})
const abortRef = useRef<(() => void) | null>(null)
useEffect(() => { useEffect(() => {
getVariables() getVariables()
@@ -99,6 +100,8 @@ const TestChat: FC<TestChatProps> = ({
useEffect(() => { useEffect(() => {
return () => { return () => {
abortRef.current?.()
abortRef.current = null
audioPollingRef.current.forEach(timer => clearInterval(timer)) audioPollingRef.current.forEach(timer => clearInterval(timer))
audioPollingRef.current.clear() audioPollingRef.current.clear()
} }
@@ -262,7 +265,8 @@ const TestChat: FC<TestChatProps> = ({
draftRun( draftRun(
application.id, application.id,
formatParams((msg || message) as string, conversationId, files, params), formatParams((msg || message) as string, conversationId, files, params),
handleStreamMessage handleStreamMessage,
(abort) => { abortRef.current = abort }
) )
.catch(() => { .catch(() => {
updateErrorAssistantMessage(0) updateErrorAssistantMessage(0)
@@ -373,7 +377,8 @@ const TestChat: FC<TestChatProps> = ({
draftRun( draftRun(
application.id, application.id,
formatParams((msg || message) as string, conversationId, files, params), formatParams((msg || message) as string, conversationId, files, params),
handleWorkflowStreamMessage handleWorkflowStreamMessage,
(abort) => { abortRef.current = abort }
) )
.catch((error) => { .catch((error) => {
const errorInfo = JSON.parse(error.message) const errorInfo = JSON.parse(error.message)

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:26:44 * @Date: 2026-02-03 16:26:44
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-20 13:53:05 * @Last Modified time: 2026-04-21 14:50:21
*/ */
/** /**
* AI Prompt Assistant Modal * AI Prompt Assistant Modal
@@ -61,11 +61,14 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
const aiPromptVariableModalRef = useRef<AiPromptVariableModalRef>(null) const aiPromptVariableModalRef = useRef<AiPromptVariableModalRef>(null)
const editorRef = useRef<any>(null) const editorRef = useRef<any>(null)
const currentPromptValueRef = useRef<string>('') const currentPromptValueRef = useRef<string>('')
const abortRef = useRef<(() => void) | null>(null)
const values = Form.useWatch([], form) const values = Form.useWatch([], form)
/** Close modal and reset state */ /** Close modal and reset state */
const handleClose = () => { const handleClose = () => {
abortRef.current?.()
abortRef.current = null
setVisible(false); setVisible(false);
setLoading(false) setLoading(false)
setChatList([]) setChatList([])
@@ -148,7 +151,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
updatePromptMessages(promptSession, { updatePromptMessages(promptSession, {
...values, ...values,
skill: source === 'skills' skill: source === 'skills'
}, handleStreamMessage) }, handleStreamMessage, undefined, abort => { abortRef.current = abort })
.finally(() => { .finally(() => {
setLoading(false) setLoading(false)
}) })
@@ -221,7 +224,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
</Form.Item> </Form.Item>
<ChatContent <ChatContent
classNames="rb:h-105.5 rb:pb-[15px]!" classNames="rb:h-[calc(100vh-330px)] rb:pb-[15px]!"
contentClassNames="rb:max-w-75!" contentClassNames="rb:max-w-75!"
empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />} empty={<Empty url={ConversationEmptyIcon} title={t(`${source}.promptChatEmpty`)} isNeedSubTitle={false} size={[140, 100]} className="rb:h-full" />}
data={chatList || []} data={chatList || []}
@@ -292,10 +295,10 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
{values?.current_prompt {values?.current_prompt
? <Editor ? <Editor
ref={editorRef} ref={editorRef}
className="rb:h-119 rb:bg-white! rb:border-none! rb:p-0!" className="rb:h-[calc(100vh-278px)] rb:bg-white! rb:border-none! rb:p-0!"
onChange={(value) => form.setFieldValue('current_prompt', value)} onChange={(value) => form.setFieldValue('current_prompt', value)}
/> />
: <Empty url={analysisEmptyIcon} title={t(`${source}.promptOptimizationEmpty`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-119 rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" /> : <Empty url={analysisEmptyIcon} title={t(`${source}.promptOptimizationEmpty`)} isNeedSubTitle={false} size={[270, 170]} className="rb:h-[calc(100vh-278px)] rb:w-70 rb:mx-auto! rb:text-center! rb:text-[12px]! rb:leading-4!" />
} }
</Form.Item> </Form.Item>
</div> </div>

View File

@@ -73,11 +73,14 @@ const Chat: FC<ChatProps> = ({
const [message, setMessage] = useState<string | undefined>(undefined) const [message, setMessage] = useState<string | undefined>(undefined)
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm) const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({}) const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({})
const abortRef = useRef<(() => void) | null>(null)
useEffect(() => { useEffect(() => {
setCompareLoading(false) setCompareLoading(false)
setLoading(false) setLoading(false)
return () => { return () => {
abortRef.current?.()
abortRef.current = null
audioPollingRef.current.forEach(timer => clearInterval(timer)) audioPollingRef.current.forEach(timer => clearInterval(timer))
audioPollingRef.current.clear() audioPollingRef.current.clear()
} }
@@ -85,6 +88,8 @@ const Chat: FC<ChatProps> = ({
useEffect(() => { useEffect(() => {
return () => { return () => {
abortRef.current?.()
abortRef.current = null
audioPollingRef.current.forEach(timer => clearInterval(timer)) audioPollingRef.current.forEach(timer => clearInterval(timer))
audioPollingRef.current.clear() audioPollingRef.current.clear()
} }
@@ -393,7 +398,7 @@ const Chat: FC<ChatProps> = ({
parallel: true, parallel: true,
stream: true, stream: true,
timeout: 60, timeout: 60,
}, handleStreamMessage) }, handleStreamMessage, (abort) => { abortRef.current = abort })
.catch(() => { .catch(() => {
setLoading(false) setLoading(false)
setCompareLoading(false) setCompareLoading(false)
@@ -537,7 +542,8 @@ const Chat: FC<ChatProps> = ({
} }
}), }),
}, },
handleStreamMessage handleStreamMessage,
(abort) => { abortRef.current = abort }
) )
.catch(() => { .catch(() => {
setLoading(false) setLoading(false)

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 16:58:03 * @Date: 2026-02-03 16:58:03
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-13 18:32:58 * @Last Modified time: 2026-04-21 14:27:15
*/ */
/** /**
* Conversation Page * Conversation Page
@@ -53,6 +53,7 @@ const Conversation: FC = () => {
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const toolbarRef = useRef<ChatToolbarRef>(null) const toolbarRef = useRef<ChatToolbarRef>(null)
const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map()) const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
const abortRef = useRef<(() => void) | null>(null)
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`)) const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
const [fileList, setFileList] = useState<any[]>([]) const [fileList, setFileList] = useState<any[]>([])
const [webSearch, setWebSearch] = useState(false) const [webSearch, setWebSearch] = useState(false)
@@ -67,6 +68,8 @@ const Conversation: FC = () => {
useEffect(() => { useEffect(() => {
return () => { return () => {
abortRef.current?.()
abortRef.current = null
audioPollingRef.current.forEach((timer) => clearInterval(timer)) audioPollingRef.current.forEach((timer) => clearInterval(timer))
audioPollingRef.current.clear() audioPollingRef.current.clear()
} }
@@ -150,6 +153,8 @@ const Conversation: FC = () => {
const handleChangeHistory = (id: string | null) => { const handleChangeHistory = (id: string | null) => {
if (id !== conversation_id) setConversationId(id) if (id !== conversation_id) setConversationId(id)
if (!id) setMessage('') if (!id) setMessage('')
abortRef.current?.()
abortRef.current = null
} }
useEffect(() => { useEffect(() => {
@@ -406,7 +411,7 @@ const Conversation: FC = () => {
}), }),
variables: params, variables: params,
thinking, thinking,
}, handleStreamMessage, shareToken) }, handleStreamMessage, shareToken, (abort) => { abortRef.current = abort })
.catch(() => { .catch(() => {
setLoading(false) setLoading(false)
streamLoadingRef.current = false streamLoadingRef.current = false

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 17:30:11 * @Date: 2026-02-03 17:30:11
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-26 15:46:30 * @Last Modified time: 2026-04-21 14:54:14
*/ */
/** /**
* Result Component * Result Component
@@ -10,7 +10,7 @@
* Shows text preprocessing, knowledge extraction, node/edge creation, and deduplication * Shows text preprocessing, knowledge extraction, node/edge creation, and deduplication
*/ */
import { type FC, useState } from 'react' import { type FC, useState, useRef, useEffect } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Space, Button, Progress, Form, Input, Flex } from 'antd' import { Space, Button, Progress, Form, Input, Flex } from 'antd'
@@ -105,7 +105,14 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
const [runForm] = Form.useForm() const [runForm] = Form.useForm()
const customText = Form.useWatch(['custom_text'], runForm) const customText = Form.useWatch(['custom_text'], runForm)
const abortRef = useRef<(() => void) | null>(null)
useEffect(() => {
return () => {
abortRef.current?.()
abortRef.current = null;
}
}, [])
/** Run pilot test */ /** Run pilot test */
const handleRun = () => { const handleRun = () => {
if(!id) return if(!id) return
@@ -229,11 +236,13 @@ const Result: FC<ResultProps> = ({ loading, handleSave }) => {
}) })
} }
setRunLoading(true) setRunLoading(true)
abortRef.current?.()
abortRef.current = null;
pilotRunMemoryExtractionConfig({ pilotRunMemoryExtractionConfig({
config_id: id, config_id: id,
dialogue_text: t('memoryExtractionEngine.exampleText'), dialogue_text: t('memoryExtractionEngine.exampleText'),
custom_text: runForm.getFieldValue('custom_text') custom_text: runForm.getFieldValue('custom_text')
}, handleStreamMessage) }, handleStreamMessage, (abort) => { abortRef.current = abort })
.finally(() => { .finally(() => {
setRunLoading(false) setRunLoading(false)
}) })

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 17:44:15 * @Date: 2026-02-03 17:44:15
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-03-27 15:14:58 * @Last Modified time: 2026-04-21 14:24:00
*/ */
/** /**
* Prompt Editor Component * Prompt Editor Component
@@ -46,9 +46,17 @@ const Prompt: FC = () => {
const promptSaveModalRef = useRef<PromptSaveModalRef>(null) const promptSaveModalRef = useRef<PromptSaveModalRef>(null)
const editorRef = useRef<any>(null) const editorRef = useRef<any>(null)
const currentPromptValueRef = useRef<string>(undefined) const currentPromptValueRef = useRef<string>(undefined)
const abortRef = useRef<(() => void) | null>(null)
const values = Form.useWatch([], form) const values = Form.useWatch([], form)
const [editVo, setEditVo] = useState<HistoryItem | null>(null) const [editVo, setEditVo] = useState<HistoryItem | null>(null)
useEffect(() => {
return () => {
abortRef.current?.()
abortRef.current = null
}
}, [])
useEffect(() => { useEffect(() => {
setEditVo(state) setEditVo(state)
}, [state]) }, [state])
@@ -126,7 +134,7 @@ const Prompt: FC = () => {
} }
}) })
}; };
updatePromptMessages((promptSession) as string, values, handleStreamMessage) updatePromptMessages((promptSession) as string, values, handleStreamMessage, undefined, (abort) => { abortRef.current = abort })
.finally(() => { .finally(() => {
setLoading(false) setLoading(false)
}) })

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-06 21:10:56 * @Date: 2026-02-06 21:10:56
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-15 15:57:35 * @Last Modified time: 2026-04-21 14:59:13
*/ */
/** /**
* Workflow Chat Component * Workflow Chat Component
@@ -51,6 +51,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
const { setChatHistory } = useWorkflowStore() const { setChatHistory } = useWorkflowStore()
const conversationIdRef = useRef<string>('draft') const conversationIdRef = useRef<string>('draft')
const toolbarRef = useRef<ChatToolbarRef>(null) const toolbarRef = useRef<ChatToolbarRef>(null)
const abortRef = useRef<(() => void) | null>(null)
const [toolbarReady, setToolbarReady] = useState(false) const [toolbarReady, setToolbarReady] = useState(false)
const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => { const toolbarCallbackRef = useCallback((node: ChatToolbarRef | null) => {
(toolbarRef as React.MutableRefObject<ChatToolbarRef | null>).current = node (toolbarRef as React.MutableRefObject<ChatToolbarRef | null>).current = node
@@ -65,6 +66,8 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
const [fileList, setFileList] = useState<any[]>([]) const [fileList, setFileList] = useState<any[]>([])
const [message, setMessage] = useState<string | undefined>(undefined) const [message, setMessage] = useState<string | undefined>(undefined)
console.log('abortRef', abortRef)
/** /**
* Opens the chat drawer and loads workflow variables from the start node * Opens the chat drawer and loads workflow variables from the start node
*/ */
@@ -116,6 +119,8 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
* Closes the drawer and resets all state * Closes the drawer and resets all state
*/ */
const handleClose = () => { const handleClose = () => {
abortRef.current?.()
abortRef.current = null;
setOpen(false) setOpen(false)
setToolbarReady(false) setToolbarReady(false)
setChatList([]) setChatList([])
@@ -395,7 +400,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
]) ])
setLoading(true) setLoading(true)
setStreamLoading(true) setStreamLoading(true)
draftRun(appId, data, handleStreamMessage) draftRun(appId, data, handleStreamMessage, abort => { abortRef.current = abort })
.catch((error) => { .catch((error) => {
const errorInfo = JSON.parse(error.message) const errorInfo = JSON.parse(error.message)
setChatList(prev => { setChatList(prev => {

View File

@@ -2,7 +2,7 @@
* @Author: ZhaoYing * @Author: ZhaoYing
* @Date: 2026-02-03 15:39:59 * @Date: 2026-02-03 15:39:59
* @Last Modified by: ZhaoYing * @Last Modified by: ZhaoYing
* @Last Modified time: 2026-04-13 10:44:19 * @Last Modified time: 2026-04-21 14:15:33
*/ */
import { type FC, useEffect, useState, useMemo } from "react"; import { type FC, useEffect, useState, useMemo } from "react";
import clsx from 'clsx' import clsx from 'clsx'
@@ -153,7 +153,9 @@ const Properties: FC<PropertiesProps> = ({
selectedNode?.setData({ selectedNode?.setData({
...nodeData, ...nodeData,
...allRest, ...allRest,
}) },
// { deep: false }
)
} }
}, [values, selectedNode, form]) }, [values, selectedNode, form])