diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 63dbea3..a4f70f9 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -263,6 +263,14 @@ "logs.duration": "Duration", "logs.showing": "{{from}}-{{to}} of {{total}}", "logs.prev": "Prev", - "logs.next": "Next" + "logs.next": "Next", + "缓存命中 Tokens": "Cache Hit Tokens", + "缓存创建 Tokens": "Cache Creation Tokens", + "流式": "Streaming", + "是": "Yes", + "否": "No", + "首字时间": "First Token Time", + "计费方式": "Billing Method", + "订阅抵扣": "Subscription Deduction" } } diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index f410ab0..65c26bf 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -263,6 +263,14 @@ "logs.duration": "耗时", "logs.showing": "第 {{from}}-{{to}} 条,共 {{total}} 条", "logs.prev": "上一页", - "logs.next": "下一页" + "logs.next": "下一页", + "缓存命中 Tokens": "缓存命中 Tokens", + "缓存创建 Tokens": "缓存创建 Tokens", + "流式": "流式", + "是": "是", + "否": "否", + "首字时间": "首字时间", + "计费方式": "计费方式", + "订阅抵扣": "订阅抵扣" } } diff --git a/src/pages/Logs.jsx b/src/pages/Logs.jsx index 64b5c4f..11f0156 100644 --- a/src/pages/Logs.jsx +++ b/src/pages/Logs.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { ChevronDown, ChevronRight } from 'lucide-react'; import { getUserLogs, Q } from '../api'; import { useCurrency } from '../context/SiteContext'; @@ -10,6 +11,15 @@ function formatTime(unix) { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } +function getLogOther(otherStr) { + if (!otherStr) return null; + try { + return JSON.parse(otherStr); + } catch (e) { + return null; + } +} + export default function Logs() { const { t } = useTranslation(); const { symbol, rate } = useCurrency(); @@ -19,6 +29,7 @@ export default function Logs() { const [loading, setLoading] = useState(true); const [modelFilter, setModelFilter] = useState(''); const [tokenFilter, setTokenFilter] = useState(''); + const [expandedRows, setExpandedRows] = useState({}); const pageSize = 20; const load = useCallback(async () => { @@ -40,6 +51,50 @@ export default function Logs() { const totalPages = Math.max(1, Math.ceil(total / pageSize)); + const toggleRow = (logId) => { + setExpandedRows(prev => ({ ...prev, [logId]: !prev[logId] })); + }; + + const getExpandData = (log) => { + const other = getLogOther(log.other); + if (!other) return []; + + const data = []; + + // Cache hits + if (other.cache_tokens > 0) { + data.push({ key: t('缓存命中 Tokens'), value: other.cache_tokens.toLocaleString() }); + } + + // Cache creation + if (other.cache_creation_tokens > 0) { + data.push({ key: t('缓存创建 Tokens'), value: other.cache_creation_tokens.toLocaleString() }); + } + + // Request ID + if (log.request_id) { + data.push({ key: 'Request ID', value: log.request_id }); + } + + // Stream info + if (log.is_stream !== undefined) { + data.push({ key: t('流式'), value: log.is_stream ? t('是') : t('否') }); + } + + // First response time for streaming + if (log.is_stream && other.frt) { + const frtSeconds = (parseFloat(other.frt) / 1000.0).toFixed(1); + data.push({ key: t('首字时间'), value: `${frtSeconds}s` }); + } + + // Billing source + if (other.billing_source === 'subscription') { + data.push({ key: t('计费方式'), value: t('订阅抵扣') }); + } + + return data; + }; + return (
{/* Header */} @@ -92,6 +147,7 @@ export default function Logs() { + @@ -102,46 +158,98 @@ export default function Logs() { - {logs.map((log, i) => ( - - - - - - - - - - ))} + {logs.map((log, i) => { + const expandData = getExpandData(log); + const hasExpandData = expandData.length > 0; + const isExpanded = expandedRows[log.id]; + return ( + + hasExpandData && toggleRow(log.id)} + > + + + + + + + + + + {isExpanded && hasExpandData && ( + + + + )} + + ); + })}
{t('logs.time')} {t('logs.model')} {t('logs.token')}
{formatTime(log.created_at)} - {log.model_name || '-'} - {log.token_name || '-'}{log.prompt_tokens?.toLocaleString() || '0'}{log.completion_tokens?.toLocaleString() || '0'} - {log.quota > 0 ? `${symbol}${(log.quota / Q * rate).toFixed(6)}` : '-'} - - {log.use_time > 0 ? `${log.use_time}s` : '-'} -
+ {hasExpandData && ( + isExpanded ? : + )} + {formatTime(log.created_at)} + {log.model_name || '-'} + {log.token_name || '-'}{log.prompt_tokens?.toLocaleString() || '0'}{log.completion_tokens?.toLocaleString() || '0'} + {log.quota > 0 ? `${symbol}${(log.quota / Q * rate).toFixed(6)}` : '-'} + + {log.use_time > 0 ? `${log.use_time}s` : '-'} +
+
+ {expandData.map((item, idx) => ( +
+ {item.key} + {item.value} +
+ ))} +
+
{/* Mobile cards */}
- {logs.map((log, i) => ( -
-
- {log.model_name || '-'} - - {log.quota > 0 ? `${symbol}${(log.quota / Q * rate).toFixed(6)}` : '-'} - + {logs.map((log, i) => { + const expandData = getExpandData(log); + const hasExpandData = expandData.length > 0; + const isExpanded = expandedRows[log.id]; + return ( +
+
hasExpandData && toggleRow(log.id)} + > +
+ {hasExpandData && ( + isExpanded ? : + )} + {log.model_name || '-'} +
+ + {log.quota > 0 ? `${symbol}${(log.quota / Q * rate).toFixed(6)}` : '-'} + +
+
+ {formatTime(log.created_at)} + {log.prompt_tokens || 0} / {log.completion_tokens || 0} tokens +
+ {log.token_name && ( +
{log.token_name}
+ )} + {isExpanded && hasExpandData && ( +
+ {expandData.map((item, idx) => ( +
+ {item.key} + {item.value} +
+ ))} +
+ )}
-
- {formatTime(log.created_at)} - {log.prompt_tokens || 0} / {log.completion_tokens || 0} tokens -
- {log.token_name && ( -
{log.token_name}
- )} -
- ))} + ); + })}
)}