Skip to content

Commit

Permalink
🚑 feat: add message smooth animation
Browse files Browse the repository at this point in the history
  • Loading branch information
rdmclin2 committed Jun 1, 2024
1 parent e4532f9 commit 35dfd8b
Showing 1 changed file with 124 additions and 23 deletions.
147 changes: 124 additions & 23 deletions src/utils/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,79 @@
import { APIErrorResponse, ErrorTypeEnum } from '@/types/api';
import { ChatMessageError } from '@/types/chat';

const createSmoothMessage = (params: { onTextUpdate: (delta: string, text: string) => void }) => {
let buffer = '';
// why use queue: https://shareg.pt/GLBrjpK
let outputQueue: string[] = [];

// eslint-disable-next-line no-undef
let animationTimeoutId: NodeJS.Timeout | null = null;
let isAnimationActive = false;

// when you need to stop the animation, call this function
const stopAnimation = () => {
isAnimationActive = false;
if (animationTimeoutId !== null) {
clearTimeout(animationTimeoutId);
animationTimeoutId = null;
}
};

// define startAnimation function to display the text in buffer smooth
// when you need to start the animation, call this function
const startAnimation = (speed = 2) =>
new Promise<void>((resolve) => {
if (isAnimationActive) {
resolve();
return;
}

isAnimationActive = true;

const updateText = () => {
// 如果动画已经不再激活,则停止更新文本
if (!isAnimationActive) {
clearTimeout(animationTimeoutId!);
animationTimeoutId = null;
resolve();
}

// 如果还有文本没有显示
// 检查队列中是否有字符待显示
if (outputQueue.length > 0) {
// 从队列中获取前两个字符(如果存在)
const charsToAdd = outputQueue.splice(0, speed).join('');
buffer += charsToAdd;

// 更新消息内容,这里可能需要结合实际情况调整
params.onTextUpdate(charsToAdd, buffer);

// 设置下一个字符的延迟
animationTimeoutId = setTimeout(updateText, 16); // 16 毫秒的延迟模拟打字机效果
} else {
// 当所有字符都显示完毕时,清除动画状态
isAnimationActive = false;
animationTimeoutId = null;
resolve();
}
};

updateText();
});

const pushToQueue = (text: string) => {
outputQueue.push(...text.split(''));
};

return {
isAnimationActive,
isTokenRemain: () => outputQueue.length > 0,
pushToQueue,
startAnimation,
stopAnimation,
};
};

const getMessageByErrorType = (errorType: ErrorTypeEnum) => {
const errorMap = {
API_KEY_MISSING: 'OpenAI API Key 为空,请添加自定义 OpenAI API Key',
Expand All @@ -15,40 +88,68 @@ const getMessageByErrorType = (errorType: ErrorTypeEnum) => {
export const fetchSEE = async (
fetcher: () => Promise<Response>,
handler: {
onAbort?: (text: string) => void;
onMessageError?: (error: ChatMessageError) => void;
onMessageUpdate?: (text: string) => void;
},
) => {
const res = await fetcher();
let output = '';

if (!res.ok) {
const data = (await res.json()) as APIErrorResponse;
const textController = createSmoothMessage({
onTextUpdate: (delta, text) => {
output = text;

handler.onMessageError?.({
body: data.body,
message: getMessageByErrorType(data.errorType),
type: data.errorType,
});
return;
}
handler.onMessageUpdate?.(delta);
},
});

const returnRes = res.clone();
try {
const res = await fetcher();

const data = res.body;
if (!res.ok) {
const data = (await res.json()) as APIErrorResponse;

if (!data) return;
handler.onMessageError?.({
body: data.body,
message: getMessageByErrorType(data.errorType),
type: data.errorType,
});
return;
}

const reader = data.getReader();
const decoder = new TextDecoder('utf8');
const returnRes = res.clone();

let done = false;
const data = res.body;

while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value, { stream: true });
handler.onMessageUpdate?.(chunkValue);
}
if (!data) return;

textController.stopAnimation();

return returnRes;
const reader = data.getReader();
const decoder = new TextDecoder('utf8');

let done = false;

while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value, { stream: true });
textController.pushToQueue(chunkValue);

if (textController.isTokenRemain()) {
await textController.startAnimation(15);
}

if (!textController.isAnimationActive) textController.startAnimation();
}

return returnRes;
} catch (error: any) {
if ((error as TypeError).name === 'AbortError') {
textController.stopAnimation();
handler.onAbort?.(output);
} else {
handler.onMessageError?.(error);
}
}
};

0 comments on commit 35dfd8b

Please sign in to comment.