@@ -3,7 +3,7 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
33import { typedjson , useTypedLoaderData } from "remix-typedjson" ;
44import { useEffect , useState , useRef , useCallback } from "react" ;
55import { S2 , S2Error } from "@s2-dev/streamstore" ;
6- import { Clipboard , ClipboardCheck } from "lucide-react" ;
6+ import { Clipboard , ClipboardCheck , ChevronDown , ChevronUp } from "lucide-react" ;
77import { ExitIcon } from "~/assets/icons/ExitIcon" ;
88import { GitMetadata } from "~/components/GitMetadata" ;
99import { RuntimeIcon } from "~/components/RuntimeIcon" ;
@@ -410,15 +410,20 @@ export default function Page() {
410410 ) ;
411411}
412412
413- type LogsDisplayProps = {
413+ function LogsDisplay ( {
414+ logs,
415+ isStreaming,
416+ streamError,
417+ initialCollapsed = false ,
418+ } : {
414419 logs : LogEntry [ ] ;
415420 isStreaming : boolean ;
416421 streamError : string | null ;
417- } ;
418-
419- function LogsDisplay ( { logs, isStreaming, streamError } : LogsDisplayProps ) {
422+ initialCollapsed ?: boolean ;
423+ } ) {
420424 const [ copied , setCopied ] = useState ( false ) ;
421425 const [ mouseOver , setMouseOver ] = useState ( false ) ;
426+ const [ collapsed , setCollapsed ] = useState ( initialCollapsed ) ;
422427 const logsContainerRef = useRef < HTMLDivElement > ( null ) ;
423428
424429 // auto-scroll log container to bottom when new logs arrive
@@ -473,78 +478,115 @@ function LogsDisplay({ logs, isStreaming, streamError }: LogsDisplayProps) {
473478 </ div >
474479 </ div >
475480 { logs . length > 0 && (
476- < TooltipProvider >
477- < Tooltip open = { copied || mouseOver } disableHoverableContent >
478- < TooltipTrigger
479- onClick = { onCopyLogs }
480- onMouseEnter = { ( ) => setMouseOver ( true ) }
481- onMouseLeave = { ( ) => setMouseOver ( false ) }
482- className = { cn (
483- "transition-colors duration-100 focus-custom hover:cursor-pointer" ,
484- copied ? "text-success" : "text-text-dimmed hover:text-text-bright"
485- ) }
486- >
487- { copied ? < ClipboardCheck className = "size-4" /> : < Clipboard className = "size-4" /> }
488- </ TooltipTrigger >
489- < TooltipContent side = "left" className = "text-xs" >
490- { copied ? "Copied" : "Copy" }
491- </ TooltipContent >
492- </ Tooltip >
493- </ TooltipProvider >
481+ < div className = "flex items-center gap-3" >
482+ < TooltipProvider >
483+ < Tooltip open = { copied || mouseOver } disableHoverableContent >
484+ < TooltipTrigger
485+ onClick = { onCopyLogs }
486+ onMouseEnter = { ( ) => setMouseOver ( true ) }
487+ onMouseLeave = { ( ) => setMouseOver ( false ) }
488+ className = { cn (
489+ "transition-colors duration-100 focus-custom hover:cursor-pointer" ,
490+ copied ? "text-success" : "text-text-dimmed hover:text-text-bright"
491+ ) }
492+ >
493+ < div className = "size-4 shrink-0" >
494+ { copied ? (
495+ < ClipboardCheck className = "size-full" />
496+ ) : (
497+ < Clipboard className = "size-full" />
498+ ) }
499+ </ div >
500+ </ TooltipTrigger >
501+ < TooltipContent side = "left" className = "text-xs" >
502+ { copied ? "Copied" : "Copy" }
503+ </ TooltipContent >
504+ </ Tooltip >
505+ </ TooltipProvider >
506+
507+ < TooltipProvider >
508+ < Tooltip disableHoverableContent >
509+ < TooltipTrigger
510+ onClick = { ( ) => setCollapsed ( ! collapsed ) }
511+ className = { cn (
512+ "transition-colors duration-100 focus-custom hover:cursor-pointer" ,
513+ "text-text-dimmed hover:text-text-bright"
514+ ) }
515+ >
516+ { collapsed ? (
517+ < ChevronDown className = "size-4" />
518+ ) : (
519+ < ChevronUp className = "size-4" />
520+ ) }
521+ </ TooltipTrigger >
522+ < TooltipContent side = "left" className = "text-xs" >
523+ { collapsed ? "Expand" : "Collapse" }
524+ </ TooltipContent >
525+ </ Tooltip >
526+ </ TooltipProvider >
527+ </ div >
494528 ) }
495529 </ div >
496530
497- < div
498- ref = { logsContainerRef }
499- className = "h-64 grow overflow-x-auto overflow-y-scroll font-mono text-xs scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
500- >
501- < div className = "flex w-fit min-w-full flex-col" >
502- { logs . length === 0 && (
503- < div className = "flex gap-x-2.5 border-l-2 border-transparent px-2.5 py-1" >
504- { streamError ? (
505- < span className = "text-error" > Failed fetching logs</ span >
506- ) : (
507- < span className = "text-text-dimmed" >
508- { isStreaming ? "Waiting for logs..." : "No logs yet" }
509- </ span >
510- ) }
511- </ div >
531+ < div className = "relative" >
532+ < div
533+ ref = { logsContainerRef }
534+ className = { cn (
535+ "grow overflow-x-auto overflow-y-scroll font-mono text-xs transition-all duration-200 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600" ,
536+ collapsed ? "h-16" : "h-64"
512537 ) }
513- { logs . map ( ( log , index ) => {
514- return (
515- < div
516- key = { index }
517- className = { cn (
518- "flex w-full gap-x-2.5 border-l-2 px-2.5 py-1" ,
519- log . level === "error" && "border-error/60 bg-error/15 hover:bg-error/25" ,
520- log . level === "warn" && "border-warning/60 bg-warning/20 hover:bg-warning/30" ,
521- log . level === "info" && "border-transparent hover:bg-charcoal-750"
538+ >
539+ < div className = "flex w-fit min-w-full flex-col" >
540+ { logs . length === 0 && (
541+ < div className = "flex gap-x-2.5 border-l-2 border-transparent px-2.5 py-1" >
542+ { streamError ? (
543+ < span className = "text-error" > Failed fetching logs</ span >
544+ ) : (
545+ < span className = "text-text-dimmed" >
546+ { isStreaming ? "Waiting for logs..." : "No logs yet" }
547+ </ span >
522548 ) }
523- >
524- < span
525- className = { cn (
526- "select-none whitespace-nowrap py-px" ,
527- log . level === "error" && "text-error/80" ,
528- log . level === "warn" && "text-warning/70" ,
529- log . level === "info" && "text-text-dimmed"
530- ) }
531- >
532- < DateTimeAccurate date = { log . timestamp } hideDate />
533- </ span >
534- < span
549+ </ div >
550+ ) }
551+ { logs . map ( ( log , index ) => {
552+ return (
553+ < div
554+ key = { index }
535555 className = { cn (
536- "whitespace-nowrap " ,
537- log . level === "error" && "text -error" ,
538- log . level === "warn" && "text -warning" ,
539- log . level === "info" && "text-text-bright "
556+ "flex w-full gap-x-2.5 border-l-2 px-2.5 py-1 " ,
557+ log . level === "error" && "border -error/60 bg-error/15 hover:bg-error/25 " ,
558+ log . level === "warn" && "border -warning/60 bg-warning/20 hover:bg-warning/30 " ,
559+ log . level === "info" && "border-transparent hover:bg-charcoal-750 "
540560 ) }
541561 >
542- { log . message }
543- </ span >
544- </ div >
545- ) ;
546- } ) }
562+ < span
563+ className = { cn (
564+ "select-none whitespace-nowrap py-px" ,
565+ log . level === "error" && "text-error/80" ,
566+ log . level === "warn" && "text-warning/70" ,
567+ log . level === "info" && "text-text-dimmed"
568+ ) }
569+ >
570+ < DateTimeAccurate date = { log . timestamp } hideDate />
571+ </ span >
572+ < span
573+ className = { cn (
574+ "whitespace-nowrap" ,
575+ log . level === "error" && "text-error" ,
576+ log . level === "warn" && "text-warning" ,
577+ log . level === "info" && "text-text-bright"
578+ ) }
579+ >
580+ { log . message }
581+ </ span >
582+ </ div >
583+ ) ;
584+ } ) }
585+ </ div >
547586 </ div >
587+ { collapsed && (
588+ < div className = "pointer-events-none absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-charcoal-800/90 to-transparent" />
589+ ) }
548590 </ div >
549591 </ div >
550592 ) ;
0 commit comments