diff --git a/heicode/web/default/src/features/wallet/components/heicode-usage-card.tsx b/heicode/web/default/src/features/wallet/components/heicode-usage-card.tsx new file mode 100644 index 0000000..e7f26a9 --- /dev/null +++ b/heicode/web/default/src/features/wallet/components/heicode-usage-card.tsx @@ -0,0 +1,144 @@ +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Activity, ListChecks } from 'lucide-react' +import { Skeleton } from '@/components/ui/skeleton' +import { formatCompactNumber, formatQuota } from '@/lib/format' +import { getHeicodeUsage, getHeicodeLogs } from '@/lib/heicode-mcp' + +type UsageRow = { + date?: string + total_quota?: number + total_tokens?: number + prompt_tokens?: number + completion_tokens?: number + count?: number +} + +type LogRow = { + created_at?: number | string + model_name?: string + token_name?: string + prompt_tokens?: number + completion_tokens?: number + quota?: number + type?: number | string +} + +function pickNumber(value: unknown): number { + if (typeof value === 'number' && Number.isFinite(value)) return value + if (typeof value === 'string') { + const n = Number(value) + return Number.isFinite(n) ? n : 0 + } + return 0 +} + +function formatLogTime(value: number | string | undefined): string { + if (value == null) return '-' + const n = typeof value === 'number' ? value : Number(value) + const ms = Number.isFinite(n) ? (n > 1e12 ? n : n * 1000) : Date.parse(String(value)) + if (!Number.isFinite(ms)) return '-' + const d = new Date(ms) + const pad = (x: number) => String(x).padStart(2, '0') + return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` +} + +export function HeicodeUsageCard() { + const { t } = useTranslation() + const [usage, setUsage] = useState(null) + const [logs, setLogs] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let cancelled = false + setLoading(true) + Promise.allSettled([getHeicodeUsage(14), getHeicodeLogs(10, 1)]) + .then(([u, l]) => { + if (cancelled) return + setUsage(u.status === 'fulfilled' ? (u.value as UsageRow[]) : []) + setLogs(l.status === 'fulfilled' ? (l.value as LogRow[]) : []) + }) + .finally(() => { + if (!cancelled) setLoading(false) + }) + return () => { + cancelled = true + } + }, []) + + const maxQuota = useMemo(() => { + if (!usage || usage.length === 0) return 0 + return usage.reduce((m, row) => Math.max(m, pickNumber(row.total_quota)), 0) + }, [usage]) + + if (loading) { + return ( +
+
+ + +
+
+ + +
+
+ ) + } + + const usageEmpty = !usage || usage.length === 0 + const logsEmpty = !logs || logs.length === 0 + + return ( +
+
+
+ +

{t('Recent Usage (14d)')}

+
+ {usageEmpty ? ( +

{t('No usage yet')}

+ ) : ( +
+ {usage!.map((row, i) => { + const q = pickNumber(row.total_quota) + const h = maxQuota > 0 ? Math.max(2, (q / maxQuota) * 60) : 2 + return ( +
+ ) + })} +
+ )} +
+ +
+
+ +

{t('Recent Requests')}

+
+ {logsEmpty ? ( +

{t('No requests yet')}

+ ) : ( +
    + {logs!.slice(0, 6).map((row, i) => ( +
  • + {row.model_name ?? '-'} + + {formatCompactNumber(pickNumber(row.prompt_tokens) + pickNumber(row.completion_tokens))}t · {formatQuota(pickNumber(row.quota))} + + + {formatLogTime(row.created_at)} + +
  • + ))} +
+ )} +
+
+ ) +} diff --git a/heicode/web/default/src/features/wallet/index.tsx b/heicode/web/default/src/features/wallet/index.tsx index f9ac4c8..c63c778 100644 --- a/heicode/web/default/src/features/wallet/index.tsx +++ b/heicode/web/default/src/features/wallet/index.tsx @@ -13,6 +13,7 @@ import { TransferDialog } from './components/dialogs/transfer-dialog' import { RechargeFormCard } from './components/recharge-form-card' import { SubscriptionPlansCard } from './components/subscription-plans-card' import { WalletStatsCard } from './components/wallet-stats-card' +import { HeicodeUsageCard } from './components/heicode-usage-card' import { DEFAULT_DISCOUNT_RATE } from './constants' import { useTopupInfo, @@ -264,6 +265,8 @@ export function Wallet(props: WalletProps) {
+ +
diff --git a/heicode/web/default/src/i18n/locales/en.json b/heicode/web/default/src/i18n/locales/en.json index e854788..361ceb9 100644 --- a/heicode/web/default/src/i18n/locales/en.json +++ b/heicode/web/default/src/i18n/locales/en.json @@ -2050,6 +2050,10 @@ "Manage Vendors": "Manage Vendors", "Manage your API keys for accessing the service": "Manage your API keys for accessing the service", "Manage your balance and payment methods": "Manage your balance and payment methods", + "Recent Usage (14d)": "Recent Usage (14d)", + "No usage yet": "No usage yet", + "Recent Requests": "Recent Requests", + "No requests yet": "No requests yet", "Manage your security settings and account access": "Manage your security settings and account access", "Manual Disabled": "Manual Disabled", "Map fields from the user info response to local user attributes. Supports nested paths (e.g. ocs.data.id).": "Map fields from the user info response to local user attributes. Supports nested paths (e.g. ocs.data.id).", diff --git a/heicode/web/default/src/i18n/locales/zh.json b/heicode/web/default/src/i18n/locales/zh.json index af5d97a..66e8916 100644 --- a/heicode/web/default/src/i18n/locales/zh.json +++ b/heicode/web/default/src/i18n/locales/zh.json @@ -2050,6 +2050,10 @@ "Manage Vendors": "管理供应商", "Manage your API keys for accessing the service": "管理您用于访问服务的 API 密钥", "Manage your balance and payment methods": "管理您的余额和付款方式", + "Recent Usage (14d)": "近 14 天用量", + "No usage yet": "暂无用量", + "Recent Requests": "最近请求", + "No requests yet": "暂无请求", "Manage your security settings and account access": "管理您的安全设置和账户访问", "Manual Disabled": "手动禁用", "Map fields from the user info response to local user attributes. Supports nested paths (e.g. ocs.data.id).": "将用户信息响应中的字段映射到本地用户属性。支持嵌套路径(例如 ocs.data.id)。",