fix: isolate canvas mode switches

This commit is contained in:
134 2026-06-23 23:15:35 +08:00
parent 3f8219f415
commit d4936a8ca4

View File

@ -311,6 +311,14 @@ interface CanvasChatPanelProps {
type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd";
function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) {
if (mode === "clawd") return "Clawd";
if (mode === "video") return "视频";
if (mode === "detail") return "AI详情";
if (mode === "image") return "图像";
return "Agent";
}
type VideoStoryboardStreamData =
| {
stage: "detecting";
@ -1321,6 +1329,7 @@ export function CanvasChatPanel({
() => requestedChatSessionId || chatId
);
const activeChatIdRef = useRef<string>(requestedChatSessionId || chatId);
const activeMessagesRef = useRef<ChatMessage[]>([]);
const inFlightChatIdRef = useRef<string | null>(null);
const inFlightRawChatIdRef = useRef<string | null>(null);
const [chatSessions, setChatSessions] = useState<CanvasChatSession[]>([]);
@ -1528,6 +1537,29 @@ export function CanvasChatPanel({
);
}, [getSendGuard]);
const startNewChatSession = useCallback(
(options?: { title?: string; toastMessage?: string }) => {
const now = Date.now();
const newChatSessionId = `canvas-${crypto.randomUUID()}`;
const fallbackTitle = `对话 ${chatSessions.length + 1}`;
const newSession: CanvasChatSession = {
id: newChatSessionId,
title: options?.title || fallbackTitle,
createdAt: now,
updatedAt: now,
};
forceHistoryHydrationRef.current = null;
releaseSendGuard(activeChatId);
setChatSessions((prevSessions) => [newSession, ...prevSessions]);
setActiveChatId(newChatSessionId);
replaceChatSessionQuery(newChatSessionId);
toast.success(options?.toastMessage || "已新建聊天窗口");
return newChatSessionId;
},
[activeChatId, chatSessions.length, releaseSendGuard, replaceChatSessionQuery]
);
const handleTypewriterActiveChange = useCallback((typingKey: string, active: boolean) => {
setActiveTypingSections((prev) => {
if (active) {
@ -3479,6 +3511,21 @@ export function CanvasChatPanel({
: isPresetAgentSelection
? getToolbarItem("chatModePresetAgent").label
: "Agent";
const hasActiveChatContext = useCallback(() => {
const editorText = (textareaRef.current?.textContent || "")
.replace(/\u200b/g, " ")
.replace(/\s+/g, " ")
.trim();
const hasDetailSlotContent = detailSlots.some((slot) => Boolean(slot?.url));
return (
activeMessagesRef.current.length > 0 ||
Boolean(input.trim()) ||
Boolean(editorText) ||
attachments.length > 0 ||
Boolean(selectedSkillTagId) ||
hasDetailSlotContent
);
}, [attachments.length, detailSlots, input, selectedSkillTagId]);
const getComposerModeMenuItemClass = useCallback(
(mode: CanvasComposerMode) =>
cn(
@ -3801,6 +3848,58 @@ export function CanvasChatPanel({
toast.error("Clawd 未开启");
return;
}
if (mode === "agent") {
if (!effectiveAgentEnabled) {
return;
}
}
if (mode === "detail" && !canUseDetailGeneration) {
toast.error("AI详情生成功能未开启");
return;
}
const preferredImageModelId =
mode === "image"
? allModels.find(
(model) =>
model.id === lastImageModelId &&
model.id !== "auto" &&
isCanvasImageModel(model) &&
!model.isMultimodal
)?.id || firstImageModelId
: null;
if (mode === "image" && !preferredImageModelId) {
toast.error("当前没有可用的图像模型");
return;
}
const preferredVideoModelId =
mode === "video"
? allModels.find((model) => model.id === lastVideoModelId && isCanvasVideoModel(model))?.id ||
firstVideoModelId
: null;
if (mode === "video" && !preferredVideoModelId) {
toast.error("当前没有可用的视频模型");
return;
}
const isSwitchingMode = mode !== activeComposerMode;
if (
isSwitchingMode &&
(getSendGuard(activeChatId).inFlight || isClawdSubmitting || isDetailGenerating)
) {
toast.message("当前对话正在处理中,完成后再切换模式");
return;
}
if (isSwitchingMode && hasActiveChatContext()) {
const modeLabel = getCanvasComposerModeSessionLabel(mode);
startNewChatSession({
title: `${modeLabel} 对话`,
toastMessage: `已为 ${modeLabel} 新建聊天窗口`,
});
}
activeComposerModeRef.current = mode;
setComposerModeOverride(mode);
@ -3810,9 +3909,6 @@ export function CanvasChatPanel({
}
if (mode === "agent") {
if (!effectiveAgentEnabled) {
return;
}
const preferredAgentModelId =
allModels.find(
(model) =>
@ -3826,10 +3922,6 @@ export function CanvasChatPanel({
}
if (mode === "detail") {
if (!canUseDetailGeneration) {
toast.error("AI详情生成功能未开启");
return;
}
const detailModelId = effectiveAgentEnabled ? "auto" : "";
currentModelIdRef.current = detailModelId;
setSelectedModelId(detailModelId);
@ -3839,35 +3931,16 @@ export function CanvasChatPanel({
}
if (mode === "image") {
const preferredImageModelId =
allModels.find(
(model) =>
model.id === lastImageModelId &&
model.id !== "auto" &&
isCanvasImageModel(model) &&
!model.isMultimodal
)?.id || firstImageModelId;
if (!preferredImageModelId) {
toast.error("当前没有可用的图像模型");
return;
}
currentModelIdRef.current = preferredImageModelId;
setSelectedModelId(preferredImageModelId);
const nextImageModelId = preferredImageModelId || "";
currentModelIdRef.current = nextImageModelId;
setSelectedModelId(nextImageModelId);
setIsDetailGenerationMode(false);
return;
}
const preferredVideoModelId =
allModels.find((model) => model.id === lastVideoModelId && isCanvasVideoModel(model))?.id ||
firstVideoModelId;
if (!preferredVideoModelId) {
toast.error("当前没有可用的视频模型");
return;
}
currentModelIdRef.current = preferredVideoModelId;
setSelectedModelId(preferredVideoModelId);
const nextVideoModelId = preferredVideoModelId || "";
currentModelIdRef.current = nextVideoModelId;
setSelectedModelId(nextVideoModelId);
setIsDetailGenerationMode(false);
},
[
@ -3875,16 +3948,21 @@ export function CanvasChatPanel({
effectiveAgentEnabled,
isClawdModeEnabled,
allModels,
computedComposerMode,
activeChatId,
activeComposerMode,
firstImageModelId,
firstVideoModelId,
hasSelectedAgentPreset,
getSendGuard,
hasActiveChatContext,
isClawdSubmitting,
isDetailGenerating,
lastConversationModelId,
lastImageModelId,
isCanvasConversationOnlyModel,
isCanvasImageModel,
isCanvasVideoModel,
lastVideoModelId,
startNewChatSession,
]
);
@ -5262,6 +5340,10 @@ export function CanvasChatPanel({
},
});
useEffect(() => {
activeMessagesRef.current = messages;
}, [messages]);
useEffect(() => {
if (status === "ready") {
releaseSendGuard(activeChatId);
@ -10126,22 +10208,8 @@ export function CanvasChatPanel({
);
const createNewChatSession = useCallback(() => {
const now = Date.now();
const newChatSessionId = `canvas-${crypto.randomUUID()}`;
const fallbackTitle = `对话 ${chatSessions.length + 1}`;
const newSession: CanvasChatSession = {
id: newChatSessionId,
title: fallbackTitle,
createdAt: now,
updatedAt: now,
};
releaseSendGuard(activeChatId);
setChatSessions((prevSessions) => [newSession, ...prevSessions]);
setActiveChatId(newChatSessionId);
replaceChatSessionQuery(newChatSessionId);
toast.success("已新建聊天窗口");
}, [activeChatId, chatSessions.length, releaseSendGuard, replaceChatSessionQuery]);
startNewChatSession();
}, [startNewChatSession]);
const switchToSession = useCallback((sessionId: string) => {
if (!sessionId || sessionId === activeChatId) {