feat(ui): 添加 Token 消耗统计显示

- 状态栏显示当前会话 Token 消耗总量,悬停显示详情
- AI 消息底部显示本次响应的输入/输出 Token
- 会话列表顶部显示项目 Token 消耗总量
- 会话列表每项显示该会话的 Token 消耗
- 新增 Token 统计 API 客户端函数
- Server done 事件携带 usage 信息
This commit is contained in:
2025-12-18 17:01:09 +08:00
parent bac32fe8f6
commit 3ff489fbc0
8 changed files with 394 additions and 73 deletions
@@ -15,6 +15,7 @@ import {
CheckCircle2,
Loader2,
GitCompare,
Coins,
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { useState, forwardRef } from 'react';
@@ -41,11 +42,28 @@ interface ChatMessageProps {
onDenyPermission?: (requestId: string, remember: boolean) => void;
}
// 格式化 Token 数量
const formatTokens = (tokens: number): string => {
if (tokens >= 1000000) {
return `${(tokens / 1000000).toFixed(2)}M`;
}
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(1)}k`;
}
return `${tokens}`;
};
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
({ message, isStreaming = false, onAnswerQuestion, onViewDiff, onAllowPermission, onDenyPermission }, ref) => {
const isUser = message.role === 'user';
const [copied, setCopied] = useState(false);
// Token 信息
const hasTokenInfo = !isUser && message.metadata?.inputTokens !== undefined;
const inputTokens = message.metadata?.inputTokens ?? 0;
const outputTokens = message.metadata?.outputTokens ?? 0;
const totalTokens = message.metadata?.totalTokens ?? (inputTokens + outputTokens);
const handleCopy = async () => {
await navigator.clipboard.writeText(message.content ?? '');
setCopied(true);
@@ -184,6 +202,22 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
</button>
</div>
{renderContent()}
{/* Token 统计显示(仅 AI 消息,非流式输出时) */}
{hasTokenInfo && !isStreaming && totalTokens > 0 && (
<div className="mt-2 pt-2 border-t border-line/50 flex items-center gap-3 text-xs text-fg-subtle">
<span className="flex items-center gap-1" title="Token 消耗">
<Coins size={12} className="text-fg-muted" />
<span>{formatTokens(totalTokens)}</span>
</span>
<span className="text-fg-muted/40">|</span>
<span title="输入 Token">
{formatTokens(inputTokens)}
</span>
<span title="输出 Token">
{formatTokens(outputTokens)}
</span>
</div>
)}
</div>
</motion.div>
);