mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 08:54:23 +08:00
feat(agent): implement global Agent panel with site navigation and interrupt functionality
This commit is contained in:
@@ -24,7 +24,6 @@ import { NODE_DEFAULT_SIZE, getNodeSpec } from "@/constant/canvas";
|
||||
import { ActiveConnectionPath, ConnectionPath } from "@/components/canvas/canvas-connections";
|
||||
import { CanvasConfigComposer } from "@/components/canvas/canvas-config-composer";
|
||||
import { CanvasConfigNodePanel } from "@/components/canvas/canvas-config-node-panel";
|
||||
import { CANVAS_AGENT_PANEL_MOTION_MS, CanvasAssistantPanel } from "@/components/canvas/canvas-assistant-panel";
|
||||
import { CanvasNodeContextMenu } from "@/components/canvas/canvas-context-menu";
|
||||
import { CanvasNodeAngleDialog, type CanvasImageAngleParams } from "@/components/canvas/canvas-node-angle-dialog";
|
||||
import { CanvasNodeCropDialog, type CanvasImageCropRect } from "@/components/canvas/canvas-node-crop-dialog";
|
||||
@@ -40,8 +39,7 @@ import { CanvasNodePromptPanel, type CanvasNodeGenerationMode } from "@/componen
|
||||
import { CanvasToolbar } from "@/components/canvas/canvas-toolbar";
|
||||
import { AssetPickerModal, type InsertAssetPayload } from "@/components/canvas/asset-picker-modal";
|
||||
import { CanvasZoomControls } from "@/components/canvas/canvas-zoom-controls";
|
||||
import { CanvasLocalAgentPanel } from "@/components/canvas/canvas-local-agent-panel";
|
||||
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
|
||||
import { useAgentStore } from "@/stores/use-agent-store";
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { applyCanvasAgentOps, type CanvasAgentOp, type CanvasAgentSnapshot } from "@/lib/canvas/canvas-agent-ops";
|
||||
import { buildCanvasResourceReferences, buildNodeMentionReferences } from "@/lib/canvas/canvas-resource-references";
|
||||
@@ -220,9 +218,13 @@ function InfiniteCanvasPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectId = params.id || "";
|
||||
const localAgentConnected = useCanvasAgentStore((state) => state.connected);
|
||||
const localAgentActivity = useCanvasAgentStore((state) => state.activity);
|
||||
const localAgentEnabled = useCanvasAgentStore((state) => state.enabled);
|
||||
const localAgentConnected = useAgentStore((state) => state.connected);
|
||||
const localAgentActivity = useAgentStore((state) => state.activity);
|
||||
const localAgentEnabled = useAgentStore((state) => state.enabled);
|
||||
const agentPanelOpen = useAgentStore((state) => state.panelOpen);
|
||||
const toggleAgentPanel = useAgentStore((state) => state.togglePanel);
|
||||
const openAgentPanel = useAgentStore((state) => state.openPanel);
|
||||
const setAgentCanvasContext = useAgentStore((state) => state.setCanvasContext);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imageInputRef = useRef<HTMLInputElement>(null);
|
||||
const uploadTargetRef = useRef<{ nodeId?: string; position?: Position } | null>(null);
|
||||
@@ -299,12 +301,7 @@ function InfiniteCanvasPage() {
|
||||
const [superResolveNodeId, setSuperResolveNodeId] = useState<string | null>(null);
|
||||
const [angleNodeId, setAngleNodeId] = useState<string | null>(null);
|
||||
const [previewNodeId, setPreviewNodeId] = useState<string | null>(null);
|
||||
const [assistantCollapsed, setAssistantCollapsed] = useState(true);
|
||||
const [assistantMounted, setAssistantMounted] = useState(false);
|
||||
const [assistantClosing, setAssistantClosing] = useState(false);
|
||||
const [agentUndoSnapshot, setAgentUndoSnapshot] = useState<CanvasAgentSnapshot | null>(null);
|
||||
const codexAutoConnect = ["new", "recent", "choose"].includes(searchParams.get("mode") || "");
|
||||
const codexCompactAgent = codexAutoConnect && searchParams.has("agentUrl");
|
||||
const [titleEditing, setTitleEditing] = useState(false);
|
||||
const [titleDraft, setTitleDraft] = useState("");
|
||||
const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false });
|
||||
@@ -321,7 +318,6 @@ function InfiniteCanvasPage() {
|
||||
const connectingParamsRef = useRef(connectingParams);
|
||||
const connectionTargetNodeIdRef = useRef(connectionTargetNodeId);
|
||||
const selectionBoxRef = useRef(selectionBox);
|
||||
const agentCloseTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pendingConnectionCreateRef = useRef(pendingConnectionCreate);
|
||||
const generationRequestsRef = useRef(new Map<string, CanvasGenerationRequest>());
|
||||
|
||||
@@ -430,8 +426,8 @@ function InfiniteCanvasPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectLoaded || !["new", "recent", "choose"].includes(searchParams.get("mode") || "")) return;
|
||||
if (!searchParams.has("agentUrl")) openAgent();
|
||||
}, [projectLoaded, searchParams]);
|
||||
if (!searchParams.has("agentUrl")) openAgentPanel();
|
||||
}, [openAgentPanel, projectLoaded, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectLoaded || applyingHistoryRef.current || historyPausedRef.current) return;
|
||||
@@ -459,13 +455,6 @@ function InfiniteCanvasPage() {
|
||||
};
|
||||
}, [activeChatId, backgroundMode, chatSessions, connections, createHistoryEntry, nodes, projectLoaded, showImageInfo]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (agentCloseTimerRef.current) clearTimeout(agentCloseTimerRef.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectLoaded || historyPausedRef.current) return;
|
||||
updateProject(projectId, { nodes, connections, chatSessions, activeChatId, backgroundMode, showImageInfo });
|
||||
@@ -769,6 +758,11 @@ function InfiniteCanvasPage() {
|
||||
setAgentUndoSnapshot(null);
|
||||
return { ...agentUndoSnapshot, projectId, title: currentProject?.title || "未命名画布" };
|
||||
}, [agentUndoSnapshot, currentProject?.title, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
setAgentCanvasContext({ snapshot: agentSnapshot, applyOps: applyAgentOps, undoOps: undoAgentOps, canUndo: Boolean(agentUndoSnapshot) });
|
||||
return () => setAgentCanvasContext(null);
|
||||
}, [agentSnapshot, applyAgentOps, agentUndoSnapshot, setAgentCanvasContext, undoAgentOps]);
|
||||
const createNode = useCallback(
|
||||
(type: CanvasNodeType, position?: Position) => {
|
||||
const targetPosition = position || getCanvasCenter();
|
||||
@@ -1939,20 +1933,6 @@ function InfiniteCanvasPage() {
|
||||
[createAudioFileNode, createImageFileNode, createVideoFileNode, screenToCanvas],
|
||||
);
|
||||
|
||||
const pasteAssistantImage = useCallback(
|
||||
(file: File) => {
|
||||
const position = screenToCanvas((containerRef.current?.getBoundingClientRect().left || 0) + size.width / 2, (containerRef.current?.getBoundingClientRect().top || 0) + size.height / 2);
|
||||
void createImageFileNode(file, position);
|
||||
message.success("已从剪切板添加图片");
|
||||
},
|
||||
[createImageFileNode, message, screenToCanvas, size.height, size.width],
|
||||
);
|
||||
|
||||
const handleAssistantSessionsChange = useCallback((sessions: CanvasAssistantSession[], activeId: string | null) => {
|
||||
setChatSessions(sessions);
|
||||
setActiveChatId(activeId);
|
||||
}, []);
|
||||
|
||||
const startTitleEditing = useCallback(() => {
|
||||
setTitleDraft(currentProject?.title || "未命名画布");
|
||||
setTitleEditing(true);
|
||||
@@ -2488,27 +2468,6 @@ function InfiniteCanvasPage() {
|
||||
[insertAssistantImage, insertAssistantText, screenToCanvas, size.height, size.width],
|
||||
);
|
||||
|
||||
const assistantOpen = assistantMounted && !assistantCollapsed;
|
||||
const openAgent = () => {
|
||||
if (agentCloseTimerRef.current) {
|
||||
clearTimeout(agentCloseTimerRef.current);
|
||||
agentCloseTimerRef.current = null;
|
||||
}
|
||||
setAssistantMounted(true);
|
||||
setAssistantClosing(false);
|
||||
setAssistantCollapsed(false);
|
||||
};
|
||||
const closeAgent = () => {
|
||||
if (!assistantMounted || assistantClosing) return;
|
||||
setAssistantCollapsed(true);
|
||||
setAssistantClosing(true);
|
||||
agentCloseTimerRef.current = setTimeout(() => {
|
||||
agentCloseTimerRef.current = null;
|
||||
setAssistantMounted(false);
|
||||
setAssistantClosing(false);
|
||||
}, CANVAS_AGENT_PANEL_MOTION_MS);
|
||||
};
|
||||
|
||||
if (!projectLoaded) return <CanvasRefreshShell />;
|
||||
|
||||
return (
|
||||
@@ -2531,9 +2490,9 @@ function InfiniteCanvasPage() {
|
||||
onImportImage={() => handleUploadRequest()}
|
||||
onUndo={undoCanvas}
|
||||
onRedo={redoCanvas}
|
||||
agentOpen={assistantOpen}
|
||||
agentOpen={agentPanelOpen}
|
||||
compactAgentStatus={{ connected: localAgentConnected, enabled: localAgentEnabled, activity: localAgentActivity }}
|
||||
onToggleAgent={() => (assistantOpen ? closeAgent() : openAgent())}
|
||||
onToggleAgent={toggleAgentPanel}
|
||||
/>
|
||||
|
||||
<InfiniteCanvas
|
||||
@@ -2816,26 +2775,7 @@ function InfiniteCanvasPage() {
|
||||
</Modal>
|
||||
|
||||
<AssetPickerModal open={assetPickerOpen} onInsert={handleAssetInsert} onClose={() => setAssetPickerOpen(false)} />
|
||||
{codexCompactAgent && !assistantMounted ? <CanvasLocalAgentPanel headless snapshot={agentSnapshot} canUndoOps={Boolean(agentUndoSnapshot)} onApplyOps={applyAgentOps} onUndoOps={undoAgentOps} autoConnect={codexAutoConnect} /> : null}
|
||||
</section>
|
||||
{assistantMounted ? (
|
||||
<CanvasAssistantPanel
|
||||
nodes={nodes}
|
||||
selectedNodeIds={selectedNodeIds}
|
||||
snapshot={agentSnapshot}
|
||||
sessions={chatSessions}
|
||||
activeSessionId={activeChatId}
|
||||
onSelectNodeIds={setSelectedNodeIds}
|
||||
onSessionsChange={handleAssistantSessionsChange}
|
||||
onApplyOps={applyAgentOps}
|
||||
canUndoOps={Boolean(agentUndoSnapshot)}
|
||||
onUndoOps={undoAgentOps}
|
||||
onPasteImage={pasteAssistantImage}
|
||||
autoConnectLocal={codexAutoConnect}
|
||||
closing={assistantClosing}
|
||||
onCollapse={closeAgent}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user