Skip to content

Commit 7b15741

Browse files
authored
ai improvements (#73)
* reset placeholder text * input improvements * better spacing * vertical spacing * auto fit view
1 parent 0eb2147 commit 7b15741

File tree

2 files changed

+53
-4
lines changed

2 files changed

+53
-4
lines changed

app/api/ai/generate/route.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,15 @@ Node structure:
171171
}
172172
}
173173
174+
NODE POSITIONING RULES:
175+
- Nodes are squares, so use equal spacing in both directions
176+
- Horizontal spacing between sequential nodes: 250px (e.g., x: 100, then x: 350, then x: 600)
177+
- Vertical spacing for parallel branches: 250px (e.g., y: 75, y: 325, y: 575)
178+
- Start trigger node at position {"x": 100, "y": 200}
179+
- For linear workflows: increment x by 250 for each subsequent node, keep y constant
180+
- For branching workflows: keep x the same for parallel branches, space y by 250px per branch
181+
- When adding nodes to existing workflows, position new nodes 250px away from existing nodes
182+
174183
Trigger types:
175184
- Manual: {"triggerType": "Manual"}
176185
- Webhook: {"triggerType": "Webhook", "webhookPath": "/webhooks/name", ...}
@@ -221,12 +230,21 @@ WORKFLOW FLOW:
221230
- For linear workflows: trigger -> action1 -> action2 -> etc
222231
- For branching (conditions): one source can connect to multiple targets
223232
224-
Example output:
233+
Example output (linear workflow with 250px horizontal spacing):
225234
{"op": "setName", "name": "Contact Form Workflow"}
226235
{"op": "setDescription", "description": "Processes contact form submissions"}
227236
{"op": "addNode", "node": {"id": "trigger-1", "type": "trigger", "position": {"x": 100, "y": 200}, "data": {"label": "Contact Form", "type": "trigger", "config": {"triggerType": "Manual"}, "status": "idle"}}}
228-
{"op": "addNode", "node": {"id": "send-email", "type": "action", "position": {"x": 400, "y": 200}, "data": {"label": "Send Email", "type": "action", "config": {"actionType": "Send Email", "emailTo": "admin@example.com", "emailSubject": "New Contact", "emailBody": "New contact form submission"}, "status": "idle"}}}
237+
{"op": "addNode", "node": {"id": "send-email", "type": "action", "position": {"x": 350, "y": 200}, "data": {"label": "Send Email", "type": "action", "config": {"actionType": "Send Email", "emailTo": "admin@example.com", "emailSubject": "New Contact", "emailBody": "New contact form submission"}, "status": "idle"}}}
238+
{"op": "addNode", "node": {"id": "log-action", "type": "action", "position": {"x": 600, "y": 200}, "data": {"label": "Log Result", "type": "action", "config": {"actionType": "HTTP Request", "httpMethod": "POST", "endpoint": "https://api.example.com/log"}, "status": "idle"}}}
229239
{"op": "addEdge", "edge": {"id": "e1", "source": "trigger-1", "target": "send-email", "type": "default"}}
240+
{"op": "addEdge", "edge": {"id": "e2", "source": "send-email", "target": "log-action", "type": "default"}}
241+
242+
Example output (branching workflow with 250px vertical spacing):
243+
{"op": "addNode", "node": {"id": "trigger-1", "type": "trigger", "position": {"x": 100, "y": 200}, "data": {"label": "Webhook", "type": "trigger", "config": {"triggerType": "Webhook"}, "status": "idle"}}}
244+
{"op": "addNode", "node": {"id": "branch-a", "type": "action", "position": {"x": 350, "y": 75}, "data": {"label": "Branch A", "type": "action", "config": {"actionType": "Send Email"}, "status": "idle"}}}
245+
{"op": "addNode", "node": {"id": "branch-b", "type": "action", "position": {"x": 350, "y": 325}, "data": {"label": "Branch B", "type": "action", "config": {"actionType": "Send Slack Message"}, "status": "idle"}}}
246+
{"op": "addEdge", "edge": {"id": "e1", "source": "trigger-1", "target": "branch-a", "type": "default"}}
247+
{"op": "addEdge", "edge": {"id": "e2", "source": "trigger-1", "target": "branch-b", "type": "default"}}
230248
231249
REMEMBER: After adding all nodes, you MUST add edges to connect them! Every node should be reachable from the trigger.`;
232250

@@ -302,6 +320,7 @@ IMPORTANT: Output ONLY the operations needed to make the requested changes.
302320
- When connecting nodes, look at the node IDs in the current workflow list above
303321
- DO NOT output operations for existing nodes/edges unless specifically modifying them
304322
- Keep the existing workflow structure and only add/modify/remove what was requested
323+
- POSITIONING: When adding new nodes, look at existing node positions and place new nodes 250px away (horizontally or vertically) from existing nodes. Never overlap nodes.
305324
306325
Example: If user says "connect node A to node B", output:
307326
{"op": "addEdge", "edge": {"id": "e-new", "source": "A", "target": "B", "type": "default"}}`;

components/ai-elements/prompt.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { useReactFlow } from "@xyflow/react";
34
import { useAtom, useAtomValue } from "jotai";
45
import { ArrowUp } from "lucide-react";
56
import { useCallback, useEffect, useRef, useState } from "react";
@@ -34,6 +35,7 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
3435
const [_currentWorkflowId, setCurrentWorkflowId] = useAtom(currentWorkflowIdAtom);
3536
const [_currentWorkflowName, setCurrentWorkflowName] = useAtom(currentWorkflowNameAtom);
3637
const [_selectedNodeId, setSelectedNodeId] = useAtom(selectedNodeAtom);
38+
const { fitView } = useReactFlow();
3739

3840
// Filter out placeholder "add" nodes to get real nodes
3941
const realNodes = nodes.filter((node) => node.type !== "add");
@@ -59,7 +61,11 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
5961
setIsFocused(true);
6062
};
6163

62-
const handleBlur = () => {
64+
const handleBlur = (e: React.FocusEvent) => {
65+
// Don't collapse if focus is moving to another element within the container
66+
if (containerRef.current?.contains(e.relatedTarget as Node)) {
67+
return;
68+
}
6369
setIsFocused(false);
6470
if (!prompt.trim()) {
6571
setIsExpanded(false);
@@ -135,6 +141,10 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
135141
if (partialData.name) {
136142
setCurrentWorkflowName(partialData.name);
137143
}
144+
// Fit view after each update to keep all nodes visible
145+
setTimeout(() => {
146+
fitView({ padding: 0.2, duration: 200 });
147+
}, 0);
138148
},
139149
existingWorkflow
140150
);
@@ -249,6 +259,7 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
249259
// Clear and close
250260
setPrompt("");
251261
setIsExpanded(false);
262+
setIsFocused(false);
252263
inputRef.current?.blur();
253264
} catch (error) {
254265
console.error("Failed to generate workflow:", error);
@@ -271,6 +282,7 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
271282
setCurrentWorkflowName,
272283
setSelectedNodeId,
273284
onWorkflowCreated,
285+
fitView,
274286
]
275287
);
276288

@@ -288,7 +300,19 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
288300
<form
289301
aria-busy={isGenerating}
290302
aria-label="AI workflow prompt"
291-
className="relative flex items-center gap-2 rounded-lg border bg-background pl-3 pr-2 py-2 shadow-lg"
303+
className="relative flex items-center gap-2 rounded-lg border bg-background pl-3 pr-2 py-2 shadow-lg cursor-text"
304+
onClick={(e) => {
305+
// Focus textarea when clicking anywhere in the form (including padding)
306+
if (e.target === e.currentTarget || (e.target as HTMLElement).tagName !== 'BUTTON') {
307+
inputRef.current?.focus();
308+
}
309+
}}
310+
onMouseDown={(e) => {
311+
// Prevent textarea from losing focus when clicking form padding
312+
if (e.target === e.currentTarget) {
313+
e.preventDefault();
314+
}
315+
}}
292316
onSubmit={handleGenerate}
293317
role="search"
294318
>
@@ -312,6 +336,12 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
312336
if (e.key === 'Enter' && !e.shiftKey) {
313337
e.preventDefault();
314338
handleGenerate(e as any);
339+
} else if (e.key === 'Escape') {
340+
e.preventDefault();
341+
setPrompt("");
342+
setIsExpanded(false);
343+
setIsFocused(false);
344+
inputRef.current?.blur();
315345
}
316346
}}
317347
placeholder={isFocused ? "Describe your workflow with natural language..." : "Ask AI..."}

0 commit comments

Comments
 (0)