feat(canvas): add send to Agent shortcuts

This commit is contained in:
yepwait
2026-09-17 08:35:43 +08:00
parent 9868f5e8b8
commit 91c77b8222
9 changed files with 47 additions and 7 deletions

View File

@@ -2,6 +2,8 @@
## Unreleased ## Unreleased
- 新增画布多选工具栏和节点右键菜单的「发送到 Agent」快捷入口,点击后自动带入节点引用。
## v1.3.0 ## v1.3.0
- 发布正式版本 `v1.3.0`,提供 `ghcr.io/ddcat-ai/open-ai-canvas-web:1.3.0` 与 `ghcr.io/ddcat-ai/open-ai-canvas-backend:1.3.0` 的 Linux amd64/arm64 镜像,并更新 `latest` 镜像。 - 发布正式版本 `v1.3.0`,提供 `ghcr.io/ddcat-ai/open-ai-canvas-web:1.3.0` 与 `ghcr.io/ddcat-ai/open-ai-canvas-backend:1.3.0` 的 Linux amd64/arm64 镜像,并更新 `latest` 镜像。

View File

@@ -5,6 +5,12 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
# 待测试 # 待测试
## 画布节点快速引用 Agent
- 多选节点后,选区浮动工具栏应显示 `@` 图标;点击「发送到 Agent」后,Agent 输入框应自动带入所有选中节点引用。
- 单节点右键菜单(图片、视频、文本及其他节点)应显示「发送到 Agent」;点击后仅带入当前节点引用。
- 引用应以节点 mention token 保存,发送前可以继续补充操作描述。
## 欢迎页开放开关 ## 欢迎页开放开关
- 管理员在 `/admin/settings/appearance` 的英文品牌标识下方切换欢迎页,立即保存,沿用功能配置保存及审计流程,默认开启;功能开放页不再编辑该字段。 - 管理员在 `/admin/settings/appearance` 的英文品牌标识下方切换欢迎页,立即保存,沿用功能配置保存及审计流程,默认开启;功能开放页不再编辑该字段。

View File

@@ -27,11 +27,11 @@ import { CanvasCloudAgentSettings, agentPermissionLabel, agentPermissionMenuItem
import { useAgentPanelLayout } from "./use-agent-panel-layout"; import { useAgentPanelLayout } from "./use-agent-panel-layout";
import "./canvas-cloud-agent.css"; import "./canvas-cloud-agent.css";
type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void }; type CloudAgentPanelProps = { canvasId: string; domainProjectId?: string; nodeCount: number; references: CanvasResourceReference[]; open: boolean; prefillPrompt?: string; onOpen: () => void; onCollapse: () => void; onFocusNode?: (nodeId: string) => void };
type ApprovalState = { approvalId: string; detail: Record<string, unknown>; reason: string }; type ApprovalState = { approvalId: string; detail: Record<string, unknown>; reason: string };
type AgentPanelView = "chat" | "history" | "settings"; type AgentPanelView = "chat" | "history" | "settings";
export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, references, open, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) { export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, references, open, prefillPrompt, onOpen, onCollapse, onFocusNode }: CloudAgentPanelProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)]; const theme = canvasThemes[useThemeStore((state) => state.theme)];
const config = useEffectiveConfig(); const config = useEffectiveConfig();
const updateConfig = useConfigStore((state) => state.updateConfig); const updateConfig = useConfigStore((state) => state.updateConfig);
@@ -42,6 +42,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
const [connectionEpoch, setConnectionEpoch] = useState(0); const [connectionEpoch, setConnectionEpoch] = useState(0);
const [messages, setMessages] = useState<CloudAgentChatMessage[]>([]); const [messages, setMessages] = useState<CloudAgentChatMessage[]>([]);
const [prompt, setPrompt] = useState(""); const [prompt, setPrompt] = useState("");
const lastPrefillPromptRef = useRef("");
const [reasoningMode, setReasoningMode] = useState<AgentReasoningMode>("off"); const [reasoningMode, setReasoningMode] = useState<AgentReasoningMode>("off");
const [profileView, setProfileView] = useState<AgentProfileView | null>(null); const [profileView, setProfileView] = useState<AgentProfileView | null>(null);
const [profileLoading, setProfileLoading] = useState(false); const [profileLoading, setProfileLoading] = useState(false);
@@ -103,6 +104,13 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
const statusLabel = status === "waiting_approval" ? "等待审批" : status === "running" || status === "queued" ? "运行中" : status === "completed" ? "已完成" : status === "failed" ? "异常" : status === "cancelled" ? "已停止" : status === "rejected" ? "已拒绝" : "待命"; const statusLabel = status === "waiting_approval" ? "等待审批" : status === "running" || status === "queued" ? "运行中" : status === "completed" ? "已完成" : status === "failed" ? "异常" : status === "cancelled" ? "已停止" : status === "rejected" ? "已拒绝" : "待命";
const statusColor = status === "failed" ? "#e66b6b" : status === "rejected" || status === "cancelled" ? theme.node.muted : status === "waiting_approval" ? "#d6a24a" : status === "running" || status === "queued" ? "#69c29b" : theme.node.muted; const statusColor = status === "failed" ? "#e66b6b" : status === "rejected" || status === "cancelled" ? theme.node.muted : status === "waiting_approval" ? "#d6a24a" : status === "running" || status === "queued" ? "#69c29b" : theme.node.muted;
useEffect(() => {
const value = prefillPrompt?.trim();
if (!value || value === lastPrefillPromptRef.current) return;
lastPrefillPromptRef.current = value;
setPrompt(value);
}, [prefillPrompt]);
useEffect(() => { useEffect(() => {
if (!open || view !== "chat") setSkillsOpen(false); if (!open || view !== "chat") setSkillsOpen(false);
}, [open, view]); }, [open, view]);

View File

@@ -1,6 +1,6 @@
import { AnimatePresence } from "motion/react"; import { AnimatePresence } from "motion/react";
import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
import { ArrowLeft, Check, ChevronRight, Clipboard, CloudUpload, Copy, FolderOpen, FolderPlus, Image as ImageIcon, Layers3, LayoutTemplate, Link2, Maximize2, PanelTop, Pencil, Plus, Redo2, Tags, Trash2, Undo2, Upload, UserRound } from "lucide-react"; import { ArrowLeft, AtSign, Check, ChevronRight, Clipboard, CloudUpload, Copy, FolderOpen, FolderPlus, Image as ImageIcon, Layers3, LayoutTemplate, Link2, Maximize2, PanelTop, Pencil, Plus, Redo2, Tags, Trash2, Undo2, Upload, UserRound } from "lucide-react";
import { CanvasCreateMenu, type CanvasCreateCommand } from "@/components/canvas/canvas-create-menu"; import { CanvasCreateMenu, type CanvasCreateCommand } from "@/components/canvas/canvas-create-menu";
import { ASSET_CATEGORY_OPTIONS } from "@/lib/asset-category"; import { ASSET_CATEGORY_OPTIONS } from "@/lib/asset-category";
@@ -55,6 +55,7 @@ type CanvasNodeContextMenuProps = {
onSpreadSelection?: () => void; onSpreadSelection?: () => void;
onCopySelection?: () => void; onCopySelection?: () => void;
onDeleteSelection?: () => void; onDeleteSelection?: () => void;
onSendToAgent?: () => void;
}; };
export function CanvasNodeContextMenu({ export function CanvasNodeContextMenu({
@@ -94,6 +95,7 @@ export function CanvasNodeContextMenu({
onSpreadSelection, onSpreadSelection,
onCopySelection, onCopySelection,
onDeleteSelection, onDeleteSelection,
onSendToAgent,
}: CanvasNodeContextMenuProps) { }: CanvasNodeContextMenuProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)]; const theme = canvasThemes[useThemeStore((state) => state.theme)];
const [addOpen, setAddOpen] = useState(false); const [addOpen, setAddOpen] = useState(false);
@@ -200,6 +202,7 @@ export function CanvasNodeContextMenu({
<MenuButton icon={<LayoutTemplate />} label="自适应整理画布" detail="保持相对布局并加大边距" disabled={!onSpreadSelection} onClick={() => onSpreadSelection && runAction(onSpreadSelection)} /> <MenuButton icon={<LayoutTemplate />} label="自适应整理画布" detail="保持相对布局并加大边距" disabled={!onSpreadSelection} onClick={() => onSpreadSelection && runAction(onSpreadSelection)} />
<MenuDivider /> <MenuDivider />
<MenuButton icon={<Copy />} label={`复制 ${selectedCount} 个节点`} shortcut="⌘C" disabled={!onCopySelection} onClick={() => onCopySelection && runAction(onCopySelection)} /> <MenuButton icon={<Copy />} label={`复制 ${selectedCount} 个节点`} shortcut="⌘C" disabled={!onCopySelection} onClick={() => onCopySelection && runAction(onCopySelection)} />
<MenuButton icon={<AtSign />} label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} />
<MenuButton icon={<Trash2 />} label={`删除 ${selectedCount} 个节点`} danger disabled={!onDeleteSelection} onClick={() => onDeleteSelection && runAction(onDeleteSelection)} /> <MenuButton icon={<Trash2 />} label={`删除 ${selectedCount} 个节点`} danger disabled={!onDeleteSelection} onClick={() => onDeleteSelection && runAction(onDeleteSelection)} />
</> </>
) : menu.type === "node" ? ( ) : menu.type === "node" ? (
@@ -211,6 +214,7 @@ export function CanvasNodeContextMenu({
<MenuButton icon={<UserRound />} label="查看角色详情" onClick={() => runAction(onEditText)} /> <MenuButton icon={<UserRound />} label="查看角色详情" onClick={() => runAction(onEditText)} />
<MenuDivider /> <MenuDivider />
<MenuSection label="节点" /> <MenuSection label="节点" />
<MenuButton icon={<AtSign />} label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} />
<MenuButton icon={<Copy />} label="复制角色引用" shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> <MenuButton icon={<Copy />} label="复制角色引用" shortcut="⌘C" onClick={() => runAction(onCopyNode)} />
<MenuButton icon={<Layers3 />} label="创建引用副本" shortcut="⌘D" onClick={() => runAction(onDuplicate)} /> <MenuButton icon={<Layers3 />} label="创建引用副本" shortcut="⌘D" onClick={() => runAction(onDuplicate)} />
<MenuButton icon={<Trash2 />} label="删除节点" danger onClick={() => runAction(onDelete)} /> <MenuButton icon={<Trash2 />} label="删除节点" danger onClick={() => runAction(onDelete)} />
@@ -224,6 +228,7 @@ export function CanvasNodeContextMenu({
{isImage ? <MenuButton icon={<CloudUpload />} label="上传到方舟素材库" onClick={() => runAction(onUploadToArkPrivateAsset)} /> : null} {isImage ? <MenuButton icon={<CloudUpload />} label="上传到方舟素材库" onClick={() => runAction(onUploadToArkPrivateAsset)} /> : null}
<MenuDivider /> <MenuDivider />
<MenuSection label="节点" /> <MenuSection label="节点" />
<MenuButton icon={<AtSign />} label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} />
<MenuButton icon={<Copy />} label="复制节点" shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> <MenuButton icon={<Copy />} label="复制节点" shortcut="⌘C" onClick={() => runAction(onCopyNode)} />
{isImage ? <MenuButton icon={<Clipboard />} label="复制图片" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null} {isImage ? <MenuButton icon={<Clipboard />} label="复制图片" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null}
<MenuButton icon={<Link2 />} label={isImage ? "复制图片地址" : "复制视频地址"} disabled={!canCopyMediaUrl} onClick={() => runAction(onCopyMediaUrl)} /> <MenuButton icon={<Link2 />} label={isImage ? "复制图片地址" : "复制视频地址"} disabled={!canCopyMediaUrl} onClick={() => runAction(onCopyMediaUrl)} />
@@ -241,6 +246,7 @@ export function CanvasNodeContextMenu({
{isText ? <MenuButton icon={<ImageIcon />} label="用文本生图" disabled={!canGenerateFromText} onClick={() => runAction(onGenerateImage)} /> : null} {isText ? <MenuButton icon={<ImageIcon />} label="用文本生图" disabled={!canGenerateFromText} onClick={() => runAction(onGenerateImage)} /> : null}
<MenuDivider /> <MenuDivider />
<MenuSection label="副本与内容" /> <MenuSection label="副本与内容" />
<MenuButton icon={<AtSign />} label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} />
<MenuButton icon={<Copy />} label={isFrame ? `复制${isFolder ? "文件夹" : "背板"}及内容` : "复制节点"} shortcut="⌘C" onClick={() => runAction(onCopyNode)} /> <MenuButton icon={<Copy />} label={isFrame ? `复制${isFolder ? "文件夹" : "背板"}及内容` : "复制节点"} shortcut="⌘C" onClick={() => runAction(onCopyNode)} />
{isText ? <MenuButton icon={<Clipboard />} label="复制文本" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null} {isText ? <MenuButton icon={<Clipboard />} label="复制文本" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null}
<MenuButton icon={<Copy />} label={isFrame ? `创建${isFolder ? "文件夹" : "背板"}副本` : "创建参数变体"} shortcut="⌘D" onClick={() => runAction(onDuplicate)} /> <MenuButton icon={<Copy />} label={isFrame ? `创建${isFolder ? "文件夹" : "背板"}副本` : "创建参数变体"} shortcut="⌘D" onClick={() => runAction(onDuplicate)} />

View File

@@ -1,4 +1,4 @@
import { AlignHorizontalJustifyCenter, AlignHorizontalJustifyEnd, AlignHorizontalJustifyStart, AlignHorizontalSpaceAround, AlignHorizontalSpaceBetween, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, AlignVerticalSpaceAround, AlignVerticalSpaceBetween, Film, FolderTree, Grid3X3, LayoutTemplate, Link2, LoaderCircle, Workflow } from "lucide-react"; import { AlignHorizontalJustifyCenter, AlignHorizontalJustifyEnd, AlignHorizontalJustifyStart, AlignHorizontalSpaceAround, AlignHorizontalSpaceBetween, AlignVerticalJustifyCenter, AlignVerticalJustifyEnd, AlignVerticalJustifyStart, AlignVerticalSpaceAround, AlignVerticalSpaceBetween, AtSign, Film, FolderTree, Grid3X3, LayoutTemplate, Link2, LoaderCircle, Workflow } from "lucide-react";
import { registerToolbarTools, type ToolDefinition } from "@/lib/canvas/tool-registry"; import { registerToolbarTools, type ToolDefinition } from "@/lib/canvas/tool-registry";
@@ -18,6 +18,7 @@ export const selectionToolbarTools: ToolDefinition[] = [
{ id: "selection-create-storyboard", toolbar: "selection", category: "selection", label: "创建分镜组", icon: <LayoutTemplate />, defaultVisible: true, defaultOrder: 130, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onCreateStoryboard() }, { id: "selection-create-storyboard", toolbar: "selection", category: "selection", label: "创建分镜组", icon: <LayoutTemplate />, defaultVisible: true, defaultOrder: 130, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onCreateStoryboard() },
{ id: "selection-create-reference-group", toolbar: "selection", category: "selection", label: "创建引用组", icon: <FolderTree />, defaultVisible: false, defaultOrder: 140, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onCreateReferenceGroup() }, { id: "selection-create-reference-group", toolbar: "selection", category: "selection", label: "创建引用组", icon: <FolderTree />, defaultVisible: false, defaultOrder: 140, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onCreateReferenceGroup() },
{ id: "selection-batch-connect", toolbar: "selection", category: "selection", label: "批量连接", icon: <Link2 />, defaultVisible: true, defaultOrder: 145, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onBatchConnect() }, { id: "selection-batch-connect", toolbar: "selection", category: "selection", label: "批量连接", icon: <Link2 />, defaultVisible: true, defaultOrder: 145, disabled: (ctx) => ctx.selectedCount < 2, run: (ctx) => ctx.handlers.onBatchConnect() },
{ id: "selection-send-to-agent", toolbar: "selection", category: "selection", label: "发送到 Agent", icon: <AtSign />, defaultVisible: true, defaultOrder: 148, disabled: (ctx) => ctx.selectedCount < 1, run: (ctx) => ctx.handlers.onSendSelectionToAgent() },
{ {
id: "selection-merge-videos", id: "selection-merge-videos",
toolbar: "selection", toolbar: "selection",

View File

@@ -68,6 +68,7 @@ export type ToolbarHandlers = {
onCreateReferenceGroup: () => void; onCreateReferenceGroup: () => void;
onBatchConnect: () => void; onBatchConnect: () => void;
onMergeVideos: () => void; onMergeVideos: () => void;
onSendSelectionToAgent: () => void;
// 节点悬停工具栏——节点操作(均接收当前节点) // 节点悬停工具栏——节点操作(均接收当前节点)
onNodeInfo: (node: CanvasNodeData) => void; onNodeInfo: (node: CanvasNodeData) => void;
onNodeDelete: (node: CanvasNodeData) => void; onNodeDelete: (node: CanvasNodeData) => void;

View File

@@ -42,6 +42,7 @@ type CanvasProjectContextMenuProps = {
onSpreadSelection: () => void; onSpreadSelection: () => void;
onCopySelection: () => void; onCopySelection: () => void;
onDeleteSelection: () => void; onDeleteSelection: () => void;
onSendToAgent: () => void;
}; };
export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props }: CanvasProjectContextMenuProps) { export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props }: CanvasProjectContextMenuProps) {
@@ -114,6 +115,7 @@ export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props
onSpreadSelection={props.onSpreadSelection} onSpreadSelection={props.onSpreadSelection}
onCopySelection={props.onCopySelection} onCopySelection={props.onCopySelection}
onDeleteSelection={props.onDeleteSelection} onDeleteSelection={props.onDeleteSelection}
onSendToAgent={props.onSendToAgent}
/> />
); );
} }

View File

@@ -20,13 +20,14 @@ type CanvasProjectSelectionToolbarProps = {
onCreateReferenceGroup: () => void; onCreateReferenceGroup: () => void;
onBatchConnect: () => void; onBatchConnect: () => void;
onMergeVideos: () => void; onMergeVideos: () => void;
onSendSelectionToAgent: () => void;
}; };
export function CanvasProjectSelectionToolbar({ anchorRef, containerRef, count, selectedVideoCount, mergingVideos, onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos }: CanvasProjectSelectionToolbarProps) { export function CanvasProjectSelectionToolbar({ anchorRef, containerRef, count, selectedVideoCount, mergingVideos, onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos, onSendSelectionToAgent }: CanvasProjectSelectionToolbarProps) {
const theme = canvasThemes[useThemeStore((state) => state.theme)]; const theme = canvasThemes[useThemeStore((state) => state.theme)];
const handlers = { const handlers = {
onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos, onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos, onSendSelectionToAgent,
} as Partial<ToolbarHandlers> as ToolbarHandlers; } as Partial<ToolbarHandlers> as ToolbarHandlers;
const ctx: ToolContext = { const ctx: ToolContext = {

View File

@@ -77,6 +77,7 @@ import { writeCanvasNodePrompt } from "@/lib/canvas/canvas-node-prompt";
import { import {
applyCanvasConnectionPromptSync, applyCanvasConnectionPromptSync,
buildCanvasAgentMentionReferences, buildCanvasAgentMentionReferences,
canvasResourceMentionToken,
buildCanvasNodeMentionReferenceMap, buildCanvasNodeMentionReferenceMap,
buildCanvasResourceReferences, buildCanvasResourceReferences,
getContextResourceNodes, getContextResourceNodes,
@@ -291,6 +292,7 @@ function InfiniteCanvasPage() {
const [selectedConnectionId, setSelectedConnectionId] = useState<string | null>(null); const [selectedConnectionId, setSelectedConnectionId] = useState<string | null>(null);
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null); const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null); const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const [agentPrefillPrompt, setAgentPrefillPrompt] = useState("");
const [isMiniMapOpen, setIsMiniMapOpen] = useState(false); const [isMiniMapOpen, setIsMiniMapOpen] = useState(false);
const [canvasAppearance, setCanvasAppearance] = useState<CanvasAppearance>(() => canvasAppearanceForTheme(colorTheme)); const [canvasAppearance, setCanvasAppearance] = useState<CanvasAppearance>(() => canvasAppearanceForTheme(colorTheme));
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>(DEFAULT_CANVAS_BACKGROUND_MODE); const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>(DEFAULT_CANVAS_BACKGROUND_MODE);
@@ -332,6 +334,15 @@ function InfiniteCanvasPage() {
const [cinematicAgentEntry, setCinematicAgentEntry] = useState(false); const [cinematicAgentEntry, setCinematicAgentEntry] = useState(false);
const { assistantOpen, closeAgent, openAgent } = useCanvasAssistantVisibility(); const { assistantOpen, closeAgent, openAgent } = useCanvasAssistantVisibility();
const agentMentionReferences = useMemo(() => buildCanvasAgentMentionReferences(nodes), [nodes]); const agentMentionReferences = useMemo(() => buildCanvasAgentMentionReferences(nodes), [nodes]);
const sendSelectionToAgent = useCallback((nodeId?: string) => {
const ids = nodeId ? [nodeId] : Array.from(selectedNodeIdsRef.current);
const references = ids.map((id) => agentMentionReferences.find((reference) => reference.nodeId === id)).filter((reference): reference is CanvasResourceReference => Boolean(reference));
if (!references.length) return;
setAgentPrefillPrompt(`${references.map(canvasResourceMentionToken).join(" ")} `);
openAgent();
setContextMenu(null);
}, [agentMentionReferences, openAgent]);
const { tasks: activeTasks } = useCanvasActiveTasks(projectId, projectLoaded); const { tasks: activeTasks } = useCanvasActiveTasks(projectId, projectLoaded);
const { focusMode, enterFocusMode, exitFocusMode, toggleFocusMode } = useFocusMode(); const { focusMode, enterFocusMode, exitFocusMode, toggleFocusMode } = useFocusMode();
const [focusDockRevealed, setFocusDockRevealed] = useState(false); const [focusDockRevealed, setFocusDockRevealed] = useState(false);
@@ -2559,7 +2570,7 @@ function InfiniteCanvasPage() {
) : null} ) : null}
</div> </div>
<CanvasCloudAgentPanel canvasId={projectId} domainProjectId={currentProject?.projectId} nodeCount={nodes.length} references={agentMentionReferences} open={assistantOpen} onOpen={openAgent} onCollapse={closeAgent} onFocusNode={(nodeId) => { <CanvasCloudAgentPanel canvasId={projectId} domainProjectId={currentProject?.projectId} nodeCount={nodes.length} references={agentMentionReferences} prefillPrompt={agentPrefillPrompt} open={assistantOpen} onOpen={openAgent} onCollapse={closeAgent} onFocusNode={(nodeId) => {
if (!nodesRef.current.some((node) => node.id === nodeId)) { message.info("该节点已删除或尚未同步到画布"); return; } if (!nodesRef.current.some((node) => node.id === nodeId)) { message.info("该节点已删除或尚未同步到画布"); return; }
focusCanvasNode(nodeId); focusCanvasNode(nodeId);
}} /> }} />
@@ -2674,6 +2685,7 @@ function InfiniteCanvasPage() {
onCreateReferenceGroup={createReferenceGroup} onCreateReferenceGroup={createReferenceGroup}
onBatchConnect={() => beginBatchConnectionMode(Array.from(selectedNodeIds))} onBatchConnect={() => beginBatchConnectionMode(Array.from(selectedNodeIds))}
onMergeVideos={() => void mergeSelectedVideos()} onMergeVideos={() => void mergeSelectedVideos()}
onSendSelectionToAgent={() => sendSelectionToAgent()}
/> />
) : null} ) : null}
@@ -2820,6 +2832,7 @@ function InfiniteCanvasPage() {
onSpreadSelection={spreadSelectedNodes} onSpreadSelection={spreadSelectedNodes}
onCopySelection={copySelectedNodes} onCopySelection={copySelectedNodes}
onDeleteSelection={() => deleteNodes(selectedNodeIds)} onDeleteSelection={() => deleteNodes(selectedNodeIds)}
onSendToAgent={() => sendSelectionToAgent(contextMenu?.type === "node" && selectedNodeIds.size <= 1 ? contextMenu.nodeId : undefined)}
/> />
<CanvasUploadModal open={uploadModalOpen} onClose={closeUploadModal} onUpload={handleUploadFiles} /> <CanvasUploadModal open={uploadModalOpen} onClose={closeUploadModal} onUpload={handleUploadFiles} />