fix: sse render

This commit is contained in:
archer
2023-07-25 09:52:23 +08:00
parent 6fc6c99477
commit 3adb97b396
7 changed files with 107 additions and 99 deletions

View File

@@ -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, '流响应错误');
}
});

View File

@@ -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';

View File

@@ -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 (
<Modal isOpen onClose={onClose} isCentered={!isPc}>
@@ -180,7 +170,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
<Button variant={'base'} mr={3} onClick={onClose}>
</Button>
<Button isLoading={creating} onClick={handleSubmit(onclickCreate)}>
<Button isLoading={creating} onClick={handleSubmit((data) => onclickCreate(data))}>
</Button>
</ModalFooter>

View File

@@ -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);

View File

@@ -83,15 +83,7 @@ export const useChatStore = create<State>()(
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;
});
},

View File

@@ -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,

55
client/src/utils/sse.ts Normal file
View File

@@ -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 {};
}
}