fix(logs): fix terminal colors and payload rendering

This commit is contained in:
2026-06-14 12:43:09 -07:00
parent 95f54260c1
commit 7e67e480bb
2 changed files with 107 additions and 104 deletions

View File

@@ -44,11 +44,12 @@ export default function LogsPage() {
}),
});
const d = await r.json();
setLogs(
typeof d.result === "string"
? d.result
: JSON.stringify(d.result ?? d.error, null, 2),
);
let parsed = "";
try {
parsed = JSON.parse(d.result).logs;
} catch {}
setLogs(parsed || d.result || "No logs returned.");
} catch {
setLogs("Failed to load logs. Is the container running?");
} finally {

View File

@@ -1,4 +1,4 @@
"use client"
"use client";
import {
Children,
@@ -10,29 +10,29 @@ import {
useState,
type ComponentType,
type RefAttributes,
} from "react"
} from "react";
import {
motion,
useInView,
type DOMMotionComponents,
type HTMLMotionProps,
type MotionProps,
} from "motion/react"
} from "motion/react";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
interface SequenceContextValue {
completeItem: (index: number) => void
activeIndex: number
sequenceStarted: boolean
completeItem: (index: number) => void;
activeIndex: number;
sequenceStarted: boolean;
}
const SequenceContext = createContext<SequenceContextValue | null>(null)
const SequenceContext = createContext<SequenceContextValue | null>(null);
const useSequence = () => useContext(SequenceContext)
const useSequence = () => useContext(SequenceContext);
const ItemIndexContext = createContext<number | null>(null)
const useItemIndex = () => useContext(ItemIndexContext)
const ItemIndexContext = createContext<number | null>(null);
const useItemIndex = () => useContext(ItemIndexContext);
const motionElements = {
article: motion.article,
@@ -47,21 +47,21 @@ const motionElements = {
p: motion.p,
section: motion.section,
span: motion.span,
} as const
} as const;
type MotionElementType = Extract<
keyof DOMMotionComponents,
keyof typeof motionElements
>
>;
type TerminalTypingMotionComponent = ComponentType<
Omit<HTMLMotionProps<"span">, "ref"> & RefAttributes<HTMLElement>
>
>;
interface AnimatedSpanProps extends MotionProps {
children: React.ReactNode
delay?: number
className?: string
startOnView?: boolean
children: React.ReactNode;
delay?: number;
className?: string;
startOnView?: boolean;
}
export const AnimatedSpan = ({
@@ -71,25 +71,25 @@ export const AnimatedSpan = ({
startOnView = false,
...props
}: AnimatedSpanProps) => {
const elementRef = useRef<HTMLDivElement | null>(null)
const elementRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
});
const sequence = useSequence()
const itemIndex = useItemIndex()
const [hasStarted, setHasStarted] = useState(false)
const sequence = useSequence();
const itemIndex = useItemIndex();
const [hasStarted, setHasStarted] = useState(false);
useEffect(() => {
if (!sequence || itemIndex === null) return
if (!sequence.sequenceStarted) return
if (hasStarted) return
if (!sequence || itemIndex === null) return;
if (!sequence.sequenceStarted) return;
if (hasStarted) return;
if (sequence.activeIndex === itemIndex) {
setHasStarted(true)
setHasStarted(true);
}
}, [sequence, hasStarted, itemIndex])
}, [sequence, hasStarted, itemIndex]);
const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true
const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true;
return (
<motion.div
@@ -99,24 +99,24 @@ export const AnimatedSpan = ({
transition={{ duration: 0.3, delay: sequence ? 0 : delay / 1000 }}
className={cn("grid text-sm font-normal tracking-tight", className)}
onAnimationComplete={() => {
if (!sequence) return
if (itemIndex === null) return
sequence.completeItem(itemIndex)
if (!sequence) return;
if (itemIndex === null) return;
sequence.completeItem(itemIndex);
}}
{...props}
>
{children}
</motion.div>
)
}
);
};
interface TypingAnimationProps extends Omit<MotionProps, "children"> {
children: string
className?: string
duration?: number
delay?: number
as?: MotionElementType
startOnView?: boolean
children: string;
className?: string;
duration?: number;
delay?: number;
as?: MotionElementType;
startOnView?: boolean;
}
export const TypingAnimation = ({
@@ -129,52 +129,52 @@ export const TypingAnimation = ({
...props
}: TypingAnimationProps) => {
if (typeof children !== "string") {
throw new Error("TypingAnimation: children must be a string. Received:")
throw new Error("TypingAnimation: children must be a string. Received:");
}
const MotionComponent = motionElements[
Component
] as TerminalTypingMotionComponent
] as TerminalTypingMotionComponent;
const [displayedText, setDisplayedText] = useState<string>("")
const [started, setStarted] = useState(false)
const elementRef = useRef<HTMLElement | null>(null)
const [displayedText, setDisplayedText] = useState<string>("");
const [started, setStarted] = useState(false);
const elementRef = useRef<HTMLElement | null>(null);
const isInView = useInView(elementRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
});
const sequence = useSequence()
const itemIndex = useItemIndex()
const hasSequence = sequence !== null
const sequenceStarted = sequence?.sequenceStarted ?? false
const sequenceActiveIndex = sequence?.activeIndex ?? null
const sequence = useSequence();
const itemIndex = useItemIndex();
const hasSequence = sequence !== null;
const sequenceStarted = sequence?.sequenceStarted ?? false;
const sequenceActiveIndex = sequence?.activeIndex ?? null;
const sequenceCompleteItemRef = useRef<
SequenceContextValue["completeItem"] | null
>(null)
const sequenceItemIndexRef = useRef<number | null>(null)
>(null);
const sequenceItemIndexRef = useRef<number | null>(null);
useEffect(() => {
sequenceCompleteItemRef.current = sequence?.completeItem ?? null
sequenceItemIndexRef.current = itemIndex
}, [sequence?.completeItem, itemIndex])
sequenceCompleteItemRef.current = sequence?.completeItem ?? null;
sequenceItemIndexRef.current = itemIndex;
}, [sequence?.completeItem, itemIndex]);
useEffect(() => {
let startTimeout: ReturnType<typeof setTimeout> | null = null
let startTimeout: ReturnType<typeof setTimeout> | null = null;
if (hasSequence && itemIndex !== null) {
if (sequenceStarted && !started && sequenceActiveIndex === itemIndex) {
setStarted(true)
setStarted(true);
}
} else if (!startOnView || isInView) {
startTimeout = setTimeout(() => setStarted(true), delay)
startTimeout = setTimeout(() => setStarted(true), delay);
}
return () => {
if (startTimeout !== null) {
clearTimeout(startTimeout)
}
clearTimeout(startTimeout);
}
};
}, [
delay,
startOnView,
@@ -184,36 +184,36 @@ export const TypingAnimation = ({
sequenceActiveIndex,
sequenceStarted,
itemIndex,
])
]);
useEffect(() => {
let typingEffect: ReturnType<typeof setInterval> | null = null
let typingEffect: ReturnType<typeof setInterval> | null = null;
if (started) {
let i = 0
let i = 0;
typingEffect = setInterval(() => {
if (i < children.length) {
setDisplayedText(children.substring(0, i + 1))
i++
setDisplayedText(children.substring(0, i + 1));
i++;
} else {
if (typingEffect !== null) {
clearInterval(typingEffect)
clearInterval(typingEffect);
}
const completeItem = sequenceCompleteItemRef.current
const currentItemIndex = sequenceItemIndexRef.current
const completeItem = sequenceCompleteItemRef.current;
const currentItemIndex = sequenceItemIndexRef.current;
if (completeItem && currentItemIndex !== null) {
completeItem(currentItemIndex)
completeItem(currentItemIndex);
}
}
}, duration)
}, duration);
}
return () => {
if (typingEffect !== null) {
clearInterval(typingEffect)
clearInterval(typingEffect);
}
}
}, [children, duration, started])
};
}, [children, duration, started]);
return (
<MotionComponent
@@ -223,14 +223,14 @@ export const TypingAnimation = ({
>
{displayedText}
</MotionComponent>
)
}
);
};
interface TerminalProps {
children: React.ReactNode
className?: string
sequence?: boolean
startOnView?: boolean
children: React.ReactNode;
className?: string;
sequence?: boolean;
startOnView?: boolean;
}
export const Terminal = ({
@@ -239,45 +239,47 @@ export const Terminal = ({
sequence = true,
startOnView = true,
}: TerminalProps) => {
const containerRef = useRef<HTMLDivElement | null>(null)
const containerRef = useRef<HTMLDivElement | null>(null);
const isInView = useInView(containerRef as React.RefObject<Element>, {
amount: 0.3,
once: true,
})
});
const [activeIndex, setActiveIndex] = useState(0)
const sequenceHasStarted = sequence ? !startOnView || isInView : false
const [activeIndex, setActiveIndex] = useState(0);
const sequenceHasStarted = sequence ? !startOnView || isInView : false;
const contextValue = useMemo<SequenceContextValue | null>(() => {
if (!sequence) return null
if (!sequence) return null;
return {
completeItem: (index: number) => {
setActiveIndex((current) => (index === current ? current + 1 : current))
setActiveIndex((current) =>
index === current ? current + 1 : current,
);
},
activeIndex,
sequenceStarted: sequenceHasStarted,
}
}, [sequence, activeIndex, sequenceHasStarted])
};
}, [sequence, activeIndex, sequenceHasStarted]);
const wrappedChildren = useMemo(() => {
if (!sequence) return children
const array = Children.toArray(children)
if (!sequence) return children;
const array = Children.toArray(children);
return array.map((child, index) => (
<ItemIndexContext.Provider key={index} value={index}>
{child as React.ReactNode}
</ItemIndexContext.Provider>
))
}, [children, sequence])
));
}, [children, sequence]);
const content = (
<div
ref={containerRef}
className={cn(
"border-border bg-background z-0 h-full max-h-100 w-full max-w-lg rounded-xl border",
className
"z-0 h-full max-h-100 w-full max-w-lg rounded-xl border border-[#27272a] bg-[#0a0a0a] text-white",
className,
)}
>
<div className="border-border flex flex-col gap-y-2 border-b p-4">
<div className="flex flex-col gap-y-2 border-b border-[#27272a] p-4">
<div className="flex flex-row gap-x-2">
<div className="h-2 w-2 rounded-full bg-red-500"></div>
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
@@ -288,13 +290,13 @@ export const Terminal = ({
<code className="grid gap-y-1 overflow-auto">{wrappedChildren}</code>
</pre>
</div>
)
);
if (!sequence) return content
if (!sequence) return content;
return (
<SequenceContext.Provider value={contextValue}>
{content}
</SequenceContext.Provider>
)
}
);
};