import React from "react"; import { useSelector } from "react-redux"; import posthog from "posthog-js"; import { useTranslation } from "react-i18next"; import { formatTimeDelta } from "#/utils/format-time-delta"; import { ConversationRepoLink } from "./conversation-repo-link"; import { ProjectStatus, ConversationStateIndicator, } from "./conversation-state-indicator"; import { EllipsisButton } from "./ellipsis-button"; import { ConversationCardContextMenu } from "./conversation-card-context-menu"; import { SystemMessageModal } from "./system-message-modal"; import { cn } from "#/utils/utils"; import { BaseModal } from "../../shared/modals/base-modal/base-modal"; import { RootState } from "#/store"; import { I18nKey } from "#/i18n/declaration"; import { transformVSCodeUrl } from "#/utils/vscode-url-helper"; import OpenHands from "#/api/open-hands"; import { useWsClient } from "#/context/ws-client-provider"; import { isSystemMessage } from "#/types/core/guards"; interface ConversationCardProps { onClick?: () => void; onDelete?: () => void; onChangeTitle?: (title: string) => void; showOptions?: boolean; isActive?: boolean; title: string; selectedRepository: string | null; lastUpdatedAt: string; // ISO 8601 createdAt?: string; // ISO 8601 status?: ProjectStatus; variant?: "compact" | "default"; conversationId?: string; // Optional conversation ID for VS Code URL } const MAX_TIME_BETWEEN_CREATION_AND_UPDATE = 1000 * 60 * 30; // 30 minutes export function ConversationCard({ onClick, onDelete, onChangeTitle, showOptions, isActive, title, selectedRepository, // lastUpdatedAt is kept in props for backward compatibility // eslint-disable-next-line @typescript-eslint/no-unused-vars lastUpdatedAt, createdAt, status = "STOPPED", variant = "default", conversationId, }: ConversationCardProps) { const { t } = useTranslation(); const { parsedEvents } = useWsClient(); const [contextMenuVisible, setContextMenuVisible] = React.useState(false); const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view"); const [metricsModalVisible, setMetricsModalVisible] = React.useState(false); const [systemModalVisible, setSystemModalVisible] = React.useState(false); const inputRef = React.useRef(null); const systemMessage = parsedEvents.find(isSystemMessage); // Subscribe to metrics data from Redux store const metrics = useSelector((state: RootState) => state.metrics); const handleBlur = () => { if (inputRef.current?.value) { const trimmed = inputRef.current.value.trim(); onChangeTitle?.(trimmed); inputRef.current!.value = trimmed; } else { // reset the value if it's empty inputRef.current!.value = title; } setTitleMode("view"); }; const handleKeyUp = (event: React.KeyboardEvent) => { if (event.key === "Enter") { event.currentTarget.blur(); } }; const handleInputClick = (event: React.MouseEvent) => { if (titleMode === "edit") { event.preventDefault(); event.stopPropagation(); } }; const handleDelete = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); onDelete?.(); setContextMenuVisible(false); }; const handleEdit = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); setTitleMode("edit"); setContextMenuVisible(false); }; const handleDownloadViaVSCode = async ( event: React.MouseEvent, ) => { event.preventDefault(); event.stopPropagation(); posthog.capture("download_via_vscode_button_clicked"); // Fetch the VS Code URL from the API if (conversationId) { try { const data = await OpenHands.getVSCodeUrl(conversationId); if (data.vscode_url) { const transformedUrl = transformVSCodeUrl(data.vscode_url); if (transformedUrl) { window.open(transformedUrl, "_blank"); } } // VS Code URL not available } catch (error) { // Failed to fetch VS Code URL } } setContextMenuVisible(false); }; const handleDisplayCost = (event: React.MouseEvent) => { event.stopPropagation(); setMetricsModalVisible(true); }; const handleShowAgentTools = (event: React.MouseEvent) => { event.stopPropagation(); setSystemModalVisible(true); }; React.useEffect(() => { if (titleMode === "edit") { inputRef.current?.focus(); } }, [titleMode]); const hasContextMenu = !!(onDelete || onChangeTitle || showOptions); const timeBetweenUpdateAndCreation = createdAt ? new Date(lastUpdatedAt).getTime() - new Date(createdAt).getTime() : 0; const showUpdateTime = createdAt && timeBetweenUpdateAndCreation > MAX_TIME_BETWEEN_CREATION_AND_UPDATE; return ( <>
{isActive && ( )} {titleMode === "edit" && ( )} {titleMode === "view" && (

{title}

)}
{hasContextMenu && (
{ event.preventDefault(); event.stopPropagation(); setContextMenuVisible((prev) => !prev); }} />
)}
{contextMenuVisible && ( setContextMenuVisible(false)} onDelete={onDelete && handleDelete} onEdit={onChangeTitle && handleEdit} onDownloadViaVSCode={ conversationId && showOptions ? handleDownloadViaVSCode : undefined } onDisplayCost={showOptions ? handleDisplayCost : undefined} onShowAgentTools={ showOptions && systemMessage ? handleShowAgentTools : undefined } position={variant === "compact" ? "top" : "bottom"} /> )}
{selectedRepository && ( )}

{t(I18nKey.CONVERSATION$CREATED)} {showUpdateTime && ( <> {t(I18nKey.CONVERSATION$UPDATED)} )}

{(metrics?.cost !== null || metrics?.usage !== null) && (
{metrics?.cost !== null && (
{t(I18nKey.CONVERSATION$TOTAL_COST)} ${metrics.cost.toFixed(4)}
)} {metrics?.usage !== null && ( <>
{t(I18nKey.CONVERSATION$INPUT)} {metrics.usage.prompt_tokens.toLocaleString()}
Cache Hit: {metrics.usage.cache_read_tokens.toLocaleString()} Cache Write: {metrics.usage.cache_write_tokens.toLocaleString()}
{t(I18nKey.CONVERSATION$OUTPUT)} {metrics.usage.completion_tokens.toLocaleString()}
{t(I18nKey.CONVERSATION$TOTAL)} {( metrics.usage.prompt_tokens + metrics.usage.completion_tokens ).toLocaleString()}
{t(I18nKey.CONVERSATION$CONTEXT_WINDOW)}
{metrics.usage.per_turn_token.toLocaleString()} /{" "} {metrics.usage.context_window.toLocaleString()} ( {( (metrics.usage.per_turn_token / metrics.usage.context_window) * 100 ).toFixed(2)} % {t(I18nKey.CONVERSATION$USED)})
)}
)} {!metrics?.cost && !metrics?.usage && (

{t(I18nKey.CONVERSATION$NO_METRICS)}

)}
setSystemModalVisible(false)} systemMessage={systemMessage ? systemMessage.args : null} /> ); }