mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
Completely set up scaffolding for project blocks
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { BlockConfig, BLOCKS } from '../components/block/blocks'
|
||||
import { WorkflowBlock } from '../components/block/workflow-block'
|
||||
import { BlockConfig } from '../components/blocks/types/block'
|
||||
import { WorkflowBlock } from '../components/blocks/components/workflow-block/workflow-block'
|
||||
import { getBlock } from '../components/blocks/configs'
|
||||
|
||||
interface WorkflowBlock {
|
||||
id: string
|
||||
type: string
|
||||
position: { x: number; y: number }
|
||||
config: BlockConfig
|
||||
}
|
||||
|
||||
const ZOOM_SPEED = 0.005
|
||||
const MIN_ZOOM = 0.5
|
||||
@@ -10,14 +18,7 @@ const MAX_ZOOM = 2
|
||||
const CANVAS_SIZE = 5000 // 5000px x 5000px virtual canvas
|
||||
|
||||
export default function Workflow() {
|
||||
const [blocks, setBlocks] = useState<
|
||||
{
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
type: string
|
||||
config: BlockConfig
|
||||
}[]
|
||||
>([])
|
||||
const [blocks, setBlocks] = useState<WorkflowBlock[]>([])
|
||||
const [zoom, setZoom] = useState(1)
|
||||
const [pan, setPan] = useState({ x: 0, y: 0 })
|
||||
const [isPanning, setIsPanning] = useState(false)
|
||||
@@ -66,29 +67,28 @@ export default function Workflow() {
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
const blockData = JSON.parse(
|
||||
e.dataTransfer.getData('application/json')
|
||||
) as BlockConfig
|
||||
const { type } = JSON.parse(e.dataTransfer.getData('application/json'))
|
||||
const blockConfig = getBlock(type)
|
||||
|
||||
if (!blockConfig) {
|
||||
console.error('Invalid block type:', type)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the canvas element's bounding rectangle
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
|
||||
// Calculate the drop position in canvas coordinates
|
||||
// 1. Get the mouse position relative to the canvas element
|
||||
// 2. Remove the pan offset (scaled by zoom)
|
||||
// 3. Scale by zoom to get true canvas coordinates
|
||||
const mouseX = e.clientX - rect.left
|
||||
const mouseY = e.clientY - rect.top
|
||||
|
||||
const x = mouseX / zoom
|
||||
const y = mouseY / zoom
|
||||
|
||||
setBlocks((prev: any) => [
|
||||
setBlocks((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...blockData,
|
||||
id: crypto.randomUUID(),
|
||||
type,
|
||||
position: { x, y },
|
||||
config: blockConfig,
|
||||
},
|
||||
])
|
||||
} catch (err) {
|
||||
@@ -200,20 +200,16 @@ export default function Workflow() {
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{blocks.map((block, index) => {
|
||||
const blockConfig =
|
||||
BLOCKS.find((b) => b.type === block.type) || block.config
|
||||
return (
|
||||
<WorkflowBlock
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
type={block.type}
|
||||
position={block.position}
|
||||
config={blockConfig}
|
||||
name={`${blockConfig.toolbar.title} ${index + 1}`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{blocks.map((block, index) => (
|
||||
<WorkflowBlock
|
||||
key={block.id}
|
||||
id={block.id}
|
||||
type={block.type}
|
||||
position={block.position}
|
||||
config={block.config}
|
||||
name={`${block.config.toolbar.title} ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { AgentIcon, ApiIcon, ConditionalIcon } from '@/components/icons'
|
||||
|
||||
export interface SubBlockConfig {
|
||||
title: string
|
||||
type: 'short-text' | 'long-text' | 'dropdown' | 'slider' | 'group'
|
||||
options?: string[] // For dropdown
|
||||
min?: number // For slider
|
||||
max?: number // For slider
|
||||
layout?: 'full' | 'half' // Controls if the block takes full width or shares space
|
||||
}
|
||||
|
||||
export interface BlockConfig {
|
||||
type: string
|
||||
toolbar: {
|
||||
title: string
|
||||
description: string
|
||||
bgColor: string
|
||||
icon: any
|
||||
category: 'basic' | 'advanced'
|
||||
}
|
||||
workflow: {
|
||||
inputs: { [key: string]: string }
|
||||
outputs: { [key: string]: string }
|
||||
subBlocks: SubBlockConfig[]
|
||||
}
|
||||
}
|
||||
|
||||
export const BLOCKS: BlockConfig[] = [
|
||||
{
|
||||
type: 'agent',
|
||||
toolbar: {
|
||||
title: 'Agent',
|
||||
description: 'Use any LLM',
|
||||
bgColor: '#7F2FFF',
|
||||
icon: AgentIcon,
|
||||
category: 'basic',
|
||||
},
|
||||
workflow: {
|
||||
inputs: {
|
||||
prompt: 'string',
|
||||
},
|
||||
outputs: {
|
||||
response: 'string',
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
title: 'System Prompt',
|
||||
type: 'long-text',
|
||||
layout: 'full',
|
||||
},
|
||||
{
|
||||
title: 'Model',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['GPT-4', 'GPT-3.5', 'Claude'],
|
||||
},
|
||||
{
|
||||
title: 'Temperature',
|
||||
type: 'slider',
|
||||
layout: 'half',
|
||||
min: 0,
|
||||
max: 2,
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'api',
|
||||
toolbar: {
|
||||
title: 'API',
|
||||
description: 'Connect to any API',
|
||||
bgColor: '#2F55FF',
|
||||
icon: ApiIcon,
|
||||
category: 'basic',
|
||||
},
|
||||
workflow: {
|
||||
inputs: {
|
||||
url: 'string',
|
||||
method: 'string',
|
||||
},
|
||||
outputs: {
|
||||
response: 'string',
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
title: 'URL',
|
||||
type: 'long-text',
|
||||
},
|
||||
{
|
||||
title: 'Method',
|
||||
type: 'dropdown',
|
||||
options: ['GET', 'POST', 'PUT', 'DELETE'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'conditional',
|
||||
toolbar: {
|
||||
title: 'Conditional',
|
||||
description: 'Create branching logic',
|
||||
bgColor: '#FF972F',
|
||||
icon: ConditionalIcon,
|
||||
category: 'basic',
|
||||
},
|
||||
workflow: {
|
||||
inputs: {
|
||||
// Add conditional-specific inputs
|
||||
},
|
||||
outputs: {
|
||||
// Add conditional-specific outputs
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
title: 'Condition',
|
||||
type: 'dropdown',
|
||||
options: ['True', 'False'],
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
type: 'dropdown',
|
||||
options: ['Do Something', 'Do Nothing'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1,108 +0,0 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { SubBlockConfig } from './blocks'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface SubBlockProps {
|
||||
config: SubBlockConfig
|
||||
}
|
||||
|
||||
export function SubBlock({ config }: SubBlockProps) {
|
||||
const [sliderValue, setSliderValue] = useState(
|
||||
config.type === 'slider'
|
||||
? (config.min || 0) + ((config.max || 100) - (config.min || 0)) / 2
|
||||
: 0
|
||||
)
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const renderInput = () => {
|
||||
switch (config.type) {
|
||||
case 'short-text':
|
||||
return <Input className="w-full" />
|
||||
case 'long-text':
|
||||
return <Textarea className="w-full resize-none" rows={3} />
|
||||
case 'dropdown':
|
||||
return (
|
||||
<div onMouseDown={handleMouseDown}>
|
||||
<Select defaultValue={config.options?.[0]}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.options?.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
case 'slider':
|
||||
return (
|
||||
<div className="relative pt-2 pb-6">
|
||||
<Slider
|
||||
defaultValue={[
|
||||
config.type === 'slider'
|
||||
? (config.min || 0) +
|
||||
((config.max || 100) - (config.min || 0)) / 2
|
||||
: 0,
|
||||
]}
|
||||
min={config.min}
|
||||
max={config.max}
|
||||
step={0.1}
|
||||
onValueChange={(value) => setSliderValue(value[0])}
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4 [&_[class*=SliderTrack]]:h-1"
|
||||
/>
|
||||
<div
|
||||
className="absolute text-sm text-muted-foreground"
|
||||
style={{
|
||||
left: `clamp(0%, ${
|
||||
((sliderValue - (config.min || 0)) /
|
||||
((config.max || 100) - (config.min || 0))) *
|
||||
100
|
||||
}%, 100%)`,
|
||||
transform: `translateX(-${
|
||||
((sliderValue - (config.min || 0)) /
|
||||
((config.max || 100) - (config.min || 0))) *
|
||||
100 ===
|
||||
0
|
||||
? 0
|
||||
: ((sliderValue - (config.min || 0)) /
|
||||
((config.max || 100) - (config.min || 0))) *
|
||||
100 ===
|
||||
100
|
||||
? 100
|
||||
: 50
|
||||
}%)`,
|
||||
top: '24px',
|
||||
}}
|
||||
>
|
||||
{sliderValue.toFixed(1)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1" onMouseDown={handleMouseDown}>
|
||||
<Label>{config.title}</Label>
|
||||
{renderInput()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import type { BlockConfig } from './blocks'
|
||||
import type { BlockConfig } from '../../types/block'
|
||||
|
||||
export type ToolbarBlockProps = {
|
||||
type: string
|
||||
toolbar: BlockConfig['toolbar']
|
||||
config: BlockConfig
|
||||
}
|
||||
|
||||
export function ToolbarBlock({ type, toolbar }: ToolbarBlockProps) {
|
||||
export function ToolbarBlock({ config }: ToolbarBlockProps) {
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
e.dataTransfer.setData('application/json', JSON.stringify({ type }))
|
||||
e.dataTransfer.setData(
|
||||
'application/json',
|
||||
JSON.stringify({ type: config.type })
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -18,17 +20,19 @@ export function ToolbarBlock({ type, toolbar }: ToolbarBlockProps) {
|
||||
>
|
||||
<div
|
||||
className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg"
|
||||
style={{ backgroundColor: toolbar.bgColor }}
|
||||
style={{ backgroundColor: config.toolbar.bgColor }}
|
||||
>
|
||||
<toolbar.icon
|
||||
<config.toolbar.icon
|
||||
className={`text-white transition-transform duration-200 group-hover:scale-110 ${
|
||||
type === 'agent' ? 'w-[24px] h-[24px]' : 'w-[22px] h-[22px]'
|
||||
config.type === 'agent' ? 'w-[24px] h-[24px]' : 'w-[22px] h-[22px]'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="font-medium leading-none">{toolbar.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">{toolbar.description}</p>
|
||||
<h3 className="font-medium leading-none">{config.toolbar.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{config.toolbar.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
|
||||
interface DropdownProps {
|
||||
options: string[]
|
||||
defaultValue?: string
|
||||
}
|
||||
|
||||
export function Dropdown({ options, defaultValue }: DropdownProps) {
|
||||
return (
|
||||
<Select defaultValue={defaultValue ?? options[0]}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
export function LongInput() {
|
||||
return <Textarea className="w-full resize-none" rows={3} />
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
export function ShortInput() {
|
||||
return <Input className="w-full" />
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface SliderInputProps {
|
||||
min?: number
|
||||
max?: number
|
||||
defaultValue: number
|
||||
}
|
||||
|
||||
export function SliderInput({
|
||||
min = 0,
|
||||
max = 100,
|
||||
defaultValue,
|
||||
}: SliderInputProps) {
|
||||
const [sliderValue, setSliderValue] = useState(defaultValue)
|
||||
|
||||
return (
|
||||
<div className="relative pt-2 pb-6">
|
||||
<Slider
|
||||
defaultValue={[defaultValue]}
|
||||
min={min}
|
||||
max={max}
|
||||
step={0.1}
|
||||
onValueChange={(value) => setSliderValue(value[0])}
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4 [&_[class*=SliderTrack]]:h-1"
|
||||
/>
|
||||
<div
|
||||
className="absolute text-sm text-muted-foreground"
|
||||
style={{
|
||||
left: `clamp(0%, ${
|
||||
((sliderValue - min) / (max - min)) * 100
|
||||
}%, 100%)`,
|
||||
transform: `translateX(-${
|
||||
((sliderValue - min) / (max - min)) * 100 === 0
|
||||
? 0
|
||||
: ((sliderValue - min) / (max - min)) * 100 === 100
|
||||
? 100
|
||||
: 50
|
||||
}%)`,
|
||||
top: '24px',
|
||||
}}
|
||||
>
|
||||
{sliderValue.toFixed(1)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { SubBlockConfig } from '../../../types/block'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { ShortInput } from './components/short-input'
|
||||
import { LongInput } from './components/long-input'
|
||||
import { Dropdown } from './components/dropdown'
|
||||
import { SliderInput } from './components/slider-input'
|
||||
|
||||
interface SubBlockProps {
|
||||
config: SubBlockConfig
|
||||
}
|
||||
|
||||
export function SubBlock({ config }: SubBlockProps) {
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const renderInput = () => {
|
||||
switch (config.type) {
|
||||
case 'short-input':
|
||||
return <ShortInput />
|
||||
case 'long-input':
|
||||
return <LongInput />
|
||||
case 'dropdown':
|
||||
return (
|
||||
<div onMouseDown={handleMouseDown}>
|
||||
<Dropdown options={config.options ?? []} />
|
||||
</div>
|
||||
)
|
||||
case 'slider':
|
||||
return (
|
||||
<SliderInput
|
||||
min={config.min}
|
||||
max={config.max}
|
||||
defaultValue={
|
||||
(config.min || 0) + ((config.max || 100) - (config.min || 0)) / 2
|
||||
}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1" onMouseDown={handleMouseDown}>
|
||||
<Label>{config.title}</Label>
|
||||
{renderInput()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { BlockConfig, SubBlockConfig } from './blocks'
|
||||
import { BlockConfig, SubBlockConfig } from '../../types/block'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SubBlock } from './sub-block'
|
||||
import { SubBlock } from './sub-block/sub-block'
|
||||
|
||||
export interface WorkflowBlockProps {
|
||||
id: string
|
||||
@@ -69,7 +69,7 @@ export function WorkflowBlock({
|
||||
|
||||
<div className="px-4 pt-2 pb-4 space-y-4">
|
||||
{subBlockRows.map((row, rowIndex) => (
|
||||
<div key={`row-${rowIndex}`} className="flex gap-2">
|
||||
<div key={`row-${rowIndex}`} className="flex gap-4">
|
||||
{row.map((subBlock, blockIndex) => (
|
||||
<div
|
||||
key={`${id}-${rowIndex}-${blockIndex}`}
|
||||
43
app/w/components/blocks/configs/agent.ts
Normal file
43
app/w/components/blocks/configs/agent.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { AgentIcon } from '@/components/icons'
|
||||
import { BlockConfig } from '../types/block'
|
||||
|
||||
export const AgentBlock: BlockConfig = {
|
||||
type: 'agent',
|
||||
toolbar: {
|
||||
title: 'Agent',
|
||||
description: 'Use any LLM',
|
||||
bgColor: '#7F2FFF',
|
||||
icon: AgentIcon,
|
||||
category: 'basic',
|
||||
},
|
||||
workflow: {
|
||||
inputs: {
|
||||
prompt: 'string',
|
||||
context: 'string',
|
||||
},
|
||||
outputs: {
|
||||
response: 'string',
|
||||
tokens: 'number',
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
title: 'System Prompt',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
},
|
||||
{
|
||||
title: 'Model',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['GPT-4', 'GPT-3.5', 'Claude', 'Gemini'],
|
||||
},
|
||||
{
|
||||
title: 'Temperature',
|
||||
type: 'slider',
|
||||
layout: 'half',
|
||||
min: 0,
|
||||
max: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
48
app/w/components/blocks/configs/api.ts
Normal file
48
app/w/components/blocks/configs/api.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ApiIcon } from '@/components/icons'
|
||||
import { BlockConfig } from '../types/block'
|
||||
|
||||
export const ApiBlock: BlockConfig = {
|
||||
type: 'api',
|
||||
toolbar: {
|
||||
title: 'API',
|
||||
description: 'Use any API',
|
||||
bgColor: '#2F55FF',
|
||||
icon: ApiIcon,
|
||||
category: 'basic',
|
||||
},
|
||||
workflow: {
|
||||
inputs: {
|
||||
url: 'string',
|
||||
method: 'string',
|
||||
headers: 'object',
|
||||
body: 'string',
|
||||
},
|
||||
outputs: {
|
||||
response: 'string',
|
||||
statusCode: 'number',
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
title: 'URL',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
},
|
||||
{
|
||||
title: 'Method',
|
||||
type: 'dropdown',
|
||||
layout: 'half',
|
||||
options: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||
},
|
||||
{
|
||||
title: 'Headers',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
},
|
||||
{
|
||||
title: 'Body',
|
||||
type: 'long-input',
|
||||
layout: 'full',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
42
app/w/components/blocks/configs/conditional.ts
Normal file
42
app/w/components/blocks/configs/conditional.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ConditionalIcon } from '@/components/icons'
|
||||
import { BlockConfig } from '../types/block'
|
||||
|
||||
export const ConditionalBlock: BlockConfig = {
|
||||
type: 'conditional',
|
||||
toolbar: {
|
||||
title: 'Conditional',
|
||||
description: 'Add branching logic',
|
||||
bgColor: '#FF972F',
|
||||
icon: ConditionalIcon,
|
||||
category: 'basic',
|
||||
},
|
||||
workflow: {
|
||||
inputs: {
|
||||
condition: 'boolean',
|
||||
value: 'any',
|
||||
},
|
||||
outputs: {
|
||||
result: 'any',
|
||||
path: 'string',
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
title: 'Condition Type',
|
||||
type: 'dropdown',
|
||||
layout: 'full',
|
||||
options: [
|
||||
'Equals',
|
||||
'Contains',
|
||||
'Greater Than',
|
||||
'Less Than',
|
||||
'Regular Expression',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
27
app/w/components/blocks/configs/index.ts
Normal file
27
app/w/components/blocks/configs/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { BlockConfig, BlockType } from '../types/block'
|
||||
import { AgentBlock } from './agent'
|
||||
import { ApiBlock } from './api'
|
||||
import { ConditionalBlock } from './conditional'
|
||||
|
||||
// Export individual blocks
|
||||
export { AgentBlock, ApiBlock, ConditionalBlock }
|
||||
|
||||
// Combined blocks registry
|
||||
export const BLOCKS: BlockConfig[] = [
|
||||
AgentBlock,
|
||||
ApiBlock,
|
||||
ConditionalBlock,
|
||||
]
|
||||
|
||||
// Helper functions
|
||||
export const getBlock = (type: BlockType): BlockConfig | undefined =>
|
||||
BLOCKS.find(block => block.type === type)
|
||||
|
||||
export const getBlocksByCategory = (category: 'basic' | 'advanced'): BlockConfig[] =>
|
||||
BLOCKS.filter(block => block.toolbar.category === category)
|
||||
|
||||
export const getAllBlockTypes = (): BlockType[] =>
|
||||
BLOCKS.map(block => block.type)
|
||||
|
||||
export const isValidBlockType = (type: string): type is BlockType =>
|
||||
BLOCKS.some(block => block.type === type)
|
||||
2
app/w/components/blocks/index.ts
Normal file
2
app/w/components/blocks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './types/block'
|
||||
export * from './configs'
|
||||
33
app/w/components/blocks/types/block.ts
Normal file
33
app/w/components/blocks/types/block.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { SVGProps } from 'react'
|
||||
import type { JSX } from 'react'
|
||||
|
||||
export type BlockType = 'agent' | 'api' | 'conditional'
|
||||
export type BlockIcon = (props: SVGProps<SVGSVGElement>) => JSX.Element
|
||||
export type BlockCategory = 'basic' | 'advanced'
|
||||
export type SubBlockType = 'short-input' | 'long-input' | 'dropdown' | 'slider'
|
||||
export type SubBlockLayout = 'full' | 'half'
|
||||
|
||||
export interface SubBlockConfig {
|
||||
title: string
|
||||
type: SubBlockType
|
||||
options?: string[]
|
||||
min?: number
|
||||
max?: number
|
||||
layout?: SubBlockLayout
|
||||
}
|
||||
|
||||
export interface BlockConfig {
|
||||
type: BlockType
|
||||
toolbar: {
|
||||
title: string
|
||||
description: string
|
||||
bgColor: string
|
||||
icon: BlockIcon
|
||||
category: BlockCategory
|
||||
}
|
||||
workflow: {
|
||||
inputs: Record<string, string>
|
||||
outputs: Record<string, string>
|
||||
subBlocks: SubBlockConfig[]
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { ToolbarTabs } from './toolbar-tabs'
|
||||
import { ToolbarBlock } from '../block/toolbar-block'
|
||||
import { BLOCKS } from '../block/blocks'
|
||||
import { ToolbarBlock } from '../blocks/components/toolbar-block/toolbar-block'
|
||||
import { getBlocksByCategory, BlockCategory } from '../blocks'
|
||||
|
||||
export function Toolbar() {
|
||||
const [activeTab, setActiveTab] = useState<'basic' | 'advanced'>('basic')
|
||||
const [activeTab, setActiveTab] = useState<BlockCategory>('basic')
|
||||
|
||||
return (
|
||||
<div className="fixed left-14 top-0 z-1 hidden h-full w-72 border-r bg-background sm:block">
|
||||
@@ -14,15 +14,9 @@ export function Toolbar() {
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
{BLOCKS.filter((block) => block.toolbar.category === activeTab).map(
|
||||
(block) => (
|
||||
<ToolbarBlock
|
||||
key={block.type}
|
||||
type={block.type}
|
||||
toolbar={block.toolbar}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{getBlocksByCategory(activeTab).map((block) => (
|
||||
<ToolbarBlock key={block.type} config={block} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Providers from './providers'
|
||||
import { Toolbar } from './components/toolbar/toolbar'
|
||||
import { ControlBar } from './components/control-bar'
|
||||
import { ControlBar } from './components/control-bar/control-bar'
|
||||
import { Sidebar } from './components/sidebar/sidebar'
|
||||
|
||||
export default function WorkspaceLayout({
|
||||
|
||||
Reference in New Issue
Block a user