Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pro
Submodule pro updated from 1a56df to dccbea
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const ChatBox = ({
const questionGuideController = useRef(new AbortController());
const pluginController = useRef(new AbortController());
const resumeController = useRef<AbortController>();
const resumedChatIdRef = useRef<string>();
const resumedChatTargetRef = useRef<string>();

const [isLoading, setIsLoading] = useState(false);
const [feedbackId, setFeedbackId] = useState<string>();
Expand All @@ -180,6 +180,8 @@ const ChatBox = ({

const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId);
const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId);
const activeAppIdRef = useRef<string | undefined>(appId);
activeAppIdRef.current = appId;
const activeChatIdRef = useRef<string | undefined>(chatId);
activeChatIdRef.current = chatId;
const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData);
Expand All @@ -198,18 +200,21 @@ const ChatBox = ({
const syncSidebarChatGenerateStatus = useMemoizedFn(
(
status: ChatGenerateStatusEnum,
options?: { hasBeenRead?: boolean; targetChatId?: string }
options?: { hasBeenRead?: boolean; targetAppId?: string; targetChatId?: string }
) => {
const targetAppId = options?.targetAppId ?? appId;
if (targetAppId !== appId) return;

const targetChatId = options?.targetChatId ?? chatId;
if (!targetChatId) return;
setHistories((prev) => {
const idx = prev.findIndex((h) => h.chatId === targetChatId);
const idx = prev.findIndex((h) => h.chatId === targetChatId && h.appId === targetAppId);
if (idx === -1) {
queueMicrotask(loadHistories);
return [
{
chatId: targetChatId,
appId,
appId: targetAppId,
title: chatBoxData.title || t('common:core.chat.New Chat'),
customTitle: '',
top: false,
Expand All @@ -221,7 +226,7 @@ const ChatBox = ({
];
}
return prev.map((h) =>
h.chatId === targetChatId
h.chatId === targetChatId && h.appId === targetAppId
? {
...h,
chatGenerateStatus: status,
Expand Down Expand Up @@ -649,6 +654,12 @@ const ChatBox = ({
resumeController.current?.abort(new Error(reason));
});

useEffect(() => {
return () => {
abortRequest('leave');
};
}, [abortRequest]);

const hasMeaningfulAiOutput = useMemoizedFn((chat?: ChatSiteItemType) => {
if (!chat || chat.obj !== ChatRoleEnum.AI) return false;
if (chat.responseData?.length) return true;
Expand All @@ -662,16 +673,26 @@ const ChatBox = ({
});
});

const isActiveResumeTarget = useMemoizedFn(
({ appId, chatId }: { appId: string; chatId: string }) =>
activeAppIdRef.current === appId && activeChatIdRef.current === chatId
);

const getResumeUnavailablePlaceholderText = useMemoizedFn(() =>
t('chat:resume_placeholder_generating')
);

const upsertResumeAiPlaceholder = useMemoizedFn(
(responseChatId: string, text = '', status: `${ChatStatusEnum}` = ChatStatusEnum.loading) => {
(
responseChatId: string,
text = '',
status: `${ChatStatusEnum}` = ChatStatusEnum.loading,
options?: { resetExistingValue?: boolean }
) => {
setChatRecords((state) => {
const lastItem = state[state.length - 1];
if (lastItem?.dataId === responseChatId && lastItem.obj === ChatRoleEnum.AI) {
if (!text) {
if (!text && !options?.resetExistingValue) {
return state;
}

Expand All @@ -687,6 +708,7 @@ const ChatBox = ({
}
}
],
responseData: options?.resetExistingValue ? [] : item.responseData,
status,
...(status === ChatStatusEnum.finish ? { time: new Date() } : {})
}
Expand Down Expand Up @@ -833,7 +855,7 @@ const ChatBox = ({
}
];

resumedChatIdRef.current = chatId;
resumedChatTargetRef.current = `${appId}:${chatId}`;

setChatBoxData((state) =>
state.chatId === chatId
Expand Down Expand Up @@ -1255,7 +1277,7 @@ const ChatBox = ({
useEffect(() => {
setQuestionGuide([]);
setValue('chatStarted', false);
resumedChatIdRef.current = undefined;
resumedChatTargetRef.current = undefined;
abortRequest('leave');
}, [chatId, appId, abortRequest, setValue]);

Expand All @@ -1267,20 +1289,24 @@ const ChatBox = ({
!appId ||
!chatId ||
isChatting ||
chatBoxData.appId !== appId ||
chatBoxData.chatId !== chatId ||
chatBoxData.chatGenerateStatus !== ChatGenerateStatusEnum.generating ||
resumedChatIdRef.current === chatId
resumedChatTargetRef.current === `${appId}:${chatId}`
) {
return;
}

resumedChatIdRef.current = chatId;
resumedChatTargetRef.current = `${appId}:${chatId}`;

const resumeForAppId = appId;
const resumeForChatId = chatId;
const responseChatId = resumeTargetAiDataId ?? getNanoid(24);
const controller = new AbortController();
resumeController.current = controller;
scrollToBottom('auto');
let resumeFinalStatus = ChatGenerateStatusEnum.done;
let hasPreparedResumeAiRecord = false;

(async () => {
try {
Expand All @@ -1290,7 +1316,7 @@ const ChatBox = ({
outLinkAuthData,
controller,
onResumeUnavailable: () => {
if (resumeForChatId !== activeChatIdRef.current) return;
if (!isActiveResumeTarget({ appId: resumeForAppId, chatId: resumeForChatId })) return;
resumeFinalStatus = ChatGenerateStatusEnum.generating;
upsertResumeAiPlaceholder(
responseChatId,
Expand All @@ -1299,15 +1325,18 @@ const ChatBox = ({
);
},
onmessage: (message) => {
if (resumeForChatId !== activeChatIdRef.current) return;
if (!isActiveResumeTarget({ appId: resumeForAppId, chatId: resumeForChatId })) return;
if (shouldCreateResumeAiPlaceholder(message.event)) {
upsertResumeAiPlaceholder(responseChatId);
upsertResumeAiPlaceholder(responseChatId, '', ChatStatusEnum.loading, {
resetExistingValue: !hasPreparedResumeAiRecord
});
hasPreparedResumeAiRecord = true;
}
generatingMessage(message);
}
});

if (resumeForChatId !== activeChatIdRef.current) return;
if (!isActiveResumeTarget({ appId: resumeForAppId, chatId: resumeForChatId })) return;

if (completedChat) {
resumeFinalStatus = completedChat.chatGenerateStatus;
Expand Down Expand Up @@ -1362,7 +1391,7 @@ const ChatBox = ({
});
} catch (error) {
if (controller.signal.aborted) return;
if (resumeForChatId !== activeChatIdRef.current) return;
if (!isActiveResumeTarget({ appId: resumeForAppId, chatId: resumeForChatId })) return;

const isStreamError = (error as ResumeStreamErrorType | undefined)?.isStreamError === true;
resumeFinalStatus = isStreamError
Expand Down Expand Up @@ -1408,7 +1437,10 @@ const ChatBox = ({
}
} finally {
resumeController.current = undefined;
const finishedInActiveChat = activeChatIdRef.current === resumeForChatId;
const finishedInActiveChat = isActiveResumeTarget({
appId: resumeForAppId,
chatId: resumeForChatId
});
const leftWhileResuming =
controller.signal.aborted && isAbortByLeave(controller.signal.reason);

Expand All @@ -1417,7 +1449,7 @@ const ChatBox = ({
}

setChatBoxData((state) =>
state.chatId === resumeForChatId
state.appId === resumeForAppId && state.chatId === resumeForChatId
? {
...state,
chatGenerateStatus: resumeFinalStatus,
Expand All @@ -1428,19 +1460,21 @@ const ChatBox = ({

if (finishedInActiveChat) {
void postMarkChatRead({
appId,
appId: resumeForAppId,
chatId: resumeForChatId,
...outLinkAuthData
})
.catch(() => {})
.finally(() => {
syncSidebarChatGenerateStatus(resumeFinalStatus, {
targetAppId: resumeForAppId,
hasBeenRead: true,
targetChatId: resumeForChatId
});
});
} else {
syncSidebarChatGenerateStatus(resumeFinalStatus, {
targetAppId: resumeForAppId,
hasBeenRead: false,
targetChatId: resumeForChatId
});
Expand All @@ -1454,9 +1488,12 @@ const ChatBox = ({
appId,
chatId,
isChatting,
chatBoxData.appId,
chatBoxData.chatId,
chatBoxData.chatGenerateStatus,
generatingMessage,
hasMeaningfulAiOutput,
isActiveResumeTarget,
getResumeUnavailablePlaceholderText,
outLinkAuthData,
resumeTargetAiDataId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ const AppChatWindow = () => {
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);

const isCurrentChatReady = chatBoxData.appId === appId && chatBoxData.chatId === chatId;

const pane = useContextSelector(ChatPageContext, (v) => v.pane);
const chatSettings = useContextSelector(ChatPageContext, (v) => v.chatSettings);
const handlePaneChange = useContextSelector(ChatPageContext, (v) => v.handlePaneChange);
Expand Down Expand Up @@ -192,7 +194,7 @@ const AppChatWindow = () => {
<ChatBox
appId={appId}
chatId={chatId}
isReady={!loading && !!appId}
isReady={!loading && !!appId && isCurrentChatReady}
enableAutoResume
feedbackType={'user'}
chatType={ChatTypeEnum.chat}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ const HomeChatWindow = () => {
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);

const isCurrentChatReady = chatBoxData.appId === appId && chatBoxData.chatId === chatId;

const isQuickApp = useMemo(
() => chatSettings?.quickAppList.some((app) => app._id === appId),
[chatSettings?.quickAppList, appId]
Expand Down Expand Up @@ -463,7 +465,7 @@ const HomeChatWindow = () => {
<ChatBox
appId={appId}
chatId={chatId}
isReady={!loading && !!appId}
isReady={!loading && !!appId && isCurrentChatReady}
enableAutoResume
feedbackType={'user'}
chatType={ChatTypeEnum.home}
Expand Down
31 changes: 25 additions & 6 deletions projects/app/src/web/common/api/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,11 @@ function $resumefetch({ url, onmessage, onResumeUnavailable, controller }: Resum
}
return resolve({ responseText, completedChat, resumeUnavailable });
};
const onAbort = () => {
finished = true;
responseQueue = [];
return onfinish();
};
const onfailed = (err?: any) => {
finished = true;
const message = getErrText(err, error ?? '响应过程出现异常~');
Expand All @@ -424,8 +429,7 @@ function $resumefetch({ url, onmessage, onResumeUnavailable, controller }: Resum

function animateResponseLoop() {
if (signal.aborted) {
responseQueue.forEach(applyMessageItem);
return onfinish();
return onAbort();
}

if (responseQueue.length > 0) {
Expand All @@ -448,6 +452,8 @@ function $resumefetch({ url, onmessage, onResumeUnavailable, controller }: Resum
animateResponseLoop();

const enqueue = (data: ResponseQueueItemType) => {
if (signal.aborted) return;

if (resumePhase === StreamResumePhaseEnum.catchup) {
applyMessageItem(data);
return;
Expand Down Expand Up @@ -485,6 +491,8 @@ function $resumefetch({ url, onmessage, onResumeUnavailable, controller }: Resum
}
},
onmessage: ({ event, data }) => {
if (signal.aborted) return;

if (event === StreamResumePhaseEvent) {
if (data === StreamResumePhaseEnum.catchup || data === StreamResumePhaseEnum.live) {
resumePhase = data;
Expand Down Expand Up @@ -546,8 +554,7 @@ function $resumefetch({ url, onmessage, onResumeUnavailable, controller }: Resum
clearTimeout(timer);

if (controller.signal.aborted) {
finished = true;
return;
return onAbort();
}

onfailed(err);
Expand Down Expand Up @@ -601,7 +608,10 @@ type StreamResumeFetchParams = {
onResumeUnavailable?: (data: ResumeUnavailableType) => void;
controller: AbortController;
};
export function streamResumeFetch(params: StreamResumeFetchParams) {

let activeResumeController: AbortController | undefined;

export async function streamResumeFetch(params: StreamResumeFetchParams) {
const { appId, chatId, outLinkAuthData, onmessage, onResumeUnavailable, controller } = params;
const query = new URLSearchParams({ appId, chatId });

Expand All @@ -612,7 +622,16 @@ export function streamResumeFetch(params: StreamResumeFetchParams) {

const url = `/api/core/chat/resume?${query}`;

return $resumefetch({ url, onmessage, onResumeUnavailable, controller });
if (activeResumeController && activeResumeController !== controller) {
activeResumeController.abort('replace');
}
activeResumeController = controller;

return $resumefetch({ url, onmessage, onResumeUnavailable, controller }).finally(() => {
if (activeResumeController === controller) {
activeResumeController = undefined;
}
});
}

export const onOptimizePrompt = async ({
Expand Down
Loading