import React from "react"; import { useTranslation } from "react-i18next"; import ArrowDown from "#/icons/angle-down-solid.svg?react"; import ArrowUp from "#/icons/angle-up-solid.svg?react"; import { SuccessIndicator } from "./success-indicator"; import { ObservationResultStatus } from "#/components/conversation-events/chat/event-content-helpers/get-observation-result"; import { MarkdownRenderer } from "../markdown/markdown-renderer"; import { cn } from "#/utils/utils"; import { I18nKey } from "#/i18n/declaration"; import { StyledTooltip } from "#/components/shared/buttons/styled-tooltip"; import { formatEventTimestamp } from "#/utils/format-event-timestamp"; interface GenericEventMessageProps { title: React.ReactNode; details: string | React.ReactNode; success?: ObservationResultStatus; initiallyExpanded?: boolean; /** Where to place the expand/collapse chevron relative to the title. */ chevronPosition?: "before" | "after"; /** Extra content rendered at the end of the title row (right side). */ titleTrailing?: React.ReactNode; /** Optional icon rendered before the title text. */ titleIcon?: React.ReactNode; timestamp?: string; } export function GenericEventMessage({ title, details, success, initiallyExpanded = false, chevronPosition = "after", titleTrailing, titleIcon, timestamp, }: GenericEventMessageProps) { const { t, i18n } = useTranslation("openhands"); const [showDetails, setShowDetails] = React.useState(initiallyExpanded); const [isHovered, setIsHovered] = React.useState(false); const [hasFocusWithin, setHasFocusWithin] = React.useState(false); const timestampLabel = formatEventTimestamp(timestamp, i18n?.language); const chevron = details ? ( ) : null; const titleContent = (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onFocusCapture={() => setHasFocusWithin(true)} onBlurCapture={(event) => { if (!event.currentTarget.contains(event.relatedTarget)) { setHasFocusWithin(false); } }} > {chevronPosition === "before" && chevron} {titleIcon} {/* Wrap the title in a span so any whitespace inside Trans-rendered fragments (e.g. "Editing ...") is preserved by normal inline flow instead of being collapsed between anonymous flex items. */} {title} {chevronPosition === "after" && chevron}
); const titleContentWithTimestamp = timestampLabel ? ( {timestampLabel}} placement="top" isOpen={isHovered || hasFocusWithin} > {titleContent} ) : ( titleContent ); const titleRow = (
{titleContentWithTimestamp}
{titleTrailing} {success && }
); return (
{titleRow} {showDetails && (typeof details === "string" ? ( {details} ) : ( details ))}
); }