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 DOCKED_PANEL_INSET = 0;
const FLOATING_UI_GAP = 16; const FLOATING_UI_GAP = 16;
const FLOATING_BOTTOM_INSET = 20; 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_START = "【视频画布同步上下文】";
const WORKFLOW_AGENT_CONTEXT_END = "【/视频画布同步上下文】"; const WORKFLOW_AGENT_CONTEXT_END = "【/视频画布同步上下文】";
const AGENT_RESULT_MODE_LABELS: Record<AgentGenerationResultModeType, string> = const AGENT_RESULT_MODE_LABELS: Record<AgentGenerationResultModeType, string> =
@ -1988,6 +1990,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
const [videoCanvasInsertMenu, setVideoCanvasInsertMenu] = const [videoCanvasInsertMenu, setVideoCanvasInsertMenu] =
useState<VideoCanvasInsertMenuState | null>(null); useState<VideoCanvasInsertMenuState | null>(null);
const videoCanvasInsertMenuRef = useRef<HTMLDivElement | null>(null); const videoCanvasInsertMenuRef = useRef<HTMLDivElement | null>(null);
const floatingToolbarContentRef = useRef<HTMLDivElement | null>(null);
const [isVideoCanvasFileListOpen, setIsVideoCanvasFileListOpen] = const [isVideoCanvasFileListOpen, setIsVideoCanvasFileListOpen] =
useState(false); useState(false);
const [isVideoCanvasMarkerPanelOpen, setIsVideoCanvasMarkerPanelOpen] = const [isVideoCanvasMarkerPanelOpen, setIsVideoCanvasMarkerPanelOpen] =
@ -1995,6 +1998,8 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
// 面板模式: 'chat' 聊天面板, 'fileList' 文件列表 // 面板模式: 'chat' 聊天面板, 'fileList' 文件列表
const [panelMode, setPanelMode] = useState<"chat" | "fileList">("chat"); const [panelMode, setPanelMode] = useState<"chat" | "fileList">("chat");
const [viewportWidth, setViewportWidth] = useState(0); const [viewportWidth, setViewportWidth] = useState(0);
const [horizontalFloatingToolbarWidth, setHorizontalFloatingToolbarWidth] =
useState(0);
const [workflowInputs, setWorkflowInputs] = useState<AgentWorkflowInputs>({ const [workflowInputs, setWorkflowInputs] = useState<AgentWorkflowInputs>({
primarySku: "", primarySku: "",
secondarySku: "", secondarySku: "",
@ -3003,6 +3008,23 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
const panelOcclusionRight = isFloatingSidebarVisible const panelOcclusionRight = isFloatingSidebarVisible
? resolvedPanelWidth + DOCKED_PANEL_INSET ? resolvedPanelWidth + DOCKED_PANEL_INSET
: 0; : 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 const floatingToolbarOffset = isFloatingSidebarVisible
? -(panelOcclusionRight / 2) ? -(panelOcclusionRight / 2)
: 0; : 0;
@ -3487,6 +3509,43 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
); );
}, [viewportWidth]); }, [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(() => { useEffect(() => {
if ( if (
!isFloatingPanelWidthStorageReady || !isFloatingPanelWidthStorageReady ||
@ -10720,17 +10779,38 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
{/* 5. Floating Toolbar (Bottom) */} {/* 5. Floating Toolbar (Bottom) */}
<div <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" 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 <div
data-canvas-toolbar-orientation={
shouldDockFloatingToolbarToSide ? "vertical" : "horizontal"
}
ref={floatingToolbarContentRef}
className={cn( className={cn(
isVideoMode isVideoMode
? VIDEO_CANVAS_TOOLBAR_CLASS ? 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> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@ -10759,7 +10839,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "selectTool").label}{" "} {getCanvasToolbarItem(canvasToolbarConfig, "selectTool").label}{" "}
(V) (V)
</TooltipContent> </TooltipContent>
@ -10786,7 +10866,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem( getCanvasToolbarItem(
canvasToolbarConfig, canvasToolbarConfig,
@ -10795,7 +10875,10 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
} }
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
<DropdownMenuContent align="start" side="bottom"> <DropdownMenuContent
align="start"
side={floatingToolbarDropdownSide}
>
<DropdownMenuItem <DropdownMenuItem
onSelect={() => handleAddAgentImageSlotNode()} onSelect={() => handleAddAgentImageSlotNode()}
> >
@ -10843,7 +10926,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "panTool").label} (H) {getCanvasToolbarItem(canvasToolbarConfig, "panTool").label} (H)
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -10872,7 +10955,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem(canvasToolbarConfig, "saveCanvas") getCanvasToolbarItem(canvasToolbarConfig, "saveCanvas")
.label .label
@ -10903,7 +10986,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem(canvasToolbarConfig, "cloudFiles") getCanvasToolbarItem(canvasToolbarConfig, "cloudFiles")
.label .label
@ -10939,7 +11022,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem(canvasToolbarConfig, "markerPanel") getCanvasToolbarItem(canvasToolbarConfig, "markerPanel")
.label .label
@ -10976,7 +11059,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem( getCanvasToolbarItem(
canvasToolbarConfig, canvasToolbarConfig,
@ -11023,7 +11106,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "styleMode").label} {getCanvasToolbarItem(canvasToolbarConfig, "styleMode").label}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -11052,7 +11135,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "stylePro").label} {getCanvasToolbarItem(canvasToolbarConfig, "stylePro").label}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -11081,7 +11164,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem(canvasToolbarConfig, "batchProcessing") getCanvasToolbarItem(canvasToolbarConfig, "batchProcessing")
.label .label
@ -11112,7 +11195,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem( getCanvasToolbarItem(
canvasToolbarConfig, canvasToolbarConfig,
@ -11144,7 +11227,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem( getCanvasToolbarItem(
canvasToolbarConfig, canvasToolbarConfig,
@ -11173,7 +11256,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem(canvasToolbarConfig, "addVideoInput") getCanvasToolbarItem(canvasToolbarConfig, "addVideoInput")
.label .label
@ -11207,9 +11290,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
align="center" align={shouldDockFloatingToolbarToSide ? "start" : "center"}
className="w-[420px] rounded-2xl border bg-background p-0 shadow-2xl" 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="border-b px-4 py-3">
<div className="font-semibold text-foreground text-sm"> <div className="font-semibold text-foreground text-sm">
@ -11306,7 +11389,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
resolveCanvasToolbarItemWithFallback( resolveCanvasToolbarItemWithFallback(
canvasToolbarConfig, canvasToolbarConfig,
@ -11353,7 +11436,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{selectedImageNode {selectedImageNode
? resolveCanvasToolbarItemWithFallback( ? resolveCanvasToolbarItemWithFallback(
canvasToolbarConfig, canvasToolbarConfig,
@ -11398,7 +11481,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{getCanvasToolbarItem(canvasToolbarConfig, "model3d").label} {getCanvasToolbarItem(canvasToolbarConfig, "model3d").label}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@ -11422,7 +11505,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem( getCanvasToolbarItem(
canvasToolbarConfig, canvasToolbarConfig,
@ -11457,7 +11540,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem( getCanvasToolbarItem(
canvasToolbarConfig, canvasToolbarConfig,
@ -11490,7 +11573,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/> />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side={floatingToolbarTooltipSide}>
{ {
getCanvasToolbarItem( getCanvasToolbarItem(
canvasToolbarConfig, canvasToolbarConfig,

View File

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