Skip to content

Commit 2496c63

Browse files
SonAIengineclaude
andcommitted
feat: canvas:command 프론트엔드 패치 — AI 챗봇 캔버스 조작 핸들러
- patch-canvas-chatbot.js: Canvas + page.tsx 자동 패치 스크립트 - Canvas/index.tsx useImperativeHandle 확장: - addNodeAtPosition(nodeData, position) — 월드 좌표로 노드 추가 - deleteNodeById(nodeId) — 노드 삭제 - addEdgeBetween(source, sourcePort, target, targetPort) — 엣지 생성 - removeEdgeById(edgeId) — 엣지 삭제 - getAvailableNodes() — 추가 가능한 노드 스펙 목록 - canvas/page.tsx: canvas:command Tauri 이벤트 리스너 주입 - get_nodes, get_available_nodes, add_node, remove_node - connect, disconnect, update_node_param, save - canvas:result로 Rust에 결과 반환 (requestId 매칭) - patch-frontend.sh에 Step 7로 등록 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9eb2778 commit 2496c63

2 files changed

Lines changed: 262 additions & 0 deletions

File tree

scripts/patch-canvas-chatbot.js

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Canvas에 AI 챗봇 이벤트 핸들러를 패치하는 스크립트
4+
*
5+
* 1. Canvas/index.tsx의 useImperativeHandle에 누락 함수 추가
6+
* - addNodeAtPosition(nodeData, position)
7+
* - deleteNodeById(nodeId)
8+
* - addEdgeBetween(source, sourcePort, target, targetPort)
9+
* - getAvailableNodeSpecs()
10+
*
11+
* 2. canvas/page.tsx에 canvas:command 이벤트 리스너 주입
12+
*/
13+
const fs = require('fs');
14+
const path = require('path');
15+
16+
const frontendDir = process.argv[2];
17+
if (!frontendDir) {
18+
console.error('Usage: node patch-canvas-chatbot.js <frontend-dir>');
19+
process.exit(1);
20+
}
21+
22+
// ============================================================
23+
// Part 1: Canvas/index.tsx — useImperativeHandle 확장
24+
// ============================================================
25+
26+
const canvasPath = path.join(frontendDir,
27+
'src/app/components/pages/workflow/canvas/components/Canvas/index.tsx');
28+
29+
if (!fs.existsSync(canvasPath)) {
30+
console.log('[WARN] Canvas/index.tsx not found — skipping canvas patch');
31+
process.exit(0);
32+
}
33+
34+
let canvasContent = fs.readFileSync(canvasPath, 'utf8');
35+
36+
if (canvasContent.includes('addNodeAtPosition')) {
37+
console.log('[INFO] Canvas already patched');
38+
} else {
39+
console.log('[PATCH] Canvas/index.tsx — adding imperative handle functions...');
40+
41+
// Insert before the closing "}));" of useImperativeHandle
42+
// Find: "updateNodeParameter: (nodeId..." block ending with "},\n }));"
43+
canvasContent = canvasContent.replace(
44+
/updateNodeParameter: \(nodeId: string, paramId: string, value: string \| number \| boolean, skipHistory\?: boolean, label\?: string\): void => \{\s*\n\s*updateNodeParameter\(nodeId, paramId, value, skipHistory, label\);\s*\n\s*\},\s*\n\s*\}\)\);/,
45+
`updateNodeParameter: (nodeId: string, paramId: string, value: string | number | boolean, skipHistory?: boolean, label?: string): void => {
46+
updateNodeParameter(nodeId, paramId, value, skipHistory, label);
47+
},
48+
// === AI Chatbot: Canvas command API ===
49+
addNodeAtPosition: (nodeData: NodeData, position?: { x: number; y: number }): string => {
50+
const pos = position || { x: 200 + Math.random() * 400, y: 100 + Math.random() * 300 };
51+
const newNode = {
52+
id: nodeData.id + '-' + Date.now(),
53+
data: nodeData,
54+
position: pos,
55+
isExpanded: true,
56+
};
57+
addNode(newNode);
58+
return newNode.id;
59+
},
60+
deleteNodeById: (nodeId: string): boolean => {
61+
const node = nodesRef.current.find(n => n.id === nodeId);
62+
if (!node) return false;
63+
const connectedEdges = edgesRef.current.filter(
64+
e => e.source === nodeId || e.target === nodeId
65+
);
66+
deleteNode(nodeId, connectedEdges);
67+
return true;
68+
},
69+
addEdgeBetween: (sourceNode: string, sourcePort: string, targetNode: string, targetPort: string): string | null => {
70+
const edgeId = sourceNode + ':' + sourcePort + '->' + targetNode + ':' + targetPort;
71+
const newEdge = {
72+
id: edgeId,
73+
source: sourceNode,
74+
sourceHandle: sourcePort,
75+
target: targetNode,
76+
targetHandle: targetPort,
77+
};
78+
addEdge(newEdge);
79+
return edgeId;
80+
},
81+
removeEdgeById: (edgeId: string): boolean => {
82+
const edge = edgesRef.current.find(e => e.id === edgeId);
83+
if (!edge) return false;
84+
removeEdge(edgeId);
85+
return true;
86+
},
87+
getAvailableNodes: (): any[] => {
88+
return availableNodeSpecs || [];
89+
},
90+
}));`
91+
);
92+
93+
// Verify edgesRef exists (it should, since edges are managed internally)
94+
if (!canvasContent.includes('edgesRef')) {
95+
// Add edgesRef if missing
96+
canvasContent = canvasContent.replace(
97+
/const nodesRef = useRef/,
98+
'const edgesRef = useRef(edges);\n const nodesRef = useRef'
99+
);
100+
// Keep edgesRef in sync
101+
if (!canvasContent.includes('edgesRef.current = edges')) {
102+
canvasContent = canvasContent.replace(
103+
/nodesRef\.current = nodes;/,
104+
'nodesRef.current = nodes;\n edgesRef.current = edges;'
105+
);
106+
}
107+
}
108+
109+
fs.writeFileSync(canvasPath, canvasContent);
110+
console.log('[OK] Canvas/index.tsx patched');
111+
}
112+
113+
// ============================================================
114+
// Part 2: canvas/page.tsx — canvas:command 이벤트 리스너
115+
// ============================================================
116+
117+
const pagePath = path.join(frontendDir, 'src/app/canvas/page.tsx');
118+
119+
if (!fs.existsSync(pagePath)) {
120+
console.log('[WARN] canvas/page.tsx not found — skipping page patch');
121+
process.exit(0);
122+
}
123+
124+
let pageContent = fs.readFileSync(pagePath, 'utf8');
125+
126+
if (pageContent.includes('canvas:command')) {
127+
console.log('[INFO] canvas/page.tsx already patched');
128+
process.exit(0);
129+
}
130+
131+
console.log('[PATCH] canvas/page.tsx — adding canvas:command event listener...');
132+
133+
// Find a useEffect that runs after canvas is ready, and add our listener
134+
// Strategy: add a new useEffect after the imports and component setup
135+
136+
// 1. Add isTauri import if not present
137+
if (!pageContent.includes('isTauri')) {
138+
pageContent = pageContent.replace(
139+
/(import.*from '@\/app\/canvas\/types';)/,
140+
"$1\nimport { isTauri } from '@/app/_common/api/core/platform';"
141+
);
142+
}
143+
144+
// 2. Find a good place to inject the canvas:command useEffect
145+
// After "const canvasRef = useRef<any>(null);" line
146+
pageContent = pageContent.replace(
147+
/(const canvasRef = useRef<any>\(null\);)/,
148+
`$1
149+
150+
// === AI Chatbot: Canvas command handler ===
151+
useEffect(() => {
152+
if (!isTauri()) return;
153+
let unlisten: (() => void) | null = null;
154+
155+
import('@tauri-apps/api/event').then(({ listen, emit }) => {
156+
listen('canvas:command', async (event: any) => {
157+
const { requestId, action, params } = event.payload;
158+
const canvas = canvasRef.current;
159+
let result: any = { error: 'Canvas not ready' };
160+
161+
if (canvas) {
162+
try {
163+
switch (action) {
164+
case 'get_nodes': {
165+
const state = canvas.getCanvasState();
166+
result = (state.nodes || []).map((n: any) => ({
167+
id: n.id,
168+
type: n.data?.nodeName || n.data?.id,
169+
name: n.data?.name || n.data?.nodeName,
170+
position: n.position,
171+
parameters: (n.data?.parameters || []).map((p: any) => ({
172+
name: p.name, value: p.value, type: p.type
173+
})),
174+
}));
175+
break;
176+
}
177+
case 'get_available_nodes': {
178+
const specs = canvas.getAvailableNodes?.() || [];
179+
const category = params?.category;
180+
const filtered = category
181+
? specs.filter((s: any) => s.id?.startsWith(category))
182+
: specs;
183+
result = filtered.map((s: any) => ({
184+
id: s.id,
185+
name: s.nodeName || s.name,
186+
category: s.id?.split('/')[0],
187+
inputs: (s.inputs || []).map((i: any) => i.name),
188+
outputs: (s.outputs || []).map((o: any) => o.name),
189+
}));
190+
break;
191+
}
192+
case 'add_node': {
193+
const nodeType = params?.node_type;
194+
const specs = canvas.getAvailableNodes?.() || [];
195+
const spec = specs.find((s: any) => s.id === nodeType);
196+
if (!spec) {
197+
result = { error: 'Node type not found: ' + nodeType };
198+
} else {
199+
const nodeId = canvas.addNodeAtPosition(spec, params?.position);
200+
result = { success: true, node_id: nodeId };
201+
}
202+
break;
203+
}
204+
case 'remove_node': {
205+
const success = canvas.deleteNodeById?.(params?.node_id);
206+
result = { success: !!success };
207+
break;
208+
}
209+
case 'connect': {
210+
const edgeId = canvas.addEdgeBetween?.(
211+
params?.source_node, params?.source_port,
212+
params?.target_node, params?.target_port
213+
);
214+
result = edgeId ? { success: true, edge_id: edgeId } : { error: 'Failed to connect' };
215+
break;
216+
}
217+
case 'disconnect': {
218+
const success = canvas.removeEdgeById?.(params?.edge_id);
219+
result = { success: !!success };
220+
break;
221+
}
222+
case 'update_node_param': {
223+
canvas.updateNodeParameter?.(
224+
params?.node_id, params?.param_name, params?.value
225+
);
226+
result = { success: true };
227+
break;
228+
}
229+
case 'save': {
230+
// Trigger the save button click programmatically
231+
const saveBtn = document.querySelector('[data-testid="save-workflow"]') as HTMLButtonElement;
232+
if (saveBtn) {
233+
saveBtn.click();
234+
result = { success: true };
235+
} else {
236+
result = { error: 'Save button not found' };
237+
}
238+
break;
239+
}
240+
default:
241+
result = { error: 'Unknown canvas action: ' + action };
242+
}
243+
} catch (err: any) {
244+
result = { error: err.message || String(err) };
245+
}
246+
}
247+
248+
// Send result back to Rust
249+
emit('canvas:result', { requestId, result: JSON.stringify(result) });
250+
}).then(fn => { unlisten = fn; });
251+
}).catch(() => {});
252+
253+
return () => { if (unlisten) unlisten(); };
254+
}, []);`
255+
);
256+
257+
fs.writeFileSync(pagePath, pageContent);
258+
console.log('[OK] canvas/page.tsx patched');

scripts/patch-frontend.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,10 @@ fi
205205
echo "[PATCH] 사이드바 AI CLI 버튼 패치"
206206
node "$SCRIPT_DIR/patch-sidebar-cli.js" "$FRONTEND_DIR"
207207

208+
# 7. 캔버스에 AI 챗봇 이벤트 핸들러 추가 (canvas:command 처리)
209+
echo "[PATCH] 캔버스 AI 챗봇 패치"
210+
node "$SCRIPT_DIR/patch-canvas-chatbot.js" "$FRONTEND_DIR"
211+
208212
echo ""
209213
echo "================================================"
210214
echo "패치 완료!"

0 commit comments

Comments
 (0)