Created console store and console items

This commit is contained in:
Emir Karabeg
2025-01-31 13:04:23 -08:00
parent 0d0464e4d7
commit ae4ae535e9
7 changed files with 371 additions and 5 deletions

View File

@@ -1,15 +1,174 @@
'use client'
import { useState } from 'react'
import { PanelLeftClose, PanelLeft, Terminal } from 'lucide-react'
import { useState, useMemo, useEffect } from 'react'
import { formatDistanceToNow, format } from 'date-fns'
import {
PanelLeftClose,
Terminal,
XCircle,
Clock,
Calendar,
CheckCircle2,
AlertCircle,
} from 'lucide-react'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { useConsoleStore } from '@/stores/console/store'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Button } from '@/components/ui/button'
import { ConsoleEntry as ConsoleEntryType } from '@/stores/console/types'
import { useWorkflowRegistry } from '@/stores/workflow/registry'
function JSONView({ data, level = 0 }: { data: any; level?: number }) {
const [isCollapsed, setIsCollapsed] = useState(true)
if (data === null) return <span className="text-muted-foreground">null</span>
if (typeof data !== 'object') {
return (
<span
className={`${
typeof data === 'string' ? 'text-success' : 'text-info'
} break-all`}
>
{JSON.stringify(data)}
</span>
)
}
const isArray = Array.isArray(data)
const items = isArray ? data : Object.entries(data)
const isEmpty = items.length === 0
if (isEmpty) {
return <span>{isArray ? '[]' : '{}'}</span>
}
return (
<div className="relative">
<span
className="cursor-pointer select-none"
onClick={(e) => {
e.stopPropagation()
setIsCollapsed(!isCollapsed)
}}
>
{isCollapsed ? '▶' : '▼'} {isArray ? '[' : '{'}
{isCollapsed ? '...' : ''}
</span>
{!isCollapsed && (
<div className="ml-4 break-words">
{isArray
? items.map((item, index) => (
<div key={index} className="break-all">
<JSONView data={item} level={level + 1} />
{index < items.length - 1 && ','}
</div>
))
: (items as [string, any][]).map(([key, value], index) => (
<div key={key} className="break-all">
<span className="text-muted-foreground">{key}</span>:{' '}
<JSONView data={value} level={level + 1} />
{index < items.length - 1 && ','}
</div>
))}
</div>
)}
<span>{isArray ? ']' : '}'}</span>
</div>
)
}
function ConsoleEntry({ entry }: { entry: ConsoleEntryType }) {
const [isExpanded, setIsExpanded] = useState(false)
const timeAgo = useMemo(
() =>
formatDistanceToNow(new Date(entry.startedAt), {
addSuffix: true,
}),
[entry.startedAt]
)
return (
<div className="border-b border-border hover:bg-accent/50 transition-colors">
<div className="p-4 space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4" />
<span>{format(new Date(entry.startedAt), 'HH:mm:ss')}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="h-4 w-4" />
<span>Duration: {entry.durationMs}ms</span>
</div>
</div>
<div className="space-y-4">
<div className="flex items-start gap-2">
<Terminal className="h-4 w-4 text-muted-foreground mt-1" />
<div className="text-sm font-mono flex-1">
<JSONView data={entry.output} />
</div>
</div>
{entry.error && (
<div className="flex items-start gap-2 border rounded-md p-3 border-red-500 bg-red-50 text-destructive dark:border-border dark:text-foreground dark:bg-background">
<AlertCircle className="h-4 w-4 text-red-500 mt-1" />
<div className="flex-1 break-all">
<div className="font-medium">Error</div>
<pre className="text-sm whitespace-pre-wrap">{entry.error}</pre>
</div>
</div>
)}
</div>
</div>
</div>
)
}
export function Console() {
const [isCollapsed, setIsCollapsed] = useState(false)
const [width, setWidth] = useState(336) // 84 * 4 = 336px (default width)
const [isDragging, setIsDragging] = useState(false)
const entries = useConsoleStore((state) => state.entries)
const clearConsole = useConsoleStore((state) => state.clearConsole)
const { activeWorkflowId } = useWorkflowRegistry()
// Filter entries for active workflow
const filteredEntries = useMemo(() => {
return entries.filter((entry) => entry.workflowId === activeWorkflowId)
}, [entries, activeWorkflowId])
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true)
e.preventDefault()
}
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
const newWidth = window.innerWidth - e.clientX
setWidth(Math.max(336, Math.min(newWidth, window.innerWidth * 0.8)))
}
}
const handleMouseUp = () => {
setIsDragging(false)
}
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
return () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
}, [isDragging])
if (isCollapsed) {
return (
@@ -29,8 +188,38 @@ export function Console() {
}
return (
<div className="fixed right-0 top-16 z-10 h-[calc(100vh-4rem)] overflow-y-auto w-72 border-l bg-background sm:block">
{/* Console content will go here */}
<div
className="fixed right-0 top-16 z-10 h-[calc(100vh-4rem)] border-l bg-background"
style={{ width: `${width}px` }}
>
<div
className="absolute left-[-4px] top-0 bottom-0 w-4 cursor-ew-resize hover:bg-accent/50 z-50"
onMouseDown={handleMouseDown}
/>
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-sm font-medium">Console</h2>
<Button
variant="ghost"
size="sm"
onClick={clearConsole}
className="text-muted-foreground hover:text-foreground"
>
Clear
</Button>
</div>
<ScrollArea className="h-[calc(100%-4rem)]">
{filteredEntries.length === 0 ? (
<div className="flex items-center justify-center h-full text-sm text-muted-foreground pt-4">
No console entries
</div>
) : (
filteredEntries.map((entry) => (
<ConsoleEntry key={entry.id} entry={entry} />
))
)}
</ScrollArea>
<Tooltip>
<TooltipTrigger asChild>

View File

@@ -5,6 +5,7 @@ import { Executor } from '@/executor'
import { ExecutionResult } from '@/executor/types'
import { useNotificationStore } from '@/stores/notifications/store'
import { useWorkflowRegistry } from '@/stores/workflow/registry'
import { useConsoleStore } from '@/stores/console/store'
export function useWorkflowExecution() {
const [isExecuting, setIsExecuting] = useState(false)
@@ -12,6 +13,7 @@ export function useWorkflowExecution() {
const { blocks, edges } = useWorkflowStore()
const { activeWorkflowId } = useWorkflowRegistry()
const { addNotification } = useNotificationStore()
const { addConsole } = useConsoleStore()
const handleRunWorkflow = useCallback(async () => {
setIsExecuting(true)
@@ -32,6 +34,32 @@ export function useWorkflowExecution() {
const result = await executor.execute('my-run-id')
setExecutionResult(result)
// Add console entries for each block execution
if (result.logs) {
result.logs.forEach((log) => {
addConsole({
output: log.output,
error: log.error,
durationMs: log.durationMs,
startedAt: log.startedAt,
endedAt: log.endedAt,
workflowId: activeWorkflowId,
timestamp: log.startedAt // Using startedAt as the timestamp
})
})
}
// Add final execution result to console
addConsole({
output: result.output,
error: result.error,
durationMs: result.metadata?.duration || 0,
startedAt: result.metadata?.startTime || new Date().toISOString(),
endedAt: result.metadata?.endTime || new Date().toISOString(),
workflowId: activeWorkflowId,
timestamp: result.metadata?.startTime || new Date().toISOString()
})
if (result.logs) {
console.group('Detailed Block Logs')
result.logs.forEach((log) => {
@@ -74,11 +102,23 @@ export function useWorkflowExecution() {
output: { response: {} },
error: errorMessage
})
// Add error entry to console
addConsole({
output: {},
error: errorMessage,
durationMs: 0,
startedAt: new Date().toISOString(),
endedAt: new Date().toISOString(),
workflowId: activeWorkflowId,
timestamp: new Date().toISOString()
})
addNotification('error', `Workflow execution failed: ${errorMessage}`, activeWorkflowId)
} finally {
setIsExecuting(false)
}
}, [blocks, edges, addNotification, activeWorkflowId])
}, [blocks, edges, addNotification, activeWorkflowId, addConsole])
return { isExecuting, executionResult, handleRunWorkflow }
}

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

32
package-lock.json generated
View File

@@ -13,6 +13,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
@@ -2531,6 +2532,37 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz",
"integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz",

View File

@@ -16,6 +16,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",

39
stores/console/store.ts Normal file
View File

@@ -0,0 +1,39 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { ConsoleStore, ConsoleEntry } from './types'
const MAX_ENTRIES = 50
export const useConsoleStore = create<ConsoleStore>()(
devtools(
persist(
(set, get) => ({
entries: [],
addConsole: (entry) => {
set((state) => {
const newEntry: ConsoleEntry = {
...entry,
id: crypto.randomUUID(),
timestamp: new Date().toISOString(),
}
// Keep only the last MAX_ENTRIES
const newEntries = [newEntry, ...state.entries].slice(0, MAX_ENTRIES)
return { entries: newEntries }
})
},
clearConsole: () => set({ entries: [] }),
getWorkflowEntries: (workflowId) => {
return get().entries.filter((entry) => entry.workflowId === workflowId)
},
}),
{
name: 'console-store',
}
)
)
)

17
stores/console/types.ts Normal file
View File

@@ -0,0 +1,17 @@
export interface ConsoleEntry {
id: string
output: any
error?: string
durationMs: number
startedAt: string
endedAt: string
workflowId?: string | null
timestamp: string
}
export interface ConsoleStore {
entries: ConsoleEntry[]
addConsole: (entry: Omit<ConsoleEntry, 'id'>) => void
clearConsole: () => void
getWorkflowEntries: (workflowId: string) => ConsoleEntry[]
}