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"; 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 = type VideoStoryboardStreamData =
| { | {
stage: "detecting"; stage: "detecting";
@ -1321,6 +1329,7 @@ export function CanvasChatPanel({
() => requestedChatSessionId || chatId () => requestedChatSessionId || chatId
); );
const activeChatIdRef = useRef<string>(requestedChatSessionId || chatId); const activeChatIdRef = useRef<string>(requestedChatSessionId || chatId);
const activeMessagesRef = useRef<ChatMessage[]>([]);
const inFlightChatIdRef = useRef<string | null>(null); const inFlightChatIdRef = useRef<string | null>(null);
const inFlightRawChatIdRef = useRef<string | null>(null); const inFlightRawChatIdRef = useRef<string | null>(null);
const [chatSessions, setChatSessions] = useState<CanvasChatSession[]>([]); const [chatSessions, setChatSessions] = useState<CanvasChatSession[]>([]);
@ -1528,6 +1537,29 @@ export function CanvasChatPanel({
); );
}, [getSendGuard]); }, [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) => { const handleTypewriterActiveChange = useCallback((typingKey: string, active: boolean) => {
setActiveTypingSections((prev) => { setActiveTypingSections((prev) => {
if (active) { if (active) {
@ -3479,6 +3511,21 @@ export function CanvasChatPanel({
: isPresetAgentSelection : isPresetAgentSelection
? getToolbarItem("chatModePresetAgent").label ? getToolbarItem("chatModePresetAgent").label
: "Agent"; : "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( const getComposerModeMenuItemClass = useCallback(
(mode: CanvasComposerMode) => (mode: CanvasComposerMode) =>
cn( cn(
@ -3801,6 +3848,58 @@ export function CanvasChatPanel({
toast.error("Clawd 未开启"); toast.error("Clawd 未开启");
return; 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; activeComposerModeRef.current = mode;
setComposerModeOverride(mode); setComposerModeOverride(mode);
@ -3810,9 +3909,6 @@ export function CanvasChatPanel({
} }
if (mode === "agent") { if (mode === "agent") {
if (!effectiveAgentEnabled) {
return;
}
const preferredAgentModelId = const preferredAgentModelId =
allModels.find( allModels.find(
(model) => (model) =>
@ -3826,10 +3922,6 @@ export function CanvasChatPanel({
} }
if (mode === "detail") { if (mode === "detail") {
if (!canUseDetailGeneration) {
toast.error("AI详情生成功能未开启");
return;
}
const detailModelId = effectiveAgentEnabled ? "auto" : ""; const detailModelId = effectiveAgentEnabled ? "auto" : "";
currentModelIdRef.current = detailModelId; currentModelIdRef.current = detailModelId;
setSelectedModelId(detailModelId); setSelectedModelId(detailModelId);
@ -3839,35 +3931,16 @@ export function CanvasChatPanel({
} }
if (mode === "image") { if (mode === "image") {
const preferredImageModelId = const nextImageModelId = preferredImageModelId || "";
allModels.find( currentModelIdRef.current = nextImageModelId;
(model) => setSelectedModelId(nextImageModelId);
model.id === lastImageModelId &&
model.id !== "auto" &&
isCanvasImageModel(model) &&
!model.isMultimodal
)?.id || firstImageModelId;
if (!preferredImageModelId) {
toast.error("当前没有可用的图像模型");
return;
}
currentModelIdRef.current = preferredImageModelId;
setSelectedModelId(preferredImageModelId);
setIsDetailGenerationMode(false); setIsDetailGenerationMode(false);
return; return;
} }
const preferredVideoModelId = const nextVideoModelId = preferredVideoModelId || "";
allModels.find((model) => model.id === lastVideoModelId && isCanvasVideoModel(model))?.id || currentModelIdRef.current = nextVideoModelId;
firstVideoModelId; setSelectedModelId(nextVideoModelId);
if (!preferredVideoModelId) {
toast.error("当前没有可用的视频模型");
return;
}
currentModelIdRef.current = preferredVideoModelId;
setSelectedModelId(preferredVideoModelId);
setIsDetailGenerationMode(false); setIsDetailGenerationMode(false);
}, },
[ [
@ -3875,16 +3948,21 @@ export function CanvasChatPanel({
effectiveAgentEnabled, effectiveAgentEnabled,
isClawdModeEnabled, isClawdModeEnabled,
allModels, allModels,
computedComposerMode, activeChatId,
activeComposerMode,
firstImageModelId, firstImageModelId,
firstVideoModelId, firstVideoModelId,
hasSelectedAgentPreset, getSendGuard,
hasActiveChatContext,
isClawdSubmitting,
isDetailGenerating,
lastConversationModelId, lastConversationModelId,
lastImageModelId, lastImageModelId,
isCanvasConversationOnlyModel, isCanvasConversationOnlyModel,
isCanvasImageModel, isCanvasImageModel,
isCanvasVideoModel, isCanvasVideoModel,
lastVideoModelId, lastVideoModelId,
startNewChatSession,
] ]
); );
@ -5262,6 +5340,10 @@ export function CanvasChatPanel({
}, },
}); });
useEffect(() => {
activeMessagesRef.current = messages;
}, [messages]);
useEffect(() => { useEffect(() => {
if (status === "ready") { if (status === "ready") {
releaseSendGuard(activeChatId); releaseSendGuard(activeChatId);
@ -10126,22 +10208,8 @@ export function CanvasChatPanel({
); );
const createNewChatSession = useCallback(() => { const createNewChatSession = useCallback(() => {
const now = Date.now(); startNewChatSession();
const newChatSessionId = `canvas-${crypto.randomUUID()}`; }, [startNewChatSession]);
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]);
const switchToSession = useCallback((sessionId: string) => { const switchToSession = useCallback((sessionId: string) => {
if (!sessionId || sessionId === activeChatId) { if (!sessionId || sessionId === activeChatId) {