mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
refactor(frontend): metrics modal (#10968)
This commit is contained in:
@@ -12,7 +12,7 @@ import { SystemMessageModal } from "../conversation-panel/system-message-modal";
|
||||
import { MicroagentsModal } from "../conversation-panel/microagents-modal";
|
||||
import { ConfirmDeleteModal } from "../conversation-panel/confirm-delete-modal";
|
||||
import { ConfirmStopModal } from "../conversation-panel/confirm-stop-modal";
|
||||
import { MetricsModal } from "./metrics-modal";
|
||||
import { MetricsModal } from "./metrics-modal/metrics-modal";
|
||||
|
||||
export function ConversationName() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { BaseModal } from "../../shared/modals/base-modal/base-modal";
|
||||
import { BudgetDisplay } from "../conversation-panel/budget-display";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface MetricsModalProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function MetricsModal({ isOpen, onOpenChange }: MetricsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const metrics = useSelector((state: RootState) => state.metrics);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t(I18nKey.CONVERSATION$METRICS_INFO)}
|
||||
testID="metrics-modal"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{(metrics?.cost !== null || metrics?.usage !== null) && (
|
||||
<div className="rounded-md p-3">
|
||||
<div className="grid gap-3">
|
||||
{metrics?.cost !== null && (
|
||||
<div className="flex justify-between items-center pb-2">
|
||||
<span className="text-lg font-semibold">
|
||||
{t(I18nKey.CONVERSATION$TOTAL_COST)}
|
||||
</span>
|
||||
<span className="font-semibold">
|
||||
${metrics.cost.toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<BudgetDisplay
|
||||
cost={metrics?.cost ?? null}
|
||||
maxBudgetPerTask={metrics?.max_budget_per_task ?? null}
|
||||
/>
|
||||
|
||||
{metrics?.usage !== null && (
|
||||
<>
|
||||
<div className="flex justify-between items-center pb-2">
|
||||
<span>{t(I18nKey.CONVERSATION$INPUT)}</span>
|
||||
<span className="font-semibold">
|
||||
{metrics.usage.prompt_tokens.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 pl-4 text-sm">
|
||||
<span className="text-neutral-400">
|
||||
{t(I18nKey.CONVERSATION$CACHE_HIT)}
|
||||
</span>
|
||||
<span className="text-right">
|
||||
{metrics.usage.cache_read_tokens.toLocaleString()}
|
||||
</span>
|
||||
<span className="text-neutral-400">
|
||||
{t(I18nKey.CONVERSATION$CACHE_WRITE)}
|
||||
</span>
|
||||
<span className="text-right">
|
||||
{metrics.usage.cache_write_tokens.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
|
||||
<span>{t(I18nKey.CONVERSATION$OUTPUT)}</span>
|
||||
<span className="font-semibold">
|
||||
{metrics.usage.completion_tokens.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
|
||||
<span className="font-semibold">
|
||||
{t(I18nKey.CONVERSATION$TOTAL)}
|
||||
</span>
|
||||
<span className="font-bold">
|
||||
{(
|
||||
metrics.usage.prompt_tokens +
|
||||
metrics.usage.completion_tokens
|
||||
).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold">
|
||||
{t(I18nKey.CONVERSATION$CONTEXT_WINDOW)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-neutral-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, (metrics.usage.per_turn_token / metrics.usage.context_window) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<span className="text-xs text-neutral-400">
|
||||
{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)})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!metrics?.cost && !metrics?.usage && (
|
||||
<div className="rounded-md p-4 text-center">
|
||||
<p className="text-neutral-400">
|
||||
{t(I18nKey.CONVERSATION$NO_METRICS)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ContextWindowSectionProps {
|
||||
perTurnToken: number;
|
||||
contextWindow: number;
|
||||
}
|
||||
|
||||
export function ContextWindowSection({
|
||||
perTurnToken,
|
||||
contextWindow,
|
||||
}: ContextWindowSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const usagePercentage = (perTurnToken / contextWindow) * 100;
|
||||
const progressWidth = Math.min(100, usagePercentage);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-semibold">
|
||||
{t(I18nKey.CONVERSATION$CONTEXT_WINDOW)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-1.5 bg-neutral-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 transition-all duration-300"
|
||||
style={{ width: `${progressWidth}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<span className="text-xs text-neutral-400">
|
||||
{perTurnToken.toLocaleString()} / {contextWindow.toLocaleString()} (
|
||||
{usagePercentage.toFixed(2)}% {t(I18nKey.CONVERSATION$USED)})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BudgetDisplay } from "../../conversation-panel/budget-display";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface CostSectionProps {
|
||||
cost: number | null;
|
||||
maxBudgetPerTask: number | null;
|
||||
}
|
||||
|
||||
export function CostSection({ cost, maxBudgetPerTask }: CostSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (cost === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between items-center pb-2">
|
||||
<span className="text-lg font-semibold">
|
||||
{t(I18nKey.CONVERSATION$TOTAL_COST)}
|
||||
</span>
|
||||
<span className="font-semibold">${cost.toFixed(4)}</span>
|
||||
</div>
|
||||
<BudgetDisplay cost={cost} maxBudgetPerTask={maxBudgetPerTask} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function EmptyState() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="rounded-md p-4 text-center">
|
||||
<p className="text-neutral-400">{t(I18nKey.CONVERSATION$NO_METRICS)}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface MetricRowProps {
|
||||
label: ReactNode;
|
||||
value: ReactNode;
|
||||
labelClassName?: string;
|
||||
valueClassName?: string;
|
||||
}
|
||||
|
||||
export function MetricRow({
|
||||
label,
|
||||
value,
|
||||
labelClassName = "",
|
||||
valueClassName = "font-semibold",
|
||||
}: MetricRowProps) {
|
||||
return (
|
||||
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
|
||||
<span className={labelClassName}>{label}</span>
|
||||
<span className={valueClassName}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { BaseModal } from "../../../shared/modals/base-modal/base-modal";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { CostSection } from "./cost-section";
|
||||
import { UsageSection } from "./usage-section";
|
||||
import { ContextWindowSection } from "./context-window-section";
|
||||
import { EmptyState } from "./empty-state";
|
||||
|
||||
interface MetricsModalProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function MetricsModal({ isOpen, onOpenChange }: MetricsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const metrics = useSelector((state: RootState) => state.metrics);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t(I18nKey.CONVERSATION$METRICS_INFO)}
|
||||
testID="metrics-modal"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{(metrics?.cost !== null || metrics?.usage !== null) && (
|
||||
<div className="rounded-md p-3">
|
||||
<div className="grid gap-3">
|
||||
<CostSection
|
||||
cost={metrics?.cost ?? null}
|
||||
maxBudgetPerTask={metrics?.max_budget_per_task ?? null}
|
||||
/>
|
||||
|
||||
{metrics?.usage !== null && (
|
||||
<>
|
||||
<UsageSection usage={metrics.usage} />
|
||||
<ContextWindowSection
|
||||
perTurnToken={metrics.usage.per_turn_token}
|
||||
contextWindow={metrics.usage.context_window}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!metrics?.cost && !metrics?.usage && <EmptyState />}
|
||||
</div>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { MetricRow } from "./metric-row";
|
||||
|
||||
interface UsageSectionProps {
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
cache_read_tokens: number;
|
||||
cache_write_tokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function UsageSection({ usage }: UsageSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricRow
|
||||
label={t(I18nKey.CONVERSATION$INPUT)}
|
||||
value={usage.prompt_tokens.toLocaleString()}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 pl-4 text-sm">
|
||||
<span className="text-neutral-400">
|
||||
{t(I18nKey.CONVERSATION$CACHE_HIT)}
|
||||
</span>
|
||||
<span className="text-right">
|
||||
{usage.cache_read_tokens.toLocaleString()}
|
||||
</span>
|
||||
<span className="text-neutral-400">
|
||||
{t(I18nKey.CONVERSATION$CACHE_WRITE)}
|
||||
</span>
|
||||
<span className="text-right">
|
||||
{usage.cache_write_tokens.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<MetricRow
|
||||
label={t(I18nKey.CONVERSATION$OUTPUT)}
|
||||
value={usage.completion_tokens.toLocaleString()}
|
||||
/>
|
||||
|
||||
<MetricRow
|
||||
label={t(I18nKey.CONVERSATION$TOTAL)}
|
||||
value={(usage.prompt_tokens + usage.completion_tokens).toLocaleString()}
|
||||
labelClassName="font-semibold"
|
||||
valueClassName="font-bold"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user