From 4158dd33e7c01b48f5ac1e1d8b020a5636a8d600 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Thu, 27 Feb 2025 00:30:22 -0800 Subject: [PATCH] feat(custom-functions): completed UI --- .../sub-block/components/tool-input.tsx | 294 --------- .../tool-input/components/code-editor.tsx | 178 ++++++ .../components/custom-tool-modal.tsx | 404 ++++++++++++ .../components/tool-input/tool-input.tsx | 582 ++++++++++++++++++ .../components/sub-block/sub-block.tsx | 2 +- components/ui/command.tsx | 7 + stores/custom-tools/store.ts | 103 ++++ stores/index.ts | 10 +- 8 files changed, 1282 insertions(+), 298 deletions(-) delete mode 100644 app/w/[id]/components/workflow-block/components/sub-block/components/tool-input.tsx create mode 100644 app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor.tsx create mode 100644 app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal.tsx create mode 100644 app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx create mode 100644 stores/custom-tools/store.ts diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input.tsx deleted file mode 100644 index 370e623b84..0000000000 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import { useState } from 'react' -import { PlusIcon, XIcon } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from '@/components/ui/command' -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { cn } from '@/lib/utils' -import { useWorkflowStore } from '@/stores/workflow/store' -import { getAllBlocks } from '@/blocks' -import { getTool } from '@/tools' -import { useSubBlockValue } from '../hooks/use-sub-block-value' -import { ShortInput } from './short-input' - -interface ToolInputProps { - blockId: string - subBlockId: string -} - -interface StoredTool { - type: string - title: string - params: Record - isExpanded?: boolean -} - -interface ToolParam { - id: string - type: string - description?: string - requiredForToolCall: boolean -} - -// Assumes the first tool in the access array is the tool to be used -// TODO: Switch to getting tools instead of tool blocks once we switch to providers -const getToolIdFromBlock = (blockType: string): string | undefined => { - const block = getAllBlocks().find((block) => block.type === blockType) - return block?.tools.access[0] -} - -const getRequiredToolParams = (toolId: string): ToolParam[] => { - const tool = getTool(toolId) - if (!tool) return [] - - return Object.entries(tool.params) - .filter(([_, param]) => param.requiredForToolCall) - .map(([paramId, param]) => ({ - id: paramId, - type: param.type, - description: param.description, - requiredForToolCall: param.requiredForToolCall ?? false, - })) -} - -export function ToolInput({ blockId, subBlockId }: ToolInputProps) { - const [value, setValue] = useSubBlockValue(blockId, subBlockId) - const [open, setOpen] = useState(false) - const isWide = useWorkflowStore((state) => state.blocks[blockId]?.isWide) - - const toolBlocks = getAllBlocks().filter((block) => block.category === 'tools') - - const selectedTools: StoredTool[] = - Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' - ? (value as unknown as StoredTool[]) - : [] - - const handleSelectTool = (toolBlock: (typeof toolBlocks)[0]) => { - // Check if tool already exists - if (selectedTools.some((tool) => tool.type === toolBlock.type)) { - setOpen(false) - return - } - - const toolId = getToolIdFromBlock(toolBlock.type) - const newTool: StoredTool = { - type: toolBlock.type, - title: toolBlock.name, - params: {}, - isExpanded: true, - } - - // If isWide, keep tools in the same row expanded - if (isWide) { - setValue([ - ...selectedTools.map((tool, index) => ({ - ...tool, - // Keep expanded if it's in the same row as the new tool - isExpanded: Math.floor(selectedTools.length / 2) === Math.floor(index / 2), - })), - newTool, - ]) - } else { - // Original behavior for non-wide mode - setValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool]) - } - - setOpen(false) - } - - const handleRemoveTool = (toolType: string) => { - setValue(selectedTools.filter((tool) => tool.type !== toolType)) - } - - const handleParamChange = (toolType: string, paramId: string, paramValue: string) => { - setValue( - selectedTools.map((tool) => - tool.type === toolType - ? { - ...tool, - params: { - ...tool.params, - [paramId]: paramValue, - }, - } - : tool - ) - ) - } - - const toggleToolExpansion = (toolType: string) => { - setValue( - selectedTools.map((tool) => - tool.type === toolType ? { ...tool, isExpanded: !tool.isExpanded } : tool - ) - ) - } - - const IconComponent = ({ icon: Icon, className }: { icon: any; className?: string }) => { - if (!Icon) return null - return - } - - return ( -
- {selectedTools.length === 0 ? ( - - -
-
- - Add Tool -
-
-
- - - - - No tools found. - - {toolBlocks.map((block) => ( - handleSelectTool(block)} - className="flex items-center gap-2 cursor-pointer" - > -
- -
- {block.name} -
- ))} -
-
-
-
-
- ) : ( -
- {selectedTools.map((tool) => { - const toolBlock = toolBlocks.find((block) => block.type === tool.type) - const toolId = getToolIdFromBlock(tool.type) - const requiredParams = toolId ? getRequiredToolParams(toolId) : [] - - return ( -
-
-
toggleToolExpansion(tool.type)} - > -
-
- -
- {tool.title} -
-
- -
-
- - {tool.isExpanded && requiredParams.length > 0 && ( -
{ - if (e.target === e.currentTarget) { - toggleToolExpansion(tool.type) - } - }} - > - {requiredParams.map((param) => ( -
-
- {param.id} -
-
- handleParamChange(tool.type, param.id, value)} - /> -
-
- ))} -
- )} -
-
- ) - })} - - - - - - - - - No tools found. - - {toolBlocks.map((block) => ( - handleSelectTool(block)} - className="flex items-center gap-2 cursor-pointer" - > -
- -
- {block.name} -
- ))} -
-
-
-
-
-
- )} -
- ) -} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor.tsx new file mode 100644 index 0000000000..d003f44c0d --- /dev/null +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/code-editor.tsx @@ -0,0 +1,178 @@ +import { useEffect, useRef, useState } from 'react' +import type { ReactElement } from 'react' +import { highlight, languages } from 'prismjs' +import 'prismjs/components/prism-javascript' +import 'prismjs/components/prism-json' +import 'prismjs/themes/prism.css' +import Editor from 'react-simple-code-editor' +import { cn } from '@/lib/utils' + +interface CodeEditorProps { + value: string + onChange: (value: string) => void + language: 'javascript' | 'json' + placeholder?: string + className?: string + minHeight?: string +} + +export function CodeEditor({ + value, + onChange, + language, + placeholder = '', + className = '', + minHeight = '360px', +}: CodeEditorProps) { + const [code, setCode] = useState(value) + const [visualLineHeights, setVisualLineHeights] = useState([]) + const [isCollapsed, setIsCollapsed] = useState(false) + + const editorRef = useRef(null) + + useEffect(() => { + setCode(value) + }, [value]) + + useEffect(() => { + if (!editorRef.current) return + + const calculateVisualLines = () => { + const preElement = editorRef.current?.querySelector('pre') + if (!preElement) return + + const lines = code.split('\n') + const newVisualLineHeights: number[] = [] + + const container = document.createElement('div') + container.style.cssText = ` + position: absolute; + visibility: hidden; + width: ${preElement.clientWidth}px; + font-family: ${window.getComputedStyle(preElement).fontFamily}; + font-size: ${window.getComputedStyle(preElement).fontSize}; + padding: 12px; + white-space: pre-wrap; + word-break: break-word; + ` + document.body.appendChild(container) + + lines.forEach((line) => { + const lineDiv = document.createElement('div') + lineDiv.textContent = line || ' ' + container.appendChild(lineDiv) + const actualHeight = lineDiv.getBoundingClientRect().height + const lineUnits = Math.ceil(actualHeight / 21) + newVisualLineHeights.push(lineUnits) + container.removeChild(lineDiv) + }) + + document.body.removeChild(container) + setVisualLineHeights(newVisualLineHeights) + } + + const resizeObserver = new ResizeObserver(calculateVisualLines) + resizeObserver.observe(editorRef.current) + + return () => resizeObserver.disconnect() + }, [code]) + + // Calculate the number of lines to determine gutter width + const lineCount = code.split('\n').length + const gutterWidth = lineCount >= 100 ? '40px' : lineCount >= 10 ? '35px' : '30px' + + // Render helpers + const renderLineNumbers = () => { + const numbers: ReactElement[] = [] + let lineNumber = 1 + + visualLineHeights.forEach((height) => { + for (let i = 0; i < height; i++) { + numbers.push( +
0 && 'invisible')} + > + {lineNumber} +
+ ) + } + lineNumber++ + }) + + return numbers + } + + return ( +
+ {code.split('\n').length > 5 && ( + + )} + + + +
+ {code.length === 0 && placeholder && ( +
+ {placeholder} +
+ )} + + { + if (!isCollapsed) { + setCode(newCode) + onChange(newCode) + } + }} + highlight={(code) => highlight(code, languages[language], language)} + padding={12} + style={{ + fontFamily: 'inherit', + minHeight: '46px', + lineHeight: '21px', + }} + className={cn('focus:outline-none', isCollapsed && 'pointer-events-none select-none')} + textareaClassName={cn( + 'focus:outline-none focus:ring-0 bg-transparent', + isCollapsed && 'pointer-events-none' + )} + /> +
+
+ ) +} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal.tsx new file mode 100644 index 0000000000..2d1bc4734e --- /dev/null +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/components/custom-tool-modal.tsx @@ -0,0 +1,404 @@ +import { useEffect, useMemo, useState } from 'react' +import { Code, FileJson, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Label } from '@/components/ui/label' +import { cn } from '@/lib/utils' +import { useCustomToolsStore } from '@/stores/custom-tools/store' +import { CodeEditor } from './code-editor' + +interface CustomToolModalProps { + open: boolean + onOpenChange: (open: boolean) => void + onSave: (tool: CustomTool) => void + initialValues?: { + id?: string + schema: any + code: string + } +} + +export interface CustomTool { + type: 'custom-tool' + title: string + name: string + description: string + schema: any + code: string + params: Record + isExpanded?: boolean +} + +type ToolSection = 'schema' | 'code' + +export function CustomToolModal({ + open, + onOpenChange, + onSave, + initialValues, +}: CustomToolModalProps) { + const [activeSection, setActiveSection] = useState('schema') + const [jsonSchema, setJsonSchema] = useState('') + const [functionCode, setFunctionCode] = useState('') + const [schemaError, setSchemaError] = useState(null) + const [codeError, setCodeError] = useState(null) + const [isEditing, setIsEditing] = useState(false) + const [toolId, setToolId] = useState(undefined) + + const addTool = useCustomToolsStore((state) => state.addTool) + const updateTool = useCustomToolsStore((state) => state.updateTool) + + // Initialize form with initial values if provided + useEffect(() => { + if (open && initialValues) { + try { + setJsonSchema( + typeof initialValues.schema === 'string' + ? initialValues.schema + : JSON.stringify(initialValues.schema, null, 2) + ) + setFunctionCode(initialValues.code || '') + setIsEditing(true) + setToolId(initialValues.id) + } catch (error) { + console.error('Error initializing form with initial values:', error) + setSchemaError('Failed to load tool data. Please try again.') + } + } else if (open) { + // Reset form when opening without initial values + resetForm() + } + }, [open, initialValues]) + + const resetForm = () => { + setJsonSchema('') + setFunctionCode('') + setSchemaError(null) + setCodeError(null) + setActiveSection('schema') + setIsEditing(false) + setToolId(undefined) + } + + const handleClose = () => { + resetForm() + onOpenChange(false) + } + + // Pure validation function that doesn't update state + const validateJsonSchema = (schema: string): boolean => { + if (!schema) return false + + try { + const parsed = JSON.parse(schema) + + // Basic validation for function schema + if (!parsed.type || parsed.type !== 'function') { + return false + } + + if (!parsed.function || !parsed.function.name) { + return false + } + + return true + } catch (error) { + return false + } + } + + // Pure validation function that doesn't update state + const validateFunctionCode = (code: string): boolean => { + return true // Allow empty code + } + + // Memoize validation results to prevent unnecessary recalculations + const isSchemaValid = useMemo(() => validateJsonSchema(jsonSchema), [jsonSchema]) + const isCodeValid = useMemo(() => validateFunctionCode(functionCode), [functionCode]) + + const handleSave = () => { + setSchemaError(null) + setCodeError(null) + + // Validation with error messages + if (!jsonSchema) { + setSchemaError('Schema cannot be empty') + setActiveSection('schema') + return + } + + try { + const parsed = JSON.parse(jsonSchema) + + if (!parsed.type || parsed.type !== 'function') { + setSchemaError('Schema must have a "type" field set to "function"') + setActiveSection('schema') + return + } + + if (!parsed.function || !parsed.function.name) { + setSchemaError('Schema must have a "function" object with a "name" field') + setActiveSection('schema') + return + } + + // Check for duplicate tool name + const toolName = parsed.function.name + const existingTools = useCustomToolsStore.getState().getAllTools() + + // If editing, we need to find the original tool to get its ID + let originalToolId = toolId + + if (isEditing && !originalToolId) { + // If we're editing but don't have an ID, try to find the tool by its original name + const originalSchema = initialValues?.schema + const originalName = originalSchema?.function?.name + + if (originalName) { + const originalTool = existingTools.find( + (tool) => tool.schema.function.name === originalName + ) + if (originalTool) { + originalToolId = originalTool.id + } + } + } + + // Check for duplicates, excluding the current tool if editing + const isDuplicate = existingTools.some((tool) => { + // Skip the current tool when checking for duplicates + if (isEditing && tool.id === originalToolId) { + return false + } + return tool.schema.function.name === toolName + }) + + if (isDuplicate) { + setSchemaError(`A tool with the name "${toolName}" already exists`) + setActiveSection('schema') + return + } + + // Save to custom tools store + const schema = JSON.parse(jsonSchema) + const name = schema.function.name + const description = schema.function.description || '' + + if (isEditing && originalToolId) { + // Update existing tool + updateTool(originalToolId, { + title: name, + schema, + code: functionCode || '', + }) + } else { + // Add new tool + originalToolId = addTool({ + title: name, + schema, + code: functionCode || '', + }) + } + + const customTool: CustomTool = { + type: 'custom-tool', + title: name, + name, + description, + schema, + code: functionCode || '', + params: {}, + isExpanded: true, + } + + onSave(customTool) + handleClose() + } catch (error) { + console.error('Error saving custom tool:', error) + setSchemaError('Failed to save custom tool. Please check your inputs and try again.') + console.error('Error saving custom tool:', error) + setSchemaError('Failed to save custom tool. Please check your inputs and try again.') + } + } + + const handleJsonSchemaChange = (value: string) => { + setJsonSchema(value) + if (schemaError) { + setSchemaError(null) + } + } + + const handleFunctionCodeChange = (value: string) => { + setFunctionCode(value) + if (codeError) { + setCodeError(null) + } + } + + const navigationItems = [ + { + id: 'schema' as const, + label: 'Schema', + icon: FileJson, + complete: isSchemaValid, + }, + { + id: 'code' as const, + label: 'Code', + icon: Code, + complete: isCodeValid, + }, + ] + + return ( + + + +
+ + {isEditing ? 'Edit AgentTool' : 'Create Agent Tool'} + + +
+ + Step {activeSection === 'schema' ? '1' : '2'} of 2:{' '} + {activeSection === 'schema' ? 'Define schema' : 'Implement code'} + +
+ +
+
+ {navigationItems.map((item) => ( + + ))} +
+ +
+
+
+
+ + +
+ {schemaError && ( + {schemaError} + )} +
+ +
+
+ +
+
+
+ + +
+ {codeError && ( + {codeError} + )} +
+ +
+
+
+
+ + +
+ +
+ + {activeSection === 'schema' ? ( + + ) : ( + + )} +
+
+
+
+
+ ) +} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx new file mode 100644 index 0000000000..b8ee47a9e1 --- /dev/null +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/tool-input/tool-input.tsx @@ -0,0 +1,582 @@ +import { useState } from 'react' +import { PencilIcon, PlusIcon, WrenchIcon, XIcon } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { cn } from '@/lib/utils' +import { useCustomToolsStore } from '@/stores/custom-tools/store' +import { useWorkflowStore } from '@/stores/workflow/store' +import { getAllBlocks } from '@/blocks' +import { getTool } from '@/tools' +import { useSubBlockValue } from '../../hooks/use-sub-block-value' +import { ShortInput } from '../short-input' +import { CustomTool, CustomToolModal } from './components/custom-tool-modal' + +interface ToolInputProps { + blockId: string + subBlockId: string +} + +interface StoredTool { + type: string + title: string + params: Record + isExpanded?: boolean + schema?: any // For custom tools + code?: string // For custom tools implementation +} + +interface ToolParam { + id: string + type: string + description?: string + requiredForToolCall: boolean +} + +// Assumes the first tool in the access array is the tool to be used +// TODO: Switch to getting tools instead of tool blocks once we switch to providers +const getToolIdFromBlock = (blockType: string): string | undefined => { + const block = getAllBlocks().find((block) => block.type === blockType) + return block?.tools.access[0] +} + +const getRequiredToolParams = (toolId: string): ToolParam[] => { + const tool = getTool(toolId) + if (!tool) return [] + + return Object.entries(tool.params) + .filter(([_, param]) => param.requiredForToolCall) + .map(([paramId, param]) => ({ + id: paramId, + type: param.type, + description: param.description, + requiredForToolCall: param.requiredForToolCall ?? false, + })) +} + +// For custom tools, extract parameters from the schema +const getCustomToolParams = (schema: any): ToolParam[] => { + if (!schema?.function?.parameters?.properties) return [] + + const properties = schema.function.parameters.properties + const required = schema.function.parameters.required || [] + + return Object.entries(properties).map(([paramId, param]: [string, any]) => ({ + id: paramId, + type: param.type || 'string', + description: param.description || '', + requiredForToolCall: required.includes(paramId), + })) +} + +export function ToolInput({ blockId, subBlockId }: ToolInputProps) { + const [value, setValue] = useSubBlockValue(blockId, subBlockId) + const [open, setOpen] = useState(false) + const [customToolModalOpen, setCustomToolModalOpen] = useState(false) + const [editingToolIndex, setEditingToolIndex] = useState(null) + const isWide = useWorkflowStore((state) => state.blocks[blockId]?.isWide) + const customTools = useCustomToolsStore((state) => state.getAllTools()) + + const toolBlocks = getAllBlocks().filter((block) => block.category === 'tools') + + const selectedTools: StoredTool[] = + Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' + ? (value as unknown as StoredTool[]) + : [] + + const handleSelectTool = (toolBlock: (typeof toolBlocks)[0]) => { + // Check if tool already exists + if (selectedTools.some((tool) => tool.type === toolBlock.type)) { + setOpen(false) + return + } + + const toolId = getToolIdFromBlock(toolBlock.type) + const newTool: StoredTool = { + type: toolBlock.type, + title: toolBlock.name, + params: {}, + isExpanded: true, + } + + // If isWide, keep tools in the same row expanded + if (isWide) { + setValue([ + ...selectedTools.map((tool, index) => ({ + ...tool, + // Keep expanded if it's in the same row as the new tool + isExpanded: Math.floor(selectedTools.length / 2) === Math.floor(index / 2), + })), + newTool, + ]) + } else { + // Original behavior for non-wide mode + setValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool]) + } + + setOpen(false) + } + + const handleAddCustomTool = (customTool: CustomTool) => { + // Check if a tool with the same name already exists + if ( + selectedTools.some( + (tool) => + tool.type === 'custom-tool' && + tool.schema?.function?.name === customTool.schema.function.name + ) + ) { + return + } + + const newTool: StoredTool = { + type: 'custom-tool', + title: customTool.title, + params: {}, + isExpanded: true, + schema: customTool.schema, + code: customTool.code || '', + } + + // If isWide, keep tools in the same row expanded + if (isWide) { + setValue([ + ...selectedTools.map((tool, index) => ({ + ...tool, + // Keep expanded if it's in the same row as the new tool + isExpanded: Math.floor(selectedTools.length / 2) === Math.floor(index / 2), + })), + newTool, + ]) + } else { + // Original behavior for non-wide mode + setValue([...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), newTool]) + } + } + + const handleEditCustomTool = (toolIndex: number) => { + const tool = selectedTools[toolIndex] + if (tool.type !== 'custom-tool' || !tool.schema) return + + // Find the tool ID from the custom tools store based on the function name + const customToolsList = useCustomToolsStore.getState().getAllTools() + const existingTool = customToolsList.find( + (customTool) => customTool.schema.function.name === tool.schema.function.name + ) + + setEditingToolIndex(toolIndex) + setCustomToolModalOpen(true) + } + + const handleSaveCustomTool = (customTool: CustomTool) => { + if (editingToolIndex !== null) { + // Update existing tool + setValue( + selectedTools.map((tool, index) => + index === editingToolIndex + ? { + ...tool, + title: customTool.title, + schema: customTool.schema, + code: customTool.code || '', + } + : tool + ) + ) + setEditingToolIndex(null) + } else { + // Add new tool + handleAddCustomTool(customTool) + } + } + + const handleRemoveTool = (toolType: string, toolIndex: number) => { + setValue(selectedTools.filter((_, index) => index !== toolIndex)) + } + + const handleParamChange = (toolIndex: number, paramId: string, paramValue: string) => { + setValue( + selectedTools.map((tool, index) => + index === toolIndex + ? { + ...tool, + params: { + ...tool.params, + [paramId]: paramValue, + }, + } + : tool + ) + ) + } + + const toggleToolExpansion = (toolIndex: number) => { + setValue( + selectedTools.map((tool, index) => + index === toolIndex ? { ...tool, isExpanded: !tool.isExpanded } : tool + ) + ) + } + + const IconComponent = ({ icon: Icon, className }: { icon: any; className?: string }) => { + if (!Icon) return null + return + } + + return ( +
+ {selectedTools.length === 0 ? ( + + +
+
+ + Add Tool +
+
+
+ + + + + No tools found. + + { + setOpen(false) + setCustomToolModalOpen(true) + }} + className="flex items-center gap-2 cursor-pointer mb-1" + > +
+ +
+ Create Tool +
+ + {/* Display saved custom tools at the top */} + {customTools.length > 0 && ( + <> + +
+ Custom Tools +
+ + {customTools.map((customTool) => ( + { + const newTool: StoredTool = { + type: 'custom-tool', + title: customTool.title, + params: {}, + isExpanded: true, + schema: customTool.schema, + code: customTool.code, + } + + if (isWide) { + setValue([ + ...selectedTools.map((tool, index) => ({ + ...tool, + isExpanded: + Math.floor(selectedTools.length / 2) === + Math.floor(index / 2), + })), + newTool, + ]) + } else { + setValue([ + ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), + newTool, + ]) + } + setOpen(false) + }} + className="flex items-center gap-2 cursor-pointer" + > +
+ +
+ {customTool.title} +
+ ))} +
+ + + )} + + {/* Display built-in tools */} +
+ Built-in Tools +
+ + {toolBlocks.map((block) => ( + handleSelectTool(block)} + className="flex items-center gap-2 cursor-pointer" + > +
+ +
+ {block.name} +
+ ))} +
+
+
+
+
+
+ ) : ( +
+ {selectedTools.map((tool, toolIndex) => { + // Handle custom tools differently + const isCustomTool = tool.type === 'custom-tool' + const toolBlock = !isCustomTool + ? toolBlocks.find((block) => block.type === tool.type) + : null + const toolId = !isCustomTool ? getToolIdFromBlock(tool.type) : null + + // Get parameters based on tool type + const requiredParams = isCustomTool + ? getCustomToolParams(tool.schema) + : toolId + ? getRequiredToolParams(toolId) + : [] + + return ( +
+
+
{ + if (isCustomTool) { + handleEditCustomTool(toolIndex) + } else { + toggleToolExpansion(toolIndex) + } + }} + > +
+
+ {isCustomTool ? ( + + ) : ( + + )} +
+ + {tool.title} + +
+
+ +
+
+ + {tool.isExpanded && !isCustomTool && requiredParams.length > 0 && ( +
{ + if (e.target === e.currentTarget) { + toggleToolExpansion(toolIndex) + } + }} + > + {requiredParams.map((param) => ( +
+
+ {param.id} +
+
+ handleParamChange(toolIndex, param.id, value)} + /> +
+
+ ))} +
+ )} +
+
+ ) + })} + + + + + + + + + No tools found. + + { + setOpen(false) + setCustomToolModalOpen(true) + }} + className="flex items-center gap-2 cursor-pointer mb-1" + > +
+ +
+ Create Tool +
+ + {/* Display saved custom tools at the top */} + {customTools.length > 0 && ( + <> + +
+ Custom Tools +
+ + {customTools.map((customTool) => ( + { + const newTool: StoredTool = { + type: 'custom-tool', + title: customTool.title, + params: {}, + isExpanded: true, + schema: customTool.schema, + code: customTool.code, + } + + if (isWide) { + setValue([ + ...selectedTools.map((tool, index) => ({ + ...tool, + isExpanded: + Math.floor(selectedTools.length / 2) === + Math.floor(index / 2), + })), + newTool, + ]) + } else { + setValue([ + ...selectedTools.map((tool) => ({ + ...tool, + isExpanded: false, + })), + newTool, + ]) + } + setOpen(false) + }} + className="flex items-center gap-2 cursor-pointer" + > +
+ +
+ {customTool.title} +
+ ))} +
+ + + )} + + {/* Display built-in tools */} +
+ Built-in Tools +
+ + {toolBlocks.map((block) => ( + handleSelectTool(block)} + className="flex items-center gap-2 cursor-pointer" + > +
+ +
+ {block.name} +
+ ))} +
+
+
+
+
+
+
+ )} + + { + setCustomToolModalOpen(open) + if (!open) setEditingToolIndex(null) + }} + onSave={handleSaveCustomTool} + initialValues={ + editingToolIndex !== null && selectedTools[editingToolIndex]?.type === 'custom-tool' + ? { + schema: selectedTools[editingToolIndex].schema, + code: selectedTools[editingToolIndex].code || '', + } + : undefined + } + /> +
+ ) +} diff --git a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx index 74e1bafafd..f0884389d3 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx @@ -12,7 +12,7 @@ import { SliderInput } from './components/slider-input' import { Switch } from './components/switch' import { Table } from './components/table' import { TimeInput } from './components/time-input' -import { ToolInput } from './components/tool-input' +import { ToolInput } from './components/tool-input/tool-input' interface SubBlockProps { blockId: string diff --git a/components/ui/command.tsx b/components/ui/command.tsx index a776673eb2..1ebce3a7e3 100644 --- a/components/ui/command.tsx +++ b/components/ui/command.tsx @@ -39,6 +39,12 @@ import { cn } from '@/lib/utils' // This file is not typed correctly from shadcn, so we're disabling the type checker // @ts-nocheck +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck + +// This file is not typed correctly from shadcn, so we're disabling the type checker +// @ts-nocheck + const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { @@ -119,6 +125,7 @@ const CommandGroup = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { children?: React.ReactNode + className?: string } >(({ className, ...props }, ref) => ( + required?: string[] + } + } +} + +export interface CustomToolDefinition { + id: string + title: string + schema: CustomToolSchema + code: string + createdAt: string + updatedAt?: string +} + +interface CustomToolsStore { + tools: Record + addTool: (tool: Omit) => string + updateTool: ( + id: string, + updates: Partial> + ) => boolean + removeTool: (id: string) => void + getTool: (id: string) => CustomToolDefinition | undefined + getAllTools: () => CustomToolDefinition[] +} + +export const useCustomToolsStore = create()( + devtools( + persist( + (set, get) => ({ + tools: {}, + + addTool: (tool) => { + const id = crypto.randomUUID() + const newTool = { + ...tool, + id, + createdAt: new Date().toISOString(), + } + + set((state) => ({ + tools: { + ...state.tools, + [id]: newTool, + }, + })) + + return id + }, + + updateTool: (id, updates) => { + const tool = get().tools[id] + if (!tool) return false + + const updatedTool = { + ...tool, + ...updates, + updatedAt: new Date().toISOString(), + } + + set((state) => ({ + tools: { + ...state.tools, + [id]: updatedTool, + }, + })) + + return true + }, + + removeTool: (id) => { + set((state) => { + const newTools = { ...state.tools } + delete newTools[id] + return { tools: newTools } + }) + }, + + getTool: (id) => { + return get().tools[id] + }, + + getAllTools: () => { + return Object.values(get().tools) + }, + }), + { + name: 'custom-tools-store', + } + ) + ) +) diff --git a/stores/index.ts b/stores/index.ts index 4b0926553b..8d46c5e5c0 100644 --- a/stores/index.ts +++ b/stores/index.ts @@ -1,5 +1,6 @@ import { useChatStore } from './chat/store' import { useConsoleStore } from './console/store' +import { useCustomToolsStore } from './custom-tools/store' import { useExecutionStore } from './execution/store' import { useNotificationStore } from './notifications/store' import { useEnvironmentStore } from './settings/environment/store' @@ -9,9 +10,9 @@ import { useWorkflowRegistry } from './workflow/registry/store' import { useWorkflowStore } from './workflow/store' // Initialize sync manager when the store is first imported -if (typeof window !== 'undefined') { - initializeSyncManager() -} +// if (typeof window !== 'undefined') { +// initializeSyncManager() +// } // Reset all application stores to their initial state export const resetAllStores = () => { @@ -43,6 +44,7 @@ export const resetAllStores = () => { useConsoleStore.setState({ entries: [], isOpen: false }) useGeneralStore.setState({ isAutoConnectEnabled: true, isDebugModeEnabled: false }) useChatStore.setState({ messages: [], isProcessing: false, error: null }) + useCustomToolsStore.setState({ tools: {} }) } // Log the current state of all stores @@ -55,6 +57,7 @@ export const logAllStores = () => { execution: useExecutionStore.getState(), console: useConsoleStore.getState(), chat: useChatStore.getState(), + customTools: useCustomToolsStore.getState(), } console.group('Application State') @@ -77,4 +80,5 @@ export { useExecutionStore, useConsoleStore, useChatStore, + useCustomToolsStore, }