-
Notifications
You must be signed in to change notification settings - Fork 146
feat(dashboard): enhance logs UI with modular filters and error details #654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- 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>
|
Note Other AI code review bot(s) detectedCodeRabbit 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,添加了带选项卡的详细错误视图、新的过滤器组件、列可见性控制,以及对多种语言的国际化支持。同时重构了多个现有组件,引入了消息脱敏和列持久化存储等新功能。 变更
预估代码审查工作量🎯 4 (Complex) | ⏱️ ~60 minutes 可能相关的PR
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello @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
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this 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
| <span className="text-zinc-200 dark:text-zinc-700"> | ||
| {sessionReuseContext.sessionAge}s | ||
| </span> |
There was a problem hiding this comment.
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.
| <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.There was a problem hiding this 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.
| const filteredProviders = isSessionReuseFlow | ||
| ? [] | ||
| : providerChain | ||
| ?.flatMap((item) => item.decisionContext?.filteredProviders || []) | ||
| .filter((p) => p.reason === "rate_limited" || p.reason === "circuit_open") || []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 || []) || [];
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Removing the unused
MetadataTab.tsxfile and the corresponding "Metadata" tab translations from themessages/*.jsonfiles. - Refactoring to use
MetadataTabfor its intended purpose and moving the relevant content out ofSummaryTab.
There was a problem hiding this 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
anyusage, 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 windowchecks (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
anytypes 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
There was a problem hiding this 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
anyusage, 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 windowchecks (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
anytypes 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
|
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. |
There was a problem hiding this 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卡片时无法从其中的拆分推断出总数。建议:
- 在此卡片下补充缓存拆分(参考缓存tokens卡片的格式),或
- 在卡片标签/副标题中明确说明「包含缓存」
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.billingDetails与logs.billingDetails存在重复定义文件中存在两处
billingDetails定义:
- Lines 262-274:
logs.details.billingDetails- Lines 346-357:
logs.billingDetails这两个对象包含相同的键但翻译内容略有不同(如
context1m分别为 "1M 上下文" 和 "1M 上下文長度",context1mPricing格式也有差异)。建议统一使用一个定义,避免维护不一致的风险。Also applies to: 346-357
messages/en/dashboard.json (1)
262-274:details.billingDetails与logs.billingDetails存在重复定义与 zh-TW 文件相同,英文版本也存在两处
billingDetails定义。建议消除重复以保持单一数据源。Also applies to: 346-357
tests/unit/error-details-dialog-warmup-ui.test.tsx (1)
139-179: 测试用例覆盖了 warmup 跳过指示器的核心场景测试验证了当
blockedBy="warmup"时:
- 显示 "Warmup Fast Response (CCH)" 和 "Skipped" 文本
- 不显示 "Blocking Information"
但当前只有一个测试用例。建议补充以下场景以提高覆盖率:
blockedBy为其他值(如敏感词拦截)时的显示blockedBy为null时的正常请求显示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 数组
timePresets和filterPresets数组在每次渲染时都会重新创建。虽然数组较小,影响不大,但可以考虑使用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; -}
| {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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
用户可见文本需要使用 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.ttfb 和 dashboard.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.
| {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.
| const handleCopyTimeline = () => { | ||
| if (!providerChain) return; | ||
| const { timeline } = formatProviderTimeline(providerChain, tChain); | ||
| navigator.clipboard.writeText(timeline).then(() => { | ||
| setTimelineCopied(true); | ||
| setTimeout(() => setTimelineCopied(false), 2000); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
复制时间线缺少失败处理
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.
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
将硬编码标签迁移到 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.
| const hasAnyData = | ||
| sessionId || | ||
| userAgent || | ||
| endpoint || | ||
| specialSettingsContent || | ||
| costUsd || | ||
| (providerChain && providerChain.length > 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
存在未国际化的展示文本。
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.
| {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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
候选概率展示与其他面板不一致
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.
| <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> | ||
| </> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
小屏图标按钮缺少可访问名称。
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.
| 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`; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
多处硬编码文案与单位需接入 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>
)}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.
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: 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 -20Repository: 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 -40Repository: 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 fRepository: 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 -50Repository: 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 -20Repository: 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 -20Repository: 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 -45Repository: 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 -30Repository: 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 -10Repository: 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]"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n src/lib/utils/message-redaction.tsRepository: ding113/claude-code-hub
Length of output: 5937
🏁 Script executed:
rg "REDACTED_MARKER|redactMessageContent" --type ts --type tsx -B 2 -A 5Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
rg "message-redaction" --type ts --type tsxRepository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
rg "from.*message-redaction|import.*message-redaction" --type ts -B 2 -A 2Repository: ding113/claude-code-hub
Length of output: 1976
🏁 Script executed:
rg "redactMessages|redactRequestBody|redactJsonString" --type ts -B 1 -A 1Repository: ding113/claude-code-hub
Length of output: 9349
🏁 Script executed:
fd "message-redaction" -e ts -e tsxRepository: ding113/claude-code-hub
Length of output: 141
🏁 Script executed:
rg "\[REDACTED\]|\[ERROR\]|\[NULL\]" --type ts --type jsx -C 1Repository: 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 -100Repository: ding113/claude-code-hub
Length of output: 3985
🏁 Script executed:
cat -n src/lib/utils/message-redaction.test.ts | head -80Repository: 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+ 处使用),建议评估:
- 在调用方注入本地化 marker,或
- 在展示层(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.
|
Caution The CodeRabbit agent's plans did not produce any file changes. |
- 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>
There was a problem hiding this 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同时依赖filtersKey和loadStats,但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) 内容几乎完全相同。这可能导致:
- 翻译维护时需要同步更新两处
- 开发者可能误用错误的键
建议统一使用一个位置,并在代码中引用该位置。
🤖 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-200和text-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-Agent、Endpoint等面向用户的文本仍为硬编码,请使用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类型定义,供应商可能因多种原因被过滤(如disabled、model_not_supported、group_mismatch、health_check_failed等),不仅限于rate_limited或circuit_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: 存在冗余的状态检查。当
isInProgress为true时(即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: 时间过滤器计数逻辑可能存在边界情况。当前逻辑要求
startTime和endTime同时存在才计数。如果用户只设置了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 }) { // ... 现有逻辑 }这是一个可选的重构建议,当前实现功能正确。
| "failures": "failures", | ||
| "modelRedirect": "Model Redirect", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
英文本地化文案大小写不一致。
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.
| const handleCopyTimeline = () => { | ||
| if (!providerChain) return; | ||
| const { timeline } = formatProviderTimeline(providerChain, tChain); | ||
| navigator.clipboard.writeText(timeline).then(() => { | ||
| setTimelineCopied(true); | ||
| setTimeout(() => setTimelineCopied(false), 2000); | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
剪贴板操作缺少错误处理。
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.
| 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`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
避免将 0ms 误判为未知。
durationMs 为 0 时会命中 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.
There was a problem hiding this 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
| <span className="text-zinc-400 dark:text-zinc-500"> | ||
| {tChain("details.priority")}: | ||
| </span>{" "} | ||
| <span className="text-zinc-200 dark:text-zinc-700"> |
There was a problem hiding this comment.
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.
| <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.
Summary
Changes
New Components
error-details-dialog/- Modular dialog with SummaryTab, LogicTraceTab, PerformanceTab, MetadataTabfilters/- TimeFilters, IdentityFilters, RequestFilters, StatusFilters, QuickFiltersBar, ActiveFiltersDisplaycolumn-visibility-dropdown.tsx- Column show/hide managementscroll-area.tsx- Consistent scrollable containersactive-sessions-cards.tsx- Session cards componentUtilities
column-visibility.ts- Column visibility state management with localStoragemessage-redaction.ts- Sensitive data masking utilityEnhancements
Test plan
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:
SummaryTab,LogicTraceTab,PerformanceTab,MetadataTab) for better code organizationTimeFilters,IdentityFilters,RequestFilters,StatusFilters) with proper type safetyKey Improvements:
Code Quality:
Confidence Score: 4/5
provider-chain-popover.tsxfor dark mode contrast in tooltipsImportant Files Changed
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