feat(canvas): add send to Agent shortcuts
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- 新增画布多选工具栏和节点右键菜单的「发送到 Agent」快捷入口,点击后自动带入节点引用。
|
||||
|
||||
## 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` 镜像。
|
||||
|
||||
@@ -5,6 +5,12 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
|
||||
|
||||
# 待测试
|
||||
|
||||
## 画布节点快速引用 Agent
|
||||
|
||||
- 多选节点后,选区浮动工具栏应显示 `@` 图标;点击「发送到 Agent」后,Agent 输入框应自动带入所有选中节点引用。
|
||||
- 单节点右键菜单(图片、视频、文本及其他节点)应显示「发送到 Agent」;点击后仅带入当前节点引用。
|
||||
- 引用应以节点 mention token 保存,发送前可以继续补充操作描述。
|
||||
|
||||
## 欢迎页开放开关
|
||||
|
||||
- 管理员在 `/admin/settings/appearance` 的英文品牌标识下方切换欢迎页,立即保存,沿用功能配置保存及审计流程,默认开启;功能开放页不再编辑该字段。
|
||||
|
||||
@@ -27,11 +27,11 @@ import { CanvasCloudAgentSettings, agentPermissionLabel, agentPermissionMenuItem
|
||||
import { useAgentPanelLayout } from "./use-agent-panel-layout";
|
||||
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 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 config = useEffectiveConfig();
|
||||
const updateConfig = useConfigStore((state) => state.updateConfig);
|
||||
@@ -42,6 +42,7 @@ export function CanvasCloudAgentPanel({ canvasId, domainProjectId, nodeCount, re
|
||||
const [connectionEpoch, setConnectionEpoch] = useState(0);
|
||||
const [messages, setMessages] = useState<CloudAgentChatMessage[]>([]);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const lastPrefillPromptRef = useRef("");
|
||||
const [reasoningMode, setReasoningMode] = useState<AgentReasoningMode>("off");
|
||||
const [profileView, setProfileView] = useState<AgentProfileView | null>(null);
|
||||
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 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(() => {
|
||||
if (!open || view !== "chat") setSkillsOpen(false);
|
||||
}, [open, view]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AnimatePresence } from "motion/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 { ASSET_CATEGORY_OPTIONS } from "@/lib/asset-category";
|
||||
@@ -55,6 +55,7 @@ type CanvasNodeContextMenuProps = {
|
||||
onSpreadSelection?: () => void;
|
||||
onCopySelection?: () => void;
|
||||
onDeleteSelection?: () => void;
|
||||
onSendToAgent?: () => void;
|
||||
};
|
||||
|
||||
export function CanvasNodeContextMenu({
|
||||
@@ -94,6 +95,7 @@ export function CanvasNodeContextMenu({
|
||||
onSpreadSelection,
|
||||
onCopySelection,
|
||||
onDeleteSelection,
|
||||
onSendToAgent,
|
||||
}: CanvasNodeContextMenuProps) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
@@ -200,6 +202,7 @@ export function CanvasNodeContextMenu({
|
||||
<MenuButton icon={<LayoutTemplate />} label="自适应整理画布" detail="保持相对布局并加大边距" disabled={!onSpreadSelection} onClick={() => onSpreadSelection && runAction(onSpreadSelection)} />
|
||||
<MenuDivider />
|
||||
<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)} />
|
||||
</>
|
||||
) : menu.type === "node" ? (
|
||||
@@ -211,6 +214,7 @@ export function CanvasNodeContextMenu({
|
||||
<MenuButton icon={<UserRound />} label="查看角色详情" onClick={() => runAction(onEditText)} />
|
||||
<MenuDivider />
|
||||
<MenuSection label="节点" />
|
||||
<MenuButton icon={<AtSign />} label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} />
|
||||
<MenuButton icon={<Copy />} label="复制角色引用" shortcut="⌘C" onClick={() => runAction(onCopyNode)} />
|
||||
<MenuButton icon={<Layers3 />} label="创建引用副本" shortcut="⌘D" onClick={() => runAction(onDuplicate)} />
|
||||
<MenuButton icon={<Trash2 />} label="删除节点" danger onClick={() => runAction(onDelete)} />
|
||||
@@ -224,6 +228,7 @@ export function CanvasNodeContextMenu({
|
||||
{isImage ? <MenuButton icon={<CloudUpload />} label="上传到方舟素材库" onClick={() => runAction(onUploadToArkPrivateAsset)} /> : null}
|
||||
<MenuDivider />
|
||||
<MenuSection label="节点" />
|
||||
<MenuButton icon={<AtSign />} label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} />
|
||||
<MenuButton icon={<Copy />} label="复制节点" shortcut="⌘C" onClick={() => runAction(onCopyNode)} />
|
||||
{isImage ? <MenuButton icon={<Clipboard />} label="复制图片" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null}
|
||||
<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}
|
||||
<MenuDivider />
|
||||
<MenuSection label="副本与内容" />
|
||||
<MenuButton icon={<AtSign />} label="发送到 Agent" disabled={!onSendToAgent} onClick={() => onSendToAgent && runAction(onSendToAgent)} />
|
||||
<MenuButton icon={<Copy />} label={isFrame ? `复制${isFolder ? "文件夹" : "背板"}及内容` : "复制节点"} shortcut="⌘C" onClick={() => runAction(onCopyNode)} />
|
||||
{isText ? <MenuButton icon={<Clipboard />} label="复制文本" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null}
|
||||
<MenuButton icon={<Copy />} label={isFrame ? `创建${isFolder ? "文件夹" : "背板"}副本` : "创建参数变体"} shortcut="⌘D" onClick={() => runAction(onDuplicate)} />
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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-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-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",
|
||||
toolbar: "selection",
|
||||
|
||||
@@ -68,6 +68,7 @@ export type ToolbarHandlers = {
|
||||
onCreateReferenceGroup: () => void;
|
||||
onBatchConnect: () => void;
|
||||
onMergeVideos: () => void;
|
||||
onSendSelectionToAgent: () => void;
|
||||
// 节点悬停工具栏——节点操作(均接收当前节点)
|
||||
onNodeInfo: (node: CanvasNodeData) => void;
|
||||
onNodeDelete: (node: CanvasNodeData) => void;
|
||||
|
||||
@@ -42,6 +42,7 @@ type CanvasProjectContextMenuProps = {
|
||||
onSpreadSelection: () => void;
|
||||
onCopySelection: () => void;
|
||||
onDeleteSelection: () => void;
|
||||
onSendToAgent: () => void;
|
||||
};
|
||||
|
||||
export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props }: CanvasProjectContextMenuProps) {
|
||||
@@ -114,6 +115,7 @@ export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props
|
||||
onSpreadSelection={props.onSpreadSelection}
|
||||
onCopySelection={props.onCopySelection}
|
||||
onDeleteSelection={props.onDeleteSelection}
|
||||
onSendToAgent={props.onSendToAgent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,14 @@ type CanvasProjectSelectionToolbarProps = {
|
||||
onCreateReferenceGroup: () => void;
|
||||
onBatchConnect: () => 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 handlers = {
|
||||
onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos,
|
||||
onAlign, onArrange, onCreateStoryboard, onCreateReferenceGroup, onBatchConnect, onMergeVideos, onSendSelectionToAgent,
|
||||
} as Partial<ToolbarHandlers> as ToolbarHandlers;
|
||||
|
||||
const ctx: ToolContext = {
|
||||
|
||||
@@ -77,6 +77,7 @@ import { writeCanvasNodePrompt } from "@/lib/canvas/canvas-node-prompt";
|
||||
import {
|
||||
applyCanvasConnectionPromptSync,
|
||||
buildCanvasAgentMentionReferences,
|
||||
canvasResourceMentionToken,
|
||||
buildCanvasNodeMentionReferenceMap,
|
||||
buildCanvasResourceReferences,
|
||||
getContextResourceNodes,
|
||||
@@ -291,6 +292,7 @@ function InfiniteCanvasPage() {
|
||||
const [selectedConnectionId, setSelectedConnectionId] = useState<string | null>(null);
|
||||
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||
const [agentPrefillPrompt, setAgentPrefillPrompt] = useState("");
|
||||
const [isMiniMapOpen, setIsMiniMapOpen] = useState(false);
|
||||
const [canvasAppearance, setCanvasAppearance] = useState<CanvasAppearance>(() => canvasAppearanceForTheme(colorTheme));
|
||||
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>(DEFAULT_CANVAS_BACKGROUND_MODE);
|
||||
@@ -332,6 +334,15 @@ function InfiniteCanvasPage() {
|
||||
const [cinematicAgentEntry, setCinematicAgentEntry] = useState(false);
|
||||
const { assistantOpen, closeAgent, openAgent } = useCanvasAssistantVisibility();
|
||||
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 { focusMode, enterFocusMode, exitFocusMode, toggleFocusMode } = useFocusMode();
|
||||
const [focusDockRevealed, setFocusDockRevealed] = useState(false);
|
||||
@@ -2559,7 +2570,7 @@ function InfiniteCanvasPage() {
|
||||
) : null}
|
||||
</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; }
|
||||
focusCanvasNode(nodeId);
|
||||
}} />
|
||||
@@ -2674,6 +2685,7 @@ function InfiniteCanvasPage() {
|
||||
onCreateReferenceGroup={createReferenceGroup}
|
||||
onBatchConnect={() => beginBatchConnectionMode(Array.from(selectedNodeIds))}
|
||||
onMergeVideos={() => void mergeSelectedVideos()}
|
||||
onSendSelectionToAgent={() => sendSelectionToAgent()}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -2820,6 +2832,7 @@ function InfiniteCanvasPage() {
|
||||
onSpreadSelection={spreadSelectedNodes}
|
||||
onCopySelection={copySelectedNodes}
|
||||
onDeleteSelection={() => deleteNodes(selectedNodeIds)}
|
||||
onSendToAgent={() => sendSelectionToAgent(contextMenu?.type === "node" && selectedNodeIds.size <= 1 ? contextMenu.nodeId : undefined)}
|
||||
/>
|
||||
|
||||
<CanvasUploadModal open={uploadModalOpen} onClose={closeUploadModal} onUpload={handleUploadFiles} />
|
||||
|
||||
Reference in New Issue
Block a user