Skip to content

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 25, 2026

Summary

  • Refactor error-details-dialog into modular tab-based components (Summary, LogicTrace, Performance, Metadata)
  • Extract filters into dedicated components for better maintainability (Time, Identity, Request, Status filters)
  • Add column visibility management with localStorage persistence
  • Enhance provider chain popover with detailed session reuse and selection context tooltips
  • Add message redaction utility for sensitive data masking
  • Update i18n for all 5 languages (en, ja, ru, zh-CN, zh-TW)

Changes

New Components

  • error-details-dialog/ - Modular dialog with SummaryTab, LogicTraceTab, PerformanceTab, MetadataTab
  • filters/ - TimeFilters, IdentityFilters, RequestFilters, StatusFilters, QuickFiltersBar, ActiveFiltersDisplay
  • column-visibility-dropdown.tsx - Column show/hide management
  • scroll-area.tsx - Consistent scrollable containers
  • active-sessions-cards.tsx - Session cards component

Utilities

  • column-visibility.ts - Column visibility state management with localStorage
  • message-redaction.ts - Sensitive data masking utility

Enhancements

  • Provider chain popover now shows detailed session reuse info (age, priority, cost multiplier)
  • Initial selection tooltip shows selection funnel (total -> enabled -> healthy)
  • Retry popover displays cost multiplier and group tag badges

Test plan

  • Verify error details dialog tabs render correctly
  • Test filter components work independently and together
  • Confirm column visibility persists across page reloads
  • Check provider chain tooltips display correct information
  • Validate i18n translations render in all 5 languages

Generated with Claude Code

Greptile Overview

Greptile Summary

This PR refactors the logs UI into a well-structured, modular architecture with several notable improvements:

Major Changes:

  • Error details dialog split into tab-based components (SummaryTab, LogicTraceTab, PerformanceTab, MetadataTab) for better code organization
  • Filters extracted into dedicated components (TimeFilters, IdentityFilters, RequestFilters, StatusFilters) with proper type safety
  • Column visibility management added with localStorage persistence and user-scoped settings
  • Message redaction utility implements privacy protection for sensitive data in request/response bodies
  • Provider chain popover enhanced with detailed tooltips showing selection funnel (total → enabled → healthy) and session reuse context

Key Improvements:

  • Race condition protection in async operations (user search, session message checks)
  • Filter sanitization prevents runtime-leaked fields from polluting the filter state
  • Comprehensive test coverage for new utilities (message-redaction, column-visibility)
  • i18n updates for all 5 supported languages
  • Improved responsive design with better mobile support

Code Quality:

  • Clean separation of concerns with proper type definitions
  • Proper null handling and defensive programming
  • Debouncing for search inputs to reduce API calls
  • Proper cleanup in useEffect hooks

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk - it's a well-executed refactoring with comprehensive tests
  • The refactoring is clean and well-tested. The only minor issue is a potential contrast problem in tooltip text colors (dark mode). The code demonstrates good practices: race condition handling, proper TypeScript typing, comprehensive test coverage, and security-conscious design (message redaction, filter sanitization).
  • Check provider-chain-popover.tsx for dark mode contrast in tooltips

Important Files Changed

Filename Overview
src/lib/utils/message-redaction.ts New utility for redacting sensitive message content, well-tested with comprehensive coverage
src/lib/column-visibility.ts Clean implementation of column visibility persistence with localStorage, includes validation
src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx Well-structured refactor into modular dialog with proper tab management and race condition handling
src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx Enhanced with detailed tooltips showing selection funnel and session reuse info; border-zinc colors used in tooltip
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx Refactored into modular filter components, adds filter sanitization for security
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx Implements responsive column visibility with proper flex layout, improved styling

Sequence Diagram

sequenceDiagram
    participant User
    participant LogsPage as Logs Page
    participant Filters as Filter Components
    participant Table as Virtualized Table
    participant ColVis as Column Visibility
    participant Dialog as Error Details Dialog
    participant Storage as localStorage

    User->>LogsPage: Load logs page
    LogsPage->>Storage: getHiddenColumns(userId, tableId)
    Storage-->>LogsPage: hidden columns array
    LogsPage->>Filters: Render modular filters
    Note over Filters: TimeFilters, IdentityFilters,<br/>RequestFilters, StatusFilters
    
    User->>Filters: Select filter (e.g., user, date range)
    Filters->>Filters: sanitizeFilters()
    Filters->>LogsPage: onFiltersChange(sanitized)
    LogsPage->>Table: Re-render with new filters
    
    User->>ColVis: Toggle column visibility
    ColVis->>Storage: setHiddenColumns(userId, tableId, hidden)
    ColVis->>Table: onVisibilityChange(hidden)
    Table->>Table: Re-render with hidden columns
    
    User->>Table: Click status badge
    Table->>Dialog: Open error details dialog
    Dialog->>Dialog: Check hasSessionMessages(sessionId)
    Dialog->>User: Display tabs (Summary, LogicTrace, Performance)
    
    User->>Dialog: View LogicTrace tab
    Dialog->>Dialog: redactJsonString(errorDetails.request.body)
    Dialog->>User: Display redacted request/response
    
    User->>Dialog: View PerformanceTab
    Dialog->>User: Display metrics & latency breakdown
Loading

ding113 and others added 6 commits January 26, 2026 03:38
- Refactor error-details-dialog into modular tab-based components:
  - SummaryTab: key metrics overview with cost, tokens, duration
  - LogicTraceTab: provider decision chain visualization with steps
  - PerformanceTab: TTFB gauge, latency breakdown, output rate
  - MetadataTab: session, client, billing info with timeline

- Extract filters into dedicated components for better maintainability:
  - TimeFilters, IdentityFilters, RequestFilters, StatusFilters
  - QuickFiltersBar for common filter presets (today, errors, retries)
  - ActiveFiltersDisplay for showing/clearing active filter tags

- Add column visibility management with localStorage persistence
- Add message redaction utility for sensitive data masking
- Add scroll-area component for consistent scrollable containers
- Add session reuse indicator (Link2 icon) in provider chain display
- Update i18n messages for all 5 languages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add additional i18n keys for session reuse UI including:
- Session info labels (session ID, request sequence, age)
- Session reuse selection descriptions
- Cache optimization hint text

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add session reuse indicator and details in provider chain popover
- Add i18n translations for ja, ru, zh-TW locales
- Minor table display improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…text

- Enhance tooltip for single requests with session reuse details
  (session age, priority, cost multiplier)
- Add selection funnel visualization for initial selection
  (total -> enabled -> healthy providers)
- Show candidate providers with probability when multiple at same priority
- Display cost multiplier and group tag badges in retry popover trigger
- Remove redundant provider summary tooltip from logs table

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

总览

本PR扩展了仪表板日志UI,添加了带选项卡的详细错误视图、新的过滤器组件、列可见性控制,以及对多种语言的国际化支持。同时重构了多个现有组件,引入了消息脱敏和列持久化存储等新功能。

变更

组群 / 文件 变更摘要
国际化文件(仪表板翻译)
messages/en/dashboard.json, messages/ja/dashboard.json, messages/ru/dashboard.json, messages/zh-CN/dashboard.json, messages/zh-TW/dashboard.json
为日志详情视图添加了新的选项卡结构(summary、logicTrace、performance、metadata),以及对应的字段翻译。扩展了过滤器UI字符串(quickFilters、activeFilters、groups)。更新了统计信息和列可见性相关的翻译。
国际化文件(provider-chain翻译)
messages/en/provider-chain.json, messages/ja/provider-chain.json, messages/ru/provider-chain.json, messages/zh-CN/provider-chain.json, messages/zh-TW/provider-chain.json
新增 reasonsfilterReasonsdetailstechnicalTimeline 等顶级翻译键,用于provider链决策跟踪和过滤条件的本地化。
依赖管理
package.json
新增 @radix-ui/react-scroll-areaagentation 依赖。
错误详情对话框重构
src/app/[locale]/dashboard/logs/_components/error-details-dialog/
删除了原有的单体ErrorDetailsDialog组件,替换为模块化的Sheet+Tabs结构,包含4个新组件:SummaryTab、LogicTraceTab、PerformanceTab、MetadataTab,以及StepCard和LatencyBreakdownBar辅助组件。新增types.ts定义共享的选项卡属性和工具函数。
过滤器组件
src/app/[locale]/dashboard/logs/_components/filters/
新增多个过滤器组件:QuickFiltersBar(快速预设)、ActiveFiltersDisplay(活跃过滤器显示)、TimeFilters(时间范围)、IdentityFilters(用户和API密钥)、RequestFilters(提供商和模型)、StatusFilters(状态码),以及FilterSection和types/index支撑文件。
日志表格和UI
src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx, src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx, src/app/[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx, src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
新增ColumnVisibilityDropdown组件用于动态控制表格列可见性。重构VirtualizedLogsTable以支持多列隐藏。重写UsageLogsFilters为模块化过滤器架构。更新UsageLogsViewContent集成列可见性功能。
会话和统计UI
src/app/[locale]/dashboard/logs/_components/active-sessions-skeleton.tsx, src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx, src/components/customs/active-sessions-cards.tsx
重构ActiveSessionsSkeleton为Card基础布局。重写UsageLogsStatsPanel为固定的glass-morphism面板。新增ActiveSessionsCards组件展示活跃会话列表。
Provider链UI
src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx
扩展provider链视图,支持会话重用和初始选择的上下文。添加getItemStatus辅助函数。优化了多请求场景下的视觉链式渲染,包括成本倍数和分组标签显示。
消息脱敏工具
src/lib/utils/message-redaction.ts, src/lib/utils/message-redaction.test.ts
新增消息内容脱敏工具,用于redacting请求/响应体中的敏感信息。包含redactRequestBody、redactJsonString和redactMessages导出函数,及其comprehensive测试覆盖。
列可见性持久化
src/lib/column-visibility.ts, src/lib/column-visibility.test.ts
新增localStorage持久化方案用于管理用户的列可见性偏好,包含getHiddenColumns、setHiddenColumns、toggleColumn、resetColumns等导出函数,及完整测试套件。
其他组件更新
src/app/[locale]/dashboard/logs/_components/error-details-dialog.test.tsx, src/app/[locale]/dashboard/logs/_components/active-sessions-skeleton.tsx, src/app/[locale]/dashboard/logs/page.tsx, src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-details-tabs.tsx, src/app/providers.tsx, src/components/ui/scroll-area.tsx, src/components/ui/tag-input.tsx, tests/unit/error-details-dialog-warmup-ui.test.tsx
更新测试以适配Sheet和Tabs组件的新结构。重构日志页面移除getTranslations。集成Agentation开发工具。添加Radix UI ScrollArea。更新TagInput以使用Radix Portal。集成message redaction。

预估代码审查工作量

🎯 4 (Complex) | ⏱️ ~60 minutes

可能相关的PR

  • PR #581: 同时修改了日志表格和provider-chain UI(虚拟化表格和provider链popover),涉及相关的乘数/可见性渲染逻辑。

  • PR #574: 针对specialSettings字段在日志和对话框中的展示进行了统一处理和类型定义,与本PR的translations和ErrorDetailsDialog更新直接相关。

  • PR #632: 两个PR都触及仪表板日志的列可见性功能、相关翻译和组件,修改同一个表格可见性API和相关测试。

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题清晰准确地反映了PR的主要变化:增强日志UI,包含模块化过滤器和错误详情功能。
Description check ✅ Passed 描述与变更集高度相关,详细说明了重构的内容、新增组件、工具函数、国际化更新和测试计划。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/logs-ui-enhancement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a significant upgrade to the logs dashboard UI, focusing on modularity, detailed information display, and improved user interaction. It introduces a tabbed interface for error details, modularizes filtering options into distinct, collapsible sections, and adds persistent column visibility settings. These changes aim to provide a more intuitive, customizable, and informative experience for analyzing API request logs.

Highlights

  • Modular Error Details UI: The error details dialog has been refactored into modular, tab-based components (Summary, LogicTrace, Performance, Metadata) for better organization and presentation of request information.
  • Enhanced Logs Filtering: Filter components have been extracted into dedicated, reusable modules (Time, Identity, Request, Status filters) and are presented in collapsible sections with a glass morphism UI, improving maintainability and user experience.
  • Column Visibility Management: A new feature allows users to manage table column visibility, with preferences persisted in local storage for a customized viewing experience.
  • Detailed Provider Chain Popover: The provider chain popover now offers more detailed insights, including session reuse information (age, priority, cost multiplier) and initial selection context tooltips, along with visual status indicators for each step.
  • Sensitive Data Redaction: A new utility for message redaction has been implemented to mask sensitive data within API request/response bodies, enhancing privacy and security.
  • Internationalization Updates: All new UI elements and enhancements have been translated and updated across five languages (English, Japanese, Russian, Simplified Chinese, Traditional Chinese).
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request area:UI area:i18n labels Jan 25, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +151 to +153
<span className="text-zinc-200 dark:text-zinc-700">
{sessionReuseContext.sessionAge}s
</span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low contrast in dark mode - text-zinc-200 on dark background will be hard to read.

Suggested change
<span className="text-zinc-200 dark:text-zinc-700">
{sessionReuseContext.sessionAge}s
</span>
<span className="text-zinc-100 dark:text-zinc-800">
{sessionReuseContext.sessionAge}s
</span>
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx
Line: 151:153

Comment:
Low contrast in dark mode - `text-zinc-200` on dark background will be hard to read.

```suggestion
                          <span className="text-zinc-100 dark:text-zinc-800">
                            {sessionReuseContext.sessionAge}s
                          </span>
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant refactoring and enhancement of the logs and request details UI. Key changes include a complete overhaul of the ErrorDetailsDialog component, transforming it from a simple dialog into a tabbed sheet (Summary, Logic Trace, Performance, Metadata) to better organize and display detailed request information. New components like LatencyBreakdownBar, StepCard, and dedicated tab components (SummaryTab, LogicTraceTab, PerformanceTab, MetadataTab) were added to support this new structure, providing richer visualizations for performance metrics and a step-by-step trace of provider decisions. The filtering system for usage logs was also refactored, introducing QuickFiltersBar, ActiveFiltersDisplay, and FilterSection components to group filters logically and display active filters clearly. Column visibility in the logs table is now customizable and persistent via a new ColumnVisibilityDropdown. Additionally, the ActiveSessionsPanel was updated to ActiveSessionsCards with a new skeleton loader, and message content redaction logic was introduced for sensitive data. Review comments highlighted that the LogicTraceTab's filteredProviders logic was too restrictive, only considering rate_limited or circuit_open reasons, and should be expanded to include all filter reasons from decisionContext for completeness. Another comment pointed out that the MetadataTab was created but not rendered, with its content duplicated in SummaryTab, suggesting either removal or proper integration of MetadataTab to avoid dead code and confusion.

Comment on lines 108 to 112
const filteredProviders = isSessionReuseFlow
? []
: providerChain
?.flatMap((item) => item.decisionContext?.filteredProviders || [])
.filter((p) => p.reason === "rate_limited" || p.reason === "circuit_open") || [];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic to get filteredProviders seems too restrictive. It only considers providers filtered due to rate_limited or circuit_open. However, providers can be filtered for other reasons like disabled, model_not_supported, group_mismatch, or health_check_failed.

This can cause the "Health Check" step card to be either missing or display incomplete information. To ensure the logic trace is complete and accurate, all filtered providers from the decisionContext should be considered.

  const filteredProviders = isSessionReuseFlow
    ? []
    : providerChain?.flatMap((item) => item.decisionContext?.filteredProviders || []) || [];

Comment on lines +226 to +262
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as TabValue)}
className="w-full"
>
<TabsList className="w-full grid grid-cols-3 h-auto p-1">
<TabsTrigger
value="summary"
className={cn(
"flex items-center gap-1.5 px-2 py-1.5 text-xs sm:text-sm",
"data-[state=active]:bg-background"
)}
>
<FileText className="h-3.5 w-3.5 sm:h-4 sm:w-4 shrink-0" />
<span className="hidden sm:inline">{t("tabs.summary")}</span>
</TabsTrigger>
<TabsTrigger
value="logic-trace"
className={cn(
"flex items-center gap-1.5 px-2 py-1.5 text-xs sm:text-sm",
"data-[state=active]:bg-background"
)}
>
<GitBranch className="h-3.5 w-3.5 sm:h-4 sm:w-4 shrink-0" />
<span className="hidden sm:inline">{t("tabs.logicTrace")}</span>
</TabsTrigger>
<TabsTrigger
value="performance"
className={cn(
"flex items-center gap-1.5 px-2 py-1.5 text-xs sm:text-sm",
"data-[state=active]:bg-background"
)}
>
<Gauge className="h-3.5 w-3.5 sm:h-4 sm:w-4 shrink-0" />
<span className="hidden sm:inline">{t("tabs.performance")}</span>
</TabsTrigger>
</TabsList>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PR description and i18n files mention a "Metadata" tab, and the component MetadataTab.tsx exists. However, this tab is not rendered here, and its content seems to be duplicated within SummaryTab.tsx. This creates dead code and can be confusing for future maintenance.

To improve maintainability, I recommend either:

  1. Removing the unused MetadataTab.tsx file and the corresponding "Metadata" tab translations from the messages/*.json files.
  2. Refactoring to use MetadataTab for its intended purpose and moving the relevant content out of SummaryTab.

@github-actions github-actions bot added the size/L Large PR (< 1000 lines) label Jan 25, 2026
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR successfully refactors the logs UI into modular, maintainable components with comprehensive test coverage and proper i18n support across all 5 languages.

PR Size: L

  • Lines changed: 9,677 (7,238 additions + 2,439 deletions)
  • Files changed: 50

Recommendation: This is a large but well-structured refactor. The modularization improves maintainability significantly. Consider the following for future large PRs:

  • Split UI refactoring from new feature additions
  • Separate i18n updates into a dedicated commit
  • Break filter components into a separate PR from error dialog refactoring

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 1 0
Simplification 0 0 0 0

Medium Priority Issues (Consider Fixing)

1. Missing Test Coverage for Filter Components

  • Location: directory
  • Issue: New filter components (TimeFilters, IdentityFilters, RequestFilters, StatusFilters, QuickFiltersBar, ActiveFiltersDisplay) lack unit tests
  • Impact: While the main utilities have excellent test coverage (message-redaction: 25 tests, column-visibility: 26 tests), the filter UI components are untested
  • Recommendation: Add unit tests for filter components to maintain the project's high test coverage standard (80%+ requirement per CLAUDE.md)

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error Handling - Clean (proper logging, graceful fallbacks)
  • Type safety - Clean (no any usage, proper TypeScript)
  • Documentation accuracy - Clean (comments match implementation)
  • Test coverage - Good for utilities, missing for UI components
  • Code clarity - Excellent (well-structured, modular)

Positive Observations

Code Quality:

  • Excellent modularization: Error dialog split into 5 focused tab components
  • Proper error handling with console.error logging (error-details-dialog/index.tsx:115)
  • Graceful fallbacks for localStorage failures (column-visibility.ts:67, 107)
  • SSR-safe with proper typeof window checks (column-visibility.ts:55, 96)

Test Coverage:

  • Comprehensive test suites for core utilities (message-redaction: 297 lines, column-visibility: 216 lines)
  • Tests cover edge cases: empty arrays, invalid JSON, localStorage errors, SSR scenarios
  • Error dialog tests updated to match new modular structure (399 lines)

Type Safety:

  • No any types found in new code
  • Proper TypeScript interfaces and type exports
  • Strong type definitions in filters/types.ts and error-details-dialog/types.ts

i18n Compliance:

  • All user-facing strings properly internationalized across 5 languages
  • No hardcoded display text found
  • Consistent translation key structure

Security:

  • Message redaction utility properly sanitizes sensitive data
  • No XSS vulnerabilities (proper React rendering)
  • No SQL injection risks (no direct DB queries in UI layer)
  • No hardcoded secrets or credentials

Automated review by Claude AI

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR successfully refactors the logs UI into modular, maintainable components with comprehensive test coverage and proper i18n support across all 5 languages.

PR Size: L

  • Lines changed: 9,677 (7,238 additions + 2,439 deletions)
  • Files changed: 50

Recommendation: This is a large but well-structured refactor. The modularization improves maintainability significantly. Consider the following for future large PRs:

  • Split UI refactoring from new feature additions
  • Separate i18n updates into a dedicated commit
  • Break filter components into a separate PR from error dialog refactoring

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 1 0
Simplification 0 0 0 0

Medium Priority Issues (Consider Fixing)

1. Missing Test Coverage for Filter Components

  • Location: src/app/[locale]/dashboard/logs/_components/filters/ directory
  • Issue: New filter components (TimeFilters, IdentityFilters, RequestFilters, StatusFilters, QuickFiltersBar, ActiveFiltersDisplay) lack unit tests
  • Impact: While the main utilities have excellent test coverage (message-redaction: 25 tests, column-visibility: 26 tests), the filter UI components are untested
  • Recommendation: Add unit tests for filter components to maintain the project's high test coverage standard (80%+ requirement per CLAUDE.md)

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error Handling - Clean (proper logging, graceful fallbacks)
  • Type safety - Clean (no any usage, proper TypeScript)
  • Documentation accuracy - Clean (comments match implementation)
  • Test coverage - Good for utilities, missing for UI components
  • Code clarity - Excellent (well-structured, modular)

Positive Observations

Code Quality:

  • Excellent modularization: Error dialog split into 5 focused tab components
  • Proper error handling with console.error logging (error-details-dialog/index.tsx:115)
  • Graceful fallbacks for localStorage failures (column-visibility.ts:67, 107)
  • SSR-safe with proper typeof window checks (column-visibility.ts:55, 96)

Test Coverage:

  • Comprehensive test suites for core utilities (message-redaction: 297 lines, column-visibility: 216 lines)
  • Tests cover edge cases: empty arrays, invalid JSON, localStorage errors, SSR scenarios
  • Error dialog tests updated to match new modular structure (399 lines)

Type Safety:

  • No any types found in new code
  • Proper TypeScript interfaces and type exports
  • Strong type definitions in filters/types.ts and error-details-dialog/types.ts

i18n Compliance:

  • All user-facing strings properly internationalized across 5 languages
  • No hardcoded display text found
  • Consistent translation key structure

Security:

  • Message redaction utility properly sanitizes sensitive data
  • No XSS vulnerabilities (proper React rendering)
  • No SQL injection risks (no direct DB queries in UI layer)
  • No hardcoded secrets or credentials

Automated review by Claude AI

@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx (2)

45-61: 避免旧请求覆盖最新统计数据

当前没有并发请求的序号/取消逻辑,快速切换过滤器时,较慢的旧请求可能在后续返回并覆盖最新统计,导致展示错误。建议为每次请求加序号或取消逻辑,仅应用最后一次结果。

建议修复(请求序号防抖)
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
@@
   const [stats, setStats] = useState<UsageLogSummary | null>(null);
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
+  const requestIdRef = useRef(0);
@@
   const loadStats = useCallback(async () => {
+    const requestId = ++requestIdRef.current;
     setIsLoading(true);
     setError(null);
 
     try {
       const result = await getUsageLogsStats(filters);
+      if (requestId !== requestIdRef.current) {
+        return;
+      }
       if (result.ok && result.data) {
         setStats(result.data);
       } else {
         setError(!result.ok ? result.error : t("logs.error.loadFailed"));
       }
     } catch (err) {
+      if (requestId !== requestIdRef.current) {
+        return;
+      }
       console.error("Failed to load usage logs stats:", err);
       setError(t("logs.error.loadFailed"));
     } finally {
-      setIsLoading(false);
+      if (requestId === requestIdRef.current) {
+        setIsLoading(false);
+      }
     }
   }, [filters, t]);

164-178: 补充totalTokens卡片中缓存token的拆分,或调整标签说明

totalTokens包含四部分:输入 + 输出 + 缓存写 + 缓存读。但卡片仅展示输入/输出拆分,导致显示的拆分之和小于总数,容易令用户困惑。虽然缓存tokens在单独卡片中展示,但用户在看totalTokens卡片时无法从其中的拆分推断出总数。建议:

  1. 在此卡片下补充缓存拆分(参考缓存tokens卡片的格式),或
  2. 在卡片标签/副标题中明确说明「包含缓存」
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (1)

145-168: 导出时应复用 sanitizeFilters 避免无效字段

handleApply 已做过滤,但导出仍直接发送 localFilters,若上游混入 page 等运行时字段可能导致导出接口异常或被忽略。建议导出也先清洗。

示例修正
-      const result = await exportUsageLogs(localFilters);
+      const result = await exportUsageLogs(sanitizeFilters(localFilters));
🤖 Fix all issues with AI agents
In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx:
- Around line 57-98: The component LatencyBreakdownBar contains hard-coded
user-facing strings ("TTFB", "Generation", "Total:") that must be replaced with
i18n keys; update the JSX in LatencyBreakdownBar.tsx to call the i18n helper
t(...) instead of raw strings (e.g., use
t("dashboard.logs.details.performanceTab.ttfb") for TTFB,
t("dashboard.logs.details.performanceTab.generation") for Generation labels and
segments, and t("dashboard.logs.details.performanceTab.total") for the Total
label) while keeping existing conditions using ttfbPercent/generationPercent and
showLabels, and add the corresponding keys and translations to your locale JSON
files so the text renders correctly in all locales.

In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx:
- Around line 275-297: Several UI labels in LogicTraceTab.tsx are hardcoded
(e.g., "Provider", "ID", priority/cost labels and others at ranges 323-342,
350-356, 506-523), breaking i18n; replace them with i18n lookups. Update the JSX
around sessionReuseProvider usage to call t() or tChain() for each label (e.g.,
replace "Provider"/"ID"/the priority and costMultiplier label strings with
tChain("...") keys), add corresponding keys into the messages/i18n file, and use
the same pattern for the other hardcoded labels noted in the comment so all
displayed labels are rendered via t()/tChain() instead of raw strings.
- Around line 82-88: handleCopyTimeline currently calls
navigator.clipboard.writeText(...) without handling rejection; wrap the
clipboard call in a try/catch or attach a .catch handler to swallow or surface
errors and provide user feedback. Specifically, update the handleCopyTimeline
function (and the promise chain that sets timelineCopied via setTimelineCopied
and setTimeout) to catch clipboard failures and either call a fallback UI
notification (e.g., show an error toast) or silently ignore the error to prevent
unhandled promise rejections.

In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx:
- Around line 61-67: The component currently treats costUsd as falsy which hides
metadata when costUsd === 0; update the truthy checks to explicitly test for
non-null/undefined instead of truthiness: change the hasAnyData expression (the
const hasAnyData that includes costUsd) to use costUsd != null, and make the
same change to the other conditional that gates the billing/metadata rendering
(the conditional around costUsd in the render block referenced in the diff at
lines ~142-145) so that costUsd === 0 is treated as valid data.
- Around line 125-135: The component MetadataTab.tsx has hard-coded user-facing
labels (e.g., "User-Agent", "Endpoint", "tokens", "1M Context") which bypass
i18n; update the JSX to call the translation helper t(...) for each visible
string (refer to the component and props/variables userAgent, endpoint, tokens
and the string "1M Context") and add the corresponding translation keys into the
locale resource files for the five required languages; ensure the same change is
applied to the other occurrences noted in the file (the blocks rendering tokens
and the 1M context label) so all user-facing text uses t(...) consistently.

In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx:
- Around line 109-119: The hasData check in PerformanceTab.tsx incorrectly
treats cases with only output tokens as "no data"; update the hasData boolean to
also consider normalizedOutputTokens (e.g., const hasData = normalizedDurationMs
!== null || normalizedTtfbMs !== null || outputRate !== null ||
normalizedOutputTokens !== null) so that when only tokens are present the
component renders the details instead of the empty state; locate and modify the
hasData definition in the PerformanceTab component accordingly.
- Around line 183-256: Replace the hard-coded user-facing strings in
PerformanceTab.tsx with i18n keys: change the literal "Output Tokens" label and
the "tok/s" unit used in the outputRate display to use t(...) (e.g.
t("performance.outputTokens") and include the unit via
t("performance.tokensPerSecond") or interpolate into the existing
t("performance.outputRate")), updating the JSX nodes that render
normalizedOutputTokens (where formatTokenAmount is used) and
outputRate.toFixed(1) to call t for the label/unit; then add the corresponding
translation keys to the locale resource files so the texts are localized.

In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/StepCard.tsx:
- Around line 87-155: The displayed relative time string is hard-coded
("+{relativeTime.toFixed(0)}ms") and must be localized; update the StepCard
component to use the project's i18n formatting helper instead of inline text:
import and use the same formatter pattern (e.g., createFormatRelativeTime or the
i18n callback used in key-list-header.tsx) to render relativeTime (rounded as
needed) with the correct localized prefix/suffix and units, replacing the
current +{relativeTime.toFixed(0)}ms span; ensure you use the existing function
names (relativeTime, StepCard, createFormatRelativeTime) and preserve the
conditional rendering when relativeTime !== null.

In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx:
- Around line 98-105: Replace all hard-coded UI strings in SummaryTab (e.g., the
Badge text "OK"/"Error" and labels "User-Agent", "Endpoint", "tokens", "tok/s",
multiplier labels and any "Context" text) with calls to the i18n translator
t(...); add new translation keys (like summary.ok, summary.error,
summary.userAgent, summary.endpoint, summary.tokens, summary.tokensPerSec,
summary.multiplier, summary.context) and use t("summary.xxx") where those
strings appear (not only at lines 98-105 but also in the other noted ranges:
173-174, 226-235, 255-301, 324-329) so all five languages receive the entries;
ensure Badge uses t(condition ? "summary.ok" : "summary.error") and replace
every hard-coded label in JSX with the corresponding t(...) reference.

In `@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/index.tsx:
- Around line 5-124: The effect that calls hasSessionMessages in useEffect may
suffer from race conditions when open or sessionId change quickly, causing stale
promises to overwrite hasMessages/checkingMessages; update the effect to track a
local requestId or cancellation flag (e.g., using a ref like currentRequestId)
and capture the id when starting the async call in useEffect, then only call
setHasMessages and setCheckingMessages in the .then/.catch/.finally if the
captured id matches the latest ref (or if not cancelled); ensure you increment
or mark the ref when a new request starts and clear/ignore results when the
component unmounts or parameters change so outdated responses are ignored,
leaving function names useEffect, hasSessionMessages, setHasMessages,
setCheckingMessages, and sessionId/requestSequence clearly targeted.

In `@src/app/`[locale]/dashboard/logs/_components/filters/identity-filters.tsx:
- Around line 133-150: The useEffect that runs loadInitialKeys currently has an
empty dependency array and won't react to changes in filters.userId; update the
effect to include filters.userId, isAdmin, and initialKeys (or relevant
primitives) in its dependency array and add a useRef guard (e.g., loadedRef)
inside the loadInitialKeys logic to prevent duplicate loads; locate the
useEffect and the loadInitialKeys function in identity-filters.tsx and change
the dependencies and guard so when filters.userId changes the keys are fetched
(calling getKeys, setKeys, and onKeysChange as before) but repeated or
concurrent loads are prevented.

In `@src/app/`[locale]/dashboard/logs/_components/provider-chain-popover.tsx:
- Around line 146-210: The tooltip contains hard-coded English labels (e.g.,
"Age", "Initial Selection", "total", "enabled", "healthy", "candidates") inside
provider-chain-popover.tsx; replace these with i18n calls using existing helpers
(tChain and t) where appropriate: use tChain("timeline.sessionAge") fallback
removed and render the translated label instead of "Age"; add keys for
"timeline.initialSelection", "timeline.total", "timeline.enabled",
"timeline.healthy" and "details.candidates" (or similar) and call t()/tChain()
when rendering the strings around sessionReuseContext/sessionReuseItem and
selectionContext; also replace inline concatenations like `P{...}` and `({...}
candidates)` with translated templates or concatenated translated fragments
using t/tChain so all visible strings go through i18n while keeping the numeric
values from selectionContext/sessionReuseItem unchanged.
- Around line 213-233: The percent display in provider-chain-popover.tsx is
inconsistent with LogicTraceTab: update the rendering inside
selectionContext.candidatesAtPriority so the displayed value normalizes
probabilities in c.probability (if value is between 0 and 1 multiply by 100) and
then format it to a human-friendly percent (e.g., round or toFixed) before
appending '%' in the span that currently renders ({c.probability}%), ensuring
selectionContext.candidatesAtPriority, c.probability and displayName are used to
locate and update the logic.

In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx:
- Around line 340-379: The icon-only buttons (the three Button components
rendering Expand, RefreshCw, and the Play/Pause variants) lack accessible names
when the sm-hidden spans are not visible; update each Button to include an
aria-label prop that reuses the corresponding i18n text from t(...) (e.g.,
t("logs.actions.fullscreen"), t("logs.actions.refresh"),
t("logs.actions.stopAutoRefresh") / t("logs.actions.startAutoRefresh")) and
ensure the aria-label logic matches the dynamic state for the auto-refresh
button (use isAutoRefresh to pick stop vs start); reference the Button
components in this file and the t(...) calls already present to implement the
change.

In `@src/components/customs/active-sessions-cards.tsx`:
- Around line 20-35: Replace hardcoded user-facing strings in
fetchActiveSessions and formatDuration with i18n lookups: change the thrown
message "Failed to fetch active sessions" to use a translation key (e.g.,
t('activeSessions.fetchFailed', { defaultValue: 'Failed to fetch active
sessions' })) inside fetchActiveSessions, and replace "-" and units ("ms", "s",
"m", the minutes/seconds formatted output) in formatDuration with i18n keys
(e.g., t('activeSessions.duration.empty'), t('units.ms'), t('units.s'),
t('units.m') or a pluralized/templated key like
t('activeSessions.duration.minutesSeconds', { minutes, seconds })). Also audit
related UI strings such as "in"/"out" referenced elsewhere (lines noted) and
switch them to translation keys using the same translation function/hook
(useTranslation/t) to ensure all user-facing text is localized.
- Around line 145-176: Replace the router.push navigation with the locale-aware
Link from "@/i18n/routing": remove or stop using useRouter() and wrap or replace
the button that calls router.push("/dashboard/sessions") with the Link component
(href="/dashboard/sessions") so the locale prefix is preserved; import Link from
"@/i18n/routing", keep the existing button classes/contents inside the Link (or
render an anchor with the same className) and remove the now-unused
router/useRouter import.

In `@src/lib/utils/message-redaction.ts`:
- Line 8: REDACTED_MARKER is a hard-coded user-visible string — replace it with
an i18n-aware value: either accept a localized marker parameter and thread it
into redactRequestBody, redactMessages, and redactJsonString from callers, or
keep an internal neutral marker and map that marker to a localized string in the
presentation layer (e.g., during final JSON stringify/render); update all call
sites (the ~15+ uses) to pass or handle the localized marker consistently and
remove the hardcoded "[REDACTED]" constant from message-redaction.ts.
🧹 Nitpick comments (10)
src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx (1)

41-43: filtersKey 与 loadStats 依赖不一致,可能导致无效的去重

尽管引入了 filtersKey,但 loadStats 仍依赖 filters 对象本身,只要引用变化就会重建并触发 useEffect。若目的是在值不变时避免重复请求,建议让 loadStats 接收 filters 参数并只依赖 filtersKey,或直接移除 filtersKey。

Also applies to: 64-68

src/lib/column-visibility.test.ts (1)

12-34: localStorage mock 的 length 属性应为动态计算。

当前 length: 0 是静态值,但真实的 localStorage.length 应该反映当前存储的键数量。虽然当前测试未使用该属性,但为了 mock 的完整性,建议改为 getter。

建议的修复
 const mockLocalStorage = {
   getItem: vi.fn((key: string) => mockStorage[key] ?? null),
   setItem: vi.fn((key: string, value: string) => {
     mockStorage[key] = value;
   }),
   removeItem: vi.fn((key: string) => {
     delete mockStorage[key];
   }),
   clear: vi.fn(() => {
     for (const key of Object.keys(mockStorage)) {
       delete mockStorage[key];
     }
   }),
-  length: 0,
+  get length() {
+    return Object.keys(mockStorage).length;
+  },
   key: vi.fn(),
 };
src/app/providers.tsx (1)

4-26: 建议延迟加载 Agentation 以避免生产包潜在副作用。
当前为静态导入,即使仅在 development 分支渲染,构建阶段仍可能引入副作用或增大包体积。若该库仅用于开发调试,建议改为动态导入并确认其浏览器兼容性。

建议修改
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
-import { Agentation } from "agentation";
+import dynamic from "next/dynamic";
 import { ThemeProvider } from "next-themes";
 import { type ReactNode, useState } from "react";

+const Agentation = dynamic(
+  () => import("agentation").then((mod) => mod.Agentation),
+  { ssr: false },
+);
messages/zh-TW/dashboard.json (1)

262-274: details.billingDetailslogs.billingDetails 存在重复定义

文件中存在两处 billingDetails 定义:

  1. Lines 262-274: logs.details.billingDetails
  2. Lines 346-357: logs.billingDetails

这两个对象包含相同的键但翻译内容略有不同(如 context1m 分别为 "1M 上下文" 和 "1M 上下文長度",context1mPricing 格式也有差异)。建议统一使用一个定义,避免维护不一致的风险。

Also applies to: 346-357

messages/en/dashboard.json (1)

262-274: details.billingDetailslogs.billingDetails 存在重复定义

与 zh-TW 文件相同,英文版本也存在两处 billingDetails 定义。建议消除重复以保持单一数据源。

Also applies to: 346-357

tests/unit/error-details-dialog-warmup-ui.test.tsx (1)

139-179: 测试用例覆盖了 warmup 跳过指示器的核心场景

测试验证了当 blockedBy="warmup" 时:

  1. 显示 "Warmup Fast Response (CCH)" 和 "Skipped" 文本
  2. 不显示 "Blocking Information"

但当前只有一个测试用例。建议补充以下场景以提高覆盖率:

  • blockedBy 为其他值(如敏感词拦截)时的显示
  • blockedBynull 时的正常请求显示
src/app/[locale]/dashboard/logs/_components/filters/status-filters.tsx (1)

34-38: 注释与代码逻辑不一致

注释说"合并硬编码和动态状态码",但代码实际上只是过滤掉了常见状态码,返回的是非重复的动态状态码。建议更新注释以准确描述实际行为。

建议修改
-  // Merge hard-coded and dynamic status codes (deduplicated)
+  // Filter out common status codes from dynamic list (deduplication)
   const allStatusCodes = useMemo(() => {
     const dynamicOnly = dynamicStatusCodes.filter((code) => !COMMON_STATUS_CODES.includes(code));
     return dynamicOnly;
   }, [dynamicStatusCodes]);
src/app/[locale]/dashboard/logs/_components/filters/quick-filters-bar.tsx (1)

19-27: 可选优化:memoize preset 数组

timePresetsfilterPresets 数组在每次渲染时都会重新创建。虽然数组较小,影响不大,但可以考虑使用 useMemo 优化。

可选的优化方案
+import { useMemo } from "react";
...
-  const timePresets: Array<{ id: FilterPreset; label: string; icon: typeof Calendar }> = [
-    { id: "today", label: t("quickFilters.today"), icon: Calendar },
-    { id: "this-week", label: t("quickFilters.thisWeek"), icon: CalendarDays },
-  ];
-
-  const filterPresets: Array<{ id: FilterPreset; label: string; icon: typeof AlertCircle }> = [
-    { id: "errors-only", label: t("quickFilters.errorsOnly"), icon: AlertCircle },
-    { id: "show-retries", label: t("quickFilters.showRetries"), icon: RefreshCw },
-  ];
+  const timePresets = useMemo(
+    () => [
+      { id: "today" as const, label: t("quickFilters.today"), icon: Calendar },
+      { id: "this-week" as const, label: t("quickFilters.thisWeek"), icon: CalendarDays },
+    ],
+    [t]
+  );
+
+  const filterPresets = useMemo(
+    () => [
+      { id: "errors-only" as const, label: t("quickFilters.errorsOnly"), icon: AlertCircle },
+      { id: "show-retries" as const, label: t("quickFilters.showRetries"), icon: RefreshCw },
+    ],
+    [t]
+  );
src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx (1)

47-53: 考虑使用惰性初始化减少闪烁

当前实现在初始渲染时 hiddenColumns 为空数组,然后 useEffect 从 localStorage 加载数据,这可能导致短暂的 UI 闪烁(所有列先显示为可见,然后更新为实际状态)。

由于 getHiddenColumns 已经处理了 SSR 场景(typeof window === "undefined" 返回空数组),可以考虑使用惰性初始化:

建议修改
-  const [hiddenColumns, setHiddenColumnsState] = useState<LogsTableColumn[]>([]);
-
-  // Load initial state from localStorage
-  useEffect(() => {
-    const stored = getHiddenColumns(userId, tableId);
-    setHiddenColumnsState(stored);
-  }, [userId, tableId]);
+  const [hiddenColumns, setHiddenColumnsState] = useState<LogsTableColumn[]>(() =>
+    getHiddenColumns(userId, tableId)
+  );
+
+  // Sync when userId or tableId changes
+  useEffect(() => {
+    setHiddenColumnsState(getHiddenColumns(userId, tableId));
+  }, [userId, tableId]);
src/app/[locale]/dashboard/logs/_components/filters/request-filters.tsx (1)

29-38: 复用共享类型并统一路径别名。

RequestFiltersProps 已在 filters/types.ts 定义,建议直接引用避免重复维护;同时将相对路径替换为 @/ 别名以符合规范。As per coding guidelines.

建议修改
-import type { ProviderDisplay } from "@/types/provider";
-import { useLazyEndpoints, useLazyModels } from "../../_hooks/use-lazy-filter-options";
-import type { UsageLogFilters } from "./types";
+import { useLazyEndpoints, useLazyModels } from "@/app/[locale]/dashboard/logs/_hooks/use-lazy-filter-options";
+import type { RequestFiltersProps } from "./types";
...
-interface RequestFiltersProps {
-  isAdmin: boolean;
-  filters: UsageLogFilters;
-  onFiltersChange: (filters: UsageLogFilters) => void;
-  providers: ProviderDisplay[];
-  isProvidersLoading?: boolean;
-}

Comment on lines +57 to +98
{ttfbMs > 0 && (
<div
className="flex items-center justify-center bg-blue-500 text-white text-[10px] font-medium transition-all duration-300"
style={{ width: `${adjustedTtfbPercent}%` }}
title={`TTFB: ${formatMs(ttfbMs)} (${ttfbPercent.toFixed(1)}%)`}
>
{ttfbPercent >= 15 && <span>TTFB</span>}
</div>
)}

{/* Generation segment */}
{generationMs > 0 && (
<div
className="flex items-center justify-center bg-emerald-500 text-white text-[10px] font-medium transition-all duration-300"
style={{ width: `${adjustedGenerationPercent}%` }}
title={`Generation: ${formatMs(generationMs)} (${generationPercent.toFixed(1)}%)`}
>
{generationPercent >= 15 && <span>Generation</span>}
</div>
)}
</div>

{/* Labels */}
{showLabels && (
<div className="flex justify-between text-xs">
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-sm bg-blue-500" />
<span className="text-muted-foreground">TTFB:</span>
<span className="font-mono font-medium">{formatMs(ttfbMs)}</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-sm bg-emerald-500" />
<span className="text-muted-foreground">{t("generationTime")}:</span>
<span className="font-mono font-medium">{formatMs(generationMs)}</span>
</div>
</div>
)}

{/* Total */}
<div className="text-xs text-muted-foreground text-center">
Total: <span className="font-mono font-medium">{formatMs(durationMs)}</span>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

用户可见文本需要使用 i18n

根据编码规范,所有用户可见字符串必须使用 i18n。当前存在多处硬编码文本:

  • Line 61, 63, 84: "TTFB"
  • Line 72, 74: "Generation"
  • Line 97: "Total:"

注意到 Line 89 已经正确使用了 t("generationTime"),但其他位置未保持一致。

建议修改
           <div
             className="flex items-center justify-center bg-blue-500 text-white text-[10px] font-medium transition-all duration-300"
             style={{ width: `${adjustedTtfbPercent}%` }}
-            title={`TTFB: ${formatMs(ttfbMs)} (${ttfbPercent.toFixed(1)}%)`}
+            title={`${t("ttfb")}: ${formatMs(ttfbMs)} (${ttfbPercent.toFixed(1)}%)`}
           >
-            {ttfbPercent >= 15 && <span>TTFB</span>}
+            {ttfbPercent >= 15 && <span>{t("ttfb")}</span>}
           </div>
         )}

         {/* Generation segment */}
         {generationMs > 0 && (
           <div
             className="flex items-center justify-center bg-emerald-500 text-white text-[10px] font-medium transition-all duration-300"
             style={{ width: `${adjustedGenerationPercent}%` }}
-            title={`Generation: ${formatMs(generationMs)} (${generationPercent.toFixed(1)}%)`}
+            title={`${t("generationTime")}: ${formatMs(generationMs)} (${generationPercent.toFixed(1)}%)`}
           >
-            {generationPercent >= 15 && <span>Generation</span>}
+            {generationPercent >= 15 && <span>{t("generationTime")}</span>}
           </div>
         )}
       </div>

       {/* Labels */}
       {showLabels && (
         <div className="flex justify-between text-xs">
           <div className="flex items-center gap-1.5">
             <div className="h-2.5 w-2.5 rounded-sm bg-blue-500" />
-            <span className="text-muted-foreground">TTFB:</span>
+            <span className="text-muted-foreground">{t("ttfb")}:</span>
             <span className="font-mono font-medium">{formatMs(ttfbMs)}</span>
           </div>
           ...
         </div>
       )}

       {/* Total */}
       <div className="text-xs text-muted-foreground text-center">
-        Total: <span className="font-mono font-medium">{formatMs(durationMs)}</span>
+        {t("total")}: <span className="font-mono font-medium">{formatMs(durationMs)}</span>
       </div>

需要在对应的 locale JSON 文件中添加翻译键(如 dashboard.logs.details.performanceTab.ttfbdashboard.logs.details.performanceTab.total)。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ttfbMs > 0 && (
<div
className="flex items-center justify-center bg-blue-500 text-white text-[10px] font-medium transition-all duration-300"
style={{ width: `${adjustedTtfbPercent}%` }}
title={`TTFB: ${formatMs(ttfbMs)} (${ttfbPercent.toFixed(1)}%)`}
>
{ttfbPercent >= 15 && <span>TTFB</span>}
</div>
)}
{/* Generation segment */}
{generationMs > 0 && (
<div
className="flex items-center justify-center bg-emerald-500 text-white text-[10px] font-medium transition-all duration-300"
style={{ width: `${adjustedGenerationPercent}%` }}
title={`Generation: ${formatMs(generationMs)} (${generationPercent.toFixed(1)}%)`}
>
{generationPercent >= 15 && <span>Generation</span>}
</div>
)}
</div>
{/* Labels */}
{showLabels && (
<div className="flex justify-between text-xs">
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-sm bg-blue-500" />
<span className="text-muted-foreground">TTFB:</span>
<span className="font-mono font-medium">{formatMs(ttfbMs)}</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-sm bg-emerald-500" />
<span className="text-muted-foreground">{t("generationTime")}:</span>
<span className="font-mono font-medium">{formatMs(generationMs)}</span>
</div>
</div>
)}
{/* Total */}
<div className="text-xs text-muted-foreground text-center">
Total: <span className="font-mono font-medium">{formatMs(durationMs)}</span>
</div>
{ttfbMs > 0 && (
<div
className="flex items-center justify-center bg-blue-500 text-white text-[10px] font-medium transition-all duration-300"
style={{ width: `${adjustedTtfbPercent}%` }}
title={`${t("ttfb")}: ${formatMs(ttfbMs)} (${ttfbPercent.toFixed(1)}%)`}
>
{ttfbPercent >= 15 && <span>{t("ttfb")}</span>}
</div>
)}
{/* Generation segment */}
{generationMs > 0 && (
<div
className="flex items-center justify-center bg-emerald-500 text-white text-[10px] font-medium transition-all duration-300"
style={{ width: `${adjustedGenerationPercent}%` }}
title={`${t("generationTime")}: ${formatMs(generationMs)} (${generationPercent.toFixed(1)}%)`}
>
{generationPercent >= 15 && <span>{t("generationTime")}</span>}
</div>
)}
</div>
{/* Labels */}
{showLabels && (
<div className="flex justify-between text-xs">
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-sm bg-blue-500" />
<span className="text-muted-foreground">{t("ttfb")}:</span>
<span className="font-mono font-medium">{formatMs(ttfbMs)}</span>
</div>
<div className="flex items-center gap-1.5">
<div className="h-2.5 w-2.5 rounded-sm bg-emerald-500" />
<span className="text-muted-foreground">{t("generationTime")}:</span>
<span className="font-mono font-medium">{formatMs(generationMs)}</span>
</div>
</div>
)}
{/* Total */}
<div className="text-xs text-muted-foreground text-center">
{t("total")}: <span className="font-mono font-medium">{formatMs(durationMs)}</span>
</div>
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx
around lines 57 - 98, The component LatencyBreakdownBar contains hard-coded
user-facing strings ("TTFB", "Generation", "Total:") that must be replaced with
i18n keys; update the JSX in LatencyBreakdownBar.tsx to call the i18n helper
t(...) instead of raw strings (e.g., use
t("dashboard.logs.details.performanceTab.ttfb") for TTFB,
t("dashboard.logs.details.performanceTab.generation") for Generation labels and
segments, and t("dashboard.logs.details.performanceTab.total") for the Total
label) while keeping existing conditions using ttfbPercent/generationPercent and
showLabels, and add the corresponding keys and translations to your locale JSON
files so the text renders correctly in all locales.

Comment on lines 82 to 88
const handleCopyTimeline = () => {
if (!providerChain) return;
const { timeline } = formatProviderTimeline(providerChain, tChain);
navigator.clipboard.writeText(timeline).then(() => {
setTimelineCopied(true);
setTimeout(() => setTimelineCopied(false), 2000);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

复制时间线缺少失败处理

navigator.clipboard.writeText 失败会产生未处理拒绝;建议捕获异常并给出反馈或至少吞掉错误,避免控制台噪音。

修改建议
-  const handleCopyTimeline = () => {
-    if (!providerChain) return;
-    const { timeline } = formatProviderTimeline(providerChain, tChain);
-    navigator.clipboard.writeText(timeline).then(() => {
-      setTimelineCopied(true);
-      setTimeout(() => setTimelineCopied(false), 2000);
-    });
-  };
+  const handleCopyTimeline = async () => {
+    if (!providerChain) return;
+    const { timeline } = formatProviderTimeline(providerChain, tChain);
+    try {
+      await navigator.clipboard.writeText(timeline);
+      setTimelineCopied(true);
+      setTimeout(() => setTimelineCopied(false), 2000);
+    } catch {
+      // 可选:在此处记录失败或提示用户
+    }
+  };
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx
around lines 82 - 88, handleCopyTimeline currently calls
navigator.clipboard.writeText(...) without handling rejection; wrap the
clipboard call in a try/catch or attach a .catch handler to swallow or surface
errors and provide user feedback. Specifically, update the handleCopyTimeline
function (and the promise chain that sets timelineCopied via setTimelineCopied
and setTimeout) to catch clipboard failures and either call a fallback UI
notification (e.g., show an error toast) or silently ignore the error to prevent
unhandled promise rejections.

Comment on lines +275 to +297
<div>
<span className="text-muted-foreground">Provider:</span>{" "}
<span className="font-medium">{sessionReuseProvider.name}</span>
</div>
<div>
<span className="text-muted-foreground">ID:</span>{" "}
<span className="font-mono">{sessionReuseProvider.id}</span>
</div>
{sessionReuseProvider.priority !== undefined && (
<div>
<span className="text-muted-foreground">
{tChain("details.priority")}:
</span>{" "}
<span className="font-mono">P{sessionReuseProvider.priority}</span>
</div>
)}
{sessionReuseProvider.costMultiplier !== undefined && (
<div>
<span className="text-muted-foreground">
{tChain("details.costMultiplier")}:
</span>{" "}
<span className="font-mono">x{sessionReuseProvider.costMultiplier}</span>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

将硬编码标签迁移到 i18n

这里仍有多处硬编码标签(例如 “Provider/ID/Total/Enabled/After Group/After Model/providers filtered/Provider ID”),会破坏多语言支持。请抽到 messages 并用 t()/tChain() 渲染。依据编码规范。

Also applies to: 323-342, 350-356, 506-523

🤖 Prompt for AI Agents
In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx
around lines 275 - 297, Several UI labels in LogicTraceTab.tsx are hardcoded
(e.g., "Provider", "ID", priority/cost labels and others at ranges 323-342,
350-356, 506-523), breaking i18n; replace them with i18n lookups. Update the JSX
around sessionReuseProvider usage to call t() or tChain() for each label (e.g.,
replace "Provider"/"ID"/the priority and costMultiplier label strings with
tChain("...") keys), add corresponding keys into the messages/i18n file, and use
the same pattern for the other hardcoded labels noted in the comment so all
displayed labels are rendered via t()/tChain() instead of raw strings.

Comment on lines 61 to 67
const hasAnyData =
sessionId ||
userAgent ||
endpoint ||
specialSettingsContent ||
costUsd ||
(providerChain && providerChain.length > 0);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

costUsd 为 0 时被误判为无数据。

使用 truthy 判断会在 costUsd=0 时隐藏账单区块与整体元数据,建议改为 costUsd != null 判断。

建议修改
-  const hasAnyData =
-    sessionId ||
-    userAgent ||
-    endpoint ||
-    specialSettingsContent ||
-    costUsd ||
-    (providerChain && providerChain.length > 0);
+  const hasAnyData =
+    sessionId ||
+    userAgent ||
+    endpoint ||
+    specialSettingsContent ||
+    costUsd != null ||
+    (providerChain && providerChain.length > 0);
...
-      {costUsd && (
+      {costUsd != null && (

Also applies to: 142-145

🤖 Prompt for AI Agents
In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx
around lines 61 - 67, The component currently treats costUsd as falsy which
hides metadata when costUsd === 0; update the truthy checks to explicitly test
for non-null/undefined instead of truthiness: change the hasAnyData expression
(the const hasAnyData that includes costUsd) to use costUsd != null, and make
the same change to the other conditional that gates the billing/metadata
rendering (the conditional around costUsd in the render block referenced in the
diff at lines ~142-145) so that costUsd === 0 is treated as valid data.

Comment on lines +125 to +135
<p className="text-xs text-muted-foreground mb-1">User-Agent</p>
<code className="text-xs font-mono break-all">{userAgent}</code>
</div>
)}
{endpoint && (
<div className="p-3">
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Globe className="h-3 w-3" />
Endpoint
</p>
<code className="text-xs font-mono break-all">{endpoint}</code>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

存在未国际化的展示文本。

User-Agent、Endpoint、tokens、1M Context 等是面向用户的文本,当前硬编码会绕过多语言体系,请统一使用 t(...) 并补充 5 种语言键。As per coding guidelines.

建议修改
-                <p className="text-xs text-muted-foreground mb-1">User-Agent</p>
+                <p className="text-xs text-muted-foreground mb-1">
+                  {t("metadata.userAgent")}
+                </p>
...
-                  Endpoint
+                  {t("metadata.endpoint")}
...
-                <span className="font-mono">{formatTokenAmount(inputTokens)} tokens</span>
+                <span className="font-mono">
+                  {formatTokenAmount(inputTokens)} {t("billingDetails.tokensUnit")}
+                </span>
...
-                      1M Context
+                      {t("billingDetails.context1mLabel")}

Also applies to: 154-159, 166-173, 223-224

🤖 Prompt for AI Agents
In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx
around lines 125 - 135, The component MetadataTab.tsx has hard-coded user-facing
labels (e.g., "User-Agent", "Endpoint", "tokens", "1M Context") which bypass
i18n; update the JSX to call the translation helper t(...) for each visible
string (refer to the component and props/variables userAgent, endpoint, tokens
and the string "1M Context") and add the corresponding translation keys into the
locale resource files for the five required languages; ensure the same change is
applied to the other occurrences noted in the file (the blocks rendering tokens
and the 1M context label) so all user-facing text uses t(...) consistently.

Comment on lines 213 to 233
{selectionContext.candidatesAtPriority &&
selectionContext.candidatesAtPriority.length > 1 && (
<div className="text-zinc-400 dark:text-zinc-500">
{selectionContext.candidatesAtPriority.map((c, i) => (
<span key={c.id}>
{i > 0 && ", "}
<span
className={
c.name === displayName
? "text-zinc-200 dark:text-zinc-700 font-medium"
: ""
}
>
{c.name}
</span>
{c.probability !== undefined && (
<span className="text-zinc-500 dark:text-zinc-400">
({c.probability}%)
</span>
)}
</span>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

候选概率展示与其他面板不一致

LogicTraceTab 中概率按 0-1 乘 100 展示,这里直接输出 c.probability% 可能导致数值偏小或不一致。建议统一格式化。

修改建议
-                                {c.probability !== undefined && (
-                                  <span className="text-zinc-500 dark:text-zinc-400">
-                                    ({c.probability}%)
-                                  </span>
-                                )}
+                                {c.probability !== undefined && (
+                                  <span className="text-zinc-500 dark:text-zinc-400">
+                                    ({(c.probability * 100).toFixed(1)}%)
+                                  </span>
+                                )}
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/logs/_components/provider-chain-popover.tsx
around lines 213 - 233, The percent display in provider-chain-popover.tsx is
inconsistent with LogicTraceTab: update the rendering inside
selectionContext.candidatesAtPriority so the displayed value normalizes
probabilities in c.probability (if value is between 0 and 1 multiply by 100) and
then format it to a human-friendly percent (e.g., round or toFixed) before
appending '%' in the span that currently renders ({c.probability}%), ensuring
selectionContext.candidatesAtPriority, c.probability and displayName are used to
locate and update the logic.

Comment on lines 340 to 379
<Button
variant="outline"
size="sm"
onClick={() => void handleEnterFullscreen()}
className="gap-2"
className="gap-1.5 h-8"
>
<Expand className="h-4 w-4" />
{t("logs.actions.fullscreen")}
<Expand className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{t("logs.actions.fullscreen")}</span>
</Button>

<Button
variant="outline"
size="sm"
onClick={handleManualRefresh}
className="gap-2"
className="gap-1.5 h-8"
disabled={isFullscreenOpen}
>
<RefreshCw className={`h-4 w-4 ${isManualRefreshing ? "animate-spin" : ""}`} />
{t("logs.actions.refresh")}
<RefreshCw
className={`h-3.5 w-3.5 ${isManualRefreshing ? "animate-spin" : ""}`}
/>
<span className="hidden sm:inline">{t("logs.actions.refresh")}</span>
</Button>

<Button
variant={isAutoRefresh ? "default" : "outline"}
size="sm"
onClick={() => setIsAutoRefresh(!isAutoRefresh)}
className="gap-2"
className="gap-1.5 h-8"
disabled={isFullscreenOpen}
>
{isAutoRefresh ? (
<>
<Pause className="h-4 w-4" />
{t("logs.actions.stopAutoRefresh")}
<Pause className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{t("logs.actions.stopAutoRefresh")}</span>
</>
) : (
<>
<Play className="h-4 w-4" />
{t("logs.actions.startAutoRefresh")}
<Play className="h-3.5 w-3.5" />
<span className="hidden sm:inline">{t("logs.actions.startAutoRefresh")}</span>
</>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

小屏图标按钮缺少可访问名称。

sm 以下文本被隐藏后,按钮只有图标,读屏难以识别,建议补充 aria-label(复用现有 i18n 文案)。

建议修改
                 <Button
                   variant="outline"
                   size="sm"
                   onClick={() => void handleEnterFullscreen()}
                   className="gap-1.5 h-8"
+                  aria-label={t("logs.actions.fullscreen")}
                 >
                   <Expand className="h-3.5 w-3.5" />
                   <span className="hidden sm:inline">{t("logs.actions.fullscreen")}</span>
                 </Button>
 
                 <Button
                   variant="outline"
                   size="sm"
                   onClick={handleManualRefresh}
                   className="gap-1.5 h-8"
                   disabled={isFullscreenOpen}
+                  aria-label={t("logs.actions.refresh")}
                 >
                   <RefreshCw
                     className={`h-3.5 w-3.5 ${isManualRefreshing ? "animate-spin" : ""}`}
                   />
                   <span className="hidden sm:inline">{t("logs.actions.refresh")}</span>
                 </Button>
 
                 <Button
                   variant={isAutoRefresh ? "default" : "outline"}
                   size="sm"
                   onClick={() => setIsAutoRefresh(!isAutoRefresh)}
                   className="gap-1.5 h-8"
                   disabled={isFullscreenOpen}
+                  aria-label={
+                    isAutoRefresh
+                      ? t("logs.actions.stopAutoRefresh")
+                      : t("logs.actions.startAutoRefresh")
+                  }
                 >
                   {isAutoRefresh ? (
                     <>
                       <Pause className="h-3.5 w-3.5" />
                       <span className="hidden sm:inline">{t("logs.actions.stopAutoRefresh")}</span>
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-view-virtualized.tsx
around lines 340 - 379, The icon-only buttons (the three Button components
rendering Expand, RefreshCw, and the Play/Pause variants) lack accessible names
when the sm-hidden spans are not visible; update each Button to include an
aria-label prop that reuses the corresponding i18n text from t(...) (e.g.,
t("logs.actions.fullscreen"), t("logs.actions.refresh"),
t("logs.actions.stopAutoRefresh") / t("logs.actions.startAutoRefresh")) and
ensure the aria-label logic matches the dynamic state for the auto-refresh
button (use isAutoRefresh to pick stop vs start); reference the Button
components in this file and the t(...) calls already present to implement the
change.

Comment on lines +20 to +35
async function fetchActiveSessions(): Promise<ActiveSessionInfo[]> {
const result = await getActiveSessions();
if (!result.ok) {
throw new Error(result.error || "Failed to fetch active sessions");
}
return result.data;
}

function formatDuration(durationMs: number | undefined): string {
if (!durationMs) return "-";
if (durationMs < 1000) return `${durationMs}ms`;
if (durationMs < 60000) return `${(Number(durationMs) / 1000).toFixed(1)}s`;
const minutes = Math.floor(durationMs / 60000);
const seconds = Math.floor((durationMs % 60000) / 1000);
return `${minutes}m ${seconds}s`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

多处硬编码文案与单位需接入 i18n。

失败提示、时长单位、in/out 等均为面向用户的文本,建议统一通过翻译键输出。

Proposed fix
-async function fetchActiveSessions(): Promise<ActiveSessionInfo[]> {
+async function fetchActiveSessions(fallbackMessage: string): Promise<ActiveSessionInfo[]> {
   const result = await getActiveSessions();
   if (!result.ok) {
-    throw new Error(result.error || "Failed to fetch active sessions");
+    throw new Error(result.error || fallbackMessage);
   }
   return result.data;
 }
 
-function formatDuration(durationMs: number | undefined): string {
-  if (!durationMs) return "-";
-  if (durationMs < 1000) return `${durationMs}ms`;
-  if (durationMs < 60000) return `${(Number(durationMs) / 1000).toFixed(1)}s`;
+function formatDuration(
+  durationMs: number | undefined,
+  t: (key: string, values?: Record<string, unknown>) => string
+): string {
+  if (!durationMs) return t("activeSessions.duration.unknown");
+  if (durationMs < 1000) return t("activeSessions.duration.ms", { value: durationMs });
+  if (durationMs < 60000) {
+    return t("activeSessions.duration.seconds", {
+      value: (Number(durationMs) / 1000).toFixed(1),
+    });
+  }
   const minutes = Math.floor(durationMs / 60000);
   const seconds = Math.floor((durationMs % 60000) / 1000);
-  return `${minutes}m ${seconds}s`;
+  return t("activeSessions.duration.minSec", { minutes, seconds });
 }
@@
-  const { data = [], isLoading } = useQuery<ActiveSessionInfo[], Error>({
+  const { data = [], isLoading } = useQuery<ActiveSessionInfo[], Error>({
     queryKey: ["active-sessions"],
-    queryFn: fetchActiveSessions,
+    queryFn: () => fetchActiveSessions(tc("activeSessions.loadFailed")),
     refetchInterval: REFRESH_INTERVAL,
   });
@@
-              {formatDuration(session.durationMs)}
+              {formatDuration(session.durationMs, tc)}
@@
-                <span className="mr-2">{formatTokenAmount(session.inputTokens)} in</span>
+                <span className="mr-2">
+                  {tc("activeSessions.tokens.in", {
+                    value: formatTokenAmount(session.inputTokens),
+                  })}
+                </span>
               )}
               {session.outputTokens !== undefined && (
-                <span>{formatTokenAmount(session.outputTokens)} out</span>
+                <span>
+                  {tc("activeSessions.tokens.out", {
+                    value: formatTokenAmount(session.outputTokens),
+                  })}
+                </span>
               )}
As per coding guidelines, 所有面向用户的文本必须使用 i18n。

Also applies to: 99-112, 144-152

🤖 Prompt for AI Agents
In `@src/components/customs/active-sessions-cards.tsx` around lines 20 - 35,
Replace hardcoded user-facing strings in fetchActiveSessions and formatDuration
with i18n lookups: change the thrown message "Failed to fetch active sessions"
to use a translation key (e.g., t('activeSessions.fetchFailed', { defaultValue:
'Failed to fetch active sessions' })) inside fetchActiveSessions, and replace
"-" and units ("ms", "s", "m", the minutes/seconds formatted output) in
formatDuration with i18n keys (e.g., t('activeSessions.duration.empty'),
t('units.ms'), t('units.s'), t('units.m') or a pluralized/templated key like
t('activeSessions.duration.minutesSeconds', { minutes, seconds })). Also audit
related UI strings such as "in"/"out" referenced elsewhere (lines noted) and
switch them to translation keys using the same translation function/hook
(useTranslation/t) to ensure all user-facing text is localized.

Comment on lines 145 to 176
const router = useRouter();
const tc = useTranslations("customs");

const { data = [], isLoading } = useQuery<ActiveSessionInfo[], Error>({
queryKey: ["active-sessions"],
queryFn: fetchActiveSessions,
refetchInterval: REFRESH_INTERVAL,
});

return (
<Card className={cn("border-border/50", className)}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
<Activity className="h-4 w-4 text-primary" />
</div>
<div>
<CardTitle className="text-base">{tc("activeSessions.title")}</CardTitle>
<CardDescription className="text-xs">
{tc("activeSessions.summary", { count: data.length, minutes: 5 })}
</CardDescription>
</div>
</div>
<button
type="button"
onClick={() => router.push("/dashboard/sessions")}
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{tc("activeSessions.viewAll")}
<ChevronRight className="h-3.5 w-3.5" />
</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check for locale routing configuration and Next.js setup
fd -e "ts" -e "tsx" -e "json" . -path "*/app/*" -type f | head -20

Repository: ding113/claude-code-hub

Length of output: 235


🏁 Script executed:

# Look for middleware or routing configuration
fd -n "middleware" src/
fd -n "i18n" src/ -o "next.config" -o "tsconfig"

Repository: ding113/claude-code-hub

Length of output: 528


🏁 Script executed:

# Check how routes are structured and if locale segments exist
find src/app -maxdepth 2 -type d | head -20

Repository: ding113/claude-code-hub

Length of output: 486


🏁 Script executed:

# Search for similar navigation patterns using router.push and Link
rg -n "router\.push|href=.*dashboard" src/components -g "*.tsx" -A 2 -B 2 | head -40

Repository: ding113/claude-code-hub

Length of output: 2929


🏁 Script executed:

# Check the actual /dashboard/sessions route definition
fd -path "*dashboard*" -path "*sessions*" src/app -type f

Repository: ding113/claude-code-hub

Length of output: 235


🏁 Script executed:

# Check the language switcher to understand locale handling pattern
cat -n src/components/ui/language-switcher.tsx | head -50

Repository: ding113/claude-code-hub

Length of output: 1913


🏁 Script executed:

# Search for i18n configuration or locale handling utilities
rg -n "locale" src/ -g "*.ts" -g "*.tsx" | grep -i "router\|link\|href" | head -20

Repository: ding113/claude-code-hub

Length of output: 2183


🏁 Script executed:

# Check if there's a custom router hook or i18n middleware
fd "router" src/ -g "*.ts" -g "*.tsx"
rg -n "useRouter|createNavigation" src/ -g "*.ts" -g "*.tsx" | head -20

Repository: ding113/claude-code-hub

Length of output: 2084


🏁 Script executed:

# Look for middleware.ts or routing configuration
fd -name "middleware*" -o "next.config*" -o "i18n*"

Repository: ding113/claude-code-hub

Length of output: 297


🏁 Script executed:

# Check the active-sessions-cards.tsx file to see the full context
cat -n src/components/customs/active-sessions-cards.tsx | sed -n '1,50p'
cat -n src/components/customs/active-sessions-cards.tsx | sed -n '140,180p'

Repository: ding113/claude-code-hub

Length of output: 4352


🏁 Script executed:

# Verify the i18n routing configuration
cat -n src/i18n/routing.ts | head -45

Repository: ding113/claude-code-hub

Length of output: 1622


🏁 Script executed:

# Check other similar navigation issues in the codebase using wrong imports
rg -n "import.*useRouter.*from.*next/navigation" src/components -g "*.tsx" -B 2 -A 10 | grep -E "(import|router\.push)" | head -30

Repository: ding113/claude-code-hub

Length of output: 2979


🏁 Script executed:

# Verify that Link from `@/i18n/routing` is the correct approach
rg -n "Link.*href=.*dashboard" src/components -g "*.tsx" | head -10

Repository: ding113/claude-code-hub

Length of output: 325


router.push() 替换为 Link 组件以保留 locale 段。

当前代码从 next/navigation 导入的 useRouter 不会保留 locale 前缀,导致路由到默认 locale 而非当前 locale。应使用从 @/i18n/routing 导入的 Link 组件,它会自动在路由中添加 locale 前缀。

建议的修改
-import { useRouter } from "next/navigation";
@@
-export function ActiveSessionsCards({ currencyCode = "USD", className }: ActiveSessionsCardsProps) {
-  const router = useRouter();
+export function ActiveSessionsCards({ currencyCode = "USD", className }: ActiveSessionsCardsProps) {
   const tc = useTranslations("customs");
@@
-          <button
-            type="button"
-            onClick={() => router.push("/dashboard/sessions")}
-            className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
-          >
+          <Link
+            href="/dashboard/sessions"
+            className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
+          >
             {tc("activeSessions.viewAll")}
             <ChevronRight className="h-3.5 w-3.5" />
-          </button>
+          </Link>
🤖 Prompt for AI Agents
In `@src/components/customs/active-sessions-cards.tsx` around lines 145 - 176,
Replace the router.push navigation with the locale-aware Link from
"@/i18n/routing": remove or stop using useRouter() and wrap or replace the
button that calls router.push("/dashboard/sessions") with the Link component
(href="/dashboard/sessions") so the locale prefix is preserved; import Link from
"@/i18n/routing", keep the existing button classes/contents inside the Link (or
render an anchor with the same className) and remove the now-unused
router/useRouter import.

* Replaces messages[].content with [REDACTED] while preserving structure.
*/

const REDACTED_MARKER = "[REDACTED]";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/lib/utils/message-redaction.ts

Repository: ding113/claude-code-hub

Length of output: 5937


🏁 Script executed:

rg "REDACTED_MARKER|redactMessageContent" --type ts --type tsx -B 2 -A 5

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

rg "message-redaction" --type ts --type tsx

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

rg "from.*message-redaction|import.*message-redaction" --type ts -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 1976


🏁 Script executed:

rg "redactMessages|redactRequestBody|redactJsonString" --type ts -B 1 -A 1

Repository: ding113/claude-code-hub

Length of output: 9349


🏁 Script executed:

fd "message-redaction" -e ts -e tsx

Repository: ding113/claude-code-hub

Length of output: 141


🏁 Script executed:

rg "\[REDACTED\]|\[ERROR\]|\[NULL\]" --type ts --type jsx -C 1

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

cat -n src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/session-details-tabs.tsx | head -100

Repository: ding113/claude-code-hub

Length of output: 3985


🏁 Script executed:

cat -n src/lib/utils/message-redaction.test.ts | head -80

Repository: ding113/claude-code-hub

Length of output: 2841


🏁 Script executed:

grep -r "REDACTED" --include="*.ts" --include="*.tsx" | grep -v "test.ts" | grep -v "node_modules"

Repository: ding113/claude-code-hub

Length of output: 1956


[REDACTED] 为硬编码文案,在用户可见的 JSON 输出中显示,应遵循 i18n 规范。

虽然该标记用于数据结构中(非 UI 标签),但根据编码规范,所有硬编码显示文本都需要多语言支持。当前方案需要在多个函数间传递 marker 参数(15+ 处使用),建议评估:

  1. 在调用方注入本地化 marker,或
  2. 在展示层(JSON stringify 后)统一映射 marker 为本地化文案

需同步更新 redactRequestBody、redactMessages、redactJsonString 的调用处。

🤖 Prompt for AI Agents
In `@src/lib/utils/message-redaction.ts` at line 8, REDACTED_MARKER is a
hard-coded user-visible string — replace it with an i18n-aware value: either
accept a localized marker parameter and thread it into redactRequestBody,
redactMessages, and redactJsonString from callers, or keep an internal neutral
marker and map that marker to a localized string in the presentation layer
(e.g., during final JSON stringify/render); update all call sites (the ~15+
uses) to pass or handle the localized marker consistently and remove the
hardcoded "[REDACTED]" constant from message-redaction.ts.

@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Caution

The CodeRabbit agent's plans did not produce any file changes.

ding113 and others added 2 commits January 26, 2026 04:19
- LogicTraceTab: include all filtered providers, not just rate_limited/circuit_open
- LogicTraceTab: add error handling for clipboard copy
- MetadataTab: fix costUsd === 0 being treated as no data
- PerformanceTab: include outputTokens in hasData check
- error-details-dialog: fix race condition in session messages check using requestId ref
- provider-chain-popover: normalize probability display (0-1 to percentage)
- usage-logs-view: add aria-label for icon-only buttons (accessibility)
- active-sessions-cards: use Link component instead of router.push to preserve locale

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ding113 ding113 merged commit 9bd1cc5 into dev Jan 25, 2026
1 check passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 25, 2026
@github-actions github-actions bot mentioned this pull request Jan 25, 2026
6 tasks
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/app/[locale]/dashboard/logs/_components/usage-logs-stats-panel.tsx (1)

5-68: 避免筛选变化时的竞态与重复请求

useEffect 同时依赖 filtersKeyloadStats,但 loadStats 又依赖 filters,如果父组件每次 render 生成新对象,即使内容未变也会触发重复请求;同时缺少并发请求的去重/取消,旧请求可能覆盖新筛选结果。建议让 loadStats 接收 filters 参数并用 requestId 保护最新请求。

建议修复
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";

 export function UsageLogsStatsPanel({ filters, currencyCode = "USD" }: UsageLogsStatsPanelProps) {
   const t = useTranslations("dashboard");
   const [stats, setStats] = useState<UsageLogSummary | null>(null);
   const [isLoading, setIsLoading] = useState(true);
   const [error, setError] = useState<string | null>(null);
+  const requestIdRef = useRef(0);

   // Create stable filter key for dependency comparison
   const filtersKey = JSON.stringify(filters);

   // Load stats data
-  const loadStats = useCallback(async () => {
+  const loadStats = useCallback(
+    async (currentFilters: UsageLogsStatsPanelProps["filters"]) => {
+      const requestId = ++requestIdRef.current;
       setIsLoading(true);
       setError(null);

       try {
-        const result = await getUsageLogsStats(filters);
+        const result = await getUsageLogsStats(currentFilters);
+        if (requestId !== requestIdRef.current) return;
         if (result.ok && result.data) {
           setStats(result.data);
         } else {
           setError(!result.ok ? result.error : t("logs.error.loadFailed"));
         }
       } catch (err) {
+        if (requestId !== requestIdRef.current) return;
         console.error("Failed to load usage logs stats:", err);
         setError(t("logs.error.loadFailed"));
       } finally {
-        setIsLoading(false);
+        if (requestId === requestIdRef.current) {
+          setIsLoading(false);
+        }
       }
-  }, [filters, t]);
+    },
+    [t]
+  );

   // Load data on mount and when filters change
   // biome-ignore lint/correctness/useExhaustiveDependencies: filtersKey is used to detect filter changes
   useEffect(() => {
-    loadStats();
+    loadStats(filters);
   }, [filtersKey, loadStats]);
package.json (1)

57-68: 确认 agentation 采用 PolyForm Shield 商业许可证,需要确认其与项目的许可证兼容性。

agentation@1.3.2 使用的 PolyForm-Shield-1.0.0 是商业许可证,对商业使用有限制。建议在合并前确认该许可证条款是否与项目的使用场景和许可证要求相符。

@radix-ui/react-scroll-area@1.2.10 使用 MIT 许可证,无许可证问题。

messages/en/dashboard.json (1)

346-357: 发现重复的翻译键。

logs.billingDetails (行 346-357) 与 logs.details.billingDetails (行 262-274) 内容几乎完全相同。这可能导致:

  1. 翻译维护时需要同步更新两处
  2. 开发者可能误用错误的键

建议统一使用一个位置,并在代码中引用该位置。

🤖 Fix all issues with AI agents
In `@messages/en/provider-chain.json`:
- Around line 71-72: The English localization value for the key "failures" is
lowercase and inconsistent with other title-cased labels; update the JSON entry
for the "failures" key (the "failures": "failures" entry) to use title case
("Failures") so it matches sibling labels like "modelRedirect" and other
titleized strings.

In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx:
- Around line 52-59: The handleCopyTimeline function uses
navigator.clipboard.writeText(...) without error handling; update
handleCopyTimeline to append a .catch handler to
navigator.clipboard.writeText(timeline) (or use async/try-catch) to gracefully
handle failures (e.g., when clipboard API is unavailable or in non-HTTPS
contexts), and in the catch branch reset/set state or surface an error (e.g.,
avoid leaving setTimelineCopied true, optionally show a toast/error state). Keep
references: handleCopyTimeline, providerChain, formatProviderTimeline, tChain,
navigator.clipboard.writeText, and setTimelineCopied.

In `@src/components/customs/active-sessions-cards.tsx`:
- Around line 27-30: formatDuration currently treats 0 as falsy and returns "-"
which hides very short durations; change the initial check in function
formatDuration(durationMs: number | undefined) to only return "-" when
durationMs is null/undefined (e.g., durationMs == null or durationMs ===
undefined) and optionally guard against NaN (Number.isNaN(durationMs)) so that
0ms is formatted as "0ms" instead of the placeholder.
♻️ Duplicate comments (16)
src/components/customs/active-sessions-cards.tsx (1)

19-34: 面向用户的文案/单位仍是硬编码,需要接入 i18n。
该问题已在前序审查中指出,此处与既有评论重复。基于编码规范。

Also applies to: 97-105

src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/StepCard.tsx (1)

151-154: 相对时间文案仍为硬编码,需要 i18n 化处理。
该显示为用户可见文本,应通过本地化格式化函数输出。基于编码规范。

src/app/[locale]/dashboard/logs/_components/filters/identity-filters.tsx (1)

133-150: 初始 keys 加载 effect 依赖缺失,会错过 filters.userId 变化。
建议补齐依赖并用 guard 避免重复加载;此问题已提出过。

src/lib/utils/message-redaction.ts (1)

8-8: REDACTED_MARKER 为硬编码用户可见文案,应改为 i18n。
建议通过调用方注入或展示层映射本地化文案。基于编码规范。

src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx (3)

140-177: 深色模式下文本对比度不足

此问题已在之前的审查中被标记。在 border-zinc-600 dark:border-zinc-300 的边框设计中,text-zinc-200text-zinc-700 的文本颜色在深色模式下对比度可能不够理想。

建议统一调整深色模式下的文本颜色,确保在深色背景上有足够的可读性。例如,text-zinc-200 dark:text-zinc-700 可能需要调整为 text-zinc-100 dark:text-zinc-800 或类似的高对比度组合。


183-196: 硬编码的英文标签需替换为 i18n 调用

此问题已在之前的审查中被标记。第 189、192、195 行包含硬编码的英文单词 "total""enabled""healthy",违反了编码规范中要求所有用户可见字符串使用 i18n 的要求。

建议修复
                     <div className="flex items-center gap-1 text-[10px] text-zinc-200 dark:text-zinc-700">
                       <span>{selectionContext.totalProviders}</span>
-                      <span className="text-zinc-400 dark:text-zinc-500">total</span>
+                      <span className="text-zinc-400 dark:text-zinc-500">{tChain("details.total")}</span>
                       <ChevronRight className="h-2.5 w-2.5" />
                       <span>{selectionContext.enabledProviders}</span>
-                      <span className="text-zinc-400 dark:text-zinc-500">enabled</span>
+                      <span className="text-zinc-400 dark:text-zinc-500">{tChain("details.enabled")}</span>
                       <ChevronRight className="h-2.5 w-2.5" />
                       <span>{selectionContext.afterHealthCheck}</span>
-                      <span className="text-zinc-400 dark:text-zinc-500">healthy</span>
+                      <span className="text-zinc-400 dark:text-zinc-500">{tChain("details.healthy")}</span>
                     </div>

206-210: 硬编码的 "candidates" 字符串需 i18n 处理

第 208 行的 "candidates" 是硬编码的英文文本,应替换为翻译调用。根据编码规范,所有用户可见字符串必须使用 i18n。

建议修复
                         {selectionContext.candidatesAtPriority && (
                           <span className="text-zinc-400 dark:text-zinc-500">
-                            ({selectionContext.candidatesAtPriority.length} candidates)
+                            ({selectionContext.candidatesAtPriority.length} {tChain("details.candidates")})
                           </span>
                         )}
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LatencyBreakdownBar.tsx (1)

57-97: 仍有用户可见文本未走 i18n。

TTFB / Generation / Total 及相关 title/label 仍是硬编码,需要统一改为 t(...) 并补齐多语言键值。依据编码规范。

src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx (3)

125-135: 存在未国际化的展示文本。

User-AgentEndpoint 等面向用户的文本仍为硬编码,请使用 t(...) 并补充相应的翻译键。


154-159: "tokens" 单位未国际化。

tokens 作为显示单位需要国际化处理,不同语言可能有不同的表达方式。


219-224: "1M Context" 徽章文本未国际化。

根据编码规范,所有用户可见文本必须使用 i18n。建议使用已有的翻译键或新增 billingDetails.context1mBadge 键。

src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx (3)

275-298: 硬编码的标签文本需要国际化

此处及文件其他位置(如第 326-342 行、第 350-356 行、第 506-523 行)存在多个硬编码标签,如 "Provider:""ID:""Total:""Enabled:""After Group:""After Model:""Provider ID:" 等,这违反了项目的 i18n 要求。

请将这些标签迁移到翻译文件中,并使用 t()tChain() 渲染。


350-356: 健康检查步骤的副标题需要国际化

第 356 行的 "providers filtered" 是硬编码文本,应使用 i18n 键。

建议修改
             <StepCard
               step={2}
               icon={Filter}
               title={t("logicTrace.healthCheck")}
-              subtitle={`${filteredProviders.length} providers filtered`}
+              subtitle={t("logicTrace.providersFiltered", { count: filteredProviders.length })}
               status="warning"

111-113: filteredProviders 过滤逻辑可能不完整

当前实现仅从 decisionContext?.filteredProviders 提取过滤的供应商,但根据 ProviderChainItem 类型定义,供应商可能因多种原因被过滤(如 disabledmodel_not_supportedgroup_mismatchhealth_check_failed 等),不仅限于 rate_limitedcircuit_open

建议确认 decisionContext.filteredProviders 是否已包含所有被过滤的供应商,以确保健康检查步骤卡显示完整信息。

src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/SummaryTab.tsx (1)

102-104: 多处硬编码文本需要国际化

文件中存在多处硬编码的用户可见文本,违反了 i18n 要求:

  • 第 103 行:"OK" / "Error" 状态文本
  • 第 173 行:"tok/s" 单位
  • 第 226 行:"User-Agent" 标签
  • 第 234 行:"Endpoint" 标签
  • 第 255、259、273、289、299 行:"tokens" 单位
  • 第 324 行:"1M Context" 标签

请将这些文本迁移到翻译文件并使用 t() 渲染,以确保五种语言的一致性。

src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/PerformanceTab.tsx (1)

248-260: "Output Tokens" 和 "tok/s" 仍为硬编码文本

第 250 行的 "Output Tokens" 标签和第 188、259 行的 "tok/s" 单位是硬编码文本,需要使用 i18n 键。

建议修改
           {normalizedOutputTokens !== null && (
             <div className="flex justify-between items-center px-4 py-3">
-              <span className="text-sm text-muted-foreground">Output Tokens</span>
+              <span className="text-sm text-muted-foreground">{t("performance.outputTokens")}</span>
               <span className="text-sm font-mono font-medium">
                 {formatTokenAmount(normalizedOutputTokens)}
               </span>
             </div>
           )}
           {outputRate !== null && (
             <div className="flex justify-between items-center px-4 py-3">
               <span className="text-sm text-muted-foreground">{t("performance.outputRate")}</span>
-              <span className="text-sm font-mono font-medium">{outputRate.toFixed(1)} tok/s</span>
+              <span className="text-sm font-mono font-medium">
+                {t("performance.outputRateValue", { value: outputRate.toFixed(1) })}
+              </span>
             </div>
           )}

根据编码规范,所有用户可见文本必须使用 i18n。

🧹 Nitpick comments (7)
src/app/[locale]/dashboard/logs/_components/filters/filter-section.tsx (1)

11-19: 本地接口与 types.ts 中的 FilterSectionProps 命名重复。

此文件定义的 FilterSectionProps./types.ts 中导出的 FilterSectionProps 具有不同的结构和用途。虽然它们服务于不同目的(一个用于 UI 布局容器,一个用于过滤器业务逻辑),但相同的命名可能会在团队协作时造成混淆。

建议考虑重命名此接口为 CollapsibleFilterSectionProps 或类似名称以区分用途。

src/app/[locale]/dashboard/logs/_components/error-details-dialog/index.tsx (1)

156-182: 存在冗余的状态检查。

isInProgresstrue 时(即 statusCode === null),后续 !statusCode 的判断也必然为真。可以考虑简化逻辑,但当前实现不影响正确性。

可选的简化
   const getStatusBadgeClassName = () => {
     if (isInProgress) {
       return "bg-gray-100 text-gray-700 border-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600";
     }

-    if (!statusCode) {
-      return "bg-gray-100 text-gray-700 border-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600";
-    }
+    // statusCode is guaranteed to be non-null here due to isInProgress check above

     if (statusCode >= 200 && statusCode < 300) {
src/app/[locale]/dashboard/logs/_components/column-visibility-dropdown.tsx (1)

49-53: 初始化时未通知父组件隐藏列状态。

useEffect 加载初始状态后未调用 onVisibilityChange,如果父组件依赖此回调同步状态,可能导致初始渲染时状态不一致。

建议修改
   // Load initial state from localStorage
   useEffect(() => {
     const stored = getHiddenColumns(userId, tableId);
     setHiddenColumnsState(stored);
+    onVisibilityChange?.(stored);
   }, [userId, tableId]);

注意:如果父组件也从同一 localStorage 读取初始状态,则此修改可能不是必需的。请根据实际使用场景决定。

src/app/[locale]/dashboard/logs/_components/filters/request-filters.tsx (1)

318-322: 输入值立即 trim 可能影响用户体验

onChange 处理器中立即对输入值调用 trim() 会导致用户无法在输入框中输入空格。虽然 session ID 通常不包含空格,但这种行为可能会让用户感到困惑(例如从剪贴板粘贴带有前后空格的内容时)。

建议仅在提交或使用值时进行 trim,或者只移除前后空格而非中间空格:

建议修改
               onChange={(e) => {
-                const next = e.target.value.trim();
+                const next = e.target.value;
                 onFiltersChange({ ...filters, sessionId: next || undefined });
-                setSessionIdPopoverOpen(next.length >= SESSION_ID_SUGGESTION_MIN_LEN);
+                setSessionIdPopoverOpen(next.trim().length >= SESSION_ID_SUGGESTION_MIN_LEN);
               }}
src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx (1)

99-103: 时间过滤器计数逻辑可能存在边界情况。

当前逻辑要求 startTimeendTime 同时存在才计数。如果用户只设置了 startTime,该过滤器不会被显示为活跃状态,但实际上它可能仍在影响查询结果。

考虑是否应分别计数:

 const timeActiveCount = useMemo(() => {
   let count = 0;
-  if (localFilters.startTime && localFilters.endTime) count++;
+  if (localFilters.startTime) count++;
+  if (localFilters.endTime) count++;
   return count;
 }, [localFilters.startTime, localFilters.endTime]);
src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx (2)

414-416: 发现中文注释,建议使用英文保持一致性。

第 415 行包含中文注释 // 计算倍率,用于判断是否显示 Badge

为保持代码库一致性,建议改为英文:

- // 计算倍率,用于判断是否显示 Badge
+ // Calculate multiplier to determine whether to show cost badge

403-470: Provider 列的 IIFE 模式可考虑提取为组件。

当前使用立即执行函数表达式(IIFE)来计算成本倍率和渲染逻辑。虽然功能正确,但这段代码较为复杂(约 50 行),可以考虑提取为单独的组件以提高可读性:

// 可选:提取为 ProviderCellContent 组件
function ProviderCellContent({ log, tChain }: { log: LogEntry; tChain: TFunction }) {
  // ... 现有逻辑
}

这是一个可选的重构建议,当前实现功能正确。

Comment on lines +71 to +72
"failures": "failures",
"modelRedirect": "Model Redirect",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

英文本地化文案大小写不一致。
details.failures 建议与其他标题化标签保持一致。

建议修改
-    "failures": "failures",
+    "failures": "Failures",
🤖 Prompt for AI Agents
In `@messages/en/provider-chain.json` around lines 71 - 72, The English
localization value for the key "failures" is lowercase and inconsistent with
other title-cased labels; update the JSON entry for the "failures" key (the
"failures": "failures" entry) to use title case ("Failures") so it matches
sibling labels like "modelRedirect" and other titleized strings.

Comment on lines +52 to +59
const handleCopyTimeline = () => {
if (!providerChain) return;
const { timeline } = formatProviderTimeline(providerChain, tChain);
navigator.clipboard.writeText(timeline).then(() => {
setTimelineCopied(true);
setTimeout(() => setTimelineCopied(false), 2000);
});
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

剪贴板操作缺少错误处理。

navigator.clipboard.writeText() 在某些浏览器或非 HTTPS 环境下可能失败,建议添加 .catch() 处理以避免未捕获的 Promise 拒绝。

建议修改
   const handleCopyTimeline = () => {
     if (!providerChain) return;
     const { timeline } = formatProviderTimeline(providerChain, tChain);
-    navigator.clipboard.writeText(timeline).then(() => {
-      setTimelineCopied(true);
-      setTimeout(() => setTimelineCopied(false), 2000);
-    });
+    navigator.clipboard.writeText(timeline)
+      .then(() => {
+        setTimelineCopied(true);
+        setTimeout(() => setTimelineCopied(false), 2000);
+      })
+      .catch((err) => {
+        console.error("Failed to copy timeline:", err);
+      });
   };
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/dashboard/logs/_components/error-details-dialog/components/MetadataTab.tsx
around lines 52 - 59, The handleCopyTimeline function uses
navigator.clipboard.writeText(...) without error handling; update
handleCopyTimeline to append a .catch handler to
navigator.clipboard.writeText(timeline) (or use async/try-catch) to gracefully
handle failures (e.g., when clipboard API is unavailable or in non-HTTPS
contexts), and in the catch branch reset/set state or surface an error (e.g.,
avoid leaving setTimelineCopied true, optionally show a toast/error state). Keep
references: handleCopyTimeline, providerChain, formatProviderTimeline, tChain,
navigator.clipboard.writeText, and setTimelineCopied.

Comment on lines +27 to +30
function formatDuration(durationMs: number | undefined): string {
if (!durationMs) return "-";
if (durationMs < 1000) return `${durationMs}ms`;
if (durationMs < 60000) return `${(Number(durationMs) / 1000).toFixed(1)}s`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

避免将 0ms 误判为未知。
durationMs0 时会命中 falsy 分支并显示 “未知”,可能掩盖极短请求。建议仅在 null/undefined 时返回占位值。

建议修改
-  if (!durationMs) return "-";
+  if (durationMs == null) return "-";
🤖 Prompt for AI Agents
In `@src/components/customs/active-sessions-cards.tsx` around lines 27 - 30,
formatDuration currently treats 0 as falsy and returns "-" which hides very
short durations; change the initial check in function formatDuration(durationMs:
number | undefined) to only return "-" when durationMs is null/undefined (e.g.,
durationMs == null or durationMs === undefined) and optionally guard against NaN
(Number.isNaN(durationMs)) so that 0ms is formatted as "0ms" instead of the
placeholder.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

<span className="text-zinc-400 dark:text-zinc-500">
{tChain("details.priority")}:
</span>{" "}
<span className="text-zinc-200 dark:text-zinc-700">
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text-zinc-200 on tooltip's dark background may have insufficient contrast in dark mode.

Suggested change
<span className="text-zinc-200 dark:text-zinc-700">
<span className="text-zinc-100 dark:text-zinc-700">
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx
Line: 161:161

Comment:
The `text-zinc-200` on tooltip's dark background may have insufficient contrast in dark mode.

```suggestion
                          <span className="text-zinc-100 dark:text-zinc-700">
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:UI enhancement New feature or request size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants