diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 759a3b92e..1494e029d 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -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(requestedChatSessionId || chatId); + const activeMessagesRef = useRef([]); const inFlightChatIdRef = useRef(null); const inFlightRawChatIdRef = useRef(null); const [chatSessions, setChatSessions] = useState([]); @@ -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) {