feat(autofill): added settting to autofill api keys for tools and blocks

* feat(autofill): autofill api keys for tools once they are set once

* added setting to settings for envvar autofill, removed from agent block since it depends on the model selected

* add in api key autofill for agent blocks based on existing model provider mapping

* add tooltips for general settings

* resolved pr comments

* reorganized files, removed unused imports
This commit is contained in:
Waleed Latif
2025-04-09 01:36:32 -07:00
committed by GitHub
parent 0202f41e74
commit 7dbb53747c
31 changed files with 725 additions and 168 deletions
@@ -2,29 +2,9 @@
import { useEffect, useState } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { formatDistanceToNow } from 'date-fns'
import {
Atom,
BotMessageSquare,
Brain,
BrainCircuit,
ChartBar,
Code,
Database,
Eye,
HelpCircle,
Info,
LineChart,
MailIcon,
NotebookPen,
Store,
TimerIcon,
Trash,
X,
} from 'lucide-react'
import { Eye, HelpCircle, Info, Trash, X } from 'lucide-react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
@@ -45,7 +25,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Textarea } from '@/components/ui/textarea'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { formatDistanceToNow } from 'date-fns'
import { AlertCircle, Copy, Rocket, Store, Terminal, X } from 'lucide-react'
import { AlertCircle, Rocket, Store, Terminal, X } from 'lucide-react'
import { ErrorIcon } from '@/components/icons'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Download, Image as ImageIcon } from 'lucide-react'
import { Download } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface JSONViewProps {
@@ -1,6 +1,6 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { useEffect, useRef } from 'react'
import { ChevronDown, Copy, MoreVertical, Plus, Trash } from 'lucide-react'
import { highlight, languages } from 'prismjs'
import 'prismjs/components/prism-javascript'
@@ -2,7 +2,6 @@
import { useEffect, useState } from 'react'
import { PanelRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useConsoleStore } from '@/stores/panel/console/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { getAllBlocks, getBlocksByCategory } from '@/blocks'
import { BlockCategory, BlockConfig } from '@/blocks/types'
import { BlockCategory } from '@/blocks/types'
import { ToolbarBlock } from './components/toolbar-block/toolbar-block'
import { ToolbarTabs } from './components/toolbar-tabs/toolbar-tabs'
@@ -1,4 +1,3 @@
import { useRef, useState } from 'react'
import { Plus, Trash } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Check, ChevronDown, ExternalLink, RefreshCw, Search, X } from 'lucide-react'
import { Check, ChevronDown, ExternalLink, RefreshCw, X } from 'lucide-react'
import { ConfluenceIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import {
@@ -1,10 +1,9 @@
'use client'
import { useRef, useState } from 'react'
import { Info, Upload, X } from 'lucide-react'
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useNotificationStore } from '@/stores/notifications/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -1,7 +1,7 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Check, ChevronDown, RefreshCw, Search, X } from 'lucide-react'
import { Check, ChevronDown, RefreshCw } from 'lucide-react'
import { GmailIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import {
@@ -14,12 +14,7 @@ import {
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { createLogger } from '@/lib/logs/console-logger'
import {
Credential,
getProviderIdFromServiceId,
getServiceIdFromScopes,
parseProvider,
} from '@/lib/oauth'
import { Credential, getProviderIdFromServiceId, getServiceIdFromScopes } from '@/lib/oauth'
import { saveToStorage } from '@/stores/workflows/persistence'
import { OAuthRequiredModal } from '@/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal'
@@ -8,7 +8,6 @@ import { formatDateTime } from '@/lib/utils'
import { getWorkflowWithValues } from '@/stores/workflows'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { workflowSync } from '@/stores/workflows/sync'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { useSubBlockValue } from '../../hooks/use-sub-block-value'
import { ScheduleModal } from './components/schedule-modal'
@@ -1,15 +1,6 @@
import { useCallback, useState } from 'react'
import { PencilIcon, PlusIcon, WrenchIcon, XIcon } from 'lucide-react'
import { 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 {
Select,
@@ -21,6 +12,8 @@ import {
import { OAuthProvider } from '@/lib/oauth'
import { cn } from '@/lib/utils'
import { useCustomToolsStore } from '@/stores/custom-tools/store'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useToolParamsStore } from '@/stores/tool-params/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { getAllBlocks } from '@/blocks'
import { getTool } from '@/tools'
@@ -125,6 +118,32 @@ const getOperationOptions = (blockType: string): { label: string; id: string }[]
})
}
// Helper function to initialize tool parameters
const initializeToolParams = (
toolId: string,
params: ToolParam[],
toolParamsStore: {
resolveParamValue: (toolId: string, paramId: string, instanceId?: string) => string | undefined
},
isAutoFillEnabled: boolean,
instanceId?: string
): Record<string, string> => {
const initialParams: Record<string, string> = {}
// Only auto-fill parameters if the setting is enabled
if (isAutoFillEnabled) {
// For each parameter, check if we have a stored/resolved value
params.forEach((param) => {
const resolvedValue = toolParamsStore.resolveParamValue(toolId, param.id, instanceId)
if (resolvedValue) {
initialParams[param.id] = resolvedValue
}
})
}
return initialParams
}
export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
const [value, setValue] = useSubBlockValue(blockId, subBlockId)
const [open, setOpen] = useState(false)
@@ -133,6 +152,8 @@ export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
const [searchQuery, setSearchQuery] = useState('')
const isWide = useWorkflowStore((state) => state.blocks[blockId]?.isWide)
const customTools = useCustomToolsStore((state) => state.getAllTools())
const toolParamsStore = useToolParamsStore()
const isAutoFillEnvVarsEnabled = useGeneralStore((state) => state.isAutoFillEnvVarsEnabled)
const toolBlocks = getAllBlocks().filter((block) => block.category === 'tools')
@@ -166,10 +187,22 @@ export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
const operationOptions = hasOperations ? getOperationOptions(toolBlock.type) : []
const defaultOperation = operationOptions.length > 0 ? operationOptions[0].id : undefined
const toolId = getToolIdFromBlock(toolBlock.type) || toolBlock.type
const requiredParams = toolId ? getRequiredToolParams(toolId) : []
// Use the helper function to initialize parameters with blockId as instanceId
const initialParams = initializeToolParams(
toolId,
requiredParams,
toolParamsStore,
isAutoFillEnvVarsEnabled,
blockId
)
const newTool: StoredTool = {
type: toolBlock.type,
title: toolBlock.name,
params: {},
params: initialParams,
isExpanded: true,
operation: defaultOperation,
}
@@ -204,10 +237,25 @@ export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
return
}
// Get custom tool parameters from schema
const toolParams = getCustomToolParams(customTool.schema)
// Create tool ID for the custom tool
const toolId = `custom-${customTool.schema.function.name}`
// Use the helper function to initialize parameters with blockId as instanceId
const initialParams = initializeToolParams(
toolId,
toolParams,
toolParamsStore,
isAutoFillEnvVarsEnabled,
blockId
)
const newTool: StoredTool = {
type: 'custom-tool',
title: customTool.title,
params: {},
params: initialParams,
isExpanded: true,
schema: customTool.schema,
code: customTool.code || '',
@@ -270,6 +318,19 @@ export function ToolInput({ blockId, subBlockId }: ToolInputProps) {
}
const handleParamChange = (toolIndex: number, paramId: string, paramValue: string) => {
// Store the value in the tool params store for future use
const tool = selectedTools[toolIndex]
const toolId =
tool.type === 'custom-tool'
? `custom-${tool.schema?.function?.name || 'tool'}`
: getToolIdFromBlock(tool.type) || tool.type
// Only store non-empty values
if (paramValue.trim()) {
toolParamsStore.setParam(toolId, paramId, paramValue)
}
// Update the value in the workflow
setValue(
selectedTools.map((tool, index) =>
index === toolIndex
@@ -1,7 +1,130 @@
import { useCallback, useEffect, useRef } from 'react'
import { isEqual } from 'lodash'
import { useGeneralStore } from '@/stores/settings/general/store'
import { useToolParamsStore } from '@/stores/tool-params/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { getProviderFromModel } from '@/providers/utils'
/**
* Helper to handle API key auto-fill for agent blocks
*/
function handleAgentBlockApiKey(
blockId: string,
subBlockId: string,
modelValue: string | null | undefined,
storeValue: any
) {
// Only proceed if we have a model selected
if (!modelValue) return
// Get the provider for this model
const provider = getProviderFromModel(modelValue)
// Skip if we couldn't determine a provider
if (!provider || provider === 'ollama') return
const toolParamsStore = useToolParamsStore.getState()
const subBlockStore = useSubBlockStore.getState()
// Try to get a saved API key for this provider
const savedValue = toolParamsStore.resolveParamValue(provider, 'apiKey', blockId)
// If we have a valid API key, use it
if (savedValue && savedValue !== '') {
// Only update if different from current value
if (savedValue !== storeValue) {
subBlockStore.setValue(blockId, subBlockId, savedValue)
}
} else {
// No API key found for this provider - ALWAYS clear the field
subBlockStore.setValue(blockId, subBlockId, '')
}
}
/**
* Helper to handle API key auto-fill for non-agent blocks
*/
function handleStandardBlockApiKey(
blockId: string,
subBlockId: string,
blockType: string | undefined,
storeValue: any
) {
if (!blockType) return
const toolParamsStore = useToolParamsStore.getState()
// Only auto-fill if the field is empty
if (!storeValue || storeValue === '') {
// Pass the blockId as instanceId to check if this specific instance has been cleared
const savedValue = toolParamsStore.resolveParamValue(blockType, 'apiKey', blockId)
if (savedValue && savedValue !== '' && savedValue !== storeValue) {
// Auto-fill the API key from the param store
useSubBlockStore.getState().setValue(blockId, subBlockId, savedValue)
}
}
// Handle environment variable references
else if (
storeValue &&
typeof storeValue === 'string' &&
storeValue.startsWith('{{') &&
storeValue.endsWith('}}')
) {
// Pass the blockId as instanceId
const currentValue = toolParamsStore.resolveParamValue(blockType, 'apiKey', blockId)
if (currentValue !== storeValue) {
// If we got a replacement or null, update the field
if (currentValue) {
// Replacement found - update to new reference
useSubBlockStore.getState().setValue(blockId, subBlockId, currentValue)
}
}
}
}
/**
* Helper to store API key values
*/
function storeApiKeyValue(
blockId: string,
blockType: string | undefined,
modelValue: string | null | undefined,
newValue: any,
storeValue: any
) {
if (!blockType) return
const toolParamsStore = useToolParamsStore.getState()
// Check if this is an empty value for an API key field that previously had a value
// This indicates the user has deliberately cleared the field
if (
storeValue &&
storeValue !== '' &&
(newValue === null || newValue === '' || String(newValue).trim() === '')
) {
// Mark this specific instance as cleared so we don't auto-fill it
toolParamsStore.markParamAsCleared(blockId, 'apiKey')
return
}
// Only store non-empty values
if (!newValue || String(newValue).trim() === '') return
// For agent blocks, store the API key under the provider name
if (blockType === 'agent' && modelValue) {
const provider = getProviderFromModel(modelValue)
if (provider && provider !== 'ollama') {
toolParamsStore.setParam(provider, 'apiKey', String(newValue))
}
} else {
// For other blocks, store under the block type
toolParamsStore.setParam(blockType, 'apiKey', String(newValue))
}
}
/**
* Custom hook to get and set values for a sub-block in a workflow.
@@ -17,10 +140,13 @@ export function useSubBlockValue<T = any>(
subBlockId: string,
triggerWorkflowUpdate: boolean = false
): readonly [T | null, (value: T) => void] {
// Get initial value from workflow store
const blockType = useWorkflowStore(
useCallback((state) => state.blocks?.[blockId]?.type, [blockId])
)
const initialValue = useWorkflowStore(
useCallback(
(state) => state.blocks[blockId]?.subBlocks[subBlockId]?.value ?? null,
(state) => state.blocks?.[blockId]?.subBlocks?.[subBlockId]?.value ?? null,
[blockId, subBlockId]
)
)
@@ -28,11 +154,60 @@ export function useSubBlockValue<T = any>(
// Keep a ref to the latest value to prevent unnecessary re-renders
const valueRef = useRef<T | null>(null)
// Get value from subblock store
// Previous model reference for detecting model changes
const prevModelRef = useRef<string | null>(null)
// Get value from subblock store - always call this hook unconditionally
const storeValue = useSubBlockStore(
useCallback((state) => state.getValue(blockId, subBlockId), [blockId, subBlockId])
)
// Check if this is an API key field that could be auto-filled
const isApiKey =
subBlockId === 'apiKey' || (subBlockId?.toLowerCase().includes('apikey') ?? false)
// Check if auto-fill environment variables is enabled - always call this hook unconditionally
const isAutoFillEnvVarsEnabled = useGeneralStore((state) => state.isAutoFillEnvVarsEnabled)
// Always call this hook unconditionally - don't wrap it in a condition
const modelSubBlockValue = useSubBlockStore((state) =>
blockId ? state.getValue(blockId, 'model') : null
)
// Compute the modelValue after the hook call
const modelValue = blockType === 'agent' ? (modelSubBlockValue as string) : null
// When model changes for an agent block's API key, immediately check if we need to clear it
useEffect(() => {
// Only run for agent blocks with API key fields when model changes
if (blockType === 'agent' && isApiKey && modelValue !== prevModelRef.current) {
// Update the previous model reference
prevModelRef.current = modelValue
// Handle API key autofill for the new model
if (isAutoFillEnvVarsEnabled && modelValue) {
handleAgentBlockApiKey(blockId, subBlockId, modelValue, storeValue)
}
}
}, [blockId, subBlockId, blockType, isApiKey, modelValue, isAutoFillEnvVarsEnabled, storeValue])
// When component mounts, check for existing API key in toolParamsStore
useEffect(() => {
// Skip autofill if the feature is disabled in settings
if (!isAutoFillEnvVarsEnabled) return
// Only process API key fields
if (!isApiKey) return
// Handle agent blocks differently, they need to use the model to determine provider
if (blockType === 'agent') {
handleAgentBlockApiKey(blockId, subBlockId, modelValue, storeValue)
} else {
// Normal handling for non-agent blocks
handleStandardBlockApiKey(blockId, subBlockId, blockType, storeValue)
}
}, [blockId, subBlockId, blockType, storeValue, isApiKey, isAutoFillEnvVarsEnabled, modelValue])
// Update the ref if the store value changes
// This ensures we're always working with the latest value
useEffect(() => {
@@ -57,6 +232,11 @@ export function useSubBlockValue<T = any>(
? JSON.parse(JSON.stringify(newValue))
: newValue
// Handle API key storage for reuse across blocks
if (isApiKey && blockType) {
storeApiKeyValue(blockId, blockType, modelValue, newValue, storeValue)
}
// Update the subblock store with the new value
// The store's setValue method will now trigger the debounced sync automatically
useSubBlockStore.getState().setValue(blockId, subBlockId, valueCopy)
@@ -66,7 +246,7 @@ export function useSubBlockValue<T = any>(
}
}
},
[blockId, subBlockId, triggerWorkflowUpdate]
[blockId, subBlockId, blockType, isApiKey, storeValue, triggerWorkflowUpdate, modelValue]
)
// Return the current value and setter
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { AlertCircle, Check, Copy, KeySquare, Plus, Trash2 } from 'lucide-react'
import { Check, Copy, KeySquare, Plus, Trash2 } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
@@ -27,7 +27,6 @@ import { Label } from '@/components/ui/label'
import { Skeleton } from '@/components/ui/skeleton'
import { useSession } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
const logger = createLogger('ApiKeys')
@@ -1,4 +1,5 @@
import { useRouter } from 'next/navigation'
import { Info } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
@@ -20,15 +21,26 @@ import {
SelectValue,
} from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useGeneralStore } from '@/stores/settings/general/store'
import { resetAllStores } from '@/stores'
const TOOLTIPS = {
theme: 'Change the visual theme of the application.',
debugMode: 'Enable visual debugging information during execution.',
autoConnect: 'Automatically connect nodes.',
autoFillEnvVars: 'Automatically fill API keys.',
resetData: 'Permanently delete all workflows, settings, and stored data.',
}
export function General() {
const router = useRouter()
const isAutoConnectEnabled = useGeneralStore((state) => state.isAutoConnectEnabled)
const toggleAutoConnect = useGeneralStore((state) => state.toggleAutoConnect)
const isDebugModeEnabled = useGeneralStore((state) => state.isDebugModeEnabled)
const toggleDebugMode = useGeneralStore((state) => state.toggleDebugMode)
const isAutoFillEnvVarsEnabled = useGeneralStore((state) => state.isAutoFillEnvVarsEnabled)
const toggleAutoFillEnvVars = useGeneralStore((state) => state.toggleAutoFillEnvVars)
const theme = useGeneralStore((state) => state.theme)
const setTheme = useGeneralStore((state) => state.setTheme)
@@ -43,11 +55,28 @@ export function General() {
<h2 className="text-lg font-medium mb-[22px]">General Settings</h2>
<div className="space-y-4">
<div className="flex items-center justify-between py-1">
<Label htmlFor="theme-select" className="font-medium">
Theme
</Label>
<div className="flex items-center gap-2">
<Label htmlFor="theme-select" className="font-medium">
Theme
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-gray-500 p-1 h-7"
aria-label="Learn more about theme settings"
>
<Info className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[300px] p-3">
<p className="text-sm">{TOOLTIPS.theme}</p>
</TooltipContent>
</Tooltip>
</div>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger className="w-[180px]">
<SelectTrigger id="theme-select" className="w-[180px]">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
@@ -58,9 +87,26 @@ export function General() {
</Select>
</div>
<div className="flex items-center justify-between py-1">
<Label htmlFor="debug-mode" className="font-medium">
Debug mode
</Label>
<div className="flex items-center gap-2">
<Label htmlFor="debug-mode" className="font-medium">
Debug mode
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-gray-500 p-1 h-7"
aria-label="Learn more about debug mode"
>
<Info className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[300px] p-3">
<p className="text-sm">{TOOLTIPS.debugMode}</p>
</TooltipContent>
</Tooltip>
</div>
<Switch
id="debug-mode"
checked={isDebugModeEnabled}
@@ -68,22 +114,83 @@ export function General() {
/>
</div>
<div className="flex items-center justify-between py-1">
<Label htmlFor="auto-connect" className="font-medium">
Auto-connect on drop
</Label>
<div className="flex items-center gap-2">
<Label htmlFor="auto-connect" className="font-medium">
Auto-connect on drop
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-gray-500 p-1 h-7"
aria-label="Learn more about auto-connect feature"
>
<Info className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[300px] p-3">
<p className="text-sm">{TOOLTIPS.autoConnect}</p>
</TooltipContent>
</Tooltip>
</div>
<Switch
id="auto-connect"
checked={isAutoConnectEnabled}
onCheckedChange={toggleAutoConnect}
/>
</div>
<div className="flex items-center justify-between py-1">
<div className="flex items-center gap-2">
<Label htmlFor="auto-fill-env-vars" className="font-medium">
Auto-fill environment variables
</Label>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-gray-500 p-1 h-7"
aria-label="Learn more about auto-fill environment variables"
>
<Info className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[300px] p-3">
<p className="text-sm">{TOOLTIPS.autoFillEnvVars}</p>
</TooltipContent>
</Tooltip>
</div>
<Switch
id="auto-fill-env-vars"
checked={isAutoFillEnvVarsEnabled}
onCheckedChange={toggleAutoFillEnvVars}
/>
</div>
</div>
</div>
{/* Danger Zone Section */}
<div>
<div className="flex items-center justify-between py-1">
<Label className="font-medium">Reset all data</Label>
<div className="flex items-center gap-2">
<Label className="font-medium">Reset all data</Label>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-gray-500 p-1 h-7"
aria-label="Learn more about resetting all data"
>
<Info className="h-5 w-5" />
</Button>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-[300px] p-3">
<p className="text-sm">{TOOLTIPS.resetData}</p>
</TooltipContent>
</Tooltip>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm">
+1 -2
View File
@@ -4,14 +4,13 @@ import { useMemo, useState } from 'react'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import clsx from 'clsx'
import { HelpCircle, Plus, ScrollText, Settings, Store } from 'lucide-react'
import { HelpCircle, Plus, ScrollText, Settings } from 'lucide-react'
import { AgentIcon } from '@/components/icons'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { WorkflowMetadata } from '@/stores/workflows/registry/types'
import { HelpModal } from './components/help-modal/help-modal'
import { NavItem } from './components/nav-item/nav-item'
import { SettingsModal } from './components/settings-modal/settings-modal'
import { WorkflowFolders } from './components/workflow-folders/workflow-folders'
@@ -6,7 +6,6 @@ import { Button } from '@/components/ui/button'
import { CopyButton } from '@/components/ui/copy-button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { WorkflowLog } from '@/app/w/logs/stores/types'
import { formatDate } from '@/app/w/logs/utils/format-date'
import { formatCost } from '@/providers/utils'
+1 -34
View File
@@ -1,39 +1,6 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
export interface CustomToolSchema {
type: string
function: {
name: string
description?: string
parameters: {
type: string
properties: Record<string, any>
required?: string[]
}
}
}
export interface CustomToolDefinition {
id: string
title: string
schema: CustomToolSchema
code: string
createdAt: string
updatedAt?: string
}
interface CustomToolsStore {
tools: Record<string, CustomToolDefinition>
addTool: (tool: Omit<CustomToolDefinition, 'id' | 'createdAt' | 'updatedAt'>) => string
updateTool: (
id: string,
updates: Partial<Omit<CustomToolDefinition, 'id' | 'createdAt' | 'updatedAt'>>
) => boolean
removeTool: (id: string) => void
getTool: (id: string) => CustomToolDefinition | undefined
getAllTools: () => CustomToolDefinition[]
}
import { CustomToolsStore } from './types'
export const useCustomToolsStore = create<CustomToolsStore>()(
devtools(
+33
View File
@@ -0,0 +1,33 @@
export interface CustomToolSchema {
type: string
function: {
name: string
description?: string
parameters: {
type: string
properties: Record<string, any>
required?: string[]
}
}
}
export interface CustomToolDefinition {
id: string
title: string
schema: CustomToolSchema
code: string
createdAt: string
updatedAt?: string
}
export interface CustomToolsStore {
tools: Record<string, CustomToolDefinition>
addTool: (tool: Omit<CustomToolDefinition, 'id' | 'createdAt' | 'updatedAt'>) => string
updateTool: (
id: string,
updates: Partial<Omit<CustomToolDefinition, 'id' | 'createdAt' | 'updatedAt'>>
) => boolean
removeTool: (id: string) => void
getTool: (id: string) => CustomToolDefinition | undefined
getAllTools: () => CustomToolDefinition[]
}
+1 -30
View File
@@ -1,34 +1,5 @@
import { create } from 'zustand'
import { Executor } from '@/executor'
import { ExecutionContext } from '@/executor/types'
interface ExecutionState {
activeBlockIds: Set<string>
isExecuting: boolean
isDebugging: boolean
pendingBlocks: string[]
executor: Executor | null
debugContext: ExecutionContext | null
}
interface ExecutionActions {
setActiveBlocks: (blockIds: Set<string>) => void
setIsExecuting: (isExecuting: boolean) => void
setIsDebugging: (isDebugging: boolean) => void
setPendingBlocks: (blockIds: string[]) => void
setExecutor: (executor: Executor | null) => void
setDebugContext: (context: ExecutionContext | null) => void
reset: () => void
}
const initialState: ExecutionState = {
activeBlockIds: new Set(),
isExecuting: false,
isDebugging: false,
pendingBlocks: [],
executor: null,
debugContext: null,
}
import { ExecutionActions, ExecutionState, initialState } from './types'
export const useExecutionStore = create<ExecutionState & ExecutionActions>()((set) => ({
...initialState,
+30
View File
@@ -0,0 +1,30 @@
import { Executor } from '@/executor'
import { ExecutionContext } from '@/executor/types'
export interface ExecutionState {
activeBlockIds: Set<string>
isExecuting: boolean
isDebugging: boolean
pendingBlocks: string[]
executor: Executor | null
debugContext: ExecutionContext | null
}
export interface ExecutionActions {
setActiveBlocks: (blockIds: Set<string>) => void
setIsExecuting: (isExecuting: boolean) => void
setIsDebugging: (isDebugging: boolean) => void
setPendingBlocks: (blockIds: string[]) => void
setExecutor: (executor: Executor | null) => void
setDebugContext: (context: ExecutionContext | null) => void
reset: () => void
}
export const initialState: ExecutionState = {
activeBlockIds: new Set(),
isExecuting: false,
isDebugging: false,
pendingBlocks: [],
executor: null,
debugContext: null,
}
+6 -1
View File
@@ -8,6 +8,7 @@ import { useConsoleStore } from './panel/console/store'
import { useVariablesStore } from './panel/variables/store'
import { useEnvironmentStore } from './settings/environment/store'
import { getSyncManagers, initializeSyncManagers, resetSyncManagers } from './sync-registry'
import { useToolParamsStore } from './tool-params/store'
import {
loadRegistry,
loadSubblockValues,
@@ -17,6 +18,7 @@ import {
} from './workflows/persistence'
import { useWorkflowRegistry } from './workflows/registry/store'
import { useSubBlockStore } from './workflows/subblock/store'
import { workflowSync } from './workflows/sync'
import { useWorkflowStore } from './workflows/workflow/store'
const logger = createLogger('Stores')
@@ -230,6 +232,7 @@ export {
useChatStore,
useCustomToolsStore,
useVariablesStore,
useToolParamsStore,
}
// Helper function to reset all stores
@@ -254,6 +257,7 @@ export const resetAllStores = () => {
useChatStore.setState({ messages: [], isProcessing: false, error: null })
useCustomToolsStore.setState({ tools: {} })
useVariablesStore.getState().resetLoaded() // Reset variables store tracking
useToolParamsStore.getState().clear()
}
// Helper function to log all store states
@@ -269,13 +273,14 @@ export const logAllStores = () => {
customTools: useCustomToolsStore.getState(),
subBlock: useSubBlockStore.getState(),
variables: useVariablesStore.getState(),
toolParams: useToolParamsStore.getState(),
}
return state
}
// Re-export sync managers
export { workflowSync } from './sync-registry'
export { workflowSync } from './workflows/sync'
/**
* Reinitialize the application after login
+2 -6
View File
@@ -1,15 +1,11 @@
import { create } from 'zustand'
import { createLogger } from '@/lib/logs/console-logger'
import { updateOllamaProviderModels } from '@/providers/utils'
import { OllamaStore } from './types'
const logger = createLogger('OllamaStore')
interface OllamaState {
models: string[]
setModels: (models: string[]) => void
}
export const useOllamaStore = create<OllamaState>((set) => ({
export const useOllamaStore = create<OllamaStore>((set) => ({
models: [],
setModels: (models) => {
logger.info('Updating Ollama models', { models })
+4
View File
@@ -0,0 +1,4 @@
export interface OllamaStore {
models: string[]
setModels: (models: string[]) => void
}
+4 -14
View File
@@ -1,19 +1,6 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface General {
isAutoConnectEnabled: boolean
isDebugModeEnabled: boolean
theme: 'system' | 'light' | 'dark'
}
interface GeneralActions {
toggleAutoConnect: () => void
toggleDebugMode: () => void
setTheme: (theme: 'system' | 'light' | 'dark') => void
}
type GeneralStore = General & GeneralActions
import { GeneralStore } from './types'
export const useGeneralStore = create<GeneralStore>()(
devtools(
@@ -21,12 +8,15 @@ export const useGeneralStore = create<GeneralStore>()(
(set) => ({
isAutoConnectEnabled: true,
isDebugModeEnabled: false,
isAutoFillEnvVarsEnabled: true,
theme: 'system',
toggleAutoConnect: () =>
set((state) => ({
isAutoConnectEnabled: !state.isAutoConnectEnabled,
})),
toggleDebugMode: () => set((state) => ({ isDebugModeEnabled: !state.isDebugModeEnabled })),
toggleAutoFillEnvVars: () =>
set((state) => ({ isAutoFillEnvVarsEnabled: !state.isAutoFillEnvVarsEnabled })),
setTheme: (theme: 'system' | 'light' | 'dark') => set({ theme }),
}),
{
+15
View File
@@ -0,0 +1,15 @@
export interface General {
isAutoConnectEnabled: boolean
isDebugModeEnabled: boolean
isAutoFillEnvVarsEnabled: boolean
theme: 'system' | 'light' | 'dark'
}
export interface GeneralActions {
toggleAutoConnect: () => void
toggleDebugMode: () => void
toggleAutoFillEnvVars: () => void
setTheme: (theme: 'system' | 'light' | 'dark') => void
}
export type GeneralStore = General & GeneralActions
+179
View File
@@ -0,0 +1,179 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { useEnvironmentStore } from '../settings/environment/store'
import { useGeneralStore } from '../settings/general/store'
import { ToolParamsStore } from './types'
import { extractEnvVarName, findMatchingEnvVar, isEnvVarReference } from './utils'
export const useToolParamsStore = create<ToolParamsStore>()(
devtools(
persist(
(set, get) => ({
params: {},
clearedParams: {},
setParam: (toolId: string, paramId: string, value: string) => {
// If setting a non-empty value, we should remove it from clearedParams if it exists
if (value.trim() !== '') {
set((state) => {
const newClearedParams = { ...state.clearedParams }
if (newClearedParams[toolId] && newClearedParams[toolId][paramId]) {
delete newClearedParams[toolId][paramId]
// Clean up empty objects
if (Object.keys(newClearedParams[toolId]).length === 0) {
delete newClearedParams[toolId]
}
}
return { clearedParams: newClearedParams }
})
}
// Set the parameter value
set((state) => ({
params: {
...state.params,
[toolId]: {
...(state.params[toolId] || {}),
[paramId]: value,
},
},
}))
// For API keys, also store under a normalized tool name for cross-referencing
// This allows both blocks and tools to share the same parameters
if (paramId.toLowerCase() === 'apikey' || paramId.toLowerCase() === 'api_key') {
// Extract the tool name part (e.g., "exa" from "exa-search")
const baseTool = toolId.split('-')[0].toLowerCase()
if (baseTool !== toolId) {
// Set the same value for the base tool to enable cross-referencing
set((state) => ({
params: {
...state.params,
[baseTool]: {
...(state.params[baseTool] || {}),
[paramId]: value,
},
},
}))
}
}
},
markParamAsCleared: (instanceId: string, paramId: string) => {
// Mark this specific instance as cleared
set((state) => ({
clearedParams: {
...state.clearedParams,
[instanceId]: {
...(state.clearedParams[instanceId] || {}),
[paramId]: true,
},
},
}))
},
isParamCleared: (instanceId: string, paramId: string) => {
// Only check this specific instance
return !!get().clearedParams[instanceId]?.[paramId]
},
getParam: (toolId: string, paramId: string) => {
// Check for direct match first
const directValue = get().params[toolId]?.[paramId]
if (directValue) return directValue
// Try base tool name if it's a compound tool ID
if (toolId.includes('-')) {
const baseTool = toolId.split('-')[0].toLowerCase()
return get().params[baseTool]?.[paramId]
}
// Try matching against any stored tool that starts with this ID
// This helps match "exa" with "exa-search" etc.
const matchingToolIds = Object.keys(get().params).filter(
(id) => id.startsWith(toolId) || id.split('-')[0] === toolId
)
for (const id of matchingToolIds) {
const value = get().params[id]?.[paramId]
if (value) return value
}
return undefined
},
getToolParams: (toolId: string) => {
return get().params[toolId] || {}
},
isEnvVarReference,
resolveParamValue: (toolId: string, paramId: string, instanceId?: string) => {
// If this is a specific instance that has been deliberately cleared, don't auto-fill it
if (instanceId && get().isParamCleared(instanceId, paramId)) {
return undefined
}
// Check if auto-fill environment variables is enabled
const isAutoFillEnvVarsEnabled = useGeneralStore.getState().isAutoFillEnvVarsEnabled
if (!isAutoFillEnvVarsEnabled) {
// When auto-fill is disabled, we still return existing stored values, but don't
// attempt to resolve environment variables or set new values
return get().params[toolId]?.[paramId]
}
const envStore = useEnvironmentStore.getState()
// First check params store for previously entered value
const storedValue = get().getParam(toolId, paramId)
if (storedValue) {
// If the stored value is an environment variable reference like {{EXA_API_KEY}}
if (isEnvVarReference(storedValue)) {
// Extract variable name from {{VAR_NAME}}
const envVarName = extractEnvVarName(storedValue)
if (!envVarName) return undefined
// Check if this environment variable still exists
const envValue = envStore.getVariable(envVarName)
if (envValue) {
// Environment variable exists, return the reference
return storedValue
} else {
// Environment variable no longer exists
return undefined
}
}
// Return the stored value directly if it's not an env var reference
return storedValue
}
// If no stored value, try to guess based on parameter name
// This handles cases where the user hasn't entered a value yet
if (paramId.toLowerCase() === 'apikey' || paramId.toLowerCase() === 'api_key') {
const matchingVar = findMatchingEnvVar(toolId)
if (matchingVar) {
const envReference = `{{${matchingVar}}}`
get().setParam(toolId, paramId, envReference)
return envReference
}
}
// No value found
return undefined
},
clear: () => {
set({ params: {}, clearedParams: {} })
},
}),
{
name: 'tool-params-store',
}
)
)
)
+12
View File
@@ -0,0 +1,12 @@
export interface ToolParamsStore {
params: Record<string, Record<string, string>>
clearedParams: Record<string, Record<string, boolean>>
setParam: (toolId: string, paramId: string, value: string) => void
markParamAsCleared: (instanceId: string, paramId: string) => void
isParamCleared: (instanceId: string, paramId: string) => boolean
getParam: (toolId: string, paramId: string) => string | undefined
getToolParams: (toolId: string) => Record<string, string>
isEnvVarReference: (value: string) => boolean
resolveParamValue: (toolId: string, paramId: string, instanceId?: string) => string | undefined
clear: () => void
}
+39
View File
@@ -0,0 +1,39 @@
import { useEnvironmentStore } from '../settings/environment/store'
export const isEnvVarReference = (value: string): boolean => {
// Check if the value looks like {{ENV_VAR}}
return /^\{\{[a-zA-Z0-9_-]+\}\}$/.test(value)
}
export const extractEnvVarName = (value: string): string | null => {
if (!isEnvVarReference(value)) return null
return value.slice(2, -2)
}
export const generatePossibleEnvVarNames = (toolId: string): string[] => {
// Extract base tool name if it's a compound ID
const baseTool = toolId.includes('-') ? toolId.split('-')[0] : toolId
const toolPrefix = baseTool.toUpperCase()
return [
`${toolPrefix}_API_KEY`,
`${toolPrefix.replace(/-/g, '_')}_API_KEY`,
`${toolPrefix}_KEY`,
`${toolPrefix}_TOKEN`,
`${toolPrefix}`,
]
}
export const findMatchingEnvVar = (toolId: string): string | null => {
const envStore = useEnvironmentStore.getState()
const possibleVars = generatePossibleEnvVarNames(toolId)
for (const varName of possibleVars) {
const envValue = envStore.getVariable(varName)
if (envValue) {
return varName
}
}
return null
}
+1 -12
View File
@@ -4,6 +4,7 @@ import { SubBlockConfig } from '@/blocks/types'
import { loadSubblockValues, saveSubblockValues } from '../persistence'
import { useWorkflowRegistry } from '../registry/store'
import { workflowSync } from '../sync'
import { SubBlockStore } from './types'
// Add debounce utility for syncing
let syncDebounceTimer: NodeJS.Timeout | null = null
@@ -20,18 +21,6 @@ const DEBOUNCE_DELAY = 500 // 500ms delay for sync
* (e.g., inputFormat in starter block), the merge function will include it
* in the synchronized state to ensure persistence
*/
interface SubBlockState {
workflowValues: Record<string, Record<string, Record<string, any>>> // Store values per workflow ID
}
interface SubBlockStore extends SubBlockState {
setValue: (blockId: string, subBlockId: string, value: any) => void
getValue: (blockId: string, subBlockId: string) => any
clear: () => void
initializeFromWorkflow: (workflowId: string, blocks: Record<string, any>) => void
// Add debounced sync function
syncWithDB: () => void
}
export const useSubBlockStore = create<SubBlockStore>()(
devtools(
+12
View File
@@ -0,0 +1,12 @@
export interface SubBlockState {
workflowValues: Record<string, Record<string, Record<string, any>>> // Store values per workflow ID
}
export interface SubBlockStore extends SubBlockState {
setValue: (blockId: string, subBlockId: string, value: any) => void
getValue: (blockId: string, subBlockId: string) => any
clear: () => void
initializeFromWorkflow: (workflowId: string, blocks: Record<string, any>) => void
// Add debounced sync function
syncWithDB: () => void
}