feat: improve Claw shortcut mode and toolbar docking
This commit is contained in:
parent
045e36b242
commit
80f6827375
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user