mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Merge pull request #650 from simstudioai/improvement/logging-ui
improvement(logging-ui): improve logging UI to be less of information dump
This commit is contained in:
@@ -3,12 +3,15 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
AlertCircle,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Hash,
|
||||
Loader2,
|
||||
Maximize2,
|
||||
X,
|
||||
Zap,
|
||||
} from 'lucide-react'
|
||||
@@ -21,6 +24,71 @@ import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('FrozenCanvas')
|
||||
|
||||
function ExpandableDataSection({ title, data }: { title: string; data: any }) {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
const jsonString = JSON.stringify(data, null, 2)
|
||||
const isLargeData = jsonString.length > 500 || jsonString.split('\n').length > 10
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<h4 className='font-medium text-foreground text-sm'>{title}</h4>
|
||||
<div className='flex items-center gap-1'>
|
||||
{isLargeData && (
|
||||
<button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className='rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
title='Expand in modal'
|
||||
>
|
||||
<Maximize2 className='h-3 w-3' />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className='rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
>
|
||||
{isExpanded ? <ChevronUp className='h-3 w-3' /> : <ChevronDown className='h-3 w-3' />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-y-auto rounded bg-muted p-3 font-mono text-xs transition-all duration-200',
|
||||
isExpanded ? 'max-h-96' : 'max-h-32'
|
||||
)}
|
||||
>
|
||||
<pre className='whitespace-pre-wrap break-words text-foreground'>{jsonString}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal for large data */}
|
||||
{isModalOpen && (
|
||||
<div className='fixed inset-0 z-[200] flex items-center justify-center bg-black/50'>
|
||||
<div className='mx-4 h-[80vh] w-full max-w-4xl rounded-lg border bg-background shadow-lg'>
|
||||
<div className='flex items-center justify-between border-b p-4'>
|
||||
<h3 className='font-medium text-foreground text-lg'>{title}</h3>
|
||||
<button
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
className='rounded p-1 text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
>
|
||||
<X className='h-4 w-4' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='h-[calc(80vh-4rem)] overflow-auto p-4'>
|
||||
<pre className='whitespace-pre-wrap break-words font-mono text-foreground text-sm'>
|
||||
{jsonString}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function formatExecutionData(executionData: any) {
|
||||
const {
|
||||
inputData,
|
||||
@@ -80,16 +148,79 @@ function getCurrentIterationData(blockExecutionData: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: () => void }) {
|
||||
function PinnedLogs({
|
||||
executionData,
|
||||
blockId,
|
||||
workflowState,
|
||||
onClose,
|
||||
}: {
|
||||
executionData: any | null
|
||||
blockId: string
|
||||
workflowState: any
|
||||
onClose: () => void
|
||||
}) {
|
||||
// ALL HOOKS MUST BE CALLED BEFORE ANY CONDITIONAL RETURNS
|
||||
const [currentIterationIndex, setCurrentIterationIndex] = useState(0)
|
||||
|
||||
// Reset iteration index when execution data changes
|
||||
useEffect(() => {
|
||||
setCurrentIterationIndex(0)
|
||||
}, [executionData])
|
||||
|
||||
// Handle case where block has no execution data (e.g., failed workflow)
|
||||
if (!executionData) {
|
||||
const blockInfo = workflowState?.blocks?.[blockId]
|
||||
const formatted = {
|
||||
blockName: blockInfo?.name || 'Unknown Block',
|
||||
blockType: blockInfo?.type || 'unknown',
|
||||
status: 'not_executed',
|
||||
duration: 'N/A',
|
||||
input: null,
|
||||
output: null,
|
||||
errorMessage: null,
|
||||
errorStackTrace: null,
|
||||
cost: null,
|
||||
tokens: null,
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='fixed top-4 right-4 z-[100] max-h-[calc(100vh-8rem)] w-96 overflow-y-auto border-border bg-background shadow-lg'>
|
||||
<CardHeader className='pb-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<CardTitle className='flex items-center gap-2 text-foreground text-lg'>
|
||||
<Zap className='h-5 w-5' />
|
||||
{formatted.blockName}
|
||||
</CardTitle>
|
||||
<button onClick={onClose} className='rounded-sm p-1 text-foreground hover:bg-muted'>
|
||||
<X className='h-4 w-4' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Badge variant='secondary'>{formatted.blockType}</Badge>
|
||||
<Badge variant='outline'>not executed</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className='space-y-4'>
|
||||
<div className='rounded-md bg-muted/50 p-4 text-center'>
|
||||
<div className='text-muted-foreground text-sm'>
|
||||
This block was not executed because the workflow failed before reaching it.
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// Now we can safely use the execution data
|
||||
const iterationInfo = getCurrentIterationData({
|
||||
...executionData,
|
||||
currentIteration: currentIterationIndex,
|
||||
})
|
||||
|
||||
const formatted = formatExecutionData(iterationInfo.executionData)
|
||||
|
||||
const totalIterations = executionData.iterations?.length || 1
|
||||
|
||||
const goToPreviousIteration = () => {
|
||||
@@ -104,10 +235,6 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: (
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentIterationIndex(0)
|
||||
}, [executionData])
|
||||
|
||||
return (
|
||||
<Card className='fixed top-4 right-4 z-[100] max-h-[calc(100vh-8rem)] w-96 overflow-y-auto border-border bg-background shadow-lg'>
|
||||
<CardHeader className='pb-3'>
|
||||
@@ -160,14 +287,14 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: (
|
||||
<span className='text-foreground text-sm'>{formatted.duration}</span>
|
||||
</div>
|
||||
|
||||
{formatted.cost && (
|
||||
{formatted.cost && formatted.cost.total > 0 && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<DollarSign className='h-4 w-4 text-muted-foreground' />
|
||||
<span className='text-foreground text-sm'>${formatted.cost.total.toFixed(5)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formatted.tokens && (
|
||||
{formatted.tokens && formatted.tokens.total > 0 && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Hash className='h-4 w-4 text-muted-foreground' />
|
||||
<span className='text-foreground text-sm'>{formatted.tokens.total} tokens</span>
|
||||
@@ -175,21 +302,11 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: (
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className='mb-2 font-medium text-foreground text-sm'>Input</h4>
|
||||
<div className='max-h-32 overflow-y-auto rounded bg-muted p-3 font-mono text-xs'>
|
||||
<pre className='text-foreground'>{JSON.stringify(formatted.input, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<ExpandableDataSection title='Input' data={formatted.input} />
|
||||
|
||||
<div>
|
||||
<h4 className='mb-2 font-medium text-foreground text-sm'>Output</h4>
|
||||
<div className='max-h-32 overflow-y-auto rounded bg-muted p-3 font-mono text-xs'>
|
||||
<pre className='text-foreground'>{JSON.stringify(formatted.output, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<ExpandableDataSection title='Output' data={formatted.output} />
|
||||
|
||||
{formatted.cost && (
|
||||
{formatted.cost && formatted.cost.total > 0 && (
|
||||
<div>
|
||||
<h4 className='mb-2 font-medium text-foreground text-sm'>Cost Breakdown</h4>
|
||||
<div className='space-y-1 text-sm'>
|
||||
@@ -209,7 +326,7 @@ function PinnedLogs({ executionData, onClose }: { executionData: any; onClose: (
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formatted.tokens && (
|
||||
{formatted.tokens && formatted.tokens.total > 0 && (
|
||||
<div>
|
||||
<h4 className='mb-2 font-medium text-foreground text-sm'>Token Usage</h4>
|
||||
<div className='space-y-1 text-sm'>
|
||||
@@ -242,12 +359,7 @@ interface FrozenCanvasData {
|
||||
startedAt: string
|
||||
endedAt?: string
|
||||
totalDurationMs?: number
|
||||
blockStats: {
|
||||
total: number
|
||||
success: number
|
||||
error: number
|
||||
skipped: number
|
||||
}
|
||||
|
||||
cost: {
|
||||
total: number | null
|
||||
input: number | null
|
||||
@@ -284,76 +396,100 @@ export function FrozenCanvas({
|
||||
if (traceSpans && Array.isArray(traceSpans)) {
|
||||
const blockExecutionMap: Record<string, any> = {}
|
||||
|
||||
const workflowSpan = traceSpans[0]
|
||||
if (workflowSpan?.children && Array.isArray(workflowSpan.children)) {
|
||||
const traceSpansByBlockId = workflowSpan.children.reduce((acc: any, span: any) => {
|
||||
logger.debug('Processing trace spans for frozen canvas:', { traceSpans })
|
||||
|
||||
// Recursively collect all spans with blockId from the trace spans tree
|
||||
const collectBlockSpans = (spans: any[]): any[] => {
|
||||
const blockSpans: any[] = []
|
||||
|
||||
for (const span of spans) {
|
||||
// If this span has a blockId, it's a block execution
|
||||
if (span.blockId) {
|
||||
if (!acc[span.blockId]) {
|
||||
acc[span.blockId] = []
|
||||
}
|
||||
acc[span.blockId].push(span)
|
||||
blockSpans.push(span)
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
for (const [blockId, spans] of Object.entries(traceSpansByBlockId)) {
|
||||
const spanArray = spans as any[]
|
||||
|
||||
const iterations = spanArray.map((span: any) => {
|
||||
// Extract error information from span output if status is error
|
||||
let errorMessage = null
|
||||
let errorStackTrace = null
|
||||
|
||||
if (span.status === 'error' && span.output) {
|
||||
// Error information can be in different formats in the output
|
||||
if (typeof span.output === 'string') {
|
||||
errorMessage = span.output
|
||||
} else if (span.output.error) {
|
||||
errorMessage = span.output.error
|
||||
errorStackTrace = span.output.stackTrace || span.output.stack
|
||||
} else if (span.output.message) {
|
||||
errorMessage = span.output.message
|
||||
errorStackTrace = span.output.stackTrace || span.output.stack
|
||||
} else {
|
||||
// Fallback: stringify the entire output for error cases
|
||||
errorMessage = JSON.stringify(span.output)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: span.id,
|
||||
blockId: span.blockId,
|
||||
blockName: span.name,
|
||||
blockType: span.type,
|
||||
status: span.status,
|
||||
startedAt: span.startTime,
|
||||
endedAt: span.endTime,
|
||||
durationMs: span.duration,
|
||||
inputData: span.input,
|
||||
outputData: span.output,
|
||||
errorMessage,
|
||||
errorStackTrace,
|
||||
cost: span.cost || {
|
||||
input: null,
|
||||
output: null,
|
||||
total: null,
|
||||
},
|
||||
tokens: span.tokens || {
|
||||
prompt: null,
|
||||
completion: null,
|
||||
total: null,
|
||||
},
|
||||
modelUsed: span.model || null,
|
||||
metadata: {},
|
||||
}
|
||||
})
|
||||
|
||||
blockExecutionMap[blockId] = {
|
||||
iterations,
|
||||
currentIteration: 0,
|
||||
totalIterations: iterations.length,
|
||||
// Recursively check children
|
||||
if (span.children && Array.isArray(span.children)) {
|
||||
blockSpans.push(...collectBlockSpans(span.children))
|
||||
}
|
||||
}
|
||||
|
||||
return blockSpans
|
||||
}
|
||||
|
||||
const allBlockSpans = collectBlockSpans(traceSpans)
|
||||
logger.debug('Collected all block spans:', allBlockSpans)
|
||||
|
||||
// Group spans by blockId
|
||||
const traceSpansByBlockId = allBlockSpans.reduce((acc: any, span: any) => {
|
||||
if (span.blockId) {
|
||||
if (!acc[span.blockId]) {
|
||||
acc[span.blockId] = []
|
||||
}
|
||||
acc[span.blockId].push(span)
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
logger.debug('Grouped trace spans by blockId:', traceSpansByBlockId)
|
||||
|
||||
for (const [blockId, spans] of Object.entries(traceSpansByBlockId)) {
|
||||
const spanArray = spans as any[]
|
||||
|
||||
const iterations = spanArray.map((span: any) => {
|
||||
// Extract error information from span output if status is error
|
||||
let errorMessage = null
|
||||
let errorStackTrace = null
|
||||
|
||||
if (span.status === 'error' && span.output) {
|
||||
// Error information can be in different formats in the output
|
||||
if (typeof span.output === 'string') {
|
||||
errorMessage = span.output
|
||||
} else if (span.output.error) {
|
||||
errorMessage = span.output.error
|
||||
errorStackTrace = span.output.stackTrace || span.output.stack
|
||||
} else if (span.output.message) {
|
||||
errorMessage = span.output.message
|
||||
errorStackTrace = span.output.stackTrace || span.output.stack
|
||||
} else {
|
||||
// Fallback: stringify the entire output for error cases
|
||||
errorMessage = JSON.stringify(span.output)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: span.id,
|
||||
blockId: span.blockId,
|
||||
blockName: span.name,
|
||||
blockType: span.type,
|
||||
status: span.status,
|
||||
startedAt: span.startTime,
|
||||
endedAt: span.endTime,
|
||||
durationMs: span.duration,
|
||||
inputData: span.input,
|
||||
outputData: span.output,
|
||||
errorMessage,
|
||||
errorStackTrace,
|
||||
cost: span.cost || {
|
||||
input: null,
|
||||
output: null,
|
||||
total: null,
|
||||
},
|
||||
tokens: span.tokens || {
|
||||
prompt: null,
|
||||
completion: null,
|
||||
total: null,
|
||||
},
|
||||
modelUsed: span.model || null,
|
||||
metadata: {},
|
||||
}
|
||||
})
|
||||
|
||||
blockExecutionMap[blockId] = {
|
||||
iterations,
|
||||
currentIteration: 0,
|
||||
totalIterations: iterations.length,
|
||||
}
|
||||
}
|
||||
|
||||
setBlockExecutions(blockExecutionMap)
|
||||
@@ -386,8 +522,6 @@ export function FrozenCanvas({
|
||||
fetchData()
|
||||
}, [executionId])
|
||||
|
||||
// No need to create a temporary workflow - just use the workflowState directly
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center', className)} style={{ height, width }}>
|
||||
@@ -449,16 +583,18 @@ export function FrozenCanvas({
|
||||
showSubBlocks={true}
|
||||
isPannable={true}
|
||||
onNodeClick={(blockId) => {
|
||||
if (blockExecutions[blockId]) {
|
||||
setPinnedBlockId(blockId)
|
||||
}
|
||||
// Always allow clicking blocks, even if they don't have execution data
|
||||
// This is important for failed workflows where some blocks never executed
|
||||
setPinnedBlockId(blockId)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{pinnedBlockId && blockExecutions[pinnedBlockId] && (
|
||||
{pinnedBlockId && (
|
||||
<PinnedLogs
|
||||
executionData={blockExecutions[pinnedBlockId]}
|
||||
executionData={blockExecutions[pinnedBlockId] || null}
|
||||
blockId={pinnedBlockId}
|
||||
workflowState={data.workflowState}
|
||||
onClose={() => setPinnedBlockId(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -494,43 +494,6 @@ export function Sidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Enhanced Stats - only show for enhanced logs */}
|
||||
{log.metadata?.enhanced && log.metadata?.blockStats && (
|
||||
<div>
|
||||
<h3 className='mb-1 font-medium text-muted-foreground text-xs'>
|
||||
Block Execution Stats
|
||||
</h3>
|
||||
<div className='space-y-1 text-sm'>
|
||||
<div className='flex justify-between'>
|
||||
<span>Total Blocks:</span>
|
||||
<span className='font-medium'>{log.metadata.blockStats.total}</span>
|
||||
</div>
|
||||
<div className='flex justify-between'>
|
||||
<span>Successful:</span>
|
||||
<span className='font-medium text-green-600'>
|
||||
{log.metadata.blockStats.success}
|
||||
</span>
|
||||
</div>
|
||||
{log.metadata.blockStats.error > 0 && (
|
||||
<div className='flex justify-between'>
|
||||
<span>Failed:</span>
|
||||
<span className='font-medium text-red-600'>
|
||||
{log.metadata.blockStats.error}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{log.metadata.blockStats.skipped > 0 && (
|
||||
<div className='flex justify-between'>
|
||||
<span>Skipped:</span>
|
||||
<span className='font-medium text-yellow-600'>
|
||||
{log.metadata.blockStats.skipped}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Enhanced Cost - only show for enhanced logs with actual cost data */}
|
||||
{log.metadata?.enhanced && hasCostInfo && (
|
||||
<div>
|
||||
@@ -583,7 +546,7 @@ export function Sidebar({
|
||||
className='w-full justify-start gap-2'
|
||||
>
|
||||
<Eye className='h-4 w-4' />
|
||||
View Frozen Canvas
|
||||
View Snapshot
|
||||
</Button>
|
||||
<p className='mt-1 text-muted-foreground text-xs'>
|
||||
See the exact workflow state and block inputs/outputs at execution time
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronDownSquare,
|
||||
ChevronRight,
|
||||
ChevronUpSquare,
|
||||
Code,
|
||||
Cpu,
|
||||
ExternalLink,
|
||||
} from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { ChevronDown, ChevronRight, Code, Cpu, ExternalLink } from 'lucide-react'
|
||||
import {
|
||||
AgentIcon,
|
||||
ApiIcon,
|
||||
@@ -203,40 +195,11 @@ export function TraceSpansDisplay({
|
||||
// Keep track of expanded spans
|
||||
const [expandedSpans, setExpandedSpans] = useState<Set<string>>(new Set())
|
||||
|
||||
// Function to collect all span IDs recursively (for expand all functionality)
|
||||
const collectAllSpanIds = (spans: TraceSpan[]): string[] => {
|
||||
const ids: string[] = []
|
||||
|
||||
const collectIds = (span: TraceSpan) => {
|
||||
const spanId = span.id || `span-${span.name}-${span.startTime}`
|
||||
ids.push(spanId)
|
||||
|
||||
// Process children
|
||||
if (span.children && span.children.length > 0) {
|
||||
span.children.forEach(collectIds)
|
||||
}
|
||||
}
|
||||
|
||||
spans.forEach(collectIds)
|
||||
return ids
|
||||
}
|
||||
|
||||
const allSpanIds = useMemo(() => {
|
||||
if (!traceSpans || traceSpans.length === 0) return []
|
||||
return collectAllSpanIds(traceSpans)
|
||||
}, [traceSpans])
|
||||
|
||||
// Early return after all hooks
|
||||
if (!traceSpans || traceSpans.length === 0) {
|
||||
return <div className='text-muted-foreground text-sm'>No trace data available</div>
|
||||
}
|
||||
|
||||
// Format total duration for better readability
|
||||
const _formatTotalDuration = (ms: number) => {
|
||||
if (ms < 1000) return `${ms}ms`
|
||||
return `${(ms / 1000).toFixed(2)}s (${ms}ms)`
|
||||
}
|
||||
|
||||
// Find the earliest start time among all spans to be the workflow start time
|
||||
const workflowStartTime = traceSpans.reduce((earliest, span) => {
|
||||
const startTime = new Date(span.startTime).getTime()
|
||||
@@ -269,48 +232,10 @@ export function TraceSpansDisplay({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle expand all / collapse all
|
||||
const handleExpandAll = () => {
|
||||
const newExpandedSpans = new Set(allSpanIds)
|
||||
setExpandedSpans(newExpandedSpans)
|
||||
|
||||
if (onExpansionChange) {
|
||||
onExpansionChange(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCollapseAll = () => {
|
||||
setExpandedSpans(new Set())
|
||||
|
||||
if (onExpansionChange) {
|
||||
onExpansionChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if all spans are currently expanded
|
||||
const allExpanded = allSpanIds.length > 0 && allSpanIds.every((id) => expandedSpans.has(id))
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Trace Spans</div>
|
||||
<button
|
||||
onClick={allExpanded ? handleCollapseAll : handleExpandAll}
|
||||
className='flex items-center gap-1 text-muted-foreground text-xs transition-colors hover:text-foreground'
|
||||
title={allExpanded ? 'Collapse all spans' : 'Expand all spans'}
|
||||
>
|
||||
{allExpanded ? (
|
||||
<>
|
||||
<ChevronUpSquare className='h-3.5 w-3.5' />
|
||||
<span>Collapse</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDownSquare className='h-3.5 w-3.5' />
|
||||
<span>Expand</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Workflow Execution</div>
|
||||
</div>
|
||||
<div className='w-full overflow-hidden rounded-md border shadow-sm'>
|
||||
{traceSpans.map((span, index) => {
|
||||
@@ -369,7 +294,8 @@ function TraceSpanItem({
|
||||
const expanded = expandedSpans.has(spanId)
|
||||
const hasChildren = span.children && span.children.length > 0
|
||||
const hasToolCalls = span.toolCalls && span.toolCalls.length > 0
|
||||
const hasNestedItems = hasChildren || hasToolCalls
|
||||
const hasInputOutput = Boolean(span.input || span.output)
|
||||
const hasNestedItems = hasChildren || hasToolCalls || hasInputOutput
|
||||
|
||||
// Calculate timing information
|
||||
const spanStartTime = new Date(span.startTime).getTime()
|
||||
@@ -389,9 +315,6 @@ function TraceSpanItem({
|
||||
const safeStartPercent = Math.min(100, Math.max(0, relativeStartPercent))
|
||||
const safeWidthPercent = Math.max(2, Math.min(100 - safeStartPercent, actualDurationPercent))
|
||||
|
||||
// For parent-relative timing display
|
||||
const _startOffsetPercentage = totalDuration > 0 ? (startOffset / totalDuration) * 100 : 0
|
||||
|
||||
// Handle click to expand/collapse this span
|
||||
const handleSpanClick = () => {
|
||||
if (hasNestedItems) {
|
||||
@@ -605,17 +528,17 @@ function TraceSpanItem({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Children and tool calls */}
|
||||
{/* Expanded content */}
|
||||
{expanded && (
|
||||
<div>
|
||||
{/* Block Input/Output Data */}
|
||||
{(span.input || span.output) && (
|
||||
<div className='mt-2 ml-8 space-y-3 overflow-hidden'>
|
||||
<div className='mt-2 mr-4 mb-4 ml-8 space-y-3 overflow-hidden'>
|
||||
{/* Input Data */}
|
||||
{span.input && (
|
||||
<div>
|
||||
<h4 className='mb-2 font-medium text-muted-foreground text-xs'>Input</h4>
|
||||
<div className='overflow-hidden rounded-md bg-secondary/30 p-3'>
|
||||
<div className='mb-2 overflow-hidden rounded-md bg-secondary/30 p-3'>
|
||||
<BlockDataDisplay data={span.input} blockType={span.type} isInput={true} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -627,7 +550,7 @@ function TraceSpanItem({
|
||||
<h4 className='mb-2 font-medium text-muted-foreground text-xs'>
|
||||
{span.status === 'error' ? 'Error Details' : 'Output'}
|
||||
</h4>
|
||||
<div className='overflow-hidden rounded-md bg-secondary/30 p-3'>
|
||||
<div className='mb-2 overflow-hidden rounded-md bg-secondary/30 p-3'>
|
||||
<BlockDataDisplay
|
||||
data={span.output}
|
||||
blockType={span.type}
|
||||
@@ -639,12 +562,8 @@ function TraceSpanItem({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Children and tool calls */}
|
||||
{expanded && (
|
||||
<div>
|
||||
{/* Children and tool calls */}
|
||||
{/* Render child spans */}
|
||||
{hasChildren && (
|
||||
<div>
|
||||
|
||||
@@ -308,14 +308,20 @@ export default function Logs() {
|
||||
{/* Table with fixed layout */}
|
||||
<div className='w-full min-w-[800px]'>
|
||||
{/* Header */}
|
||||
<div className='border-border/50 border-b'>
|
||||
<div className='grid grid-cols-[160px_100px_1fr_120px_100px_100px] gap-4 px-4 py-3 font-medium text-muted-foreground text-xs'>
|
||||
<div>Time</div>
|
||||
<div>Status</div>
|
||||
<div>Workflow</div>
|
||||
<div className='hidden lg:block'>Trigger</div>
|
||||
<div className='hidden xl:block'>Cost</div>
|
||||
<div>Duration</div>
|
||||
<div className='px-4 py-4'>
|
||||
<div className='rounded-lg border border-border/30 bg-muted/30'>
|
||||
<div className='grid grid-cols-[160px_100px_1fr_120px_100px_100px] gap-4 px-4 py-3'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Time</div>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Status</div>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Workflow</div>
|
||||
<div className='hidden font-medium text-muted-foreground text-xs lg:block'>
|
||||
Trigger
|
||||
</div>
|
||||
<div className='hidden font-medium text-muted-foreground text-xs xl:block'>
|
||||
Cost
|
||||
</div>
|
||||
<div className='font-medium text-muted-foreground text-xs'>Duration</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,7 +350,7 @@ export default function Logs() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='space-y-1 p-4'>
|
||||
<div className='space-y-1 px-4 pb-4'>
|
||||
{logs.map((log) => {
|
||||
const formattedDate = formatDate(log.createdAt)
|
||||
const isSelected = selectedLog?.id === log.id
|
||||
@@ -360,7 +366,7 @@ export default function Logs() {
|
||||
}`}
|
||||
onClick={() => handleLogClick(log)}
|
||||
>
|
||||
<div className='grid grid-cols-[160px_100px_1fr_120px_100px_100px] gap-4 p-4'>
|
||||
<div className='grid grid-cols-[160px_100px_1fr_120px_100px_100px] gap-4 px-4 py-4'>
|
||||
{/* Time */}
|
||||
<div>
|
||||
<div className='font-medium text-sm'>{formattedDate.formatted}</div>
|
||||
@@ -403,13 +409,11 @@ export default function Logs() {
|
||||
|
||||
{/* Cost */}
|
||||
<div className='hidden xl:block'>
|
||||
<div className='text-xs'>
|
||||
<div className='text-muted-foreground text-xs'>
|
||||
{log.metadata?.enhanced && log.metadata?.cost?.total ? (
|
||||
<span className='text-muted-foreground'>
|
||||
${log.metadata.cost.total.toFixed(4)}
|
||||
</span>
|
||||
<span>${log.metadata.cost.total.toFixed(4)}</span>
|
||||
) : (
|
||||
<span className='text-muted-foreground'>—</span>
|
||||
<span className='pl-0.5'>—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,12 +84,7 @@ export interface WorkflowLog {
|
||||
cost?: CostMetadata
|
||||
blockInput?: Record<string, any>
|
||||
enhanced?: boolean
|
||||
blockStats?: {
|
||||
total: number
|
||||
success: number
|
||||
error: number
|
||||
skipped: number
|
||||
}
|
||||
|
||||
blockExecutions?: Array<{
|
||||
id: string
|
||||
blockId: string
|
||||
|
||||
@@ -194,12 +194,6 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService {
|
||||
executionId: string
|
||||
endedAt: string
|
||||
totalDurationMs: number
|
||||
blockStats: {
|
||||
total: number
|
||||
success: number
|
||||
error: number
|
||||
skipped: number
|
||||
}
|
||||
costSummary: {
|
||||
totalCost: number
|
||||
totalInputCost: number
|
||||
@@ -220,23 +214,24 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService {
|
||||
finalOutput: BlockOutputData
|
||||
traceSpans?: TraceSpan[]
|
||||
}): Promise<WorkflowExecutionLog> {
|
||||
const {
|
||||
executionId,
|
||||
endedAt,
|
||||
totalDurationMs,
|
||||
blockStats,
|
||||
costSummary,
|
||||
finalOutput,
|
||||
traceSpans,
|
||||
} = params
|
||||
const { executionId, endedAt, totalDurationMs, costSummary, finalOutput, traceSpans } = params
|
||||
|
||||
logger.debug(`Completing workflow execution ${executionId}`)
|
||||
|
||||
const level = blockStats.error > 0 ? 'error' : 'info'
|
||||
const message =
|
||||
blockStats.error > 0
|
||||
? `Workflow execution failed: ${blockStats.error} error(s), ${blockStats.success} success(es)`
|
||||
: `Workflow execution completed: ${blockStats.success} block(s) executed successfully`
|
||||
// Determine if workflow failed by checking trace spans for errors
|
||||
const hasErrors = traceSpans?.some((span: any) => {
|
||||
const checkSpanForErrors = (s: any): boolean => {
|
||||
if (s.status === 'error') return true
|
||||
if (s.children && Array.isArray(s.children)) {
|
||||
return s.children.some(checkSpanForErrors)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return checkSpanForErrors(span)
|
||||
})
|
||||
|
||||
const level = hasErrors ? 'error' : 'info'
|
||||
const message = hasErrors ? 'Workflow execution failed' : 'Workflow execution completed'
|
||||
|
||||
const [updatedLog] = await db
|
||||
.update(workflowExecutionLogs)
|
||||
@@ -245,10 +240,10 @@ export class EnhancedExecutionLogger implements IExecutionLoggerService {
|
||||
message,
|
||||
endedAt: new Date(endedAt),
|
||||
totalDurationMs,
|
||||
blockCount: blockStats.total,
|
||||
successCount: blockStats.success,
|
||||
errorCount: blockStats.error,
|
||||
skippedCount: blockStats.skipped,
|
||||
blockCount: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
skippedCount: 0,
|
||||
totalCost: costSummary.totalCost.toString(),
|
||||
totalInputCost: costSummary.totalInputCost.toString(),
|
||||
totalOutputCost: costSummary.totalOutputCost.toString(),
|
||||
|
||||
@@ -46,51 +46,6 @@ export async function loadWorkflowStateForExecution(workflowId: string): Promise
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateBlockStats(traceSpans: any[]): {
|
||||
total: number
|
||||
success: number
|
||||
error: number
|
||||
skipped: number
|
||||
} {
|
||||
if (!traceSpans || traceSpans.length === 0) {
|
||||
return { total: 0, success: 0, error: 0, skipped: 0 }
|
||||
}
|
||||
|
||||
// Recursively collect all block spans from the trace span tree
|
||||
const collectBlockSpans = (spans: any[]): any[] => {
|
||||
const blocks: any[] = []
|
||||
|
||||
for (const span of spans) {
|
||||
// Check if this span is an actual workflow block
|
||||
if (
|
||||
span.type &&
|
||||
span.type !== 'workflow' &&
|
||||
span.type !== 'provider' &&
|
||||
span.type !== 'model' &&
|
||||
span.blockId
|
||||
) {
|
||||
blocks.push(span)
|
||||
}
|
||||
|
||||
// Recursively check children
|
||||
if (span.children && Array.isArray(span.children)) {
|
||||
blocks.push(...collectBlockSpans(span.children))
|
||||
}
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
const blockSpans = collectBlockSpans(traceSpans)
|
||||
|
||||
const total = blockSpans.length
|
||||
const success = blockSpans.filter((span) => span.status === 'success').length
|
||||
const error = blockSpans.filter((span) => span.status === 'error').length
|
||||
const skipped = blockSpans.filter((span) => span.status === 'skipped').length
|
||||
|
||||
return { total, success, error, skipped }
|
||||
}
|
||||
|
||||
export function calculateCostSummary(traceSpans: any[]): {
|
||||
totalCost: number
|
||||
totalInputCost: number
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { enhancedExecutionLogger } from './enhanced-execution-logger'
|
||||
import {
|
||||
calculateBlockStats,
|
||||
calculateCostSummary,
|
||||
createEnvironmentObject,
|
||||
createTriggerObject,
|
||||
@@ -99,14 +98,12 @@ export class EnhancedLoggingSession {
|
||||
const { endedAt, totalDurationMs, finalOutput, traceSpans } = params
|
||||
|
||||
try {
|
||||
const blockStats = calculateBlockStats(traceSpans || [])
|
||||
const costSummary = calculateCostSummary(traceSpans || [])
|
||||
|
||||
await enhancedExecutionLogger.completeWorkflowExecution({
|
||||
executionId: this.executionId,
|
||||
endedAt: endedAt || new Date().toISOString(),
|
||||
totalDurationMs: totalDurationMs || 0,
|
||||
blockStats,
|
||||
costSummary,
|
||||
finalOutput: finalOutput || {},
|
||||
traceSpans: traceSpans || [],
|
||||
@@ -126,7 +123,6 @@ export class EnhancedLoggingSession {
|
||||
|
||||
async completeWithError(error?: any): Promise<void> {
|
||||
try {
|
||||
const blockStats = { total: 0, success: 0, error: 1, skipped: 0 }
|
||||
const costSummary = {
|
||||
totalCost: 0,
|
||||
totalInputCost: 0,
|
||||
@@ -141,7 +137,6 @@ export class EnhancedLoggingSession {
|
||||
executionId: this.executionId,
|
||||
endedAt: new Date().toISOString(),
|
||||
totalDurationMs: 0,
|
||||
blockStats,
|
||||
costSummary,
|
||||
finalOutput: null,
|
||||
traceSpans: [],
|
||||
|
||||
@@ -169,12 +169,7 @@ export interface WorkflowExecutionSummary {
|
||||
startedAt: string
|
||||
endedAt: string
|
||||
durationMs: number
|
||||
blockStats: {
|
||||
total: number
|
||||
success: number
|
||||
error: number
|
||||
skipped: number
|
||||
}
|
||||
|
||||
costSummary: {
|
||||
total: number
|
||||
inputCost: number
|
||||
@@ -360,12 +355,7 @@ export interface ExecutionLoggerService {
|
||||
executionId: string
|
||||
endedAt: string
|
||||
totalDurationMs: number
|
||||
blockStats: {
|
||||
total: number
|
||||
success: number
|
||||
error: number
|
||||
skipped: number
|
||||
}
|
||||
|
||||
costSummary: {
|
||||
totalCost: number
|
||||
totalInputCost: number
|
||||
|
||||
Reference in New Issue
Block a user