"use client"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import Ansi from "ansi-to-react"; import { CheckIcon, CopyIcon, TerminalIcon, Trash2Icon } from "lucide-react"; import type { ComponentProps, HTMLAttributes } from "react"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; interface TerminalContextType { output: string; isStreaming: boolean; autoScroll: boolean; onClear?: () => void; } const TerminalContext = createContext({ autoScroll: true, isStreaming: false, output: "", }); export type TerminalHeaderProps = HTMLAttributes; export const TerminalHeader = ({ className, children, ...props }: TerminalHeaderProps) => (
{children}
); export type TerminalTitleProps = HTMLAttributes; export const TerminalTitle = ({ className, children, ...props }: TerminalTitleProps) => (
{children ?? "Terminal"}
); export type TerminalStatusProps = HTMLAttributes; export const TerminalStatus = ({ className, children, ...props }: TerminalStatusProps) => { const { isStreaming } = useContext(TerminalContext); if (!isStreaming) { return null; } return (
{children}
); }; export type TerminalActionsProps = HTMLAttributes; export const TerminalActions = ({ className, children, ...props }: TerminalActionsProps) => (
{children}
); export type TerminalCopyButtonProps = ComponentProps & { onCopy?: () => void; onError?: (error: Error) => void; timeout?: number; }; export const TerminalCopyButton = ({ onCopy, onError, timeout = 2000, children, className, ...props }: TerminalCopyButtonProps) => { const [isCopied, setIsCopied] = useState(false); const timeoutRef = useRef(0); const { output } = useContext(TerminalContext); const copyToClipboard = useCallback(async () => { if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { onError?.(new Error("Clipboard API not available")); return; } try { await navigator.clipboard.writeText(output); setIsCopied(true); onCopy?.(); timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout); } catch (error) { onError?.(error as Error); } }, [output, onCopy, onError, timeout]); useEffect( () => () => { window.clearTimeout(timeoutRef.current); }, [] ); const Icon = isCopied ? CheckIcon : CopyIcon; return ( ); }; export type TerminalClearButtonProps = ComponentProps; export const TerminalClearButton = ({ children, className, ...props }: TerminalClearButtonProps) => { const { onClear } = useContext(TerminalContext); if (!onClear) { return null; } return ( ); }; export type TerminalContentProps = HTMLAttributes; export const TerminalContent = ({ className, children, ...props }: TerminalContentProps) => { const { output, isStreaming, autoScroll } = useContext(TerminalContext); const containerRef = useRef(null); useEffect(() => { if (autoScroll && containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [output, autoScroll]); return (
{children ?? (
          {output}
          {isStreaming && (
            
          )}
        
)}
); }; export type TerminalProps = HTMLAttributes & { output: string; isStreaming?: boolean; autoScroll?: boolean; onClear?: () => void; }; export const Terminal = ({ output, isStreaming = false, autoScroll = true, onClear, className, children, ...props }: TerminalProps) => { const contextValue = useMemo( () => ({ autoScroll, isStreaming, onClear, output }), [autoScroll, isStreaming, onClear, output] ); return (
{children ?? ( <>
{onClear && }
)}
); };