import React from "react"; import { Trans, useTranslation } from "react-i18next"; import { Check, CircleAlert, CircleX, Copy, X } from "lucide-react"; import { OH_STATUS_ERROR_COLOR } from "#/constants/status-colors"; import { I18nKey } from "#/i18n/declaration"; import { displayErrorToast } from "#/utils/custom-toast-handlers"; import { getAcpErrorHeaderKey } from "#/utils/acp-error-codes"; import { cn } from "#/utils/utils"; import type { ErrorClassification } from "@openhands/typescript-client"; interface ErrorMessageBannerProps { message: string; /** Structured error code (e.g. "ACPAuthRequired") used to pick a header. */ code?: string | null; onDismiss?: () => void; onRetry?: () => void; /** Recovery action (e.g. re-authenticate) shown for credential failures. */ onReauth?: () => void; classification?: ErrorClassification | null; } const DEFAULT_MAX_COLLAPSED_CHARS = 220; export function ErrorMessageBanner({ message, code, onDismiss, onRetry, onReauth, classification, }: ErrorMessageBannerProps) { const { t, i18n } = useTranslation("openhands"); const headerKey = getAcpErrorHeaderKey(code); const [isExpanded, setIsExpanded] = React.useState(false); const [isMultiLine, setIsMultiLine] = React.useState(false); const [isCopied, setIsCopied] = React.useState(false); const contentRef = React.useRef(null); const isI18nKey = i18n.exists(message, { ns: "openhands" }); const displayTextForLength = isI18nKey ? String(t(message)) : message; const shouldShowToggle = displayTextForLength.length > DEFAULT_MAX_COLLAPSED_CHARS; const isCollapsed = shouldShowToggle && !isExpanded; const handleCopy = async () => { try { await navigator.clipboard.writeText(displayTextForLength); setIsCopied(true); } catch { displayErrorToast(t(I18nKey.CHAT_INTERFACE$CHAT_MESSAGE_COPY_FAILED)); } }; React.useEffect(() => { if (!isCopied) return undefined; const timeout = setTimeout(() => { setIsCopied(false); }, 2000); return () => clearTimeout(timeout); }, [isCopied]); React.useEffect(() => { setIsCopied(false); }, [displayTextForLength]); React.useLayoutEffect(() => { const content = contentRef.current; if (!content) { return undefined; } const updateIsMultiLine = () => { const lineHeight = Number.parseFloat( getComputedStyle(content).lineHeight, ); if (!Number.isFinite(lineHeight) || lineHeight <= 0) { setIsMultiLine(false); return; } setIsMultiLine(content.getBoundingClientRect().height > lineHeight * 1.5); }; updateIsMultiLine(); const observer = new ResizeObserver(updateIsMultiLine); observer.observe(content); return () => observer.disconnect(); }, [displayTextForLength, isCollapsed, isExpanded, message]); return (
{classification != null && classification.kind !== "internal" && classification.kind !== "unknown" ? ( ) : ( )}
{headerKey && (
{t(headerKey)}
)}
{isI18nKey ? : message}
{onReauth && ( )} {shouldShowToggle && ( )}
{onRetry && ( )} {onDismiss && ( )}
); }