diff --git a/client/src/api/fetch.ts b/client/src/api/fetch.ts index f2ec88713..3db83bdae 100644 --- a/client/src/api/fetch.ts +++ b/client/src/api/fetch.ts @@ -1,6 +1,6 @@ import { sseResponseEventEnum, TaskResponseKeyEnum } from '@/constants/chat'; import { getErrText } from '@/utils/tools'; -import { parseStreamChunk } from '@/utils/adapt'; +import { parseStreamChunk, SSEParseData } from '@/utils/sse'; import type { ChatHistoryItemResType } from '@/types/chat'; interface StreamFetchProps { @@ -43,6 +43,8 @@ export const streamFetch = ({ let errMsg = ''; let responseData: ChatHistoryItemResType[] = []; + const parseData = new SSEParseData(); + const read = async () => { try { const { done, value } = await reader.read(); @@ -63,21 +65,20 @@ export const streamFetch = ({ chunkResponse.forEach((item) => { // parse json data - const data = (() => { - try { - return JSON.parse(item.data); - } catch (error) { - return item.data; - } - })(); + const { eventName, data } = parseData.parse(item); - if (item.event === sseResponseEventEnum.answer && data !== '[DONE]') { + if (!eventName || !data) return; + + if (eventName === sseResponseEventEnum.answer && data !== '[DONE]') { const answer: string = data?.choices?.[0].delta.content || ''; onMessage(answer); responseText += answer; - } else if (item.event === sseResponseEventEnum.appStreamResponse) { + } else if ( + eventName === sseResponseEventEnum.appStreamResponse && + Array.isArray(data) + ) { responseData = data; - } else if (item.event === sseResponseEventEnum.error) { + } else if (eventName === sseResponseEventEnum.error) { errMsg = getErrText(data, '流响应错误'); } }); diff --git a/client/src/components/ChatBox/index.tsx b/client/src/components/ChatBox/index.tsx index e3e1c11ec..7337bbc9f 100644 --- a/client/src/components/ChatBox/index.tsx +++ b/client/src/components/ChatBox/index.tsx @@ -41,14 +41,14 @@ import { useGlobalStore } from '@/store/global'; import { FlowModuleTypeEnum } from '@/constants/flow'; import { TaskResponseKeyEnum } from '@/constants/chat'; -import dynamic from 'next/dynamic'; -const ResponseDetailModal = dynamic(() => import('./ResponseDetailModal')); - import MyIcon from '@/components/Icon'; import Avatar from '@/components/Avatar'; import Markdown from '@/components/Markdown'; import MySelect from '@/components/Select'; import MyTooltip from '../MyTooltip'; +import dynamic from 'next/dynamic'; +const ResponseDetailModal = dynamic(() => import('./ResponseDetailModal')); + import styles from './index.module.scss'; const textareaMinH = '22px'; diff --git a/client/src/pages/app/list/component/CreateModal.tsx b/client/src/pages/app/list/component/CreateModal.tsx index 36a834e17..87319a2a1 100644 --- a/client/src/pages/app/list/component/CreateModal.tsx +++ b/client/src/pages/app/list/component/CreateModal.tsx @@ -23,6 +23,7 @@ import { postCreateApp } from '@/api/app'; import { useRouter } from 'next/router'; import { appTemplates } from '@/constants/flow/ModuleTemplate'; import { useGlobalStore } from '@/store/global'; +import { useRequest } from '@/hooks/useRequest'; import Avatar from '@/components/Avatar'; import MyTooltip from '@/components/MyTooltip'; @@ -34,7 +35,6 @@ type FormType = { const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) => { const [refresh, setRefresh] = useState(false); - const [creating, setCreating] = useState(false); const { toast } = useToast(); const router = useRouter(); const theme = useTheme(); @@ -74,32 +74,22 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: ( [setValue, toast] ); - const onclickCreate = useCallback( - async (data: FormType) => { - setCreating(true); - try { - const id = await postCreateApp({ - avatar: data.avatar, - name: data.name, - modules: appTemplates.find((item) => item.id === data.templateId)?.modules || [] - }); - toast({ - title: '创建成功', - status: 'success' - }); - router.push(`/app/detail?appId=${id}`); - onClose(); - onSuccess(); - } catch (error) { - toast({ - title: getErrText(error, '创建应用异常'), - status: 'error' - }); - } - setCreating(false); + const { mutate: onclickCreate, isLoading: creating } = useRequest({ + mutationFn: async (data: FormType) => { + return postCreateApp({ + avatar: data.avatar, + name: data.name, + modules: appTemplates.find((item) => item.id === data.templateId)?.modules || [] + }); }, - [onClose, onSuccess, router, toast] - ); + onSuccess(id: string) { + router.push(`/app/detail?appId=${id}`); + onSuccess(); + onClose(); + }, + successToast: '创建成功', + errorToast: '创建应用异常' + }); return ( @@ -180,7 +170,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: ( - diff --git a/client/src/service/moduleDispatch/chat/oneapi.ts b/client/src/service/moduleDispatch/chat/oneapi.ts index 9b1f6fa45..bdf834dcf 100644 --- a/client/src/service/moduleDispatch/chat/oneapi.ts +++ b/client/src/service/moduleDispatch/chat/oneapi.ts @@ -7,7 +7,8 @@ import { ChatContextFilter } from '@/service/utils/chat/index'; import type { ChatItemType, QuoteItemType } from '@/types/chat'; import type { ChatHistoryItemResType } from '@/types/chat'; import { ChatModuleEnum, ChatRoleEnum, sseResponseEventEnum } from '@/constants/chat'; -import { parseStreamChunk, textAdaptGptResponse } from '@/utils/adapt'; +import { SSEParseData, parseStreamChunk } from '@/utils/sse'; +import { textAdaptGptResponse } from '@/utils/adapt'; import { getOpenAIApi, axiosConfig } from '@/service/ai/openai'; import { TaskResponseKeyEnum } from '@/constants/chat'; import { getChatModel } from '@/service/utils/data'; @@ -270,38 +271,28 @@ function getMaxTokens({ async function streamResponse({ res, response }: { res: NextApiResponse; response: any }) { let answer = ''; let error: any = null; - - const clientRes = async (data: string) => { - const { content = '' } = (() => { - try { - const json = JSON.parse(data); - const content: string = json?.choices?.[0].delta.content || ''; - error = json.error; - answer += content; - return { content }; - } catch (error) { - return {}; - } - })(); - - if (res.closed || error) return; - - if (data !== '[DONE]') { - sseResponse({ - res, - event: sseResponseEventEnum.answer, - data: textAdaptGptResponse({ - text: content - }) - }); - } - }; + const parseData = new SSEParseData(); try { for await (const chunk of response.data as any) { if (res.closed) break; const parse = parseStreamChunk(chunk); - parse.forEach((item) => clientRes(item.data)); + parse.forEach((item) => { + const { data } = parseData.parse(item); + if (!data || data === '[DONE]') return; + + const content: string = data?.choices?.[0].delta.content || ''; + error = data.error; + answer += content; + + sseResponse({ + res, + event: sseResponseEventEnum.answer, + data: textAdaptGptResponse({ + text: content + }) + }); + }); } } catch (error) { console.log('pipe error', error); diff --git a/client/src/store/chat.ts b/client/src/store/chat.ts index 6a0dbfb46..f51d9c359 100644 --- a/client/src/store/chat.ts +++ b/client/src/store/chat.ts @@ -83,15 +83,7 @@ export const useChatStore = create()( return [history, ...state.history]; } })(); - // newHistory.sort(function (a, b) { - // if (a.top === true && b.top === false) { - // return -1; - // } else if (a.top === false && b.top === true) { - // return 1; - // } else { - // return 0; - // } - // }); + state.history = newHistory; }); }, diff --git a/client/src/utils/adapt.ts b/client/src/utils/adapt.ts index 7eab12d9b..176a1b8c8 100644 --- a/client/src/utils/adapt.ts +++ b/client/src/utils/adapt.ts @@ -60,27 +60,6 @@ export const textAdaptGptResponse = ({ }); }; -const decoder = new TextDecoder(); -export const parseStreamChunk = (value: BufferSource) => { - const chunk = decoder.decode(value); - const chunkLines = chunk.split('\n\n').filter((item) => item); - const chunkResponse = chunkLines.map((item) => { - const splitEvent = item.split('\n'); - if (splitEvent.length === 2) { - return { - event: splitEvent[0].replace('event: ', ''), - data: splitEvent[1].replace('data: ', '') - }; - } - return { - event: '', - data: splitEvent[0].replace('data: ', '') - }; - }); - - return chunkResponse; -}; - export const appModule2FlowNode = ({ item, onChangeNode, diff --git a/client/src/utils/sse.ts b/client/src/utils/sse.ts new file mode 100644 index 000000000..c4aa808ae --- /dev/null +++ b/client/src/utils/sse.ts @@ -0,0 +1,55 @@ +const decoder = new TextDecoder(); + +export const parseStreamChunk = (value: BufferSource) => { + const chunk = decoder.decode(value); + const chunkLines = chunk.split('\n\n').filter((item) => item); + const chunkResponse = chunkLines.map((item) => { + const splitEvent = item.split('\n'); + if (splitEvent.length === 2) { + return { + event: splitEvent[0].replace('event: ', ''), + data: splitEvent[1].replace('data: ', '') + }; + } + return { + event: '', + data: splitEvent[0].replace('data: ', '') + }; + }); + + return chunkResponse; +}; + +export class SSEParseData { + storeReadData = ''; + storeEventName = ''; + + parse(item: { event: string; data: string }) { + if (item.data === '[DONE]') return { eventName: item.event, data: item.data }; + + if (item.event) { + this.storeEventName = item.event; + } + + try { + const formatData = this.storeReadData + item.data; + const parseData = JSON.parse(formatData); + const eventName = this.storeEventName; + + this.storeReadData = ''; + this.storeEventName = ''; + + return { + eventName, + data: parseData + }; + } catch (error) { + if (typeof item.data === 'string') { + this.storeReadData += item.data; + } else { + this.storeReadData = ''; + } + } + return {}; + } +}