From ae4ae535e9cac9be68e204785dd9e7b9601cdfcf Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Fri, 31 Jan 2025 13:04:23 -0800 Subject: [PATCH] Created console store and console items --- app/w/components/console/console.tsx | 197 +++++++++++++++++++++++++- app/w/hooks/use-workflow-execution.ts | 42 +++++- components/ui/scroll-area.tsx | 48 +++++++ package-lock.json | 32 +++++ package.json | 1 + stores/console/store.ts | 39 +++++ stores/console/types.ts | 17 +++ 7 files changed, 371 insertions(+), 5 deletions(-) create mode 100644 components/ui/scroll-area.tsx create mode 100644 stores/console/store.ts create mode 100644 stores/console/types.ts diff --git a/app/w/components/console/console.tsx b/app/w/components/console/console.tsx index 37244c609..e0c07edd5 100644 --- a/app/w/components/console/console.tsx +++ b/app/w/components/console/console.tsx @@ -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 null + if (typeof data !== 'object') { + return ( + + {JSON.stringify(data)} + + ) + } + + const isArray = Array.isArray(data) + const items = isArray ? data : Object.entries(data) + const isEmpty = items.length === 0 + + if (isEmpty) { + return {isArray ? '[]' : '{}'} + } + + return ( +
+ { + e.stopPropagation() + setIsCollapsed(!isCollapsed) + }} + > + {isCollapsed ? '▶' : '▼'} {isArray ? '[' : '{'} + {isCollapsed ? '...' : ''} + + {!isCollapsed && ( +
+ {isArray + ? items.map((item, index) => ( +
+ + {index < items.length - 1 && ','} +
+ )) + : (items as [string, any][]).map(([key, value], index) => ( +
+ {key}:{' '} + + {index < items.length - 1 && ','} +
+ ))} +
+ )} + {isArray ? ']' : '}'} +
+ ) +} + +function ConsoleEntry({ entry }: { entry: ConsoleEntryType }) { + const [isExpanded, setIsExpanded] = useState(false) + + const timeAgo = useMemo( + () => + formatDistanceToNow(new Date(entry.startedAt), { + addSuffix: true, + }), + [entry.startedAt] + ) + + return ( +
+
+
+
+ + {format(new Date(entry.startedAt), 'HH:mm:ss')} +
+
+ + Duration: {entry.durationMs}ms +
+
+ +
+
+ +
+ +
+
+ + {entry.error && ( +
+ +
+
Error
+
{entry.error}
+
+
+ )} +
+
+
+ ) +} 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 ( -
- {/* Console content will go here */} +
+
+ +
+

Console

+ +
+ + + {filteredEntries.length === 0 ? ( +
+ No console entries +
+ ) : ( + filteredEntries.map((entry) => ( + + )) + )} +
diff --git a/app/w/hooks/use-workflow-execution.ts b/app/w/hooks/use-workflow-execution.ts index dbf0dd309..69a08bddd 100644 --- a/app/w/hooks/use-workflow-execution.ts +++ b/app/w/hooks/use-workflow-execution.ts @@ -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 } } \ No newline at end of file diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx new file mode 100644 index 000000000..0b4a48d87 --- /dev/null +++ b/components/ui/scroll-area.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)) +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)) +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName + +export { ScrollArea, ScrollBar } diff --git a/package-lock.json b/package-lock.json index 7790aedb5..955d87b1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5a07c8df3..3d0116e4f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/stores/console/store.ts b/stores/console/store.ts new file mode 100644 index 000000000..6dcf2df18 --- /dev/null +++ b/stores/console/store.ts @@ -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()( + 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', + } + ) + ) +) \ No newline at end of file diff --git a/stores/console/types.ts b/stores/console/types.ts new file mode 100644 index 000000000..d1ed6f716 --- /dev/null +++ b/stores/console/types.ts @@ -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) => void + clearConsole: () => void + getWorkflowEntries: (workflowId: string) => ConsoleEntry[] +} \ No newline at end of file