From d8af4bb48df4e5ee886d0d24dcfb2fd0555dc967 Mon Sep 17 00:00:00 2001 From: 134 Date: Fri, 26 Jun 2026 18:18:43 +0800 Subject: [PATCH] feat: add Claw skill shortcuts and isolate mode chats --- app/api/canvas/clawd-chat/route.ts | 26 +- .../[id]/CanvasEditorLegacyPageClient.tsx | 27 +- components/canvas/CanvasChatPanel.tsx | 515 +++++++++++++++--- lib/canvas-skill-tags.ts | 136 +++++ 4 files changed, 626 insertions(+), 78 deletions(-) diff --git a/app/api/canvas/clawd-chat/route.ts b/app/api/canvas/clawd-chat/route.ts index 04e1a1479..ff281dd24 100644 --- a/app/api/canvas/clawd-chat/route.ts +++ b/app/api/canvas/clawd-chat/route.ts @@ -334,7 +334,10 @@ export async function POST(request: Request) { temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, - videoMetadata: null, + videoMetadata: { + composerMode: "clawd", + source: "openclaw", + }, }; const assistantMessage = { id: assistantMessageId, @@ -347,7 +350,14 @@ export async function POST(request: Request) { temporaryVideoUrl: null, permanentVideoUrl: null, archivedAt: null, - videoMetadata: null, + videoMetadata: { + composerMode: "clawd", + source: "openclaw", + requestId: + upstreamPayload && typeof upstreamPayload === "object" + ? (upstreamPayload as Record).request_id || null + : null, + }, }; await saveMessages({ messages: [userMessage, assistantMessage] }); @@ -364,14 +374,22 @@ export async function POST(request: Request) { role: "user", parts: userMessage.parts, createdAt: userMessage.createdAt.toISOString(), - metadata: { createdAt: userMessage.createdAt.toISOString() }, + metadata: { + createdAt: userMessage.createdAt.toISOString(), + composerMode: "clawd", + }, + videoMetadata: userMessage.videoMetadata, }, { id: assistantMessage.id, role: "assistant", parts: assistantMessage.parts, createdAt: assistantMessage.createdAt.toISOString(), - metadata: { createdAt: assistantMessage.createdAt.toISOString() }, + metadata: { + createdAt: assistantMessage.createdAt.toISOString(), + composerMode: "clawd", + }, + videoMetadata: assistantMessage.videoMetadata, }, ], }); diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index 600ef6f3d..7d517de98 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -1133,8 +1133,7 @@ function normalizeImportedCanvasState(payload: unknown): { } const FLOATING_PANEL_WIDTH = 400; -const FLOATING_PANEL_MAX_WIDTH = 760; -const FLOATING_PANEL_MIN_CANVAS_WIDTH = 520; +const FLOATING_PANEL_MAX_VIEWPORT_RATIO = 0.75; const FLOATING_PANEL_WIDTH_STORAGE_KEY = "canvas.chatPanel.width.v1"; const FLOATING_PANEL_INSET = 16; const DOCKED_PANEL_INSET = 0; @@ -1142,6 +1141,7 @@ const FLOATING_UI_GAP = 16; const FLOATING_BOTTOM_INSET = 20; const FLOATING_TOOLBAR_ESTIMATED_WIDTH = 680; const FLOATING_TOOLBAR_SIDE_DOCK_CLEARANCE = 48; +const FLOATING_TOOLBAR_SIDE_DOCK_VISIBLE_CANVAS_RATIO = 0.58; const WORKFLOW_AGENT_CONTEXT_START = "【视频画布同步上下文】"; const WORKFLOW_AGENT_CONTEXT_END = "【/视频画布同步上下文】"; const AGENT_RESULT_MODE_LABELS: Record = @@ -1154,17 +1154,19 @@ const AGENT_RESULT_MODE_LABELS: Record = function getMaxFloatingPanelWidth(viewportWidth: number) { if (viewportWidth <= 0) { - return FLOATING_PANEL_MAX_WIDTH; + return FLOATING_PANEL_WIDTH; } const viewportLimitedMax = Math.max( - FLOATING_PANEL_WIDTH, - viewportWidth - FLOATING_PANEL_MIN_CANVAS_WIDTH - DOCKED_PANEL_INSET + 0, + viewportWidth - DOCKED_PANEL_INSET ); + const ratioLimitedMax = + viewportLimitedMax * FLOATING_PANEL_MAX_VIEWPORT_RATIO; return Math.max( FLOATING_PANEL_WIDTH, - Math.min(FLOATING_PANEL_MAX_WIDTH, viewportLimitedMax) + Math.min(viewportLimitedMax, ratioLimitedMax) ); } @@ -3012,13 +3014,20 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { viewportWidth > 0 ? Math.max(viewportWidth - panelOcclusionRight, 0) : 0; const horizontalFloatingToolbarWidthForDocking = horizontalFloatingToolbarWidth || FLOATING_TOOLBAR_ESTIMATED_WIDTH; - const shouldDockFloatingToolbarToSide = - !isVideoMode && - isFloatingSidebarVisible && + const isFloatingToolbarWidthCrowded = visibleCanvasWidth > 0 && visibleCanvasWidth < horizontalFloatingToolbarWidthForDocking + FLOATING_TOOLBAR_SIDE_DOCK_CLEARANCE * 2; + const isFloatingToolbarCanvasCrowded = + viewportWidth > 0 && + visibleCanvasWidth > 0 && + visibleCanvasWidth / viewportWidth <= + FLOATING_TOOLBAR_SIDE_DOCK_VISIBLE_CANVAS_RATIO; + const shouldDockFloatingToolbarToSide = + !isVideoMode && + isFloatingSidebarVisible && + (isFloatingToolbarWidthCrowded || isFloatingToolbarCanvasCrowded); const floatingToolbarTooltipSide = shouldDockFloatingToolbarToSide ? "right" : "top"; diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 4255c8565..b05ee6812 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -326,6 +326,47 @@ function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) { return "Agent"; } +function normalizeCanvasComposerSessionMode( + value: unknown, + fallback: CanvasComposerMode = "image" +): CanvasComposerMode { + return value === "agent" || + value === "image" || + value === "video" || + value === "detail" || + value === "clawd" + ? value + : fallback; +} + +function getCanvasChatSessionMode( + session: Pick | null | undefined, + fallback: CanvasComposerMode = "image" +) { + if (!session) return fallback; + if (session.mode) { + return normalizeCanvasComposerSessionMode(session.mode, fallback); + } + return session.clawdSkillTagId ? "clawd" : fallback; +} + +function isClawdChatMessage(message: unknown) { + if (!message || typeof message !== "object") return false; + const raw = message as Record; + const metadata = + raw.metadata && typeof raw.metadata === "object" && !Array.isArray(raw.metadata) + ? raw.metadata + : {}; + const videoMetadata = + raw.videoMetadata && + typeof raw.videoMetadata === "object" && + !Array.isArray(raw.videoMetadata) + ? raw.videoMetadata + : {}; + + return metadata.composerMode === "clawd" || videoMetadata.composerMode === "clawd"; +} + const CLAWD_TABLE_ACCEPT = ".xlsx,.csv,.tsv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv,text/tab-separated-values"; const CLAWD_TABLE_ALLOWED_EXTENSIONS = new Set(["xlsx", "csv", "tsv"]); @@ -1600,6 +1641,7 @@ type MessageVote = { interface CanvasChatSession { id: string; title: string; + mode: CanvasComposerMode; createdAt: number; updatedAt: number; clawdSkillTagId?: string | null; @@ -1899,6 +1941,7 @@ export function CanvasChatPanel({ const [downloadProgress, setDownloadProgress] = useState(null); const [downloadingOriginalUrl, setDownloadingOriginalUrl] = useState(""); + const [addingClawdImageUrl, setAddingClawdImageUrl] = useState(""); const downloadAbortControllerRef = useRef(null); const fullscreenPreviewSrc = useMemo(() => { if (!fullscreenPreview?.url) return ""; @@ -2045,13 +2088,29 @@ export function CanvasChatPanel({ }, [getSendGuard]); const startNewChatSession = useCallback( - (options?: { title?: string; toastMessage?: string }) => { + (options?: { + mode?: CanvasComposerMode; + title?: string; + toastMessage?: string; + }) => { const now = Date.now(); const newChatSessionId = `canvas-${crypto.randomUUID()}`; - const fallbackTitle = `对话 ${chatSessions.length + 1}`; + const sessionMode = normalizeCanvasComposerSessionMode( + options?.mode, + "image" + ); + const sameModeSessionCount = chatSessions.filter( + (session) => getCanvasChatSessionMode(session) === sessionMode + ).length; + const modeLabel = getCanvasComposerModeSessionLabel(sessionMode); + const fallbackTitle = + sessionMode === "image" + ? `对话 ${sameModeSessionCount + 1}` + : `${modeLabel} 对话 ${sameModeSessionCount + 1}`; const newSession: CanvasChatSession = { id: newChatSessionId, title: options?.title || fallbackTitle, + mode: sessionMode, createdAt: now, updatedAt: now, }; @@ -2064,7 +2123,7 @@ export function CanvasChatPanel({ toast.success(options?.toastMessage || "已新建聊天窗口"); return newChatSessionId; }, - [activeChatId, chatSessions.length, releaseSendGuard, replaceChatSessionQuery] + [activeChatId, chatSessions, releaseSendGuard, replaceChatSessionQuery] ); const setSessionClawdSkillTag = useCallback( @@ -2390,6 +2449,7 @@ export function CanvasChatPanel({ const fallbackSession: CanvasChatSession = { id: rootChatId, title: "对话 1", + mode: "image", createdAt: now, updatedAt: now, }; @@ -2398,6 +2458,7 @@ export function CanvasChatPanel({ ? { id: requestedChatSessionId, title: "新对话", + mode: "image", createdAt: now, updatedAt: now, } @@ -2411,18 +2472,26 @@ export function CanvasChatPanel({ const sanitized: CanvasChatSession[] = parsed .filter((session) => session?.id) - .map((session): CanvasChatSession => ({ - id: session.id, - title: (session.title || "").trim() || "新对话", - createdAt: Number(session.createdAt) || now, - updatedAt: Number(session.updatedAt) || now, - clawdSkillTagId: + .map((session): CanvasChatSession => { + const clawdSkillTagId = typeof (session as CanvasChatSession).clawdSkillTagId === "string" ? (session as CanvasChatSession).clawdSkillTagId || null - : null, - clawdSkillTagPromptApplied: - (session as CanvasChatSession).clawdSkillTagPromptApplied === true, - })); + : null; + + return { + id: session.id, + title: (session.title || "").trim() || "新对话", + mode: normalizeCanvasComposerSessionMode( + (session as CanvasChatSession).mode, + clawdSkillTagId ? "clawd" : "image" + ), + createdAt: Number(session.createdAt) || now, + updatedAt: Number(session.updatedAt) || now, + clawdSkillTagId, + clawdSkillTagPromptApplied: + (session as CanvasChatSession).clawdSkillTagPromptApplied === true, + }; + }); let nextSessions = sanitized; const requiredSessions = requestedSession @@ -2712,6 +2781,83 @@ export function CanvasChatPanel({ [focusCanvasOnNode, setTransientViewport] ); + const handleAddClawdImageToCanvas = useCallback( + async (imageUrl: string) => { + const normalizedUrl = String(imageUrl || "").trim(); + if (!/^https?:\/\//i.test(normalizedUrl)) { + toast.error("图片地址无效"); + return; + } + + const imageKey = normalizeImageIdentityUrl(normalizedUrl); + const existingNode = useCanvasStore.getState().nodes.find((node) => { + if (node.type !== "image") return false; + const src = String(((node.data as any) || {}).src || "").trim(); + return imageKey && normalizeImageIdentityUrl(src) === imageKey; + }); + + if (existingNode) { + selectNodes([existingNode.id]); + focusCanvasOnNode(existingNode); + toast.message("这张图片已在画布中,已为你定位"); + return; + } + + setAddingClawdImageUrl(normalizedUrl); + try { + const dimensions = await getImageDimensions(normalizedUrl); + const currentNodes = useCanvasStore.getState().nodes; + const anchorNode = findCanvasImageAnchorNode(currentNodes, { + selectedNodes: + generationAnchorNodesRef.current.length > 0 + ? generationAnchorNodesRef.current + : selectedImageNodesRef.current, + }); + const size = { width: dimensions.width, height: dimensions.height }; + const position = getAnchoredGenerationPosition({ + anchorNode, + existingNodes: currentNodes, + fallbackNodeCount: currentNodes.length, + size, + viewport: useCanvasStore.getState().viewport, + }); + + const nodeId = addNode({ + type: "image", + position, + size, + data: { + src: normalizedUrl, + status: "completed", + title: "Claw 图片", + prompt: "Claw 对话图片", + metadata: { + originalWidth: dimensions.originalWidth, + originalHeight: dimensions.originalHeight, + source: "clawd-chat", + sourceChatId: activeChatId, + sourceImageKey: imageKey || undefined, + placementMode: "manual-clawd-image", + manuallyAddedAt: Date.now(), + }, + }, + connections: anchorNode?.id ? [anchorNode.id] : [], + }); + + selectNodes([nodeId]); + focusCanvasOnNode({ position, size }); + void autoNameGeneratedImageNode(nodeId, normalizedUrl); + toast.success("已添加到画布"); + } catch (error) { + console.error("[CanvasChat] Failed to add Claw image to canvas:", error); + toast.error("添加到画布失败"); + } finally { + setAddingClawdImageUrl(""); + } + }, + [activeChatId, addNode, autoNameGeneratedImageNode, focusCanvasOnNode, selectNodes] + ); + const animateCanvasAgentViewport = useCallback( ( targetViewport: { x: number; y: number; zoom: number }, @@ -3901,6 +4047,17 @@ export function CanvasChatPanel({ const activeComposerMode = composerModeOverride || computedComposerMode; const isVideoToolbarDark = isVideoWorkspace || activeComposerMode === "video"; const isClawdMode = activeComposerMode === "clawd" && isClawdModeEnabled; + const activeChatSessionMode = getCanvasChatSessionMode( + activeChatSession, + activeComposerMode + ); + const currentModeChatSessions = useMemo( + () => + chatSessions.filter( + (session) => getCanvasChatSessionMode(session) === activeComposerMode + ), + [activeComposerMode, chatSessions] + ); const agentSelectableModels = useMemo( () => effectiveAgentEnabled ? [SMART_AUTO_MODEL, ...conversationOnlyModels] : conversationOnlyModels, @@ -4319,22 +4476,6 @@ 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 || - clawdTableFiles.length > 0 || - Boolean(selectedSkillTagId) || - hasDetailSlotContent - ); - }, [attachments.length, clawdTableFiles.length, detailSlots, input, selectedSkillTagId]); const getComposerModeMenuItemClass = useCallback( (mode: CanvasComposerMode) => cn( @@ -4692,7 +4833,12 @@ export function CanvasChatPanel({ return null; } - const isSwitchingMode = mode !== activeComposerMode; + const activeSessionMode = getCanvasChatSessionMode( + chatSessions.find((session) => session.id === activeChatId) || null, + activeComposerMode + ); + const isSwitchingMode = + mode !== activeComposerMode || activeSessionMode !== mode; let targetChatSessionId = activeChatId; if ( isSwitchingMode && @@ -4702,12 +4848,28 @@ export function CanvasChatPanel({ return null; } - if (isSwitchingMode && hasActiveChatContext()) { + if (isSwitchingMode) { const modeLabel = getCanvasComposerModeSessionLabel(mode); - targetChatSessionId = startNewChatSession({ - title: `${modeLabel} 对话`, - toastMessage: `已为 ${modeLabel} 新建聊天窗口`, - }); + const targetSession = chatSessions + .filter((session) => getCanvasChatSessionMode(session) === mode) + .sort((a, b) => b.updatedAt - a.updatedAt)[0]; + + if (targetSession) { + targetChatSessionId = targetSession.id; + if (targetSession.id !== activeChatId) { + forceHistoryHydrationRef.current = toRawCanvasChatId(targetSession.id); + releaseSendGuard(activeChatId); + setActiveChatId(targetSession.id); + replaceChatSessionQuery(targetSession.id); + toast.success(`已切换到 ${modeLabel} 对话`); + } + } else { + targetChatSessionId = startNewChatSession({ + mode, + title: `${modeLabel} 对话`, + toastMessage: `已为 ${modeLabel} 新建聊天窗口`, + }); + } } activeComposerModeRef.current = mode; @@ -4764,7 +4926,6 @@ export function CanvasChatPanel({ firstImageModelId, firstVideoModelId, getSendGuard, - hasActiveChatContext, isClawdSubmitting, isDetailGenerating, lastConversationModelId, @@ -4773,6 +4934,9 @@ export function CanvasChatPanel({ isCanvasImageModel, isCanvasVideoModel, lastVideoModelId, + chatSessions, + releaseSendGuard, + replaceChatSessionQuery, startNewChatSession, ] ); @@ -6454,6 +6618,10 @@ export function CanvasChatPanel({ const created: CanvasChatSession = { id: activeChatId, title: derivedTitle || fallbackTitle, + mode: normalizeCanvasComposerSessionMode( + activeComposerModeRef.current, + "image" + ), createdAt: now, updatedAt: now, }; @@ -6479,6 +6647,7 @@ export function CanvasChatPanel({ }, [activeChatId, messages]); useEffect(() => { + if (activeChatSessionMode === "clawd") return; if (!hasHydratedHistoryRef.current) return; const activeStreamState = streamImageGenerationStateByChatId[activeChatId]; const shouldDeferAssistantMediaAutosync = @@ -6530,6 +6699,10 @@ export function CanvasChatPanel({ if (message.role !== "assistant" || processedMessageIds.current.has(message.id)) { continue; } + if (isClawdChatMessage(message)) { + processedMessageIds.current.add(message.id); + continue; + } const nearestUserImageKeys = getNearestUserImageUrlKeys(messages, messageIndex); const nearestUserImageUrls = getNearestUserImageUrls(messages, messageIndex); @@ -6827,6 +7000,7 @@ export function CanvasChatPanel({ }; }, [ activeChatId, + activeChatSessionMode, messages, addNode, nodes, @@ -6865,11 +7039,13 @@ export function CanvasChatPanel({ // 为历史节点补全可编辑的原始提示词(旧数据里可能只有“对话生成”占位文案) useEffect(() => { + if (activeChatSessionMode === "clawd") return; if (!messages.length || !nodes.length) return; const promptByImageUrl = new Map(); messages.forEach((message, messageIndex) => { if (message.role !== "assistant") return; + if (isClawdChatMessage(message)) return; const content = message.parts?.map((p: any) => p.text || "").join("") || ""; const imageUrls = extractImageUrls(content); if (!imageUrls.length) return; @@ -6905,7 +7081,7 @@ export function CanvasChatPanel({ }, }); }); - }, [messages, nodes, updateNode]); + }, [activeChatSessionMode, messages, nodes, updateNode]); // 滚动到底部 useEffect(() => { @@ -9741,9 +9917,23 @@ export function CanvasChatPanel({ role: "user", parts: [{ type: "text", text: optimisticMessageText }], createdAt: optimisticCreatedAt, - metadata: { createdAt: optimisticCreatedAt.toISOString() }, + metadata: { + createdAt: optimisticCreatedAt.toISOString(), + composerMode: "clawd", + }, } as ChatMessage; + setChatSessions((prevSessions) => + prevSessions.map((session) => + session.id === activeChatId + ? { + ...session, + mode: "clawd", + updatedAt: Date.now(), + } + : session + ) + ); setSendGuard(activeChatId, { inFlight: true, signature: sendSignature, @@ -10370,6 +10560,62 @@ export function CanvasChatPanel({ } }, [addNode]); + const uploadImageFileForComposerAttachment = useCallback(async ( + file: File + ): Promise<{ + imageUrl: string; + name: string; + contentType: string; + assetId?: string; + width?: number; + height?: number; + } | null> => { + if (!file.type.startsWith("image/")) { + toast.error("请选择图片文件"); + return null; + } + + if (file.size > 50 * 1024 * 1024) { + toast.error("图片大小不能超过 50MB"); + return null; + } + + const ext = file.type.split("/")[1] || "png"; + const fileName = + (file.name || "").trim() || `上传图片-${Date.now()}.${ext}`; + + try { + const formData = new FormData(); + formData.append("file", file); + formData.append("purpose", "canvas-upload"); + + const response = await fetchUploadWithRetry(formData); + if (!response.ok) { + throw new Error("上传失败"); + } + + const data = await response.json(); + const imageUrl = data.url as string; + const assetId = + typeof data.assetId === "string" && data.assetId.trim() + ? data.assetId.trim() + : undefined; + const dimensions = await getImageDimensions(imageUrl); + + return { + imageUrl, + name: fileName, + contentType: file.type || "image/jpeg", + assetId, + width: dimensions.originalWidth || dimensions.width, + height: dimensions.originalHeight || dimensions.height, + }; + } catch (_error) { + toast.error("图片上传失败"); + return null; + } + }, []); + const handlePastedImages = useCallback(async (files: File[]) => { if (files.length === 0) return; @@ -10377,6 +10623,44 @@ export function CanvasChatPanel({ const baseNodeCount = nodes.length; let uploadIndex = 0; + if (isClawdMode) { + let handledCount = 0; + for (const file of files) { + const uploaded = await uploadImageFileForComposerAttachment(file); + if (!uploaded) continue; + + handledCount += 1; + setAttachments((prev) => { + const newIndex = prev.length; + setTimeout(() => { + insertImageTagInEditor(uploaded.imageUrl, uploaded.name, newIndex); + (textareaRef.current as HTMLDivElement | null)?.focus(); + }, 0); + + return [ + ...prev, + { + url: uploaded.imageUrl, + name: uploaded.name, + contentType: uploaded.contentType, + assetId: uploaded.assetId, + width: uploaded.width, + height: uploaded.height, + }, + ]; + }); + } + + if (handledCount > 0) { + toast.success( + handledCount === 1 + ? `图片已添加为 ${CLAW_MODE_LABEL} 附件` + : `已添加 ${handledCount} 张图片为 ${CLAW_MODE_LABEL} 附件` + ); + } + return; + } + if (hasAgentUploadSlots) { const slotCount = agentUploadSlots.length; const currentSlots = Array.from({ length: slotCount }, (_, index) => attachments[index] ?? null); @@ -10500,11 +10784,13 @@ export function CanvasChatPanel({ attachments, hasAgentUploadSlots, insertImageTagInEditor, + isClawdMode, isVideoModel, maybeAutoSetCanvasTitle, nodes.length, updateAttachmentAtAgentSlot, updateAttachmentAtVideoSlot, + uploadImageFileForComposerAttachment, uploadImageFileAndAddToCanvas, videoFrameSlots.length, ]); @@ -10584,11 +10870,42 @@ export function CanvasChatPanel({ addClawdTableFiles(tableFiles); } - selectedFiles = imageFiles; - if (selectedFiles.length === 0) { - e.target.value = ""; - return; + let handledImageCount = 0; + for (const file of imageFiles) { + const uploaded = await uploadImageFileForComposerAttachment(file); + if (!uploaded) continue; + + handledImageCount += 1; + setAttachments((prev) => { + const newIndex = prev.length; + setTimeout(() => { + insertImageTagInEditor(uploaded.imageUrl, uploaded.name, newIndex); + (textareaRef.current as HTMLDivElement | null)?.focus(); + }, 0); + + return [ + ...prev, + { + url: uploaded.imageUrl, + name: uploaded.name, + contentType: uploaded.contentType, + assetId: uploaded.assetId, + width: uploaded.width, + height: uploaded.height, + }, + ]; + }); } + + if (handledImageCount > 0) { + toast.success( + handledImageCount === 1 + ? `图片已添加为 ${CLAW_MODE_LABEL} 附件` + : `已添加 ${handledImageCount} 张图片为 ${CLAW_MODE_LABEL} 附件` + ); + } + e.target.value = ""; + return; } let firstImageUrl: string | null = null; @@ -11415,8 +11732,8 @@ export function CanvasChatPanel({ ); const createNewChatSession = useCallback(() => { - startNewChatSession(); - }, [startNewChatSession]); + startNewChatSession({ mode: activeComposerMode }); + }, [activeComposerMode, startNewChatSession]); const switchToSession = useCallback((sessionId: string) => { if (!sessionId || sessionId === activeChatId) { @@ -11582,10 +11899,10 @@ export function CanvasChatPanel({ >
聊天窗口记录
- {chatSessions.length === 0 ? ( + {currentModeChatSessions.length === 0 ? (
暂无记录
) : ( - chatSessions.map((session, index) => { + currentModeChatSessions.map((session, index) => { const isActive = session.id === activeChatId; const timeLabel = new Date(session.updatedAt).toLocaleTimeString([], { hour: "2-digit", @@ -11824,7 +12141,7 @@ export function CanvasChatPanel({ 预设快捷功能
- {canvasSkillTags.slice(0, 4).map((tag) => { + {canvasSkillTags.map((tag) => { const isActive = selectedSkillTag?.id === tag.id; const TagIcon = getCanvasSkillIconComponent(tag.iconName); return ( @@ -11930,6 +12247,9 @@ export function CanvasChatPanel({ message.role === "assistant" && !hasAssistantMedia && Boolean(clawdTypewriterMessageIds[message.id]); + const isClawAssistantMessage = + message.role === "assistant" && + (isClawdMode || isClawdChatMessage(message)); const messageTimestamp = getMessageTimestamp(message); const prevMessageTimestamp = @@ -12136,6 +12456,7 @@ export function CanvasChatPanel({ shouldTypeClawAssistantMessage); const shouldShowDoneSummary = !isDetailGenerationMode && + !isClawAssistantMessage && imageUrls.length > 0 && !plainText.includes(CANVAS_IMAGE_DONE_SUMMARY) && !isLatestAssistantDuringGeneration; @@ -12279,24 +12600,46 @@ export function CanvasChatPanel({ setFullscreenPreview({ url }); }} /> - +
+ {isClawAssistantMessage ? ( + + ) : null} + +
))}
@@ -13309,6 +13652,48 @@ export function CanvasChatPanel({ ) : null} + {isClawdMode && activeClawdSkillTag && !activeClawdSkillTagPromptApplied ? ( +
+
+ {activeClawdSkillTag.iconUrl ? ( + {activeClawdSkillTag.name} { + if (event.currentTarget.src !== activeClawdSkillTag.iconUrl) { + event.currentTarget.src = activeClawdSkillTag.iconUrl || ""; + } + }} + /> + ) : ( + + )} + {activeClawdSkillTag.name} + + 待发送 + + +
+
+ ) : null} {!isDetailGenerationMode && hasWorkflowContext && workflowContext && (