1- import { useMemo , useState } from 'react'
1+ import { useEffect , useMemo , useState } from 'react'
22import { format } from 'date-fns'
3- import {
4- AlertCircle ,
5- AlertTriangle ,
6- Calendar ,
7- ChevronDown ,
8- ChevronUp ,
9- Clock ,
10- Terminal ,
11- } from 'lucide-react'
3+ import { AlertCircle , Check , ChevronDown , ChevronUp , Copy } from 'lucide-react'
124import { Button } from '@/components/ui/button'
135import { getBlock } from '@/blocks'
146import type { ConsoleEntry as ConsoleEntryType } from '@/stores/panel/console/types'
@@ -19,168 +11,175 @@ interface ConsoleEntryProps {
1911 consoleWidth : number
2012}
2113
22- // Maximum character length for a word before it's broken up
23- const MAX_WORD_LENGTH = 25
24-
25- const WordWrap = ( { text } : { text : string } ) => {
26- if ( ! text ) return null
27-
28- // Split text into words, keeping spaces and punctuation
29- const parts = text . split ( / ( \s + ) / g)
30-
31- return (
32- < >
33- { parts . map ( ( part , index ) => {
34- // If the part is whitespace or shorter than the max length, render it as is
35- if ( part . match ( / \s + / ) || part . length <= MAX_WORD_LENGTH ) {
36- return < span key = { index } > { part } </ span >
37- }
38-
39- // For long words, break them up into chunks
40- const chunks = [ ]
41- for ( let i = 0 ; i < part . length ; i += MAX_WORD_LENGTH ) {
42- chunks . push ( part . substring ( i , i + MAX_WORD_LENGTH ) )
43- }
44-
45- return (
46- < span key = { index } className = 'break-all' >
47- { chunks . map ( ( chunk , chunkIndex ) => (
48- < span key = { chunkIndex } > { chunk } </ span >
49- ) ) }
50- </ span >
51- )
52- } ) }
53- </ >
54- )
55- }
56-
5714export function ConsoleEntry ( { entry, consoleWidth } : ConsoleEntryProps ) {
58- const [ isExpanded , setIsExpanded ] = useState ( false )
59- const [ expandAllJson , setExpandAllJson ] = useState ( false )
15+ const [ isExpanded , setIsExpanded ] = useState ( true ) // Default expanded
16+ const [ showCopySuccess , setShowCopySuccess ] = useState ( false )
6017
6118 const blockConfig = useMemo ( ( ) => {
6219 if ( ! entry . blockType ) return null
6320 return getBlock ( entry . blockType )
6421 } , [ entry . blockType ] )
6522
66- const BlockIcon = blockConfig ?. icon
67-
68- // Helper function to check if data has nested objects or arrays
69- const hasNestedStructure = ( data : any ) : boolean => {
70- if ( data === null || typeof data !== 'object' ) return false
71-
72- // Check if it's an empty object or array
73- if ( Object . keys ( data ) . length === 0 ) return false
23+ const handleCopy = ( ) => {
24+ const stringified = JSON . stringify ( entry . output , null , 2 )
25+ navigator . clipboard . writeText ( stringified )
26+ setShowCopySuccess ( true )
27+ }
7428
75- // For arrays, check if any element is an object
76- if ( Array . isArray ( data ) ) {
77- return data . some ( ( item ) => typeof item === 'object' && item !== null )
29+ useEffect ( ( ) => {
30+ if ( showCopySuccess ) {
31+ const timer = setTimeout ( ( ) => {
32+ setShowCopySuccess ( false )
33+ } , 2000 )
34+ return ( ) => clearTimeout ( timer )
7835 }
36+ } , [ showCopySuccess ] )
7937
80- // For objects, check if any value is an object
81- return Object . values ( data ) . some ( ( value ) => typeof value === 'object' && value !== null )
82- }
38+ const BlockIcon = blockConfig ?. icon
39+ const blockColor = blockConfig ?. bgColor || '#6B7280'
8340
8441 return (
85- < div
86- className = { `border-border border-b transition-colors ${
87- ! entry . error && ! entry . warning && entry . success ? 'cursor-pointer hover:bg-accent/50' : ''
88- } `}
89- onClick = { ( ) => ! entry . error && ! entry . warning && entry . success && setIsExpanded ( ! isExpanded ) }
90- >
91- < div className = 'space-y-4 p-4' >
42+ < div className = 'space-y-3' >
43+ { /* Header: Icon | Block name */ }
44+ < div className = 'flex items-center gap-2' >
45+ { BlockIcon && (
46+ < div
47+ className = 'flex h-5 w-5 items-center justify-center rounded-md'
48+ style = { { backgroundColor : blockColor } }
49+ >
50+ < BlockIcon className = 'h-3 w-3 text-white' />
51+ </ div >
52+ ) }
53+ < span className = 'font-normal text-base text-sm leading-normal' >
54+ { entry . blockName || 'Unknown Block' }
55+ </ span >
56+ </ div >
57+
58+ { /* Duration tag | Time tag */ }
59+ < div className = 'flex items-center gap-2' >
9260 < div
93- className = { `${
94- consoleWidth >= 400 ? 'flex items-center justify-between ' : 'grid grid-cols-1 gap-4 '
61+ className = { `flex h-5 items-center rounded-lg px-2 ${
62+ entry . error ? 'bg-[#F6D2D2] dark:bg-[#442929] ' : 'bg-secondary '
9563 } `}
9664 >
97- { entry . blockName && (
98- < div className = 'flex items-center gap-2 text-sm' >
99- { BlockIcon ? (
100- < BlockIcon className = 'h-4 w-4 text-muted-foreground' />
101- ) : (
102- < Terminal className = 'h-4 w-4 text-muted-foreground' />
103- ) }
104- < span className = 'text-muted-foreground' > { entry . blockName } </ span >
65+ { entry . error ? (
66+ < div className = 'flex items-center gap-1' >
67+ < AlertCircle className = 'h-3 w-3 text-[#DC2626] dark:text-[#F87171]' />
68+ < span className = 'font-normal text-[#DC2626] text-xs leading-normal dark:text-[#F87171]' >
69+ Error
70+ </ span >
10571 </ div >
72+ ) : (
73+ < span className = 'font-normal text-muted-foreground text-xs leading-normal' >
74+ { entry . durationMs ?? 0 } ms
75+ </ span >
10676 ) }
107- < div
108- className = { `${
109- consoleWidth >= 400 ? 'flex gap-4' : 'grid grid-cols-2 gap-4'
110- } text-muted-foreground text-sm`}
111- >
112- < div className = 'flex items-center gap-2' >
113- < Calendar className = 'h-4 w-4' />
114- < span > { entry . startedAt ? format ( new Date ( entry . startedAt ) , 'HH:mm:ss' ) : 'N/A' } </ span >
115- </ div >
116- < div className = 'flex items-center gap-2' >
117- < Clock className = 'h-4 w-4' />
118- < span > Duration: { entry . durationMs ?? 0 } ms</ span >
119- </ div >
120- </ div >
12177 </ div >
78+ < div className = 'flex h-5 items-center rounded-lg bg-secondary px-2' >
79+ < span className = 'font-normal text-muted-foreground text-xs leading-normal' >
80+ { entry . startedAt ? format ( new Date ( entry . startedAt ) , 'HH:mm:ss' ) : 'N/A' }
81+ </ span >
82+ </ div >
83+ </ div >
12284
123- < div className = 'space-y-4' >
124- { ! entry . error && ! entry . warning && (
125- < div className = 'flex items-start gap-2' >
126- < Terminal className = 'mt-1 h-4 w-4 text-muted-foreground' />
127- < div className = 'overflow-wrap-anywhere relative flex-1 whitespace-normal break-normal font-mono text-sm' >
128- { entry . output != null && (
129- < div className = 'absolute top-0 right-0 z-10' >
130- < Button
131- variant = 'ghost'
132- size = 'sm'
133- className = 'h-6 px-2 text-muted-foreground hover:text-foreground'
134- onClick = { ( e ) => {
135- e . stopPropagation ( )
136- setExpandAllJson ( ! expandAllJson )
137- } }
138- >
139- < span className = 'flex items-center' >
140- { expandAllJson ? (
141- < >
142- < ChevronUp className = 'mr-1 h-3 w-3' />
143- < span className = 'text-xs' > Collapse</ span >
144- </ >
145- ) : (
146- < >
147- < ChevronDown className = 'mr-1 h-3 w-3' />
148- < span className = 'text-xs' > Expand</ span >
149- </ >
150- ) }
151- </ span >
152- </ Button >
153- </ div >
154- ) }
155- < JSONView data = { entry . output } initiallyExpanded = { expandAllJson } />
156- </ div >
85+ { /* Response area */ }
86+ < div className = 'space-y-2' >
87+ { /* Error display */ }
88+ { entry . error && (
89+ < div className = 'rounded-lg bg-[#F6D2D2] p-3 dark:bg-[#442929]' >
90+ < div className = 'whitespace-pre-wrap font-normal text-[#DC2626] text-sm leading-normal dark:text-[#F87171]' >
91+ { entry . error }
15792 </ div >
158- ) }
93+ </ div >
94+ ) }
15995
160- { entry . error && (
161- < div className = 'flex items-start gap-2 rounded-md border border-red-500 bg-red-50 p-3 text-destructive dark:border-border dark:bg-background dark:text-foreground' >
162- < AlertCircle className = 'mt-1 h-4 w-4 flex-shrink-0 text-red-500' />
163- < div className = 'min-w-0 flex-1' >
164- < div className = 'font-medium' > Error</ div >
165- < div className = 'w-full overflow-hidden whitespace-pre-wrap text-sm' >
166- < WordWrap text = { entry . error } />
96+ { /* Warning display */ }
97+ { entry . warning && (
98+ < div className = 'rounded-lg border-yellow-200 bg-yellow-50 p-3 dark:border-yellow-800/50' >
99+ < div className = 'mb-1 font-normal text-sm text-yellow-800 leading-normal dark:text-yellow-200' >
100+ Warning
101+ </ div >
102+ < div className = 'whitespace-pre-wrap font-normal text-sm text-yellow-700 leading-normal dark:text-yellow-300' >
103+ { entry . warning }
104+ </ div >
105+ </ div >
106+ ) }
107+
108+ { /* Success output */ }
109+ { ! entry . error && ! entry . warning && entry . output != null && (
110+ < div className = 'rounded-lg bg-secondary/50 p-3' >
111+ { isExpanded ? (
112+ < div className = 'relative' >
113+ { /* Copy and Expand/Collapse buttons */ }
114+ < div className = 'absolute top-0 right-0 z-10 flex items-center gap-1' >
115+ < Button
116+ variant = 'ghost'
117+ size = 'sm'
118+ className = 'h-6 w-6 p-0 hover:bg-transparent'
119+ onClick = { handleCopy }
120+ >
121+ { showCopySuccess ? (
122+ < Check className = 'h-3 w-3 text-gray-500' />
123+ ) : (
124+ < Copy className = 'h-3 w-3 text-muted-foreground' />
125+ ) }
126+ </ Button >
127+ < Button
128+ variant = 'ghost'
129+ size = 'sm'
130+ className = 'h-6 w-6 p-0 hover:bg-transparent'
131+ onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
132+ >
133+ < ChevronUp className = 'h-3 w-3 text-muted-foreground' />
134+ </ Button >
135+ </ div >
136+ < div className = 'overflow-hidden pr-16 font-mono font-normal text-muted-foreground text-sm leading-normal' >
137+ < JSONView data = { entry . output } />
167138 </ div >
168139 </ div >
169- </ div >
170- ) }
171-
172- { entry . warning && (
173- < div className = 'flex items-start gap-2 rounded-md border border-yellow-500 bg-yellow-50 p-3 text-yellow-700 dark:border-border dark:bg-background dark:text-yellow-500' >
174- < AlertTriangle className = 'mt-1 h-4 w-4 flex-shrink-0 text-yellow-500' />
175- < div className = 'min-w-0 flex-1' >
176- < div className = 'font-medium' > Warning</ div >
177- < div className = 'w-full overflow-hidden whitespace-pre-wrap text-sm' >
178- < WordWrap text = { entry . warning } />
140+ ) : (
141+ < div className = 'relative' >
142+ < div className = 'absolute top-0 right-0 z-10 flex items-center gap-1' >
143+ < Button
144+ variant = 'ghost'
145+ size = 'sm'
146+ className = 'h-6 w-6 p-0 hover:bg-transparent'
147+ onClick = { handleCopy }
148+ >
149+ { showCopySuccess ? (
150+ < Check className = 'h-3 w-3 text-gray-500' />
151+ ) : (
152+ < Copy className = 'h-3 w-3 text-muted-foreground' />
153+ ) }
154+ </ Button >
155+ < Button
156+ variant = 'ghost'
157+ size = 'sm'
158+ className = 'h-6 w-6 p-0 hover:bg-transparent'
159+ onClick = { ( ) => setIsExpanded ( ! isExpanded ) }
160+ >
161+ < ChevronDown className = 'h-3 w-3 text-muted-foreground' />
162+ </ Button >
163+ </ div >
164+ < div
165+ className = 'cursor-pointer pr-16 font-mono font-normal text-muted-foreground text-sm leading-normal'
166+ onClick = { ( ) => setIsExpanded ( true ) }
167+ >
168+ { '{...}' }
179169 </ div >
180170 </ div >
171+ ) }
172+ </ div >
173+ ) }
174+
175+ { /* No output message */ }
176+ { ! entry . error && ! entry . warning && entry . output == null && (
177+ < div className = 'rounded-lg bg-secondary/50 p-3' >
178+ < div className = 'text-center font-normal text-muted-foreground text-sm leading-normal' >
179+ No output
181180 </ div >
182- ) }
183- </ div >
181+ </ div >
182+ ) }
184183 </ div >
185184 </ div >
186185 )
0 commit comments