feat: improve Claw shortcut mode and toolbar docking

This commit is contained in:
134 2026-06-26 17:27:59 +08:00
parent 045e36b242
commit 80f6827375
2 changed files with 348 additions and 60 deletions

View File

@ -1140,6 +1140,8 @@ const FLOATING_PANEL_INSET = 16;
const DOCKED_PANEL_INSET = 0;
const FLOATING_UI_GAP = 16;
const FLOATING_BOTTOM_INSET = 20;
const FLOATING_TOOLBAR_ESTIMATED_WIDTH = 680;
const FLOATING_TOOLBAR_SIDE_DOCK_CLEARANCE = 48;
const WORKFLOW_AGENT_CONTEXT_START = "【视频画布同步上下文】";
const WORKFLOW_AGENT_CONTEXT_END = "【/视频画布同步上下文】";
const AGENT_RESULT_MODE_LABELS: Record<AgentGenerationResultModeType, string> =
@ -1988,6 +1990,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
const [videoCanvasInsertMenu, setVideoCanvasInsertMenu] =
useState<VideoCanvasInsertMenuState | null>(null);
const videoCanvasInsertMenuRef = useRef<HTMLDivElement | null>(null);
const floatingToolbarContentRef = useRef<HTMLDivElement | null>(null);
const [isVideoCanvasFileListOpen, setIsVideoCanvasFileListOpen] =
useState(false);
const [isVideoCanvasMarkerPanelOpen, setIsVideoCanvasMarkerPanelOpen] =
@ -1995,6 +1998,8 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
// 面板模式: 'chat' 聊天面板, 'fileList' 文件列表
const [panelMode, setPanelMode] = useState<"chat" | "fileList">("chat");
const [viewportWidth, setViewportWidth] = useState(0);
const [horizontalFloatingToolbarWidth, setHorizontalFloatingToolbarWidth] =
useState(0);
const [workflowInputs, setWorkflowInputs] = useState<AgentWorkflowInputs>({
primarySku: "",
secondarySku: "",
@ -3003,6 +3008,23 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
const panelOcclusionRight = isFloatingSidebarVisible
? resolvedPanelWidth + DOCKED_PANEL_INSET
: 0;
const visibleCanvasWidth =
viewportWidth > 0 ? Math.max(viewportWidth - panelOcclusionRight, 0) : 0;
const horizontalFloatingToolbarWidthForDocking =
horizontalFloatingToolbarWidth || FLOATING_TOOLBAR_ESTIMATED_WIDTH;
const shouldDockFloatingToolbarToSide =
!isVideoMode &&
isFloatingSidebarVisible &&
visibleCanvasWidth > 0 &&
visibleCanvasWidth <
horizontalFloatingToolbarWidthForDocking +
FLOATING_TOOLBAR_SIDE_DOCK_CLEARANCE * 2;
const floatingToolbarTooltipSide = shouldDockFloatingToolbarToSide
? "right"
: "top";
const floatingToolbarDropdownSide = shouldDockFloatingToolbarToSide
? "right"
: "bottom";
const floatingToolbarOffset = isFloatingSidebarVisible
? -(panelOcclusionRight / 2)
: 0;
@ -3487,6 +3509,43 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
);
}, [viewportWidth]);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const toolbarElement = floatingToolbarContentRef.current;
if (!toolbarElement) {
return;
}
const measureHorizontalToolbar = () => {
if (toolbarElement.dataset.canvasToolbarOrientation !== "horizontal") {
return;
}
const nextWidth = Math.ceil(toolbarElement.getBoundingClientRect().width);
if (!Number.isFinite(nextWidth) || nextWidth <= 0) {
return;
}
setHorizontalFloatingToolbarWidth((currentWidth) =>
Math.abs(currentWidth - nextWidth) > 1 ? nextWidth : currentWidth
);
};
measureHorizontalToolbar();
const resizeObserver = new ResizeObserver(measureHorizontalToolbar);
resizeObserver.observe(toolbarElement);
window.addEventListener("resize", measureHorizontalToolbar);
return () => {
resizeObserver.disconnect();
window.removeEventListener("resize", measureHorizontalToolbar);
};
}, []);
useEffect(() => {
if (
!isFloatingPanelWidthStorageReady ||
@ -10720,17 +10779,38 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
{/* 5. Floating Toolbar (Bottom) */}
<div
className="-translate-x-1/2 absolute left-1/2 z-20"
className={cn(
"absolute z-20",
shouldDockFloatingToolbarToSide
? "-translate-y-1/2"
: "-translate-x-1/2 left-1/2"
)}
data-canvas-floating-toolbar="true"
style={{ bottom: `${FLOATING_BOTTOM_INSET}px` }}
style={
shouldDockFloatingToolbarToSide
? { left: `${FLOATING_UI_GAP}px`, top: "50%" }
: { bottom: `${FLOATING_BOTTOM_INSET}px` }
}
>
<div
data-canvas-toolbar-orientation={
shouldDockFloatingToolbarToSide ? "vertical" : "horizontal"
}
ref={floatingToolbarContentRef}
className={cn(
isVideoMode
? VIDEO_CANVAS_TOOLBAR_CLASS
: "flex items-center gap-1 rounded-[24px] border bg-white px-2.5 py-1.5 shadow-2xl dark:bg-zinc-950"
: "border bg-white shadow-2xl dark:bg-zinc-950",
!isVideoMode &&
(shouldDockFloatingToolbarToSide
? "flex flex-col items-center gap-1 rounded-[24px] px-1.5 py-2.5"
: "flex items-center gap-1 rounded-[24px] px-2.5 py-1.5")
)}
style={{ marginLeft: `${floatingToolbarOffset}px` }}
style={
shouldDockFloatingToolbarToSide
? undefined
: { marginLeft: `${floatingToolbarOffset}px` }
}
>
<Tooltip>
<TooltipTrigger asChild>
@ -10759,7 +10839,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "selectTool").label}{" "}
(V)
</TooltipContent>
@ -10786,7 +10866,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(
canvasToolbarConfig,
@ -10795,7 +10875,10 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
}
</TooltipContent>
</Tooltip>
<DropdownMenuContent align="start" side="bottom">
<DropdownMenuContent
align="start"
side={floatingToolbarDropdownSide}
>
<DropdownMenuItem
onSelect={() => handleAddAgentImageSlotNode()}
>
@ -10843,7 +10926,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "panTool").label} (H)
</TooltipContent>
</Tooltip>
@ -10872,7 +10955,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
)}
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(canvasToolbarConfig, "saveCanvas")
.label
@ -10903,7 +10986,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(canvasToolbarConfig, "cloudFiles")
.label
@ -10939,7 +11022,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(canvasToolbarConfig, "markerPanel")
.label
@ -10976,7 +11059,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(
canvasToolbarConfig,
@ -11023,7 +11106,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "styleMode").label}
</TooltipContent>
</Tooltip>
@ -11052,7 +11135,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "stylePro").label}
</TooltipContent>
</Tooltip>
@ -11081,7 +11164,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(canvasToolbarConfig, "batchProcessing")
.label
@ -11112,7 +11195,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(
canvasToolbarConfig,
@ -11144,7 +11227,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(
canvasToolbarConfig,
@ -11173,7 +11256,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(canvasToolbarConfig, "addVideoInput")
.label
@ -11207,9 +11290,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
align={shouldDockFloatingToolbarToSide ? "start" : "center"}
className="w-[420px] rounded-2xl border bg-background p-0 shadow-2xl"
side="top"
side={shouldDockFloatingToolbarToSide ? "right" : "top"}
>
<div className="border-b px-4 py-3">
<div className="font-semibold text-foreground text-sm">
@ -11306,7 +11389,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
resolveCanvasToolbarItemWithFallback(
canvasToolbarConfig,
@ -11353,7 +11436,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{selectedImageNode
? resolveCanvasToolbarItemWithFallback(
canvasToolbarConfig,
@ -11398,7 +11481,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "model3d").label}
</TooltipContent>
</Tooltip>
@ -11422,7 +11505,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(
canvasToolbarConfig,
@ -11457,7 +11540,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
)}
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(
canvasToolbarConfig,
@ -11490,7 +11573,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
</Button>
</TooltipTrigger>
<TooltipContent side="top">
<TooltipContent side={floatingToolbarTooltipSide}>
{
getCanvasToolbarItem(
canvasToolbarConfig,

View File

@ -362,6 +362,17 @@ function normalizeClawdComposerText(value: string) {
.trim();
}
function buildClawdSkillTagPrompt(tag: Pick<CanvasSkillTag, "name" | "prompt"> | null) {
if (!tag) return "";
const tagName = String(tag.name || "").trim() || "快捷标签";
const tagPrompt = String(tag.prompt || "").trim();
return [
`【快捷标签:${tagName}】`,
tagPrompt || `用户选择了「${tagName}」快捷标签,请按该标签对应能力处理。`,
].join("\n");
}
type ClawdComposerSubmissionSnapshot = {
chatId: string;
canvasId?: string;
@ -1591,6 +1602,8 @@ interface CanvasChatSession {
title: string;
createdAt: number;
updatedAt: number;
clawdSkillTagId?: string | null;
clawdSkillTagPromptApplied?: boolean;
}
const CANVAS_CHAT_QUERY_PARAM = "chat";
@ -2054,6 +2067,51 @@ export function CanvasChatPanel({
[activeChatId, chatSessions.length, releaseSendGuard, replaceChatSessionQuery]
);
const setSessionClawdSkillTag = useCallback(
(
chatSessionId: string,
tagId: string | null,
options?: { promptApplied?: boolean }
) => {
if (!chatSessionId) return;
const normalizedTagId = String(tagId || "").trim() || null;
setChatSessions((prevSessions) =>
prevSessions.map((session) =>
session.id === chatSessionId
? {
...session,
clawdSkillTagId: normalizedTagId,
clawdSkillTagPromptApplied:
normalizedTagId === null
? false
: options?.promptApplied === true,
updatedAt: Date.now(),
}
: session
)
);
},
[]
);
const markSessionClawdSkillTagPromptApplied = useCallback(
(chatSessionId: string, tagId: string) => {
if (!chatSessionId || !tagId) return;
setChatSessions((prevSessions) =>
prevSessions.map((session) =>
session.id === chatSessionId && session.clawdSkillTagId === tagId
? {
...session,
clawdSkillTagPromptApplied: true,
updatedAt: Date.now(),
}
: session
)
);
},
[]
);
const handleTypewriterActiveChange = useCallback((typingKey: string, active: boolean) => {
setActiveTypingSections((prev) => {
if (active) {
@ -2351,13 +2409,19 @@ export function CanvasChatPanel({
const saved = localStorage.getItem(sessionStorageKey);
const parsed = saved ? (JSON.parse(saved) as CanvasChatSession[]) : [];
const sanitized = parsed
const sanitized: CanvasChatSession[] = parsed
.filter((session) => session?.id)
.map((session) => ({
.map((session): CanvasChatSession => ({
id: session.id,
title: (session.title || "").trim() || "新对话",
createdAt: Number(session.createdAt) || now,
updatedAt: Number(session.updatedAt) || now,
clawdSkillTagId:
typeof (session as CanvasChatSession).clawdSkillTagId === "string"
? (session as CanvasChatSession).clawdSkillTagId || null
: null,
clawdSkillTagPromptApplied:
(session as CanvasChatSession).clawdSkillTagPromptApplied === true,
}));
let nextSessions = sanitized;
@ -3584,6 +3648,17 @@ export function CanvasChatPanel({
() => canvasSkillTags.find((tag) => tag.id === selectedSkillTagId) || null,
[canvasSkillTags, selectedSkillTagId]
);
const activeChatSession = useMemo(
() => chatSessions.find((session) => session.id === activeChatId) || null,
[activeChatId, chatSessions]
);
const activeClawdSkillTag = useMemo(() => {
const tagId = String(activeChatSession?.clawdSkillTagId || "").trim();
if (!tagId) return null;
return canvasSkillTags.find((tag) => tag.id === tagId) || null;
}, [activeChatSession?.clawdSkillTagId, canvasSkillTags]);
const activeClawdSkillTagPromptApplied =
activeChatSession?.clawdSkillTagPromptApplied === true;
const selectedAgentPreset = useMemo(
() => (agentPresetResponse?.success && agentPresetResponse.data ? agentPresetResponse.data : null),
[agentPresetResponse]
@ -3600,6 +3675,10 @@ export function CanvasChatPanel({
() => getCanvasSkillIconComponent(selectedSkillTag?.iconName),
[selectedSkillTag?.iconName]
);
const ActiveClawdSkillTagIcon = useMemo(
() => getCanvasSkillIconComponent(activeClawdSkillTag?.iconName),
[activeClawdSkillTag?.iconName]
);
useEffect(() => {
if (!selectedSkillTagId) return;
@ -4571,21 +4650,21 @@ export function CanvasChatPanel({
]);
const handleSelectGenerationMode = useCallback(
(mode: CanvasComposerMode) => {
(mode: CanvasComposerMode): string | null => {
hiddenPromptRef.current = null;
autoContextRef.current = null;
if (mode === "clawd" && !isClawdModeEnabled) {
toast.error(`${CLAW_MODE_LABEL} 未开启`);
return;
return null;
}
if (mode === "agent") {
if (!effectiveAgentEnabled) {
return;
return null;
}
}
if (mode === "detail" && !canUseDetailGeneration) {
toast.error("AI详情生成功能未开启");
return;
return null;
}
const preferredImageModelId =
@ -4600,7 +4679,7 @@ export function CanvasChatPanel({
: null;
if (mode === "image" && !preferredImageModelId) {
toast.error("当前没有可用的图像模型");
return;
return null;
}
const preferredVideoModelId =
@ -4610,21 +4689,22 @@ export function CanvasChatPanel({
: null;
if (mode === "video" && !preferredVideoModelId) {
toast.error("当前没有可用的视频模型");
return;
return null;
}
const isSwitchingMode = mode !== activeComposerMode;
let targetChatSessionId = activeChatId;
if (
isSwitchingMode &&
(getSendGuard(activeChatId).inFlight || isClawdSubmitting || isDetailGenerating)
) {
toast.message("当前对话正在处理中,完成后再切换模式");
return;
return null;
}
if (isSwitchingMode && hasActiveChatContext()) {
const modeLabel = getCanvasComposerModeSessionLabel(mode);
startNewChatSession({
targetChatSessionId = startNewChatSession({
title: `${modeLabel} 对话`,
toastMessage: `已为 ${modeLabel} 新建聊天窗口`,
});
@ -4635,7 +4715,7 @@ export function CanvasChatPanel({
if (mode === "clawd") {
setIsDetailGenerationMode(false);
return;
return targetChatSessionId;
}
if (mode === "agent") {
@ -4648,7 +4728,7 @@ export function CanvasChatPanel({
currentModelIdRef.current = preferredAgentModelId;
setSelectedModelId(preferredAgentModelId);
setIsDetailGenerationMode(false);
return;
return targetChatSessionId;
}
if (mode === "detail") {
@ -4657,7 +4737,7 @@ export function CanvasChatPanel({
setSelectedModelId(detailModelId);
setIsDetailGenerationMode(true);
setIsDetailWorkbenchCollapsed(true);
return;
return targetChatSessionId;
}
if (mode === "image") {
@ -4665,13 +4745,14 @@ export function CanvasChatPanel({
currentModelIdRef.current = nextImageModelId;
setSelectedModelId(nextImageModelId);
setIsDetailGenerationMode(false);
return;
return targetChatSessionId;
}
const nextVideoModelId = preferredVideoModelId || "";
currentModelIdRef.current = nextVideoModelId;
setSelectedModelId(nextVideoModelId);
setIsDetailGenerationMode(false);
return targetChatSessionId;
},
[
canUseDetailGeneration,
@ -4707,6 +4788,42 @@ export function CanvasChatPanel({
isClawdModeEnabled,
]);
const handleSelectCanvasSkillTag = useCallback(
(tag: CanvasSkillTag) => {
if (!isClawdModeEnabled) {
toast.error(`${CLAW_MODE_LABEL} 未开启`);
return;
}
if (getSendGuard(activeChatId).inFlight || isClawdSubmitting || isDetailGenerating) {
toast.message("当前对话正在处理中,完成后再切换模式");
return;
}
const targetChatSessionId = handleSelectGenerationMode("clawd");
if (!targetChatSessionId) {
return;
}
setSessionClawdSkillTag(targetChatSessionId, tag.id, {
promptApplied: false,
});
window.setTimeout(() => {
setSelectedSkillTagId(tag.id);
const editor = textareaRef.current as HTMLDivElement | null;
editor?.focus();
}, 0);
},
[
activeChatId,
getSendGuard,
handleSelectGenerationMode,
isClawdModeEnabled,
isClawdSubmitting,
isDetailGenerating,
setSessionClawdSkillTag,
]
);
useEffect(() => {
if (!effectiveAgentEnabled || typeof window === "undefined") return;
const targetWindow = window as CanvasAgentPendingRequestWindow;
@ -9535,26 +9652,51 @@ export function CanvasChatPanel({
const textContent = normalizeClawdComposerText(input);
const tableFilesSnapshot = [...clawdTableFiles];
const hasTableFiles = tableFilesSnapshot.length > 0;
const clawdSkillTagForRequest = activeClawdSkillTag || selectedSkillTag;
const shouldIncludeClawdSkillTagPrompt = Boolean(
clawdSkillTagForRequest && !activeClawdSkillTagPromptApplied
);
const clawdSkillTagPrompt = shouldIncludeClawdSkillTagPrompt
? buildClawdSkillTagPrompt(clawdSkillTagForRequest)
: "";
const hasPendingClawdSkillTagPrompt = Boolean(clawdSkillTagPrompt);
const attachmentsSnapshot = [...attachments];
const imageAttachmentsSnapshot = attachmentsSnapshot
.map((attachment) => sanitizeChatAttachment(attachment))
.filter((attachment): attachment is PickedObjectAttachment => Boolean(attachment));
const hasImageAttachments = imageAttachmentsSnapshot.length > 0;
if (!textContent && !hasTableFiles && !hasImageAttachments) {
if (
!textContent &&
!hasTableFiles &&
!hasImageAttachments &&
!hasPendingClawdSkillTagPrompt
) {
return;
}
const baseRequestMessage =
textContent || getClawdDefaultPrompt(hasTableFiles, hasImageAttachments);
textContent ||
(clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt
? `请按「${clawdSkillTagForRequest.name}」快捷功能处理。`
: getClawdDefaultPrompt(hasTableFiles, hasImageAttachments));
const imageReferenceText = buildClawdImageReferenceText(
imageAttachmentsSnapshot
);
const requestMessage = [baseRequestMessage, imageReferenceText]
const requestMessage = [
baseRequestMessage,
clawdSkillTagPrompt,
imageReferenceText,
]
.filter(Boolean)
.join("\n\n");
const optimisticBaseMessage =
textContent ||
(clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt
? `快捷功能:${clawdSkillTagForRequest.name}`
: baseRequestMessage);
const optimisticMessageText = [
buildClawdTableDisplayText(baseRequestMessage, tableFilesSnapshot),
buildClawdTableDisplayText(optimisticBaseMessage, tableFilesSnapshot),
buildClawdImageDisplayText(imageAttachmentsSnapshot),
]
.filter(Boolean)
@ -9563,6 +9705,8 @@ export function CanvasChatPanel({
chatId: activeChatId,
mode: "clawd",
text: requestMessage,
skillTagId: clawdSkillTagForRequest?.id || null,
skillTagPrompt: clawdSkillTagPrompt,
tableFiles: tableFilesSnapshot
.map((file) => `${file.name}:${file.size}:${file.lastModified}`)
.sort(),
@ -9578,7 +9722,7 @@ export function CanvasChatPanel({
}
const nextProcessingHint = shouldShowClawdProcessingHint(
textContent,
[textContent, clawdSkillTagPrompt].filter(Boolean).join("\n\n"),
hasTableFiles,
hasImageAttachments
)
@ -9615,6 +9759,9 @@ export function CanvasChatPanel({
startedAt: clawdSubmissionStartedAt,
submittedTexts: [
textContent,
clawdSkillTagForRequest?.name || "",
clawdSkillTagForRequest?.prompt || "",
clawdSkillTagPrompt,
baseRequestMessage,
requestMessage,
optimisticMessageText,
@ -9637,6 +9784,7 @@ export function CanvasChatPanel({
});
setAttachments([]);
setClawdTableFiles([]);
setSelectedSkillTagId(null);
clearSelection();
try {
@ -9666,6 +9814,13 @@ export function CanvasChatPanel({
throw new Error(responseData?.error || `${CLAW_MODE_LABEL} 请求失败`);
}
if (clawdSkillTagPrompt && clawdSkillTagForRequest) {
markSessionClawdSkillTagPromptApplied(
activeChatId,
clawdSkillTagForRequest.id
);
}
const returnedMessages = Array.isArray(responseData?.messages)
? (responseData.messages as ChatMessage[])
: [];
@ -9711,6 +9866,9 @@ export function CanvasChatPanel({
toast.error(error instanceof Error ? error.message : `${CLAW_MODE_LABEL} 请求失败`);
setClawdTableFiles(tableFilesSnapshot);
setAttachments(attachmentsSnapshot);
if (clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt) {
setSelectedSkillTagId(clawdSkillTagForRequest.id);
}
const editableDiv = textareaRef.current as HTMLDivElement | null;
const currentEditorText = (editableDiv?.textContent || "")
.replace(/\u200b/g, " ")
@ -10042,9 +10200,11 @@ export function CanvasChatPanel({
chatScope,
rawChatId,
input,
attachments,
clawdTableFiles,
hasAgentUploadSlots,
attachments,
clawdTableFiles,
activeClawdSkillTag,
activeClawdSkillTagPromptApplied,
hasAgentUploadSlots,
hasWorkflowContext,
selectedSkillTag,
selectedAgentPreset,
@ -10081,8 +10241,9 @@ export function CanvasChatPanel({
clearRestoredClawdComposerText,
shouldBlockDuplicateSend,
status,
mutate,
buildHistoryMessagesUrl,
mutate,
markSessionClawdSkillTagPromptApplied,
buildHistoryMessagesUrl,
workflowContext,
]);
@ -10713,7 +10874,9 @@ export function CanvasChatPanel({
const hasEnoughAgentUploads = !hasAgentUploadSlots || attachments.length >= agentRequiredUploadCount;
const composerPlaceholder = isWorkspacePresentation
? isClawdMode
? `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格`
? activeClawdSkillTag
? `继续向 ${CLAW_MODE_LABEL} 提问,当前使用「${activeClawdSkillTag.name}」`
: `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格`
: isDetailGenerationMode
? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)"
: isVideoWorkspace
@ -10724,7 +10887,9 @@ export function CanvasChatPanel({
? "可选补充本次生图要求,例如:高级感、暖灰色、近景特写、生成3张。"
: "可选补充本次生成要求,例如:更高级、更生活化、暖调灯光。"
: isClawdMode
? `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格`
? activeClawdSkillTag
? `继续向 ${CLAW_MODE_LABEL} 提问,当前使用「${activeClawdSkillTag.name}」`
: `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格`
: isDetailGenerationMode
? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)"
: isVideoWorkspace
@ -10749,7 +10914,12 @@ export function CanvasChatPanel({
? Math.min(100, Math.round((detailProgress.current / detailProgress.total) * 100))
: 0;
const canSend = isClawdMode
? Boolean(input.trim() || attachments.length > 0 || clawdTableFiles.length > 0) && !isClawdSubmitting
? Boolean(
input.trim() ||
attachments.length > 0 ||
clawdTableFiles.length > 0 ||
(activeClawdSkillTag && !activeClawdSkillTagPromptApplied)
) && !isClawdSubmitting
: isDetailGenerationMode
? detailValidationIssues.length === 0 && !isDetailGenerating
: hasInputOrAttachments &&
@ -11332,6 +11502,42 @@ export function CanvasChatPanel({
>
{displayCanvasName}
</h2>
{isClawdMode && activeClawdSkillTag ? (
<span
className={cn(
"inline-flex max-w-[172px] shrink-0 items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-medium leading-none",
isVideoWorkspace
? "border-border/60 bg-background/80 text-foreground/80"
: "border-violet-200 bg-violet-50 text-violet-700 dark:border-violet-400/30 dark:bg-violet-500/15 dark:text-violet-200"
)}
title={
activeClawdSkillTagPromptApplied
? `当前 Claw 对话正在使用「${activeClawdSkillTag.name}」快捷标签`
: `首次发送会带入「${activeClawdSkillTag.name}」快捷标签提示词`
}
>
{activeClawdSkillTag.iconUrl ? (
<img
src={buildPreviewImageUrl(activeClawdSkillTag.iconUrl, 32)}
alt={activeClawdSkillTag.name}
className="h-3.5 w-3.5 shrink-0 rounded-sm object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== activeClawdSkillTag.iconUrl) {
event.currentTarget.src = activeClawdSkillTag.iconUrl || "";
}
}}
/>
) : (
<ActiveClawdSkillTagIcon className="h-3.5 w-3.5 shrink-0" />
)}
<span className="truncate">{activeClawdSkillTag.name}</span>
<span className="shrink-0 text-[10px] opacity-70">
{activeClawdSkillTagPromptApplied ? "使用中" : "待带入"}
</span>
</span>
) : null}
</div>
<div className="flex shrink-0 items-center">
<div className="flex items-center gap-1">
@ -11625,11 +11831,7 @@ export function CanvasChatPanel({
<button
key={tag.id}
type="button"
onClick={() => {
setSelectedSkillTagId(tag.id);
const editor = textareaRef.current as HTMLDivElement | null;
editor?.focus();
}}
onClick={() => handleSelectCanvasSkillTag(tag)}
className={cn(
"inline-flex h-[34px] max-w-full items-center justify-center gap-1.5 rounded-full border px-3.5 text-[13px] font-medium leading-none transition-colors",
isActive
@ -13080,7 +13282,10 @@ export function CanvasChatPanel({
: "min-h-[132px]"
)}
>
{!isWorkspacePresentation && !isDetailGenerationMode && selectedSkillTag && (
{!isWorkspacePresentation &&
!isDetailGenerationMode &&
!isClawdMode &&
selectedSkillTag ? (
<div className="mb-2 flex">
<button
type="button"
@ -13103,7 +13308,7 @@ export function CanvasChatPanel({
<X className="h-3.5 w-3.5 text-[#9ca3af] dark:text-zinc-400" />
</button>
</div>
)}
) : null}
{!isDetailGenerationMode && hasWorkflowContext && workflowContext && (
<div
className={cn(