Spaces:
Build error
Build error
| import { PayloadAction } from "@reduxjs/toolkit"; | |
| import { useEffect, useState } from "react"; | |
| import { Trans, useTranslation } from "react-i18next"; | |
| import Markdown from "react-markdown"; | |
| import { Link } from "react-router"; | |
| import remarkGfm from "remark-gfm"; | |
| import { useConfig } from "#/hooks/query/use-config"; | |
| import { I18nKey } from "#/i18n/declaration"; | |
| import ArrowDown from "#/icons/angle-down-solid.svg?react"; | |
| import ArrowUp from "#/icons/angle-up-solid.svg?react"; | |
| import CheckCircle from "#/icons/check-circle-solid.svg?react"; | |
| import XCircle from "#/icons/x-circle-solid.svg?react"; | |
| import { OpenHandsAction } from "#/types/core/actions"; | |
| import { OpenHandsObservation } from "#/types/core/observations"; | |
| import { cn } from "#/utils/utils"; | |
| import { code } from "../markdown/code"; | |
| import { ol, ul } from "../markdown/list"; | |
| import { paragraph } from "../markdown/paragraph"; | |
| import { MonoComponent } from "./mono-component"; | |
| import { PathComponent } from "./path-component"; | |
| const trimText = (text: string, maxLength: number): string => { | |
| if (!text) return ""; | |
| return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text; | |
| }; | |
| interface ExpandableMessageProps { | |
| id?: string; | |
| message: string; | |
| type: string; | |
| success?: boolean; | |
| observation?: PayloadAction<OpenHandsObservation>; | |
| action?: PayloadAction<OpenHandsAction>; | |
| } | |
| export function ExpandableMessage({ | |
| id, | |
| message, | |
| type, | |
| success, | |
| observation, | |
| action, | |
| }: ExpandableMessageProps) { | |
| const { data: config } = useConfig(); | |
| const { t, i18n } = useTranslation(); | |
| const [showDetails, setShowDetails] = useState(true); | |
| const [details, setDetails] = useState(message); | |
| const [translationId, setTranslationId] = useState<string | undefined>(id); | |
| const [translationParams, setTranslationParams] = useState< | |
| Record<string, unknown> | |
| >({ | |
| observation, | |
| action, | |
| }); | |
| useEffect(() => { | |
| // If we have a translation ID, process it | |
| if (id && i18n.exists(id)) { | |
| let processedObservation = observation; | |
| let processedAction = action; | |
| if (action && action.payload.action === "run") { | |
| const trimmedCommand = trimText(action.payload.args.command, 80); | |
| processedAction = { | |
| ...action, | |
| payload: { | |
| ...action.payload, | |
| args: { | |
| ...action.payload.args, | |
| command: trimmedCommand, | |
| }, | |
| }, | |
| }; | |
| } | |
| if (observation && observation.payload.observation === "run") { | |
| const trimmedCommand = trimText(observation.payload.extras.command, 80); | |
| processedObservation = { | |
| ...observation, | |
| payload: { | |
| ...observation.payload, | |
| extras: { | |
| ...observation.payload.extras, | |
| command: trimmedCommand, | |
| }, | |
| }, | |
| }; | |
| } | |
| setTranslationId(id); | |
| setTranslationParams({ | |
| observation: processedObservation, | |
| action: processedAction, | |
| }); | |
| setDetails(message); | |
| setShowDetails(false); | |
| } | |
| }, [id, message, observation, action, i18n.language]); | |
| const statusIconClasses = "h-4 w-4 ml-2 inline"; | |
| if ( | |
| config?.FEATURE_FLAGS.ENABLE_BILLING && | |
| config?.APP_MODE === "saas" && | |
| id === I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS | |
| ) { | |
| return ( | |
| <div | |
| data-testid="out-of-credits" | |
| className="flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2 border-danger" | |
| > | |
| <div className="text-sm w-full"> | |
| <div className="font-bold text-danger"> | |
| {t(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS)} | |
| </div> | |
| <Link | |
| className="mt-2 mb-2 w-full h-10 rounded flex items-center justify-center gap-2 bg-primary text-[#0D0F11]" | |
| to="/settings/billing" | |
| > | |
| {t(I18nKey.BILLING$CLICK_TO_TOP_UP)} | |
| </Link> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div | |
| className={cn( | |
| "flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2", | |
| type === "error" ? "border-danger" : "border-neutral-300", | |
| )} | |
| > | |
| <div className="text-sm w-full"> | |
| <div className="flex flex-row justify-between items-center w-full"> | |
| <span | |
| className={cn( | |
| "font-bold", | |
| type === "error" ? "text-danger" : "text-neutral-300", | |
| )} | |
| > | |
| {translationId && i18n.exists(translationId) ? ( | |
| <Trans | |
| i18nKey={translationId} | |
| values={translationParams} | |
| components={{ | |
| bold: <strong />, | |
| path: <PathComponent />, | |
| cmd: <MonoComponent />, | |
| }} | |
| /> | |
| ) : ( | |
| message | |
| )} | |
| <button | |
| type="button" | |
| onClick={() => setShowDetails(!showDetails)} | |
| className="cursor-pointer text-left" | |
| > | |
| {showDetails ? ( | |
| <ArrowUp | |
| className={cn( | |
| "h-4 w-4 ml-2 inline", | |
| type === "error" ? "fill-danger" : "fill-neutral-300", | |
| )} | |
| /> | |
| ) : ( | |
| <ArrowDown | |
| className={cn( | |
| "h-4 w-4 ml-2 inline", | |
| type === "error" ? "fill-danger" : "fill-neutral-300", | |
| )} | |
| /> | |
| )} | |
| </button> | |
| </span> | |
| {type === "action" && success !== undefined && ( | |
| <span className="flex-shrink-0"> | |
| {success ? ( | |
| <CheckCircle | |
| data-testid="status-icon" | |
| className={cn(statusIconClasses, "fill-success")} | |
| /> | |
| ) : ( | |
| <XCircle | |
| data-testid="status-icon" | |
| className={cn(statusIconClasses, "fill-danger")} | |
| /> | |
| )} | |
| </span> | |
| )} | |
| </div> | |
| {showDetails && ( | |
| <div className="text-sm"> | |
| <Markdown | |
| components={{ | |
| code, | |
| ul, | |
| ol, | |
| p: paragraph, | |
| }} | |
| remarkPlugins={[remarkGfm]} | |
| > | |
| {details} | |
| </Markdown> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |