mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-26 07:18:38 -05:00
Compare commits
4 Commits
fix/s-tool
...
fix/ci
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ecd07b12c | ||
|
|
841cb638fb | ||
|
|
c7db48e3a2 | ||
|
|
4d844651c2 |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -27,8 +27,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Extract version from commit message
|
- name: Extract version from commit message
|
||||||
id: extract
|
id: extract
|
||||||
|
env:
|
||||||
|
COMMIT_MSG: ${{ github.event.head_commit.message }}
|
||||||
run: |
|
run: |
|
||||||
COMMIT_MSG="${{ github.event.head_commit.message }}"
|
|
||||||
# Only tag versions on main branch
|
# Only tag versions on main branch
|
||||||
if [ "${{ github.ref }}" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
if [ "${{ github.ref }}" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then
|
||||||
VERSION="${BASH_REMATCH[1]}"
|
VERSION="${BASH_REMATCH[1]}"
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export { LandingLoopNode } from './landing-canvas/landing-block/landing-loop-nod
|
|||||||
export { LandingNode } from './landing-canvas/landing-block/landing-node'
|
export { LandingNode } from './landing-canvas/landing-block/landing-node'
|
||||||
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
export type { LoopBlockProps } from './landing-canvas/landing-block/loop-block'
|
||||||
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
export { LoopBlock } from './landing-canvas/landing-block/loop-block'
|
||||||
export type { TagProps } from './landing-canvas/landing-block/tag'
|
export type { SubBlockRowProps, TagProps } from './landing-canvas/landing-block/tag'
|
||||||
export { Tag } from './landing-canvas/landing-block/tag'
|
export { SubBlockRow, Tag } from './landing-canvas/landing-block/tag'
|
||||||
export type {
|
export type {
|
||||||
LandingBlockNode,
|
LandingBlockNode,
|
||||||
LandingCanvasProps,
|
LandingCanvasProps,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BookIcon } from 'lucide-react'
|
|
||||||
import {
|
import {
|
||||||
Tag,
|
SubBlockRow,
|
||||||
type TagProps,
|
type SubBlockRowProps,
|
||||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/tag'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data structure for a landing card component
|
* Data structure for a landing card component
|
||||||
|
* Matches the workflow block structure from the application
|
||||||
*/
|
*/
|
||||||
export interface LandingCardData {
|
export interface LandingCardData {
|
||||||
/** Icon element to display in the card header */
|
/** Icon element to display in the card header */
|
||||||
@@ -15,8 +15,8 @@ export interface LandingCardData {
|
|||||||
color: string | '#f6f6f6'
|
color: string | '#f6f6f6'
|
||||||
/** Name/title of the card */
|
/** Name/title of the card */
|
||||||
name: string
|
name: string
|
||||||
/** Optional tags to display at the bottom of the card */
|
/** Optional subblock rows to display below the header */
|
||||||
tags?: TagProps[]
|
tags?: SubBlockRowProps[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +28,8 @@ export interface LandingBlockProps extends LandingCardData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing block component that displays a card with icon, name, and optional tags
|
* Landing block component that displays a card with icon, name, and optional subblock rows
|
||||||
|
* Styled to match the application's workflow blocks
|
||||||
* @param props - Component properties including icon, color, name, tags, and className
|
* @param props - Component properties including icon, color, name, tags, and className
|
||||||
* @returns A styled block card component
|
* @returns A styled block card component
|
||||||
*/
|
*/
|
||||||
@@ -39,33 +40,37 @@ export const LandingBlock = React.memo(function LandingBlock({
|
|||||||
tags,
|
tags,
|
||||||
className,
|
className,
|
||||||
}: LandingBlockProps) {
|
}: LandingBlockProps) {
|
||||||
|
const hasContentBelowHeader = tags && tags.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`z-10 flex w-64 flex-col items-start gap-3 rounded-[14px] border border-[#E5E5E5] bg-[#FEFEFE] p-3 ${className ?? ''}`}
|
className={`z-10 flex w-[250px] flex-col rounded-[8px] border border-[#E5E5E5] bg-white ${className ?? ''}`}
|
||||||
style={{
|
|
||||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className='flex w-full items-center justify-between'>
|
{/* Header - matches workflow-block.tsx header styling */}
|
||||||
<div className='flex items-center gap-2.5'>
|
<div
|
||||||
|
className={`flex items-center justify-between p-[8px] ${hasContentBelowHeader ? 'border-[#E5E5E5] border-b' : ''}`}
|
||||||
|
>
|
||||||
|
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
|
||||||
<div
|
<div
|
||||||
className='flex h-6 w-6 items-center justify-center rounded-[8px] text-white'
|
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
||||||
style={{ backgroundColor: color as string }}
|
style={{ background: color as string }}
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<p className='text-base text-card-foreground'>{name}</p>
|
<span className='truncate font-medium text-[#171717] text-[16px]' title={name}>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<BookIcon className='h-4 w-4 text-muted-foreground' />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tags && tags.length > 0 ? (
|
{/* Content - SubBlock Rows matching workflow-block.tsx */}
|
||||||
<div className='flex flex-wrap gap-2'>
|
{hasContentBelowHeader && (
|
||||||
|
<div className='flex flex-col gap-[8px] p-[8px]'>
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<Tag key={tag.label} icon={tag.icon} label={tag.label} />
|
<SubBlockRow key={tag.label} icon={tag.icon} label={tag.label} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ import {
|
|||||||
type LandingCardData,
|
type LandingCardData,
|
||||||
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
} from '@/app/(landing)/components/hero/components/landing-canvas/landing-block/landing-block'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Y offset from block top - matches HANDLE_POSITIONS.DEFAULT_Y_OFFSET
|
||||||
|
*/
|
||||||
|
const HANDLE_Y_OFFSET = 20
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React Flow node component for the landing canvas
|
* React Flow node component for the landing canvas
|
||||||
* Includes CSS animations and connection handles
|
* Styled to match the application's workflow blocks
|
||||||
* @param props - Component properties containing node data
|
* @param props - Component properties containing node data
|
||||||
* @returns A React Flow compatible node component
|
* @returns A React Flow compatible node component
|
||||||
*/
|
*/
|
||||||
@@ -41,15 +46,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
type='target'
|
type='target'
|
||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
style={{
|
style={{
|
||||||
width: '12px',
|
width: '7px',
|
||||||
height: '12px',
|
height: '20px',
|
||||||
background: '#FEFEFE',
|
background: '#D1D1D1',
|
||||||
border: '1px solid #E5E5E5',
|
border: 'none',
|
||||||
borderRadius: '50%',
|
borderRadius: '2px 0 0 2px',
|
||||||
top: '50%',
|
top: `${HANDLE_Y_OFFSET}px`,
|
||||||
left: '-20px',
|
left: '-7px',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 2,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
/>
|
/>
|
||||||
@@ -59,15 +64,15 @@ export const LandingNode = React.memo(function LandingNode({ data }: { data: Lan
|
|||||||
type='source'
|
type='source'
|
||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
style={{
|
style={{
|
||||||
width: '12px',
|
width: '7px',
|
||||||
height: '12px',
|
height: '20px',
|
||||||
background: '#FEFEFE',
|
background: '#D1D1D1',
|
||||||
border: '1px solid #E5E5E5',
|
border: 'none',
|
||||||
borderRadius: '50%',
|
borderRadius: '0 2px 2px 0',
|
||||||
top: '50%',
|
top: `${HANDLE_Y_OFFSET}px`,
|
||||||
right: '-20px',
|
right: '-7px',
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
zIndex: 2,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
isConnectable={false}
|
isConnectable={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface LoopBlockProps {
|
|||||||
/**
|
/**
|
||||||
* Loop block container component that provides a styled container
|
* Loop block container component that provides a styled container
|
||||||
* for grouping related elements with a dashed border
|
* for grouping related elements with a dashed border
|
||||||
|
* Styled to match the application's subflow containers
|
||||||
* @param props - Component properties including children and styling
|
* @param props - Component properties including children and styling
|
||||||
* @returns A styled loop container component
|
* @returns A styled loop container component
|
||||||
*/
|
*/
|
||||||
@@ -29,33 +30,33 @@ export const LoopBlock = React.memo(function LoopBlock({
|
|||||||
style={{
|
style={{
|
||||||
width: '1198px',
|
width: '1198px',
|
||||||
height: '528px',
|
height: '528px',
|
||||||
borderRadius: '14px',
|
borderRadius: '8px',
|
||||||
background: 'rgba(59, 130, 246, 0.10)',
|
background: 'rgba(59, 130, 246, 0.08)',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Custom dashed border with SVG */}
|
{/* Custom dashed border with SVG - 8px border radius to match blocks */}
|
||||||
<svg
|
<svg
|
||||||
className='pointer-events-none absolute inset-0 h-full w-full'
|
className='pointer-events-none absolute inset-0 h-full w-full'
|
||||||
style={{ borderRadius: '14px' }}
|
style={{ borderRadius: '8px' }}
|
||||||
preserveAspectRatio='none'
|
preserveAspectRatio='none'
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
className='landing-loop-animated-dash'
|
className='landing-loop-animated-dash'
|
||||||
d='M 1183.5 527.5
|
d='M 1190 527.5
|
||||||
L 14 527.5
|
L 8 527.5
|
||||||
A 13.5 13.5 0 0 1 0.5 514
|
A 7.5 7.5 0 0 1 0.5 520
|
||||||
L 0.5 14
|
L 0.5 8
|
||||||
A 13.5 13.5 0 0 1 14 0.5
|
A 7.5 7.5 0 0 1 8 0.5
|
||||||
L 1183.5 0.5
|
L 1190 0.5
|
||||||
A 13.5 13.5 0 0 1 1197 14
|
A 7.5 7.5 0 0 1 1197.5 8
|
||||||
L 1197 514
|
L 1197.5 520
|
||||||
A 13.5 13.5 0 0 1 1183.5 527.5 Z'
|
A 7.5 7.5 0 0 1 1190 527.5 Z'
|
||||||
fill='none'
|
fill='none'
|
||||||
stroke='#3B82F6'
|
stroke='#3B82F6'
|
||||||
strokeWidth='1'
|
strokeWidth='1'
|
||||||
strokeDasharray='12 12'
|
strokeDasharray='8 8'
|
||||||
strokeLinecap='round'
|
strokeLinecap='round'
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,25 +1,52 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties for a tag component
|
* Properties for a subblock row component
|
||||||
|
* Matches the SubBlockRow pattern from workflow-block.tsx
|
||||||
*/
|
*/
|
||||||
export interface TagProps {
|
export interface SubBlockRowProps {
|
||||||
/** Icon element to display in the tag */
|
/** Icon element to display (optional, for visual context) */
|
||||||
icon: React.ReactNode
|
icon?: React.ReactNode
|
||||||
/** Text label for the tag */
|
/** Text label for the row title */
|
||||||
label: string
|
label: string
|
||||||
|
/** Optional value to display on the right side */
|
||||||
|
value?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag component for displaying labeled icons in a compact format
|
* Kept for backwards compatibility
|
||||||
* @param props - Tag properties including icon and label
|
|
||||||
* @returns A styled tag component
|
|
||||||
*/
|
*/
|
||||||
export const Tag = React.memo(function Tag({ icon, label }: TagProps) {
|
export type TagProps = SubBlockRowProps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SubBlockRow component matching the workflow block's subblock row style
|
||||||
|
* @param props - Row properties including label and optional value
|
||||||
|
* @returns A styled row component
|
||||||
|
*/
|
||||||
|
export const SubBlockRow = React.memo(function SubBlockRow({ label, value }: SubBlockRowProps) {
|
||||||
|
// Split label by colon to separate title and value if present
|
||||||
|
const [title, displayValue] = label.includes(':')
|
||||||
|
? label.split(':').map((s) => s.trim())
|
||||||
|
: [label, value]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex w-fit items-center gap-1 rounded-[8px] border border-gray-300 bg-white px-2 py-0.5'>
|
<div className='flex items-center gap-[8px]'>
|
||||||
<div className='h-3 w-3 text-muted-foreground'>{icon}</div>
|
<span className='min-w-0 truncate text-[#888888] text-[14px] capitalize' title={title}>
|
||||||
<p className='text-muted-foreground text-xs leading-normal'>{label}</p>
|
{title}
|
||||||
|
</span>
|
||||||
|
{displayValue && (
|
||||||
|
<span
|
||||||
|
className='flex-1 truncate text-right text-[#171717] text-[14px]'
|
||||||
|
title={displayValue}
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag component - alias for SubBlockRow for backwards compatibility
|
||||||
|
*/
|
||||||
|
export const Tag = SubBlockRow
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import { LandingFlow } from '@/app/(landing)/components/hero/components/landing-
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Visual constants for landing node dimensions
|
* Visual constants for landing node dimensions
|
||||||
|
* Matches BLOCK_DIMENSIONS from the application
|
||||||
*/
|
*/
|
||||||
export const CARD_WIDTH = 256
|
export const CARD_WIDTH = 250
|
||||||
export const CARD_HEIGHT = 92
|
export const CARD_HEIGHT = 100
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing block node with positioning information
|
* Landing block node with positioning information
|
||||||
|
|||||||
@@ -4,33 +4,29 @@ import React from 'react'
|
|||||||
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
import { type EdgeProps, getSmoothStepPath, Position } from 'reactflow'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom edge component with animated dotted line that floats between handles
|
* Custom edge component with animated dashed line
|
||||||
|
* Styled to match the application's workflow edges with rectangular handles
|
||||||
* @param props - React Flow edge properties
|
* @param props - React Flow edge properties
|
||||||
* @returns An animated dotted edge component
|
* @returns An animated dashed edge component
|
||||||
*/
|
*/
|
||||||
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
||||||
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, data } =
|
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style } = props
|
||||||
props
|
|
||||||
|
|
||||||
// Adjust the connection points to create floating effect
|
// Adjust the connection points to connect flush with rectangular handles
|
||||||
// Account for handle size (12px) and additional spacing
|
// Handle width is 7px, positioned at -7px from edge
|
||||||
const handleRadius = 6 // Half of handle width (12px)
|
|
||||||
const floatingGap = 1 // Additional gap for floating effect
|
|
||||||
|
|
||||||
// Calculate adjusted positions based on edge direction
|
|
||||||
let adjustedSourceX = sourceX
|
let adjustedSourceX = sourceX
|
||||||
let adjustedTargetX = targetX
|
let adjustedTargetX = targetX
|
||||||
|
|
||||||
if (sourcePosition === Position.Right) {
|
if (sourcePosition === Position.Right) {
|
||||||
adjustedSourceX = sourceX + handleRadius + floatingGap
|
adjustedSourceX = sourceX + 1
|
||||||
} else if (sourcePosition === Position.Left) {
|
} else if (sourcePosition === Position.Left) {
|
||||||
adjustedSourceX = sourceX - handleRadius - floatingGap
|
adjustedSourceX = sourceX - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetPosition === Position.Left) {
|
if (targetPosition === Position.Left) {
|
||||||
adjustedTargetX = targetX - handleRadius - floatingGap
|
adjustedTargetX = targetX - 1
|
||||||
} else if (targetPosition === Position.Right) {
|
} else if (targetPosition === Position.Right) {
|
||||||
adjustedTargetX = targetX + handleRadius + floatingGap
|
adjustedTargetX = targetX + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const [path] = getSmoothStepPath({
|
const [path] = getSmoothStepPath({
|
||||||
@@ -40,8 +36,8 @@ export const LandingEdge = React.memo(function LandingEdge(props: EdgeProps) {
|
|||||||
targetY,
|
targetY,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
borderRadius: 20,
|
borderRadius: 8,
|
||||||
offset: 10,
|
offset: 16,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,16 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import { ArrowUp, CodeIcon } from 'lucide-react'
|
||||||
ArrowUp,
|
|
||||||
BinaryIcon,
|
|
||||||
BookIcon,
|
|
||||||
CalendarIcon,
|
|
||||||
CodeIcon,
|
|
||||||
Globe2Icon,
|
|
||||||
MessageSquareIcon,
|
|
||||||
VariableIcon,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { type Edge, type Node, Position } from 'reactflow'
|
import { type Edge, type Node, Position } from 'reactflow'
|
||||||
import {
|
import {
|
||||||
@@ -23,7 +14,6 @@ import {
|
|||||||
JiraIcon,
|
JiraIcon,
|
||||||
LinearIcon,
|
LinearIcon,
|
||||||
NotionIcon,
|
NotionIcon,
|
||||||
OpenAIIcon,
|
|
||||||
OutlookIcon,
|
OutlookIcon,
|
||||||
PackageSearchIcon,
|
PackageSearchIcon,
|
||||||
PineconeIcon,
|
PineconeIcon,
|
||||||
@@ -65,67 +55,56 @@ const SERVICE_TEMPLATES = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing blocks for the canvas preview
|
* Landing blocks for the canvas preview
|
||||||
|
* Styled to match the application's workflow blocks with subblock rows
|
||||||
*/
|
*/
|
||||||
const LANDING_BLOCKS: LandingManualBlock[] = [
|
const LANDING_BLOCKS: LandingManualBlock[] = [
|
||||||
{
|
{
|
||||||
id: 'schedule',
|
id: 'schedule',
|
||||||
name: 'Schedule',
|
name: 'Schedule',
|
||||||
color: '#7B68EE',
|
color: '#7B68EE',
|
||||||
icon: <ScheduleIcon className='h-4 w-4' />,
|
icon: <ScheduleIcon className='h-[16px] w-[16px] text-white' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 8, y: 60 },
|
mobile: { x: 8, y: 60 },
|
||||||
tablet: { x: 40, y: 120 },
|
tablet: { x: 40, y: 120 },
|
||||||
desktop: { x: 60, y: 180 },
|
desktop: { x: 60, y: 180 },
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [{ label: 'Time: 09:00AM Daily' }, { label: 'Timezone: PST' }],
|
||||||
{ icon: <CalendarIcon className='h-3 w-3' />, label: '09:00AM Daily' },
|
|
||||||
{ icon: <Globe2Icon className='h-3 w-3' />, label: 'PST' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'knowledge',
|
id: 'knowledge',
|
||||||
name: 'Knowledge',
|
name: 'Knowledge',
|
||||||
color: '#00B0B0',
|
color: '#00B0B0',
|
||||||
icon: <PackageSearchIcon className='h-4 w-4' />,
|
icon: <PackageSearchIcon className='h-[16px] w-[16px] text-white' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 120, y: 140 },
|
mobile: { x: 120, y: 140 },
|
||||||
tablet: { x: 220, y: 200 },
|
tablet: { x: 220, y: 200 },
|
||||||
desktop: { x: 420, y: 241 },
|
desktop: { x: 420, y: 241 },
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [{ label: 'Source: Product Vector DB' }, { label: 'Limit: 10' }],
|
||||||
{ icon: <BookIcon className='h-3 w-3' />, label: 'Product Vector DB' },
|
|
||||||
{ icon: <BinaryIcon className='h-3 w-3' />, label: 'Limit: 10' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'agent',
|
id: 'agent',
|
||||||
name: 'Agent',
|
name: 'Agent',
|
||||||
color: '#802FFF',
|
color: '#802FFF',
|
||||||
icon: <AgentIcon className='h-4 w-4' />,
|
icon: <AgentIcon className='h-[16px] w-[16px] text-white' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 340, y: 60 },
|
mobile: { x: 340, y: 60 },
|
||||||
tablet: { x: 540, y: 120 },
|
tablet: { x: 540, y: 120 },
|
||||||
desktop: { x: 880, y: 142 },
|
desktop: { x: 880, y: 142 },
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [{ label: 'Model: gpt-5' }, { label: 'Prompt: You are a support ag...' }],
|
||||||
{ icon: <OpenAIIcon className='h-3 w-3' />, label: 'gpt-5' },
|
|
||||||
{ icon: <MessageSquareIcon className='h-3 w-3' />, label: 'You are a support ag...' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'function',
|
id: 'function',
|
||||||
name: 'Function',
|
name: 'Function',
|
||||||
color: '#FF402F',
|
color: '#FF402F',
|
||||||
icon: <CodeIcon className='h-4 w-4' />,
|
icon: <CodeIcon className='h-[16px] w-[16px] text-white' />,
|
||||||
positions: {
|
positions: {
|
||||||
mobile: { x: 480, y: 220 },
|
mobile: { x: 480, y: 220 },
|
||||||
tablet: { x: 740, y: 280 },
|
tablet: { x: 740, y: 280 },
|
||||||
desktop: { x: 880, y: 340 },
|
desktop: { x: 880, y: 340 },
|
||||||
},
|
},
|
||||||
tags: [
|
tags: [{ label: 'Language: Python' }, { label: 'Code: time = "2025-09-01...' }],
|
||||||
{ icon: <CodeIcon className='h-3 w-3' />, label: 'Python' },
|
|
||||||
{ icon: <VariableIcon className='h-3 w-3' />, label: 'time = "2025-09-01...' },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ function PricingCard({
|
|||||||
*/
|
*/
|
||||||
export default function LandingPricing() {
|
export default function LandingPricing() {
|
||||||
return (
|
return (
|
||||||
<section id='pricing' className='px-4 pt-[19px] sm:px-0 sm:pt-0' aria-label='Pricing plans'>
|
<section id='pricing' className='px-4 pt-[23px] sm:px-0 sm:pt-[4px]' aria-label='Pricing plans'>
|
||||||
<h2 className='sr-only'>Pricing Plans</h2>
|
<h2 className='sr-only'>Pricing Plans</h2>
|
||||||
<div className='relative mx-auto w-full max-w-[1289px]'>
|
<div className='relative mx-auto w-full max-w-[1289px]'>
|
||||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-0 lg:grid-cols-4'>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface NavProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
export default function Nav({ hideAuthButtons = false, variant = 'landing' }: NavProps = {}) {
|
||||||
const [githubStars, setGithubStars] = useState('25.8k')
|
const [githubStars, setGithubStars] = useState('26.1k')
|
||||||
const [isHovered, setIsHovered] = useState(false)
|
const [isHovered, setIsHovered] = useState(false)
|
||||||
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
const [isLoginHovered, setIsLoginHovered] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const SpotifyBlock: BlockConfig<ToolResponse> = {
|
|||||||
'Integrate Spotify into your workflow. Search for tracks, albums, artists, and playlists. Manage playlists, access your library, control playback, browse podcasts and audiobooks.',
|
'Integrate Spotify into your workflow. Search for tracks, albums, artists, and playlists. Manage playlists, access your library, control playback, browse podcasts and audiobooks.',
|
||||||
docsLink: 'https://docs.sim.ai/tools/spotify',
|
docsLink: 'https://docs.sim.ai/tools/spotify',
|
||||||
category: 'tools',
|
category: 'tools',
|
||||||
|
hideFromToolbar: true,
|
||||||
bgColor: '#000000',
|
bgColor: '#000000',
|
||||||
icon: SpotifyIcon,
|
icon: SpotifyIcon,
|
||||||
subBlocks: [
|
subBlocks: [
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { useUndoRedoStore } from '@/stores/undo-redo'
|
|||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import { filterNewEdges, mergeSubblockState } from '@/stores/workflows/utils'
|
import { filterNewEdges, filterValidEdges, mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
import type { BlockState, Loop, Parallel, Position } from '@/stores/workflows/workflow/types'
|
import type { BlockState, Loop, Parallel, Position } from '@/stores/workflows/workflow/types'
|
||||||
|
|
||||||
@@ -226,9 +226,12 @@ export function useCollaborativeWorkflow() {
|
|||||||
case EDGES_OPERATIONS.BATCH_ADD_EDGES: {
|
case EDGES_OPERATIONS.BATCH_ADD_EDGES: {
|
||||||
const { edges } = payload
|
const { edges } = payload
|
||||||
if (Array.isArray(edges) && edges.length > 0) {
|
if (Array.isArray(edges) && edges.length > 0) {
|
||||||
const newEdges = filterNewEdges(edges, useWorkflowStore.getState().edges)
|
const blocks = useWorkflowStore.getState().blocks
|
||||||
|
const currentEdges = useWorkflowStore.getState().edges
|
||||||
|
const validEdges = filterValidEdges(edges, blocks)
|
||||||
|
const newEdges = filterNewEdges(validEdges, currentEdges)
|
||||||
if (newEdges.length > 0) {
|
if (newEdges.length > 0) {
|
||||||
useWorkflowStore.getState().batchAddEdges(newEdges)
|
useWorkflowStore.getState().batchAddEdges(newEdges, { skipValidation: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -1004,7 +1007,11 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
if (edges.length === 0) return false
|
if (edges.length === 0) return false
|
||||||
|
|
||||||
const newEdges = filterNewEdges(edges, useWorkflowStore.getState().edges)
|
// Filter out invalid edges (e.g., edges targeting trigger blocks) and duplicates
|
||||||
|
const blocks = useWorkflowStore.getState().blocks
|
||||||
|
const currentEdges = useWorkflowStore.getState().edges
|
||||||
|
const validEdges = filterValidEdges(edges, blocks)
|
||||||
|
const newEdges = filterNewEdges(validEdges, currentEdges)
|
||||||
if (newEdges.length === 0) return false
|
if (newEdges.length === 0) return false
|
||||||
|
|
||||||
const operationId = crypto.randomUUID()
|
const operationId = crypto.randomUUID()
|
||||||
@@ -1020,7 +1027,7 @@ export function useCollaborativeWorkflow() {
|
|||||||
userId: session?.user?.id || 'unknown',
|
userId: session?.user?.id || 'unknown',
|
||||||
})
|
})
|
||||||
|
|
||||||
useWorkflowStore.getState().batchAddEdges(newEdges)
|
useWorkflowStore.getState().batchAddEdges(newEdges, { skipValidation: true })
|
||||||
|
|
||||||
if (!options?.skipUndoRedo) {
|
if (!options?.skipUndoRedo) {
|
||||||
newEdges.forEach((edge) => undoRedo.recordAddEdge(edge.id))
|
newEdges.forEach((edge) => undoRedo.recordAddEdge(edge.id))
|
||||||
@@ -1484,9 +1491,23 @@ export function useCollaborativeWorkflow() {
|
|||||||
|
|
||||||
if (blocks.length === 0) return false
|
if (blocks.length === 0) return false
|
||||||
|
|
||||||
|
// Filter out invalid edges (e.g., edges targeting trigger blocks)
|
||||||
|
// Combine existing blocks with new blocks for validation
|
||||||
|
const existingBlocks = useWorkflowStore.getState().blocks
|
||||||
|
const newBlocksMap = blocks.reduce(
|
||||||
|
(acc, block) => {
|
||||||
|
acc[block.id] = block
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, BlockState>
|
||||||
|
)
|
||||||
|
const allBlocks = { ...existingBlocks, ...newBlocksMap }
|
||||||
|
const validEdges = filterValidEdges(edges, allBlocks)
|
||||||
|
|
||||||
logger.info('Batch adding blocks collaboratively', {
|
logger.info('Batch adding blocks collaboratively', {
|
||||||
blockCount: blocks.length,
|
blockCount: blocks.length,
|
||||||
edgeCount: edges.length,
|
edgeCount: validEdges.length,
|
||||||
|
filteredEdges: edges.length - validEdges.length,
|
||||||
})
|
})
|
||||||
|
|
||||||
const operationId = crypto.randomUUID()
|
const operationId = crypto.randomUUID()
|
||||||
@@ -1496,16 +1517,18 @@ export function useCollaborativeWorkflow() {
|
|||||||
operation: {
|
operation: {
|
||||||
operation: BLOCKS_OPERATIONS.BATCH_ADD_BLOCKS,
|
operation: BLOCKS_OPERATIONS.BATCH_ADD_BLOCKS,
|
||||||
target: OPERATION_TARGETS.BLOCKS,
|
target: OPERATION_TARGETS.BLOCKS,
|
||||||
payload: { blocks, edges, loops, parallels, subBlockValues },
|
payload: { blocks, edges: validEdges, loops, parallels, subBlockValues },
|
||||||
},
|
},
|
||||||
workflowId: activeWorkflowId || '',
|
workflowId: activeWorkflowId || '',
|
||||||
userId: session?.user?.id || 'unknown',
|
userId: session?.user?.id || 'unknown',
|
||||||
})
|
})
|
||||||
|
|
||||||
useWorkflowStore.getState().batchAddBlocks(blocks, edges, subBlockValues)
|
useWorkflowStore.getState().batchAddBlocks(blocks, validEdges, subBlockValues, {
|
||||||
|
skipEdgeValidation: true,
|
||||||
|
})
|
||||||
|
|
||||||
if (!options?.skipUndoRedo) {
|
if (!options?.skipUndoRedo) {
|
||||||
undoRedo.recordBatchAddBlocks(blocks, edges, subBlockValues)
|
undoRedo.recordBatchAddBlocks(blocks, validEdges, subBlockValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import type { Edge } from 'reactflow'
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
import { mergeSubBlockValues, mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
|
||||||
|
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import { normalizeName } from '@/executor/constants'
|
import { isAnnotationOnlyBlock, normalizeName } from '@/executor/constants'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import type {
|
import type {
|
||||||
BlockState,
|
BlockState,
|
||||||
@@ -17,6 +18,32 @@ import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
|||||||
|
|
||||||
const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath']
|
const WEBHOOK_SUBBLOCK_FIELDS = ['webhookId', 'triggerPath']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an edge is valid (source and target exist, not annotation-only, target is not a trigger)
|
||||||
|
*/
|
||||||
|
function isValidEdge(
|
||||||
|
edge: Edge,
|
||||||
|
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
||||||
|
): boolean {
|
||||||
|
const sourceBlock = blocks[edge.source]
|
||||||
|
const targetBlock = blocks[edge.target]
|
||||||
|
if (!sourceBlock || !targetBlock) return false
|
||||||
|
if (isAnnotationOnlyBlock(sourceBlock.type)) return false
|
||||||
|
if (isAnnotationOnlyBlock(targetBlock.type)) return false
|
||||||
|
if (TriggerUtils.isTriggerBlock(targetBlock)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters edges to only include valid ones (target exists and is not a trigger block)
|
||||||
|
*/
|
||||||
|
export function filterValidEdges(
|
||||||
|
edges: Edge[],
|
||||||
|
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
||||||
|
): Edge[] {
|
||||||
|
return edges.filter((edge) => isValidEdge(edge, blocks))
|
||||||
|
}
|
||||||
|
|
||||||
export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] {
|
export function filterNewEdges(edgesToAdd: Edge[], currentEdges: Edge[]): Edge[] {
|
||||||
return edgesToAdd.filter((edge) => {
|
return edgesToAdd.filter((edge) => {
|
||||||
if (edge.source === edge.target) return false
|
if (edge.source === edge.target) return false
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import { create } from 'zustand'
|
|||||||
import { devtools } from 'zustand/middleware'
|
import { devtools } from 'zustand/middleware'
|
||||||
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
|
||||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
|
||||||
import { getBlock } from '@/blocks'
|
import { getBlock } from '@/blocks'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { isAnnotationOnlyBlock, normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
import { filterNewEdges, getUniqueBlockName, mergeSubblockState } from '@/stores/workflows/utils'
|
import {
|
||||||
|
filterNewEdges,
|
||||||
|
filterValidEdges,
|
||||||
|
getUniqueBlockName,
|
||||||
|
mergeSubblockState,
|
||||||
|
} from '@/stores/workflows/utils'
|
||||||
import type {
|
import type {
|
||||||
Position,
|
Position,
|
||||||
SubBlockState,
|
SubBlockState,
|
||||||
@@ -91,26 +95,6 @@ function resolveInitialSubblockValue(config: SubBlockConfig): unknown {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidEdge(
|
|
||||||
edge: Edge,
|
|
||||||
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
|
||||||
): boolean {
|
|
||||||
const sourceBlock = blocks[edge.source]
|
|
||||||
const targetBlock = blocks[edge.target]
|
|
||||||
if (!sourceBlock || !targetBlock) return false
|
|
||||||
if (isAnnotationOnlyBlock(sourceBlock.type)) return false
|
|
||||||
if (isAnnotationOnlyBlock(targetBlock.type)) return false
|
|
||||||
if (TriggerUtils.isTriggerBlock(targetBlock)) return false
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterValidEdges(
|
|
||||||
edges: Edge[],
|
|
||||||
blocks: Record<string, { type: string; triggerMode?: boolean }>
|
|
||||||
): Edge[] {
|
|
||||||
return edges.filter((edge) => isValidEdge(edge, blocks))
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
blocks: {},
|
blocks: {},
|
||||||
edges: [],
|
edges: [],
|
||||||
@@ -356,7 +340,8 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
data?: Record<string, any>
|
data?: Record<string, any>
|
||||||
}>,
|
}>,
|
||||||
edges?: Edge[],
|
edges?: Edge[],
|
||||||
subBlockValues?: Record<string, Record<string, unknown>>
|
subBlockValues?: Record<string, Record<string, unknown>>,
|
||||||
|
options?: { skipEdgeValidation?: boolean }
|
||||||
) => {
|
) => {
|
||||||
const currentBlocks = get().blocks
|
const currentBlocks = get().blocks
|
||||||
const currentEdges = get().edges
|
const currentEdges = get().edges
|
||||||
@@ -381,7 +366,10 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (edges && edges.length > 0) {
|
if (edges && edges.length > 0) {
|
||||||
const validEdges = filterValidEdges(edges, newBlocks)
|
// Skip validation if already validated by caller (e.g., collaborative layer)
|
||||||
|
const validEdges = options?.skipEdgeValidation
|
||||||
|
? edges
|
||||||
|
: filterValidEdges(edges, newBlocks)
|
||||||
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
|
const existingEdgeIds = new Set(currentEdges.map((e) => e.id))
|
||||||
for (const edge of validEdges) {
|
for (const edge of validEdges) {
|
||||||
if (!existingEdgeIds.has(edge.id)) {
|
if (!existingEdgeIds.has(edge.id)) {
|
||||||
@@ -516,11 +504,12 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
|||||||
get().updateLastSaved()
|
get().updateLastSaved()
|
||||||
},
|
},
|
||||||
|
|
||||||
batchAddEdges: (edges: Edge[]) => {
|
batchAddEdges: (edges: Edge[], options?: { skipValidation?: boolean }) => {
|
||||||
const blocks = get().blocks
|
const blocks = get().blocks
|
||||||
const currentEdges = get().edges
|
const currentEdges = get().edges
|
||||||
|
|
||||||
const validEdges = filterValidEdges(edges, blocks)
|
// Skip validation if already validated by caller (e.g., collaborative layer)
|
||||||
|
const validEdges = options?.skipValidation ? edges : filterValidEdges(edges, blocks)
|
||||||
const filtered = filterNewEdges(validEdges, currentEdges)
|
const filtered = filterNewEdges(validEdges, currentEdges)
|
||||||
const newEdges = [...currentEdges]
|
const newEdges = [...currentEdges]
|
||||||
|
|
||||||
|
|||||||
@@ -203,12 +203,13 @@ export interface WorkflowActions {
|
|||||||
batchAddBlocks: (
|
batchAddBlocks: (
|
||||||
blocks: BlockState[],
|
blocks: BlockState[],
|
||||||
edges?: Edge[],
|
edges?: Edge[],
|
||||||
subBlockValues?: Record<string, Record<string, unknown>>
|
subBlockValues?: Record<string, Record<string, unknown>>,
|
||||||
|
options?: { skipEdgeValidation?: boolean }
|
||||||
) => void
|
) => void
|
||||||
batchRemoveBlocks: (ids: string[]) => void
|
batchRemoveBlocks: (ids: string[]) => void
|
||||||
batchToggleEnabled: (ids: string[]) => void
|
batchToggleEnabled: (ids: string[]) => void
|
||||||
batchToggleHandles: (ids: string[]) => void
|
batchToggleHandles: (ids: string[]) => void
|
||||||
batchAddEdges: (edges: Edge[]) => void
|
batchAddEdges: (edges: Edge[], options?: { skipValidation?: boolean }) => void
|
||||||
batchRemoveEdges: (ids: string[]) => void
|
batchRemoveEdges: (ids: string[]) => void
|
||||||
clear: () => Partial<WorkflowState>
|
clear: () => Partial<WorkflowState>
|
||||||
updateLastSaved: () => void
|
updateLastSaved: () => void
|
||||||
|
|||||||
Reference in New Issue
Block a user