diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 2dd004b..dd31793 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -194,6 +194,11 @@ export const jaOverrides = { 'usage.events.resetColumns': '列幅をリセット', 'usage.events.resizeHint': 'ドラッグで列幅を調整、ダブルクリックでリセット', 'usage.events.dragHint': 'ヘッダーの境界をドラッグして列幅を調整', + 'usage.events.columnSettings': '列設定', + 'usage.events.columnSettingsDescription': 'リクエスト詳細に表示する列を選択します。', + 'usage.events.columnsSelected': '{total} 列中 {selected} 列を選択', + 'usage.events.selectAllColumns': 'すべて選択', + 'usage.events.applyColumns': '適用', 'usage.column.time': '日時', 'usage.column.model': 'モデル', 'usage.column.provider': 'Provider', @@ -206,7 +211,9 @@ export const jaOverrides = { 'usage.column.output': '出力', 'usage.column.reasoning': '推論', 'usage.column.cache': 'キャッシュ', + 'usage.column.cacheRate': 'キャッシュ率', 'usage.column.total': '合計', + 'usage.column.speed': '生成速度', 'usage.key.noRemark': 'メモなし', 'usage.previous': '前へ', 'usage.next': '次へ', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 670df61..24aa439 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -194,6 +194,11 @@ export const en: Record = { 'usage.events.resetColumns': 'Reset Widths', 'usage.events.resizeHint': 'Drag to resize, double-click to reset', 'usage.events.dragHint': 'Drag column header borders to resize', + 'usage.events.columnSettings': 'Column Settings', + 'usage.events.columnSettingsDescription': 'Choose which columns appear in request details.', + 'usage.events.columnsSelected': '{selected} of {total} columns selected', + 'usage.events.selectAllColumns': 'Select All', + 'usage.events.applyColumns': 'Apply', 'usage.column.time': 'Time', 'usage.column.model': 'Model', 'usage.column.provider': 'Provider', @@ -206,7 +211,9 @@ export const en: Record = { 'usage.column.output': 'Output', 'usage.column.reasoning': 'Reasoning', 'usage.column.cache': 'Cache', + 'usage.column.cacheRate': 'Cache Rate', 'usage.column.total': 'Total', + 'usage.column.speed': 'Generation Speed', 'usage.key.noRemark': 'No remark', 'usage.previous': 'Previous', 'usage.next': 'Next', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 9d4cd7d..28eb9e1 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -192,6 +192,11 @@ export const zhCN = { 'usage.events.resetColumns': '重置列宽', 'usage.events.resizeHint': '拖动调整列宽,双击重置本列', 'usage.events.dragHint': '可拖动表头边线调整每列宽度', + 'usage.events.columnSettings': '列设置', + 'usage.events.columnSettingsDescription': '选择要在请求明细中显示的列。', + 'usage.events.columnsSelected': '已选择 {selected} / {total} 列', + 'usage.events.selectAllColumns': '选择全部', + 'usage.events.applyColumns': '应用', 'usage.column.time': '时间', 'usage.column.model': '模型', 'usage.column.provider': 'Provider', @@ -204,7 +209,9 @@ export const zhCN = { 'usage.column.output': '输出', 'usage.column.reasoning': '思考', 'usage.column.cache': '缓存', + 'usage.column.cacheRate': '缓存率', 'usage.column.total': '总计', + 'usage.column.speed': '生成速度', 'usage.key.noRemark': '未备注', 'usage.previous': '上一页', 'usage.next': '下一页', diff --git a/src/pages/UsageRecordsPage.tsx b/src/pages/UsageRecordsPage.tsx index 9931a2f..d7b211c 100644 --- a/src/pages/UsageRecordsPage.tsx +++ b/src/pages/UsageRecordsPage.tsx @@ -6,6 +6,7 @@ import { BarChart3, CircleDollarSign, Clock3, + Columns3Cog, Database, FilterX, Key, @@ -24,6 +25,7 @@ import { } from 'lucide-react'; import { getCurrentLocale, useI18n } from '../i18n'; import type { MessageKey } from '../i18n/resources'; +import { formatCacheReadRate, formatGenerationSpeed } from '../services/usageMetrics'; import { formatUsageNumber } from '../services/usageNumber'; type UsageTab = 'overview' | 'analysis' | 'events' | 'pricing'; @@ -978,7 +980,9 @@ type EventColumnKey = | 'total' | 'result' | 'latency' - | 'ttft'; + | 'ttft' + | 'speed' + | 'cacheRate'; type EventColumnDef = { key: EventColumnKey; @@ -998,13 +1002,42 @@ const EVENT_COLUMNS: readonly EventColumnDef[] = [ { key: 'output', labelKey: 'usage.column.output', defaultWidth: 76, minWidth: 50, align: 'center' }, { key: 'reasoning', labelKey: 'usage.column.reasoning', defaultWidth: 76, minWidth: 50, align: 'center' }, { key: 'cache', labelKey: 'usage.column.cache', defaultWidth: 76, minWidth: 50, align: 'center' }, + { key: 'cacheRate', labelKey: 'usage.column.cacheRate', defaultWidth: 88, minWidth: 65, align: 'center' }, { key: 'total', labelKey: 'usage.column.total', defaultWidth: 84, minWidth: 60, align: 'center' }, { key: 'result', labelKey: 'usage.column.result', defaultWidth: 160, minWidth: 90, align: 'center' }, { key: 'latency', labelKey: 'usage.column.latency', defaultWidth: 88, minWidth: 65, align: 'center' }, { key: 'ttft', labelKey: 'usage.column.ttft', defaultWidth: 88, minWidth: 65, align: 'center' }, + { key: 'speed', labelKey: 'usage.column.speed', defaultWidth: 92, minWidth: 70, align: 'center' }, ] as const; const EVENT_COL_WIDTHS_STORAGE_KEY = 'cpa-gui.usage-events-col-widths.v1'; +const EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v1'; + +const getAllEventColumnKeys = () => EVENT_COLUMNS.map((column) => column.key); + +const getInitialVisibleColumns = (): EventColumnKey[] => { + try { + const raw = localStorage.getItem(EVENT_VISIBLE_COLS_STORAGE_KEY); + if (raw) { + const parsed: unknown = JSON.parse(raw); + if (Array.isArray(parsed)) { + const knownKeys = new Set(getAllEventColumnKeys()); + const seen = new Set(); + const savedKeys = parsed.filter((key): key is EventColumnKey => { + if (typeof key !== 'string' || !knownKeys.has(key as EventColumnKey) || seen.has(key as EventColumnKey)) { + return false; + } + seen.add(key as EventColumnKey); + return true; + }); + if (savedKeys.length > 0) return savedKeys; + } + } + } catch { + // fallback to all columns + } + return getAllEventColumnKeys(); +}; const getInitialColumnWidths = (): Record => { const initial: Record = {} as any; @@ -1053,12 +1086,140 @@ function UsageResultCell({ record }: { record: UsageRecord }) { ); } +function UsageEventCell({ + record, + columnKey, + noRemarkLabel, +}: { + record: UsageRecord; + columnKey: EventColumnKey; + noRemarkLabel: string; +}) { + switch (columnKey) { + case 'time': + return ( + + {formatTime(record.timestamp)} + + ); + case 'model': + return ( + + {record.alias || record.model} + {record.alias || record.reasoning_effort ? ( + + {record.alias ? record.model : ''} + {record.alias && record.reasoning_effort ? ' · ' : ''} + {record.reasoning_effort} + + ) : null} + + ); + case 'provider': + return ( + + {record.provider || '—'} + + ); + case 'source': + return ( + + {record.source_display || record.source || '—'} + + ); + case 'key': + return ( + + {record.api_key_remark || noRemarkLabel} + {record.api_key_display || '—'} + + ); + case 'input': + return ( + + {compactNumber(record.tokens.input_tokens)} + + ); + case 'output': + return ( + + {compactNumber(record.tokens.output_tokens)} + + ); + case 'reasoning': + return ( + + {compactNumber(record.tokens.reasoning_tokens)} + + ); + case 'cache': + return ( + 0 + ? ` / Creation: ${record.tokens.cache_creation_tokens.toLocaleString()} tokens` + : '' + }`} + > + {compactNumber(record.tokens.cache_read_tokens)} + + ); + case 'cacheRate': { + const value = formatCacheReadRate({ + inputTokens: record.tokens.input_tokens, + cacheReadTokens: record.tokens.cache_read_tokens, + }); + return {value}; + } + case 'total': + return ( + + {compactNumber(record.tokens.total_tokens)} + + ); + case 'result': + return ; + case 'latency': + return ( + + {compactNumber(record.latency_ms)} ms + + ); + case 'ttft': + return ( + + {record.ttft_ms == null ? '—' : `${compactNumber(record.ttft_ms)} ms`} + + ); + case 'speed': { + const value = formatGenerationSpeed({ + outputTokens: record.tokens.output_tokens, + latencyMs: record.latency_ms, + ttftMs: record.ttft_ms, + }); + return {value}; + } + } +} + function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page: number) => void }) { const { t } = useI18n(); const [widths, setWidths] = useState>(getInitialColumnWidths); + const [visibleColumnKeys, setVisibleColumnKeys] = useState(getInitialVisibleColumns); + const [columnSettingsOpen, setColumnSettingsOpen] = useState(false); + const [draftVisibleColumnKeys, setDraftVisibleColumnKeys] = useState(visibleColumnKeys); const [resizingCol, setResizingCol] = useState(null); + const columnDialogRef = useRef(null); + const visibleColumnKeySet = new Set(visibleColumnKeys); + const visibleColumns = EVENT_COLUMNS.filter((column) => visibleColumnKeySet.has(column.key)); const isCustomized = EVENT_COLUMNS.some((col) => widths[col.key] !== col.defaultWidth); + const noRemarkLabel = t('usage.key.noRemark'); + + useEffect(() => { + if (columnSettingsOpen) columnDialogRef.current?.focus(); + }, [columnSettingsOpen]); const resetAllWidths = () => { const defaults: Record = {} as any; @@ -1071,6 +1232,34 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page: } catch {} }; + const openColumnSettings = () => { + setDraftVisibleColumnKeys(visibleColumnKeys); + setColumnSettingsOpen(true); + }; + + const toggleDraftColumn = (key: EventColumnKey) => { + setDraftVisibleColumnKeys((current) => { + if (current.includes(key)) { + return current.length > 1 ? current.filter((columnKey) => columnKey !== key) : current; + } + return EVENT_COLUMNS.filter((column) => current.includes(column.key) || column.key === key).map((column) => column.key); + }); + }; + + const applyColumnSettings = () => { + const next = draftVisibleColumnKeys.length > 0 ? draftVisibleColumnKeys : getAllEventColumnKeys(); + setVisibleColumnKeys(next); + try { + localStorage.setItem(EVENT_VISIBLE_COLS_STORAGE_KEY, JSON.stringify(next)); + } catch {} + setColumnSettingsOpen(false); + }; + + const resetVisibleColumns = () => { + const allColumns = getAllEventColumnKeys(); + setDraftVisibleColumnKeys(allColumns); + }; + const resetSingleColumn = (key: EventColumnKey, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -1125,7 +1314,7 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page: window.addEventListener('pointerup', onPointerUp); }; - const totalTableWidth = EVENT_COLUMNS.reduce((sum, col) => sum + (widths[col.key] ?? col.defaultWidth), 0); + const totalTableWidth = visibleColumns.reduce((sum, col) => sum + (widths[col.key] ?? col.defaultWidth), 0); return (
@@ -1139,6 +1328,15 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
+ {isCustomized ? (