Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions app/api/ai/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ Node structure:
}
}

NODE POSITIONING RULES:
- Nodes are squares, so use equal spacing in both directions
- Horizontal spacing between sequential nodes: 250px (e.g., x: 100, then x: 350, then x: 600)
- Vertical spacing for parallel branches: 250px (e.g., y: 75, y: 325, y: 575)
- Start trigger node at position {"x": 100, "y": 200}
- For linear workflows: increment x by 250 for each subsequent node, keep y constant
- For branching workflows: keep x the same for parallel branches, space y by 250px per branch
- When adding nodes to existing workflows, position new nodes 250px away from existing nodes

Trigger types:
- Manual: {"triggerType": "Manual"}
- Webhook: {"triggerType": "Webhook", "webhookPath": "/webhooks/name", ...}
Expand Down Expand Up @@ -221,12 +230,21 @@ WORKFLOW FLOW:
- For linear workflows: trigger -> action1 -> action2 -> etc
- For branching (conditions): one source can connect to multiple targets

Example output:
Example output (linear workflow with 250px horizontal spacing):
{"op": "setName", "name": "Contact Form Workflow"}
{"op": "setDescription", "description": "Processes contact form submissions"}
{"op": "addNode", "node": {"id": "trigger-1", "type": "trigger", "position": {"x": 100, "y": 200}, "data": {"label": "Contact Form", "type": "trigger", "config": {"triggerType": "Manual"}, "status": "idle"}}}
{"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"}}}
{"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"}}}
{"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"}}}
{"op": "addEdge", "edge": {"id": "e1", "source": "trigger-1", "target": "send-email", "type": "default"}}
{"op": "addEdge", "edge": {"id": "e2", "source": "send-email", "target": "log-action", "type": "default"}}

Example output (branching workflow with 250px vertical spacing):
{"op": "addNode", "node": {"id": "trigger-1", "type": "trigger", "position": {"x": 100, "y": 200}, "data": {"label": "Webhook", "type": "trigger", "config": {"triggerType": "Webhook"}, "status": "idle"}}}
{"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"}}}
{"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"}}}
{"op": "addEdge", "edge": {"id": "e1", "source": "trigger-1", "target": "branch-a", "type": "default"}}
{"op": "addEdge", "edge": {"id": "e2", "source": "trigger-1", "target": "branch-b", "type": "default"}}

REMEMBER: After adding all nodes, you MUST add edges to connect them! Every node should be reachable from the trigger.`;

Expand Down Expand Up @@ -302,6 +320,7 @@ IMPORTANT: Output ONLY the operations needed to make the requested changes.
- When connecting nodes, look at the node IDs in the current workflow list above
- DO NOT output operations for existing nodes/edges unless specifically modifying them
- Keep the existing workflow structure and only add/modify/remove what was requested
- 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.

Example: If user says "connect node A to node B", output:
{"op": "addEdge", "edge": {"id": "e-new", "source": "A", "target": "B", "type": "default"}}`;
Expand Down
34 changes: 32 additions & 2 deletions components/ai-elements/prompt.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

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

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

const handleBlur = () => {
const handleBlur = (e: React.FocusEvent) => {
// Don't collapse if focus is moving to another element within the container
if (containerRef.current?.contains(e.relatedTarget as Node)) {
return;
}
setIsFocused(false);
if (!prompt.trim()) {
setIsExpanded(false);
Expand Down Expand Up @@ -135,6 +141,10 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
if (partialData.name) {
setCurrentWorkflowName(partialData.name);
}
// Fit view after each update to keep all nodes visible
setTimeout(() => {
fitView({ padding: 0.2, duration: 200 });
}, 0);
},
existingWorkflow
);
Expand Down Expand Up @@ -249,6 +259,7 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
// Clear and close
setPrompt("");
setIsExpanded(false);
setIsFocused(false);
inputRef.current?.blur();
} catch (error) {
console.error("Failed to generate workflow:", error);
Expand All @@ -271,6 +282,7 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
setCurrentWorkflowName,
setSelectedNodeId,
onWorkflowCreated,
fitView,
]
);

Expand All @@ -288,7 +300,19 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
<form
aria-busy={isGenerating}
aria-label="AI workflow prompt"
className="relative flex items-center gap-2 rounded-lg border bg-background pl-3 pr-2 py-2 shadow-lg"
className="relative flex items-center gap-2 rounded-lg border bg-background pl-3 pr-2 py-2 shadow-lg cursor-text"
onClick={(e) => {
// Focus textarea when clicking anywhere in the form (including padding)
if (e.target === e.currentTarget || (e.target as HTMLElement).tagName !== 'BUTTON') {
inputRef.current?.focus();
}
}}
onMouseDown={(e) => {
// Prevent textarea from losing focus when clicking form padding
if (e.target === e.currentTarget) {
e.preventDefault();
}
}}
onSubmit={handleGenerate}
role="search"
>
Expand All @@ -312,6 +336,12 @@ export function AIPrompt({ workflowId, onWorkflowCreated }: AIPromptProps) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleGenerate(e as any);
} else if (e.key === 'Escape') {
e.preventDefault();
setPrompt("");
setIsExpanded(false);
setIsFocused(false);
inputRef.current?.blur();
}
}}
placeholder={isFocused ? "Describe your workflow with natural language..." : "Ask AI..."}
Expand Down