feat(workflow-chat) (#269)

* feat(workflow-chat): added control bar switch

* feat(workflow-chat): finished UI

* feat(workflow-chat): added logic to execute workflows and return value selected
This commit is contained in:
Emir Karabeg
2025-04-16 02:48:19 -07:00
committed by GitHub
parent af97c0160f
commit b6d5348bbf
13 changed files with 635 additions and 15 deletions

View File

@@ -898,6 +898,9 @@ export function ControlBar() {
'bg-[#802FFF] hover:bg-[#7028E6]',
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'text-white transition-all duration-200',
(isExecuting || isMultiRunning) &&
!isCancelling &&
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none',
'rounded-l-none h-10'
)}

View File

@@ -0,0 +1,343 @@
'use client'
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react'
import { ArrowUp, ChevronDown } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { cn } from '@/lib/utils'
import { useExecutionStore } from '@/stores/execution/store'
import { useChatStore } from '@/stores/panel/chat/store'
import { useConsoleStore } from '@/stores/panel/console/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import { getBlock } from '@/blocks'
import { useWorkflowExecution } from '../../../../hooks/use-workflow-execution'
import { ChatMessage } from './components/chat-message'
interface ChatProps {
panelWidth: number
chatMessage: string
setChatMessage: (message: string) => void
}
export function Chat({ panelWidth, chatMessage, setChatMessage }: ChatProps) {
const [isOutputDropdownOpen, setIsOutputDropdownOpen] = useState(false)
const { activeWorkflowId } = useWorkflowRegistry()
const { messages, addMessage, selectedWorkflowOutputs, setSelectedWorkflowOutput } =
useChatStore()
const { entries } = useConsoleStore()
const blocks = useWorkflowStore((state) => state.blocks)
const messagesEndRef = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
// Use the execution store state to track if a workflow is executing
const { isExecuting } = useExecutionStore()
// Get workflow execution functionality
const { handleRunWorkflow, executionResult } = useWorkflowExecution()
// Get workflow outputs for the dropdown
const workflowOutputs = useMemo(() => {
const outputs: {
id: string
label: string
blockId: string
blockName: string
blockType: string
path: string
}[] = []
if (!activeWorkflowId) return outputs
// Process blocks to extract outputs
Object.values(blocks).forEach((block) => {
const blockName = block.name.replace(/\s+/g, '').toLowerCase()
// Add response outputs
if (block.outputs && typeof block.outputs === 'object') {
const addOutput = (path: string, outputObj: any, prefix = '') => {
const fullPath = prefix ? `${prefix}.${path}` : path
if (typeof outputObj === 'object' && outputObj !== null) {
// For objects, recursively add each property
Object.entries(outputObj).forEach(([key, value]) => {
addOutput(key, value, fullPath)
})
} else {
// Add leaf node as output option
outputs.push({
id: `${block.id}_${fullPath}`,
label: `${blockName}.${fullPath}`,
blockId: block.id,
blockName: block.name,
blockType: block.type,
path: fullPath,
})
}
}
// Start with the response object
if (block.outputs.response) {
addOutput('response', block.outputs.response)
}
}
})
return outputs
}, [blocks, activeWorkflowId])
// Get output entries from console for the dropdown
const outputEntries = useMemo(() => {
if (!activeWorkflowId) return []
return entries.filter((entry) => entry.workflowId === activeWorkflowId && entry.output)
}, [entries, activeWorkflowId])
// Get filtered messages for current workflow
const workflowMessages = useMemo(() => {
if (!activeWorkflowId) return []
return messages
.filter((msg) => msg.workflowId === activeWorkflowId)
.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())
}, [messages, activeWorkflowId])
// Get selected workflow output
const selectedOutput = useMemo(() => {
if (!activeWorkflowId) return null
const selectedId = selectedWorkflowOutputs[activeWorkflowId]
if (!selectedId) return outputEntries[0]?.id || null
return selectedId
}, [selectedWorkflowOutputs, activeWorkflowId, outputEntries])
// Get selected output display name
const selectedOutputDisplayName = useMemo(() => {
if (!selectedOutput) return 'Select output source'
const output = workflowOutputs.find((o) => o.id === selectedOutput)
return output
? `${output.blockName.replace(/\s+/g, '').toLowerCase()}.${output.path}`
: 'Select output source'
}, [selectedOutput, workflowOutputs])
// Get selected output block info
const selectedOutputInfo = useMemo(() => {
if (!selectedOutput) return null
const output = workflowOutputs.find((o) => o.id === selectedOutput)
if (!output) return null
return {
blockName: output.blockName,
blockId: output.blockId,
blockType: output.blockType,
path: output.path,
}
}, [selectedOutput, workflowOutputs])
// Auto-scroll to bottom when new messages are added
useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [workflowMessages])
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOutputDropdownOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
// Handle send message
const handleSendMessage = async () => {
if (!chatMessage.trim() || !activeWorkflowId || isExecuting) return
// Store the message being sent for reference
const sentMessage = chatMessage.trim()
// Add user message
addMessage({
content: sentMessage,
workflowId: activeWorkflowId,
type: 'user',
})
// Clear input
setChatMessage('')
// Execute the workflow to generate a response, passing the chat message as input
// The workflow execution will trigger block executions which will add messages to the chat via the console store
await handleRunWorkflow({ input: sentMessage })
}
// Handle key press
const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSendMessage()
}
}
// Handle output selection
const handleOutputSelection = (value: string) => {
if (activeWorkflowId) {
setSelectedWorkflowOutput(activeWorkflowId, value)
setIsOutputDropdownOpen(false)
}
}
// Group output options by block
const groupedOutputs = useMemo(() => {
const groups: Record<string, typeof workflowOutputs> = {}
// Group by block name
workflowOutputs.forEach((output) => {
if (!groups[output.blockName]) {
groups[output.blockName] = []
}
groups[output.blockName].push(output)
})
return groups
}, [workflowOutputs])
// Get block color for an output
const getOutputColor = (blockId: string, blockType: string) => {
// Try to get the block's color from its configuration
const blockConfig = getBlock(blockType)
return blockConfig?.bgColor || '#2F55FF' // Default blue if not found
}
return (
<div className="flex flex-col h-full">
{/* Output Source Dropdown */}
<div className="flex-none border-b px-4 py-2" ref={dropdownRef}>
<div className="relative">
<button
onClick={() => setIsOutputDropdownOpen(!isOutputDropdownOpen)}
className={`flex w-full items-center justify-between px-3 py-1.5 text-sm rounded-md transition-colors ${
isOutputDropdownOpen
? 'bg-accent text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
}`}
disabled={workflowOutputs.length === 0}
>
{selectedOutputInfo ? (
<div className="flex items-center gap-2 w-[calc(100%-24px)] overflow-hidden">
<div
className="flex items-center justify-center w-5 h-5 rounded flex-shrink-0"
style={{
backgroundColor: getOutputColor(
selectedOutputInfo.blockId,
selectedOutputInfo.blockType
),
}}
>
<span className="w-3 h-3 text-white font-bold text-xs">
{selectedOutputInfo.blockName.charAt(0).toUpperCase()}
</span>
</div>
<span className="truncate">{selectedOutputDisplayName}</span>
</div>
) : (
<span className="truncate w-[calc(100%-24px)]">{selectedOutputDisplayName}</span>
)}
<ChevronDown
className={`h-4 w-4 transition-transform ml-1 flex-shrink-0 ${
isOutputDropdownOpen ? 'rotate-180' : ''
}`}
/>
</button>
{isOutputDropdownOpen && workflowOutputs.length > 0 && (
<div className="absolute z-50 mt-1 w-full bg-popover rounded-md border shadow-md overflow-hidden">
<div className="max-h-[240px] overflow-y-auto">
{Object.entries(groupedOutputs).map(([blockName, outputs]) => (
<div key={blockName}>
<div className="px-2 pt-1.5 pb-0.5 text-xs font-medium text-muted-foreground border-t first:border-t-0">
{blockName}
</div>
<div>
{outputs.map((output) => (
<button
key={output.id}
onClick={() => handleOutputSelection(output.id)}
className={cn(
'flex items-center gap-2 text-sm text-left w-full px-3 py-1.5',
'hover:bg-accent hover:text-accent-foreground',
'focus:bg-accent focus:text-accent-foreground focus:outline-none',
selectedOutput === output.id && 'bg-accent text-accent-foreground'
)}
>
<div
className="flex items-center justify-center w-5 h-5 rounded flex-shrink-0"
style={{
backgroundColor: getOutputColor(output.blockId, output.blockType),
}}
>
<span className="w-3 h-3 text-white font-bold text-xs">
{blockName.charAt(0).toUpperCase()}
</span>
</div>
<span className="truncate max-w-[calc(100%-28px)]">{output.path}</span>
</button>
))}
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Main layout with fixed heights to ensure input stays visible */}
<div className="flex flex-col flex-1 overflow-hidden">
{/* Chat messages section - Scrollable area */}
<div className="flex-1 overflow-hidden">
<ScrollArea className="h-full">
<div>
{workflowMessages.length === 0 ? (
<div className="flex items-center justify-center h-32 text-sm text-muted-foreground">
No messages yet
</div>
) : (
workflowMessages.map((message) => (
<ChatMessage key={message.id} message={message} containerWidth={panelWidth} />
))
)}
<div ref={messagesEndRef} />
</div>
</ScrollArea>
</div>
{/* Input section - Fixed height */}
<div className="flex-none border-t bg-background pt-4 px-4 pb-4 relative -mt-[1px]">
<div className="flex gap-2">
<Input
value={chatMessage}
onChange={(e) => setChatMessage(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="Type a message..."
className="flex-1 focus-visible:ring-0 focus-visible:ring-offset-0 h-10"
disabled={!activeWorkflowId || isExecuting}
/>
<Button
onClick={handleSendMessage}
size="icon"
disabled={!chatMessage.trim() || !activeWorkflowId || isExecuting}
className="h-10 w-10 bg-[#802FFF] hover:bg-[#7028E6] text-white"
>
<ArrowUp className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,97 @@
import { useMemo } from 'react'
import { format, formatDistanceToNow } from 'date-fns'
import { Clock, Terminal, User } from 'lucide-react'
import { JSONView } from '../../console/components/json-view/json-view'
interface ChatMessageProps {
message: {
id: string
content: any
timestamp: string | Date
type: 'user' | 'workflow'
}
containerWidth: number
}
// Maximum character length for a word before it's broken up
const MAX_WORD_LENGTH = 25
const WordWrap = ({ text }: { text: string }) => {
if (!text) return null
// Split text into words, keeping spaces and punctuation
const parts = text.split(/(\s+)/g)
return (
<>
{parts.map((part, index) => {
// If the part is whitespace or shorter than the max length, render it as is
if (part.match(/\s+/) || part.length <= MAX_WORD_LENGTH) {
return <span key={index}>{part}</span>
}
// For long words, break them up into chunks
const chunks = []
for (let i = 0; i < part.length; i += MAX_WORD_LENGTH) {
chunks.push(part.substring(i, i + MAX_WORD_LENGTH))
}
return (
<span key={index} className="break-all">
{chunks.map((chunk, chunkIndex) => (
<span key={chunkIndex}>{chunk}</span>
))}
</span>
)
})}
</>
)
}
export function ChatMessage({ message, containerWidth }: ChatMessageProps) {
const messageDate = useMemo(() => new Date(message.timestamp), [message.timestamp])
const relativeTime = useMemo(() => {
return formatDistanceToNow(messageDate, { addSuffix: true })
}, [messageDate])
// Check if content is a JSON object
const isJsonObject = useMemo(() => {
return typeof message.content === 'object' && message.content !== null
}, [message.content])
// Format message content based on type
const formattedContent = useMemo(() => {
if (isJsonObject) {
return JSON.stringify(message.content) // Return stringified version for type safety
}
return String(message.content)
}, [message.content, isJsonObject])
return (
<div className="w-full border-b border-border p-4 space-y-4 hover:bg-accent/50 transition-colors">
{/* Header with time on left and message type on right */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-muted-foreground">{relativeTime}</span>
</div>
<div className="flex items-center gap-2 text-sm">
{message.type !== 'user' && <span className="text-muted-foreground">Workflow</span>}
</div>
</div>
{/* Message content with proper word wrapping */}
<div className="text-sm font-mono flex-1 break-normal whitespace-normal overflow-wrap-anywhere relative">
{isJsonObject ? (
<JSONView data={message.content} initiallyExpanded={false} />
) : (
<div className="whitespace-pre-wrap text-foreground break-words">
<WordWrap text={formattedContent} />
</div>
)}
</div>
</div>
)
}

View File

@@ -20,7 +20,7 @@ export function Console({ panelWidth }: ConsoleProps) {
return (
<ScrollArea className="h-full">
<div className="pb-16">
<div>
{filteredEntries.length === 0 ? (
<div className="flex items-center justify-center h-32 text-sm text-muted-foreground pt-4">
No console entries

View File

@@ -156,7 +156,7 @@ export function Variables({ panelWidth }: VariablesProps) {
return (
<ScrollArea className="h-full">
<div className="p-4 pb-16 space-y-3">
<div className="p-4 space-y-3">
{/* Variables List */}
{workflowVariables.length === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-sm text-muted-foreground pt-4">

View File

@@ -3,15 +3,18 @@
import { useEffect, useState } from 'react'
import { PanelRight } from 'lucide-react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useChatStore } from '@/stores/panel/chat/store'
import { useConsoleStore } from '@/stores/panel/console/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { usePanelStore } from '../../../../../stores/panel/store'
import { Chat } from './components/chat/chat'
import { Console } from './components/console/console'
import { Variables } from './components/variables/variables'
export function Panel() {
const [width, setWidth] = useState(336) // 84 * 4 = 336px (default width)
const [isDragging, setIsDragging] = useState(false)
const [chatMessage, setChatMessage] = useState<string>('')
const isOpen = usePanelStore((state) => state.isOpen)
const togglePanel = usePanelStore((state) => state.togglePanel)
@@ -19,6 +22,7 @@ export function Panel() {
const setActiveTab = usePanelStore((state) => state.setActiveTab)
const clearConsole = useConsoleStore((state) => state.clearConsole)
const clearChat = useChatStore((state) => state.clearChat)
const { activeWorkflowId } = useWorkflowRegistry()
const handleMouseDown = (e: React.MouseEvent) => {
@@ -68,7 +72,7 @@ export function Panel() {
return (
<div
className="fixed right-0 top-16 z-10 h-[calc(100vh-4rem)] border-l bg-background"
className="fixed right-0 top-16 z-10 h-[calc(100vh-4rem)] border-l bg-background flex flex-col"
style={{ width: `${width}px` }}
>
<div
@@ -76,8 +80,19 @@ export function Panel() {
onMouseDown={handleMouseDown}
/>
<div className="flex items-center justify-between h-14 px-4 border-b">
{/* Panel Header */}
<div className="flex-none flex items-center justify-between h-14 px-4 border-b">
<div className="flex gap-2">
<button
onClick={() => setActiveTab('chat')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
activeTab === 'chat'
? 'bg-accent text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
}`}
>
Chat
</button>
<button
onClick={() => setActiveTab('console')}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
@@ -100,9 +115,11 @@ export function Panel() {
</button>
</div>
{activeTab === 'console' && (
{(activeTab === 'console' || activeTab === 'chat') && (
<button
onClick={() => clearConsole(activeWorkflowId)}
onClick={() =>
activeTab === 'console' ? clearConsole(activeWorkflowId) : clearChat(activeWorkflowId)
}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
true ? 'text-muted-foreground hover:text-foreground hover:bg-accent/50' : ''
}`}
@@ -112,20 +129,24 @@ export function Panel() {
)}
</div>
<div className="h-[calc(100%-4rem)]">
{activeTab === 'console' ? (
{/* Panel Content */}
<div className="flex-1 overflow-hidden">
{activeTab === 'chat' ? (
<Chat panelWidth={width} chatMessage={chatMessage} setChatMessage={setChatMessage} />
) : activeTab === 'console' ? (
<Console panelWidth={width} />
) : (
<Variables panelWidth={width} />
)}
</div>
<div className="absolute left-0 right-0 bottom-0 h-16 bg-background border-t">
{/* Panel Footer */}
<div className="flex-none h-16 bg-background border-t flex items-center">
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={togglePanel}
className="absolute left-4 bottom-[18px] flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
className="ml-4 flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground hover:bg-accent"
>
<PanelRight className="h-5 w-5 transform rotate-180" />
<span className="sr-only">Close Panel</span>

View File

@@ -23,7 +23,7 @@ export function useWorkflowExecution() {
const { activeWorkflowId } = useWorkflowRegistry()
const { addNotification } = useNotificationStore()
const { toggleConsole } = useConsoleStore()
const { togglePanel, setActiveTab } = usePanelStore()
const { togglePanel, setActiveTab, activeTab } = usePanelStore()
const { getAllVariables } = useEnvironmentStore()
const { isDebugModeEnabled } = useGeneralStore()
const { getVariablesByWorkflowId, variables } = useVariablesStore()
@@ -73,7 +73,7 @@ export function useWorkflowExecution() {
}
}
const handleRunWorkflow = useCallback(async () => {
const handleRunWorkflow = useCallback(async (workflowInput?: any) => {
if (!activeWorkflowId) return
// Reset execution result and set execution state
@@ -92,7 +92,9 @@ export function useWorkflowExecution() {
}
// Set active tab to console
setActiveTab('console')
if (activeTab !== 'console' && activeTab !== 'chat') {
setActiveTab('console')
}
const executionId = uuidv4()
@@ -144,7 +146,7 @@ export function useWorkflowExecution() {
workflow,
currentBlockStates,
envVarValues,
undefined,
workflowInput,
workflowVariables
)
setExecutor(newExecutor)

View File

@@ -775,6 +775,7 @@ export class Executor {
endedAt: blockLog.endedAt,
workflowId: context.workflowId,
timestamp: blockLog.startedAt,
blockId: block.id,
blockName: block.metadata?.name || 'Unnamed Block',
blockType: block.metadata?.id || 'unknown',
})

View File

@@ -0,0 +1,60 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { ChatMessage, ChatStore } from './types'
// MAX across all workflows
const MAX_MESSAGES = 50
export const useChatStore = create<ChatStore>()(
devtools(
persist(
(set, get) => ({
messages: [],
selectedWorkflowOutputs: {},
addMessage: (message) => {
set((state) => {
const newMessage: ChatMessage = {
...message,
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
}
// Keep only the last MAX_MESSAGES
const newMessages = [newMessage, ...state.messages].slice(0, MAX_MESSAGES)
return { messages: newMessages }
})
},
clearChat: (workflowId: string | null) => {
set((state) => ({
messages: state.messages.filter(
(message) => !workflowId || message.workflowId !== workflowId
),
}))
},
getWorkflowMessages: (workflowId) => {
return get().messages.filter((message) => message.workflowId === workflowId)
},
setSelectedWorkflowOutput: (workflowId, outputId) => {
set((state) => ({
selectedWorkflowOutputs: {
...state.selectedWorkflowOutputs,
[workflowId]: outputId,
},
}))
},
getSelectedWorkflowOutput: (workflowId) => {
return get().selectedWorkflowOutputs[workflowId] || null
},
}),
{
name: 'chat-store',
}
)
)
)

View File

@@ -0,0 +1,18 @@
export interface ChatMessage {
id: string
content: any
workflowId: string | null
type: 'user' | 'workflow'
timestamp: string
blockId?: string
}
export interface ChatStore {
messages: ChatMessage[]
selectedWorkflowOutputs: Record<string, string>
addMessage: (message: Omit<ChatMessage, 'id' | 'timestamp'>) => void
clearChat: (workflowId: string | null) => void
getWorkflowMessages: (workflowId: string) => ChatMessage[]
setSelectedWorkflowOutput: (workflowId: string, outputId: string) => void
getSelectedWorkflowOutput: (workflowId: string) => string | null
}

View File

@@ -1,6 +1,7 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { ConsoleEntry, ConsoleStore } from './types'
import { useChatStore } from '../chat/store'
// MAX across all workflows
const MAX_ENTRIES = 50
@@ -39,6 +40,28 @@ const redactApiKeys = (obj: any): any => {
return result
}
/**
* Gets a nested property value from an object using a path string
* @param obj The object to get the value from
* @param path The path to the value (e.g. 'response.content')
* @returns The value at the path, or undefined if not found
*/
const getValueByPath = (obj: any, path: string): any => {
if (!obj || !path) return undefined;
const pathParts = path.split('.');
let current = obj;
for (const part of pathParts) {
if (current === null || current === undefined || typeof current !== 'object') {
return undefined;
}
current = current[part];
}
return current;
}
export const useConsoleStore = create<ConsoleStore>()(
devtools(
persist(
@@ -65,6 +88,57 @@ export const useConsoleStore = create<ConsoleStore>()(
// Keep only the last MAX_ENTRIES
const newEntries = [newEntry, ...state.entries].slice(0, MAX_ENTRIES)
// Check if this block matches a selected workflow output
if (entry.workflowId && entry.blockName) {
const chatStore = useChatStore.getState()
const selectedOutputId = chatStore.getSelectedWorkflowOutput(entry.workflowId)
if (selectedOutputId) {
// The selectedOutputId format is "{blockId}_{path}"
// We need to extract both components
const idParts = selectedOutputId.split('_');
const selectedBlockId = idParts[0];
// Reconstruct the path by removing the blockId part
const selectedPath = idParts.slice(1).join('.');
console.log(`[Chat Output] Selected Output ID: ${selectedOutputId}`);
console.log(`[Chat Output] Block ID: ${selectedBlockId}, Path: ${selectedPath}`);
console.log(`[Chat Output] Current Block ID: ${entry.blockId}`);
// If this block matches the selected output for this workflow
if (selectedBlockId && entry.blockId === selectedBlockId) {
// Extract the specific value from the output using the path
let specificValue: any = undefined;
if (selectedPath) {
specificValue = getValueByPath(entry.output, selectedPath);
console.log(`[Chat Output] Found value:`, specificValue);
} else {
console.log(`[Chat Output] No path specified, using entire output`);
specificValue = entry.output;
}
// Format the value appropriately for display
let formattedValue: string;
if (specificValue === undefined) {
formattedValue = "Output value not found";
} else if (typeof specificValue === 'object') {
formattedValue = JSON.stringify(specificValue, null, 2);
} else {
formattedValue = String(specificValue);
}
// Add the specific value to chat, not the whole output
chatStore.addMessage({
content: formattedValue,
workflowId: entry.workflowId,
type: 'workflow',
blockId: entry.blockId,
})
}
}
}
return { entries: newEntries }
})
},

View File

@@ -10,6 +10,7 @@ export interface ConsoleEntry {
timestamp: string
blockName?: string
blockType?: string
blockId?: string
}
export interface ConsoleStore {

View File

@@ -1,4 +1,4 @@
export type PanelTab = 'console' | 'variables'
export type PanelTab = 'console' | 'variables' | 'chat'
export interface PanelStore {
isOpen: boolean