Added Firecrawl web scrape tool/block and switch toggle sub-block

This commit is contained in:
Waleed Latif
2025-01-29 22:27:41 -08:00
parent 38e3731111
commit dc64f0b388
12 changed files with 323 additions and 13 deletions

View File

@@ -0,0 +1,18 @@
import { Switch as UISwitch } from '@/components/ui/switch'
import { useSubBlockValue } from '../hooks/use-sub-block-value'
interface SwitchProps {
blockId: string
subBlockId: string
}
export function Switch({ blockId, subBlockId }: SwitchProps) {
const [value, setValue] = useSubBlockValue(blockId, subBlockId)
return (
<UISwitch
checked={Boolean(value)}
onCheckedChange={(checked) => setValue(checked)}
/>
)
}

View File

@@ -6,6 +6,7 @@ import { Dropdown } from './components/dropdown'
import { SliderInput } from './components/slider-input'
import { Table } from './components/table'
import { Code } from './components/code'
import { Switch } from './components/switch'
interface SubBlockProps {
blockId: string
@@ -70,6 +71,13 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
)
case 'code':
return <Code blockId={blockId} subBlockId={config.id} />
case 'switch':
return (
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">{config.title}</Label>
<Switch blockId={blockId} subBlockId={config.id} />
</div>
)
default:
return null
}
@@ -77,7 +85,7 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
return (
<div className="space-y-1" onMouseDown={handleMouseDown}>
<Label>{config.title}</Label>
{config.type !== 'switch' && <Label>{config.title}</Label>}
{renderInput()}
</div>
)

66
app/w/page.tsx Normal file
View File

@@ -0,0 +1,66 @@
"use client"
import { useState } from "react"
import { WorkflowBlock } from "./components/workflow-block/workflow-block"
import { BlockConfig, BlockCategory, BlockIcon } from "@/blocks/types"
import { LayoutDashboard } from "lucide-react"
const WorkflowIcon: BlockIcon = (props) => <LayoutDashboard {...props} />
interface WorkflowBlockState {
id: string
type: string
position: { x: number; y: number }
config: BlockConfig
name: string
}
export default function Page() {
const [workflows, setWorkflows] = useState<WorkflowBlockState[]>([])
const createNewWorkflow = () => {
const newWorkflow: WorkflowBlockState = {
id: `workflow-${workflows.length + 1}`,
type: "default",
position: { x: 0, y: 0 },
config: {
type: "default",
toolbar: {
title: "New Workflow",
description: "Empty workflow",
bgColor: "#808080",
icon: WorkflowIcon,
category: "basic" as BlockCategory
},
tools: {
access: []
},
workflow: {
subBlocks: [],
inputs: {},
outputs: {}
}
},
name: `New Workflow ${workflows.length + 1}`
}
setWorkflows([...workflows, newWorkflow])
}
return (
<div>
<button onClick={createNewWorkflow}>New Workflow</button>
<div>
{workflows.map((workflow) => (
<WorkflowBlock
key={workflow.id}
id={workflow.id}
type={workflow.type}
position={workflow.position}
config={workflow.config}
name={workflow.name}
/>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,49 @@
import { BlockConfig } from '../types'
import { FirecrawlIcon } from '@/components/icons'
export const FirecrawlScrapeBlock: BlockConfig = {
type: 'firecrawlscrape',
toolbar: {
title: 'Firecrawl Scraper',
description: 'Extract clean content from any webpage',
bgColor: '#FF6B6B',
icon: FirecrawlIcon,
category: 'advanced'
},
tools: {
access: ['firecrawl.scrape']
},
workflow: {
inputs: {
apiKey: { type: 'string', required: true },
url: { type: 'string', required: true },
scrapeOptions: { type: 'json', required: false }
},
outputs: {
response: 'any'
},
subBlocks: [
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
layout: 'full',
placeholder: 'Enter your Firecrawl API key',
password: true
},
{
id: 'url',
title: 'Website URL',
type: 'short-input',
layout: 'full',
placeholder: 'Enter the webpage URL to scrape'
},
{
id: 'onlyMainContent',
title: 'Only Main Content',
type: 'switch',
layout: 'half'
}
]
}
}

View File

@@ -5,16 +5,18 @@ import { AgentBlock } from './blocks/agent'
import { ApiBlock } from './blocks/api'
import { FunctionBlock } from './blocks/function'
import { CrewAIVisionBlock } from './blocks/crewai'
import { FirecrawlScrapeBlock } from './blocks/firecrawl'
// Export blocks for ease of use
export { AgentBlock, ApiBlock, FunctionBlock, CrewAIVisionBlock }
export { AgentBlock, ApiBlock, FunctionBlock, CrewAIVisionBlock, FirecrawlScrapeBlock }
// Registry of all block configurations
const blocks: Record<string, BlockConfig> = {
agent: AgentBlock,
api: ApiBlock,
function: FunctionBlock,
crewaivision: CrewAIVisionBlock
crewaivision: CrewAIVisionBlock,
firecrawlscrape: FirecrawlScrapeBlock
}
// Build a reverse mapping of tools to block types

View File

@@ -6,7 +6,7 @@ export type BlockCategory = 'basic' | 'advanced'
export type OutputType = 'string' | 'number' | 'json' | 'boolean' | 'any'
export type ParamType = 'string' | 'number' | 'boolean' | 'json'
export type SubBlockType = 'short-input' | 'long-input' | 'dropdown' | 'slider' | 'table' | 'code'
export type SubBlockType = 'short-input' | 'long-input' | 'dropdown' | 'slider' | 'table' | 'code' | 'switch'
export type SubBlockLayout = 'full' | 'half'
export type OutputConfig = OutputType | {

View File

@@ -344,7 +344,7 @@ export function SectionIcon(props: SVGProps<SVGSVGElement>) {
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.88889 22.2222V7.77778M4.88889 22.2222C4.31752 22.2222 3.75898 22.3917 3.28391 22.7091C2.80883 23.0265 2.43856 23.4777 2.2199 24.0056C2.00125 24.5335 1.94404 25.1143 2.05551 25.6747C2.16698 26.2351 2.44212 26.7498 2.84614 27.1539C3.25016 27.5579 3.76491 27.833 4.3253 27.9445C4.88569 28.056 5.46654 27.9987 5.99442 27.7801C6.5223 27.5614 6.97348 27.1912 7.29091 26.7161C7.60835 26.241 7.77778 25.6825 7.77778 25.1111M4.88889 22.2222C5.65507 22.2222 6.38987 22.5266 6.93164 23.0684C7.47341 23.6101 7.77778 24.3449 7.77778 25.1111M4.88889 7.77778C5.65507 7.77778 6.38987 7.47341 6.93164 6.93164C7.47341 6.38987 7.77778 5.65507 7.77778 4.88889M4.88889 7.77778C4.31752 7.77778 3.75898 7.60835 3.28391 7.29091C2.80883 6.97348 2.43856 6.5223 2.2199 5.99442C2.00125 5.46654 1.94404 4.88569 2.05551 4.3253C2.16698 3.76491 2.44212 3.25016 2.84614 2.84614C3.25016 2.44212 3.76491 2.16698 4.3253 2.05551C4.88569 1.94404 5.46654 2.00125 5.99442 2.2199C6.5223 2.43856 6.97348 2.80883 7.29091 3.28391C7.60835 3.75898 7.77778 4.31752 7.77778 4.88889M7.77778 25.1111H22.2222M7.77778 4.88889H22.2222M22.2222 4.88889C22.2222 5.65507 22.5266 6.38987 23.0684 6.93164C23.6101 7.47341 24.3449 7.77778 25.1111 7.77778M22.2222 4.88889C22.2222 4.31752 22.3917 3.75898 22.7091 3.28391C23.0265 2.80883 23.4777 2.43856 24.0056 2.2199C24.5335 2.00125 25.1143 1.94404 25.6747 2.05551C26.2351 2.16698 26.7498 2.44212 27.1539 2.84614C27.5579 3.25016 27.833 3.76491 27.9445 4.3253C28.056 4.88569 27.9987 5.46654 27.7801 5.99442C27.5614 6.5223 27.1912 6.97348 26.7161 7.29091C26.241 7.60835 25.6825 7.77778 25.1111 7.77778M25.1111 7.77778V22.2222M25.1111 22.2222C24.3449 22.2222 23.6101 22.5266 23.0684 23.0684C22.5266 23.6101 22.2222 24.3449 22.2222 25.1111M25.1111 22.2222C25.6825 22.2222 26.241 22.3917 26.7161 22.7091C27.1912 23.0265 27.5614 23.4777 27.7801 24.0056C27.9987 24.5335 28 25.1143 27.9445 25.6747C27.833 26.2351 27.5579 26.7498 27.1539 27.1539C26.7498 27.5579 26.2351 27.833 25.6747 27.9445C25.1143 28.056 24.5335 27.9987 24.0056 27.7801C23.4777 27.5614 23.0265 27.1912 22.7091 26.7161C22.3917 26.241 22.2222 25.6825 22.2222 25.1111"
d="M4.88889 22.2222V7.77778M4.88889 22.2222C4.31752 22.2222 3.75898 22.3917 3.28391 22.7091C2.80883 23.0265 2.43856 23.4777 2.2199 24.0056C2.00125 24.5335 1.94404 25.1143 2.05551 25.6747C2.16698 26.2351 2.44212 26.7498 2.84614 27.1539C3.25016 27.5579 3.76491 27.833 4.3253 27.9445C4.88569 28.056 5.46654 27.9987 5.99442 27.7801C6.5223 27.5614 6.97348 27.1912 7.29091 26.7161C7.60835 26.241 7.77778 25.6825 7.77778 25.1111M4.88889 22.2222C5.65507 22.2222 6.38987 22.5266 6.93164 23.0684C7.47341 23.6101 7.77778 24.3449 7.77778 25.1111M4.88889 7.77778C5.65507 7.77778 6.38987 7.47341 6.93164 6.93164C7.47341 6.38987 7.77778 5.65507 7.77778 4.88889M4.88889 7.77778C4.31752 7.77778 3.75898 7.60835 3.28391 7.29091C2.80883 6.97348 2.43856 6.5223 2.2199 5.99442C2.00125 5.46654 1.94404 4.88569 2.05551 4.3253C2.16698 3.76491 2.44212 3.25016 2.84614 2.84614C3.25016 2.44212 3.76491 2.16698 4.3253 2.05551C4.88569 1.94404 5.46654 2.00125 5.99442 2.2199C6.5223 2.43856 6.97348 2.80883 7.29091 3.28391C7.60835 3.75898 7.77778 4.31752 7.77778 4.88889M7.77778 25.1111H22.2222M7.77778 4.88889H22.2222M22.2222 4.88889C22.2222 5.65507 22.5266 6.38987 23.0684 6.93164C23.6101 7.47341 24.3449 7.77778 25.1111 7.77778M22.2222 4.88889C22.2222 4.31752 22.3917 3.75898 22.7091 3.28391C23.0265 2.80883 23.4777 2.43856 24.0056 2.2199C24.5335 2.00125 25.1143 1.94404 25.6747 2.05551C26.2351 2.16698 26.7498 2.44212 27.1539 2.84614C27.5579 3.25016 27.833 3.76491 27.9445 4.3253C28 4.88569 27.9987 5.46654 27.7801 5.99442C27.5614 6.5223 27.1912 6.97348 26.7161 7.29091C26.241 7.60835 25.6825 7.77778 25.1111 7.77778M25.1111 7.77778V22.2222M25.1111 22.2222C24.3449 22.2222 23.6101 22.5266 23.0684 23.0684C22.5266 23.6101 22.2222 24.3449 22.2222 25.1111M25.1111 22.2222C25.6825 22.2222 26.241 22.3917 26.7161 22.7091C27.1912 23.0265 27.5614 23.4777 27.7801 24.0056C27.9987 24.5335 28 25.1143 27.9445 25.6747C27.833 26.2351 27.5579 26.7498 27.1539 27.1539C26.7498 27.5579 26.2351 27.833 25.6747 27.9445C25.1143 28.056 24.5335 27.9987 24.0056 27.7801C23.4777 27.5614 23.0265 27.1912 22.7091 26.7161C22.3917 26.241 22.2222 25.6825 22.2222 25.1111"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
@@ -892,3 +892,21 @@ export function SalesforceIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export const FirecrawlIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
<circle cx="12" cy="12" r="3" />
</svg>
)

26
components/ui/switch.tsx Normal file
View File

@@ -0,0 +1,26 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-blue-500 data-[state=unchecked]:bg-gray-200",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

30
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.6",
"class-variance-authority": "^0.7.1",
@@ -2568,6 +2569,35 @@
}
}
},
"node_modules/@radix-ui/react-switch": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz",
"integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-use-previous": "1.1.0",
"@radix-ui/react-use-size": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",

View File

@@ -18,6 +18,7 @@
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.6",
"class-variance-authority": "^0.7.1",

91
tools/firecrawl/scrape.ts Normal file
View File

@@ -0,0 +1,91 @@
import { ToolConfig, ToolResponse } from '../types'
interface ScrapeParams {
apiKey: string
url: string
scrapeOptions?: {
onlyMainContent?: boolean
formats?: string[]
}
}
interface ScrapeResponse extends ToolResponse {
success: boolean
data: {
markdown: string
html?: string
metadata: {
title: string
description: string
language: string
keywords: string
robots: string
ogTitle: string
ogDescription: string
ogUrl: string
ogImage: string
ogLocaleAlternate: string[]
ogSiteName: string
sourceURL: string
statusCode: number
}
}
}
export const scrapeTool: ToolConfig<ScrapeParams, ScrapeResponse> = {
id: 'firecrawl.scrape',
name: 'Firecrawl Website Scraper',
description: 'Extract clean content from any webpage in markdown format',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
description: 'Firecrawl API key'
},
url: {
type: 'string',
required: true,
description: 'The URL to scrape content from'
},
scrapeOptions: {
type: 'json',
required: false,
description: 'Options for content scraping'
}
},
request: {
method: 'POST',
url: 'https://api.firecrawl.dev/v1/scrape',
headers: (params) => ({
'Content-Type': 'application/json',
'Authorization': `Bearer ${params.apiKey}`
}),
body: (params) => ({
url: params.url,
formats: params.scrapeOptions?.formats || ["markdown"]
})
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!data.success) {
throw new Error(data.error?.message || 'Unknown error occurred')
}
return {
success: data.success,
data: data.data,
output: data.data.markdown
}
},
transformError: (error) => {
const message = error.error?.message || error.message
const code = error.error?.type || error.code
return `${message} (${code})`
}
}

View File

@@ -1,5 +1,5 @@
import { ToolConfig } from './types'
import { chatTool as openaiChat } from './openai/chat'
import { ToolConfig } from './types'
import { chatTool as openAIChat } from './openai/chat'
import { chatTool as anthropicChat } from './anthropic/chat'
import { chatTool as googleChat } from './google/chat'
import { chatTool as xaiChat } from './xai/chat'
@@ -8,13 +8,13 @@ import { reasonerTool as deepseekReasoner } from './deepseek/reasoner'
import { requestTool as httpRequest } from './http/request'
import { contactsTool as hubspotContacts } from './hubspot/contacts'
import { opportunitiesTool as salesforceOpportunities } from './salesforce/opportunities'
import { functionExecuteTool as functionExecute } from './function/execute'
import { functionExecuteTool as functionExecute } from './function/execute'
import { visionTool as crewAIVision } from './crewai/vision'
import { scrapeTool } from './firecrawl/scrape'
// Registry of all available tools
export const tools: Record<string, ToolConfig> = {
// AI Models
'openai.chat': openaiChat,
'openai.chat': openAIChat,
'anthropic.chat': anthropicChat,
'google.chat': googleChat,
'xai.chat': xaiChat,
@@ -28,7 +28,9 @@ export const tools: Record<string, ToolConfig> = {
// Function Tools
'function.execute': functionExecute,
// CrewAI Tools
'crewai.vision': crewAIVision
'crewai.vision': crewAIVision,
// Firecrawl Tools
'firecrawl.scrape': scrapeTool
}
// Get a tool by its ID
@@ -65,8 +67,7 @@ export async function executeTool(
throw new Error(tool.transformError(error))
}
const data = await response.json()
return tool.transformResponse(data)
return tool.transformResponse(response)
} catch (error) {
throw new Error(tool.transformError(error))
}