From 73f919e28d46f442e19a14803ca17558fcfe60d9 Mon Sep 17 00:00:00 2001 From: Saumya Date: Wed, 16 Jul 2025 10:05:12 +0530 Subject: [PATCH] auth page refactor --- .../[agentId]/workflow/[workflowId]/page.tsx | 102 +++- frontend/src/app/auth/page.tsx | 561 +++++++----------- .../react-flow/EdgeTypes/WorkflowEdge.tsx | 69 +++ .../workflows/react-flow/EdgeTypes/index.ts | 7 + .../react-flow/NodeTypes/ConditionNode.tsx | 169 ++++++ .../react-flow/NodeTypes/StepNode.tsx | 309 ++++++++++ .../workflows/react-flow/NodeTypes/index.ts | 9 + .../react-flow/ReactFlowWorkflowBuilder.tsx | 407 +++++++++++++ .../react-flow/hooks/useEdgeClick.ts | 85 +++ .../workflows/react-flow/hooks/useLayout.ts | 173 ++++++ .../components/workflows/react-flow/index.css | 386 ++++++++++++ .../components/workflows/react-flow/utils.ts | 19 + .../workflows/react-flow/utils/conversion.ts | 411 +++++++++++++ 13 files changed, 2361 insertions(+), 346 deletions(-) create mode 100644 frontend/src/components/workflows/react-flow/EdgeTypes/WorkflowEdge.tsx create mode 100644 frontend/src/components/workflows/react-flow/EdgeTypes/index.ts create mode 100644 frontend/src/components/workflows/react-flow/NodeTypes/ConditionNode.tsx create mode 100644 frontend/src/components/workflows/react-flow/NodeTypes/StepNode.tsx create mode 100644 frontend/src/components/workflows/react-flow/NodeTypes/index.ts create mode 100644 frontend/src/components/workflows/react-flow/ReactFlowWorkflowBuilder.tsx create mode 100644 frontend/src/components/workflows/react-flow/hooks/useEdgeClick.ts create mode 100644 frontend/src/components/workflows/react-flow/hooks/useLayout.ts create mode 100644 frontend/src/components/workflows/react-flow/index.css create mode 100644 frontend/src/components/workflows/react-flow/utils.ts create mode 100644 frontend/src/components/workflows/react-flow/utils/conversion.ts diff --git a/frontend/src/app/(dashboard)/agents/config/[agentId]/workflow/[workflowId]/page.tsx b/frontend/src/app/(dashboard)/agents/config/[agentId]/workflow/[workflowId]/page.tsx index ed41c06b..4694635a 100644 --- a/frontend/src/app/(dashboard)/agents/config/[agentId]/workflow/[workflowId]/page.tsx +++ b/frontend/src/app/(dashboard)/agents/config/[agentId]/workflow/[workflowId]/page.tsx @@ -5,9 +5,10 @@ import { useParams, useRouter } from 'next/navigation'; import { ArrowLeft, Save, - Edit2, Settings, GitBranch, + ToggleLeft, + ToggleRight, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -19,6 +20,7 @@ import { useCreateAgentWorkflow, useUpdateAgentWorkflow, useAgentWorkflows } fro import { CreateWorkflowRequest, UpdateWorkflowRequest } from '@/hooks/react-query/agents/workflow-utils'; import { useAgentTools } from '@/hooks/react-query/agents/use-agent-tools'; import { ConditionalWorkflowBuilder, ConditionalStep } from '@/components/agents/workflows/conditional-workflow-builder'; +import { ReactFlowWorkflowBuilder } from '@/components/workflows/react-flow/ReactFlowWorkflowBuilder'; const convertToNestedJSON = (steps: ConditionalStep[]): any[] => { let globalOrder = 1; @@ -42,7 +44,6 @@ const convertToNestedJSON = (steps: ConditionalStep[]): any[] => { return jsonStep; }); }; - return convertStepsWithNesting(steps); }; @@ -51,6 +52,11 @@ const reconstructFromNestedJSON = (nestedSteps: any[]): ConditionalStep[] => { const convertStepsFromNested = (stepList: any[]): ConditionalStep[] => { return stepList.map((step) => { + console.log('Converting nested step:', step.name, step.type); + if (step.children && step.children.length > 0) { + console.log(` Step ${step.name} has ${step.children.length} children:`, step.children.map((c: any) => c.name)); + } + const conditionalStep: ConditionalStep = { id: step.id || Math.random().toString(36).substr(2, 9), name: step.name, @@ -59,17 +65,23 @@ const reconstructFromNestedJSON = (nestedSteps: any[]): ConditionalStep[] => { config: step.config || {}, order: step.order || step.step_order || 0, enabled: step.enabled !== false, - hasIssues: step.hasIssues || false + hasIssues: step.hasIssues || false, + children: [] // Initialize children array }; + // Handle condition metadata if (step.type === 'condition' && step.conditions) { + console.log(` Adding conditions to ${step.name}:`, step.conditions); conditionalStep.conditions = step.conditions; } + + // Handle children - this is crucial for nested conditions if (step.children && Array.isArray(step.children) && step.children.length > 0) { + console.log(` Converting ${step.children.length} children for ${step.name}`); conditionalStep.children = convertStepsFromNested(step.children); - } else { - conditionalStep.children = []; } + + console.log(` Converted step ${step.name}:`, conditionalStep); return conditionalStep; }); }; @@ -201,8 +213,17 @@ export default function WorkflowPage() { const [triggerPhrase, setTriggerPhrase] = useState(''); const [isDefault, setIsDefault] = useState(false); const [steps, setSteps] = useState([]); + + // Debug wrapper for setSteps + const setStepsWithDebug = useCallback((newSteps: ConditionalStep[]) => { + console.log('=== PARENT: setSteps called ==='); + console.log('New steps:', newSteps); + console.log('New steps count:', newSteps.length); + setSteps(newSteps); + }, []); const [isSettingsPopoverOpen, setIsSettingsPopoverOpen] = useState(false); const [isLoading, setIsLoading] = useState(isEditing); + const [useReactFlow, setUseReactFlow] = useState(true); useEffect(() => { if (isEditing && workflows.length > 0) { @@ -215,15 +236,38 @@ export default function WorkflowPage() { let treeSteps: ConditionalStep[]; try { // Check if workflow has proper nested structure - if (workflow.steps.some((step: any) => step.children && Array.isArray(step.children))) { + const hasNestedStructure = workflow.steps.some((step: any) => step.children && Array.isArray(step.children) && step.children.length > 0); + console.log('Has nested structure:', hasNestedStructure); + console.log('First few steps to check:', workflow.steps.slice(0, 3)); + + if (hasNestedStructure) { + console.log('Using reconstructFromNestedJSON'); treeSteps = reconstructFromNestedJSON(workflow.steps); } else { + console.log('Using reconstructFromFlatJSON'); treeSteps = reconstructFromFlatJSON(workflow.steps); } } catch (error) { console.warn('Error reconstructing workflow steps, using fallback:', error); treeSteps = reconstructFromFlatJSON(workflow.steps); } + + console.log('=== LOADING EXISTING WORKFLOW ==='); + console.log('Raw workflow from API:', workflow); + console.log('Raw workflow steps:', workflow.steps); + console.log('Loaded workflow steps:', treeSteps); + console.log('Loaded steps count:', treeSteps.length); + + // Debug: Check if steps have children + treeSteps.forEach((step, index) => { + console.log(`Step ${index}: ${step.name} (${step.type})`); + if (step.children && step.children.length > 0) { + console.log(` Has ${step.children.length} children:`, step.children.map(c => `${c.name} (${c.type})`)); + } else { + console.log(' No children'); + } + }); + setSteps(treeSteps); setIsLoading(false); } else if (!isLoadingWorkflows) { @@ -239,7 +283,14 @@ export default function WorkflowPage() { toast.error('Please enter a workflow name'); return; } + + console.log('=== SAVING WORKFLOW ==='); + console.log('Steps before conversion:', steps); + console.log('Steps count:', steps.length); + const nestedSteps = convertToNestedJSON(steps); + console.log('Nested steps after conversion:', nestedSteps); + try { if (isEditing) { const updateRequest: UpdateWorkflowRequest = { @@ -336,6 +387,29 @@ export default function WorkflowPage() { +
+ +
-
-
+
+
+ {useReactFlow ? ( + + ) : ( + )}
diff --git a/frontend/src/app/auth/page.tsx b/frontend/src/app/auth/page.tsx index c3599390..6465c5df 100644 --- a/frontend/src/app/auth/page.tsx +++ b/frontend/src/app/auth/page.tsx @@ -4,10 +4,8 @@ import Link from 'next/link'; import { SubmitButton } from '@/components/ui/submit-button'; import { Input } from '@/components/ui/input'; import GoogleSignIn from '@/components/GoogleSignIn'; -import { FlickeringGrid } from '@/components/home/ui/flickering-grid'; import { useMediaQuery } from '@/hooks/use-media-query'; -import { useState, useEffect, useRef, Suspense } from 'react'; -import { useScroll } from 'motion/react'; +import { useState, useEffect, Suspense } from 'react'; import { signIn, signUp, forgotPassword } from './actions'; import { useSearchParams, useRouter } from 'next/navigation'; import { @@ -29,6 +27,9 @@ import { DialogFooter, } from '@/components/ui/dialog'; import GitHubSignIn from '@/components/GithubSignIn'; +import { KortixLogo } from '@/components/sidebar/kortix-logo'; +import { FlickeringGrid } from '@/components/home/ui/flickering-grid'; +import { Ripple } from '@/components/ui/ripple'; function LoginContent() { const router = useRouter(); @@ -39,20 +40,15 @@ function LoginContent() { const message = searchParams.get('message'); const isSignUp = mode === 'signup'; - const tablet = useMediaQuery('(max-width: 1024px)'); + const isMobile = useMediaQuery('(max-width: 768px)'); const [mounted, setMounted] = useState(false); - const [isScrolling, setIsScrolling] = useState(false); - const scrollTimeout = useRef(null); - const { scrollY } = useScroll(); - // Redirect if user is already logged in, checking isLoading state useEffect(() => { if (!isLoading && user) { router.push(returnUrl || '/dashboard'); } }, [user, isLoading, router, returnUrl]); - // Determine if message is a success message const isSuccessMessage = message && (message.includes('Check your email') || @@ -83,30 +79,6 @@ function LoginContent() { } }, [isSuccessMessage]); - // Detect when scrolling is active to reduce animation complexity - useEffect(() => { - const unsubscribe = scrollY.on('change', () => { - setIsScrolling(true); - - // Clear any existing timeout - if (scrollTimeout.current) { - clearTimeout(scrollTimeout.current); - } - - // Set a new timeout - scrollTimeout.current = setTimeout(() => { - setIsScrolling(false); - }, 300); // Wait 300ms after scroll stops - }); - - return () => { - unsubscribe(); - if (scrollTimeout.current) { - clearTimeout(scrollTimeout.current); - } - }; - }, [scrollY]); - const handleSignIn = async (prevState: any, formData: FormData) => { if (returnUrl) { formData.append('returnUrl', returnUrl); @@ -222,368 +194,283 @@ function LoginContent() { // Show loading spinner while checking auth state if (isLoading) { return ( -
- -
+
+ +
); } // Registration success view if (registrationSuccess) { return ( -
-
-
-
- {/* Background elements from the original view */} -
-
-
-
-
- -
-
-
-
-
- -
- - {/* Success content */} -
-
-
- -
- -

- Check your email -

- -

- We've sent a confirmation link to: -

- -

- {registrationEmail || 'your email address'} -

- -
-

- Click the link in the email to activate your account. If - you don't see the email, check your spam folder. -

-
- -
- - Return to home - - -
-
-
+
+
+
+
+
-
+ +

+ Check your email +

+ +

+ We've sent a confirmation link to: +

+ +

+ {registrationEmail || 'your email address'} +

+ +
+

+ Click the link in the email to activate your account. If you don't see the email, check your spam folder. +

+
+ +
+ + Return to home + + +
+
- +
); } return ( -
-
- {/* Hero-like header with flickering grid */} -
-
- {/* Left side flickering grid with gradient fades */} -
- {/* Horizontal fade from left to right */} -
- - {/* Vertical fade from top */} -
- - {/* Vertical fade to bottom */} -
- -
- -
-
- - {/* Right side flickering grid with gradient fades */} -
- {/* Horizontal fade from right to left */} -
- - {/* Vertical fade from top */} -
- - {/* Vertical fade to bottom */} -
- -
- -
-
- - {/* Center content background with rounded bottom */} -
- - {/* Header content */} -
- - - +
+
+ + + +
+
+
+
+
+ + Back to home - - - -

- {isSignUp ? 'Join Suna' : 'Welcome back'} -

-

- {isSignUp - ? 'Create your account and start building with AI' - : 'Sign in to your account to continue'} -

+ +

+ {isSignUp ? 'Create your account' : 'Log into your account'} +

+
+ {message && !isSuccessMessage && ( +
+ + {message} +
+ )} +
+ +
-
- - {/* Auth form card */} -
-
- {/* Non-registration related messages */} - {message && !isSuccessMessage && ( -
- - {message} +
+
+
+
+
+ + or continue with email + +
+
+
+
+ +
+
+ +
+ {isSignUp && ( +
+
)} - - {/* OAuth Sign In */} -
-
- -
-
- -
-
- - - {/* Divider */} -
-
-
-
-
- - or continue with email - -
-
- - {/* Form */} - -
- -
- -
- -
- - {isSignUp && ( -
- -
- )} - -
- {!isSignUp ? ( - <> - - Sign in - - - - Create new account - - - ) : ( - <> - - Sign up - - - - Back to sign in - - - )} -
- - {!isSignUp && ( -
- -
- )} -
+ Sign in + -
- By continuing, you agree to our{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - + + Create new account + + + ) : ( + <> + + Sign up + + + + Back to sign in + + + )}
+ {!isSignUp && ( +
+ +
+ )} + +
+ By continuing, you agree to our{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Policy +
-
+
+
+
+ +
+
+ +
+
+ +
+
- - {/* Forgot Password Dialog */} - +
- - Reset Password - + Reset Password
- - Enter your email address and we'll send you a link to reset your - password. + + Enter your email address and we'll send you a link to reset your password.
- -
+ setForgotPasswordEmail(e.target.value)} - className="h-12 rounded-full bg-background border-border" + className="h-11 rounded-xl" required /> - {forgotPasswordStatus.message && (
{forgotPasswordStatus.success ? ( - + ) : ( - + )} - - {forgotPasswordStatus.message} - + {forgotPasswordStatus.message}
)} - - - + +
-
+ ); } @@ -591,9 +478,9 @@ export default function Login() { return ( -
- +
+
+
} > diff --git a/frontend/src/components/workflows/react-flow/EdgeTypes/WorkflowEdge.tsx b/frontend/src/components/workflows/react-flow/EdgeTypes/WorkflowEdge.tsx new file mode 100644 index 00000000..7444002c --- /dev/null +++ b/frontend/src/components/workflows/react-flow/EdgeTypes/WorkflowEdge.tsx @@ -0,0 +1,69 @@ +import { + BaseEdge, + EdgeLabelRenderer, + EdgeProps, + getSmoothStepPath, +} from '@xyflow/react'; +import useEdgeClick from '../hooks/useEdgeClick'; + +export default function WorkflowEdge({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style, + markerEnd, + label, + labelStyle, + labelBgStyle, +}: EdgeProps) { + const onClick = useEdgeClick(id); + + const [edgePath, edgeCenterX, edgeCenterY] = getSmoothStepPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + borderRadius: 8, // Add some rounding to the perpendicular bends + }); + + return ( + <> + + + {label && ( +
+ {label} +
+ )} + +
+ + ); +} \ No newline at end of file diff --git a/frontend/src/components/workflows/react-flow/EdgeTypes/index.ts b/frontend/src/components/workflows/react-flow/EdgeTypes/index.ts new file mode 100644 index 00000000..92630bb4 --- /dev/null +++ b/frontend/src/components/workflows/react-flow/EdgeTypes/index.ts @@ -0,0 +1,7 @@ +import WorkflowEdge from './WorkflowEdge'; + +const edgeTypes = { + workflow: WorkflowEdge, +}; + +export default edgeTypes; \ No newline at end of file diff --git a/frontend/src/components/workflows/react-flow/NodeTypes/ConditionNode.tsx b/frontend/src/components/workflows/react-flow/NodeTypes/ConditionNode.tsx new file mode 100644 index 00000000..e23d6c22 --- /dev/null +++ b/frontend/src/components/workflows/react-flow/NodeTypes/ConditionNode.tsx @@ -0,0 +1,169 @@ +import { memo, useState } from 'react'; +import { Handle, Position } from '@xyflow/react'; +import { GitBranch, MoreHorizontal, Edit2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Trash2 } from 'lucide-react'; +import { getConditionLabel } from '../utils'; +import { useReactFlow } from '@xyflow/react'; + +const ConditionNode = ({ id, data, selected }: any) => { + const { setNodes } = useReactFlow(); + + const [isEditOpen, setIsEditOpen] = useState(false); + const [editData, setEditData] = useState({ + conditionType: data.conditionType || 'if', + expression: data.expression || '', + }); + + const handleSave = () => { + setNodes((nodes) => + nodes.map((node) => { + if (node.id === id) { + return { + ...node, + data: { + ...node.data, + conditionType: editData.conditionType, + expression: editData.expression, + }, + }; + } + return node; + }) + ); + setIsEditOpen(false); + }; + + return ( +
+ +
+
+ + {getConditionLabel(data.conditionType)} +
+ {data.expression && data.conditionType !== 'else' && ( +
+ {data.expression} +
+ )} +
+ + + + + e.stopPropagation()}> +
+
+

Edit Condition

+
+ +
+ + +
+ {editData.conditionType !== 'else' && ( +
+ + setEditData({ ...editData, expression: e.target.value })} + placeholder="e.g., user asks about pricing" + className="h-8" + /> +
+ )} +
+ + +
+
+
+
+ + + + + + + + + +
+
+ +
+ ); +}; + +export default memo(ConditionNode); \ No newline at end of file diff --git a/frontend/src/components/workflows/react-flow/NodeTypes/StepNode.tsx b/frontend/src/components/workflows/react-flow/NodeTypes/StepNode.tsx new file mode 100644 index 00000000..cc3e881f --- /dev/null +++ b/frontend/src/components/workflows/react-flow/NodeTypes/StepNode.tsx @@ -0,0 +1,309 @@ +import { memo, useState } from 'react'; +import { Handle, Position } from '@xyflow/react'; +import { AlertTriangle, MoreHorizontal, Edit2, Check, ChevronsUpDown } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Trash2 } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { useReactFlow } from '@xyflow/react'; + +const normalizeToolName = (toolName: string, toolType: 'agentpress' | 'mcp') => { + if (toolType === 'agentpress') { + const agentPressMapping: Record = { + 'sb_shell_tool': 'Shell Tool', + 'sb_files_tool': 'Files Tool', + 'sb_browser_tool': 'Browser Tool', + 'sb_deploy_tool': 'Deploy Tool', + 'sb_expose_tool': 'Expose Tool', + 'web_search_tool': 'Web Search', + 'sb_vision_tool': 'Vision Tool', + 'data_providers_tool': 'Data Providers', + }; + return agentPressMapping[toolName] || toolName; + } else { + return toolName + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + } +}; + +const StepNode = ({ id, data, selected }: any) => { + const { setNodes } = useReactFlow(); + + const [isEditOpen, setIsEditOpen] = useState(false); + const [isToolSelectOpen, setIsToolSelectOpen] = useState(false); + const [editData, setEditData] = useState({ + name: data.name || '', + description: data.description || '', + tool: data.tool || '', + }); + + const handleSave = () => { + setNodes((nodes) => + nodes.map((node) => { + if (node.id === id) { + return { + ...node, + data: { + ...node.data, + name: editData.name, + description: editData.description, + tool: editData.tool, + hasIssues: !editData.name || editData.name === 'New Step', + }, + }; + } + return node; + }) + ); + setIsEditOpen(false); + }; + + const getDisplayToolName = (toolName: string) => { + if (!toolName) return null; + + if (data.agentTools) { + const agentpressTool = data.agentTools.agentpress_tools?.find((t: any) => t.name === toolName); + if (agentpressTool) { + return normalizeToolName(agentpressTool.name, 'agentpress'); + } + + const mcpTool = data.agentTools.mcp_tools?.find((t: any) => + `${t.server}:${t.name}` === toolName || t.name === toolName + ); + if (mcpTool) { + return normalizeToolName(mcpTool.name, 'mcp'); + } + } + + return toolName; + }; + + return ( +
+ +
+
+
+
+ {data.hasIssues && ( + + )} +

+ {data.name} {data.stepNumber ? data.stepNumber : ''} +

+
+ {data.description && ( +

{data.description}

+ )} + {data.tool && ( +
+ 🔧 {getDisplayToolName(data.tool)} +
+ )} +
+
+ + + + + e.stopPropagation()}> +
+
+

Edit Step

+
+ +
+ + setEditData({ ...editData, name: e.target.value })} + placeholder="Step name" + className="h-8" + /> +
+ +
+ +