mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-08 22:48:14 -05:00
Created console store and console items
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
48
components/ui/scroll-area.tsx
Normal file
48
components/ui/scroll-area.tsx
Normal 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
32
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
39
stores/console/store.ts
Normal 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
17
stores/console/types.ts
Normal 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[]
|
||||
}
|
||||
Reference in New Issue
Block a user