fix(my-usage): UX improvements for quota and statistics cards#794
fix(my-usage): UX improvements for quota and statistics cards#794ding113 merged 6 commits intoding113:devfrom
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual `${currency} ${num.toFixed(2)}` formatting with
`formatCurrency()` so quota values display "$3.50" instead of "USD 3.50",
consistent with all other currency displays in the app.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ards Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… reset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello @miraserver, 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 several user experience enhancements to the 'My Usage' section. It refines the presentation of quota information by introducing more intuitive visual cues like infinity icons and currency symbols. Additionally, it improves the clarity and navigability of provider group details through the use of badges and tooltips, and makes the model breakdown statistics more user-friendly by implementing pagination. Highlights
Changelog
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
|
📝 WalkthroughWalkthrough对 my-usage 模块的多处 UI 与交互进行重构:Provider/Group 显示改为 Badge+Tooltip,Quota 卡片重构为 QuotaBlock/QuotaRow 并调整货币/无限值显示,StatisticsSummaryCard 添加客户端分页及翻页控件,新增多语言分页文案。 Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 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 |
There was a problem hiding this comment.
Code Review
The pull request introduces several UX improvements to the usage and statistics cards, such as visual consistency with Badge components, currency symbol formatting, and pagination for model breakdowns. Key improvements include the use of tooltips for abbreviated model names and a cleaner layout for quota information. However, there are robustness issues in the abbreviation logic that could lead to crashes with malformed input, and the pagination reset logic should be refined to avoid disrupting users during auto-refreshes. Additionally, a regression in the empty state display for statistics should be addressed.
|
|
||
| const prefix = letterParts | ||
| .slice(0, 3) | ||
| .map((w) => w[0].toUpperCase()) |
There was a problem hiding this comment.
The map operation will throw an error if letterParts contains an empty string (e.g., if the model name has double hyphens), as w[0] would be undefined. It's safer to use optional chaining or filter out empty strings.
| .map((w) => w[0].toUpperCase()) | |
| .map((w) => w[0]?.toUpperCase()) | |
| .filter(Boolean) |
| } | ||
| return parts | ||
| .slice(0, 3) | ||
| .map((w) => w[0].toUpperCase()) |
| // biome-ignore lint/correctness/useExhaustiveDependencies: intentional reset on stats identity change | ||
| useEffect(() => { | ||
| setBreakdownPage(1); | ||
| }, [stats]); |
There was a problem hiding this comment.
Resetting the breakdown page whenever stats changes causes the UI to jump back to the first page every time the auto-refresh occurs (every 30 seconds). This is disruptive if the user is inspecting items on a later page. The page should only reset when the date range changes.
| // biome-ignore lint/correctness/useExhaustiveDependencies: intentional reset on stats identity change | |
| useEffect(() => { | |
| setBreakdownPage(1); | |
| }, [stats]); | |
| // Reset breakdown page when date range changes | |
| useEffect(() => { | |
| setBreakdownPage(1); | |
| }, [dateRange.startDate, dateRange.endDate]); |
| {keyPageItems.length > 0 && ( | ||
| <div className="space-y-2"> | ||
| <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> | ||
| {t("keyStats")} | ||
| </p> | ||
| <ModelBreakdownColumn | ||
| pageItems={keyPageItems} | ||
| currencyCode={currencyCode} | ||
| totalCost={stats.totalCost} | ||
| keyPrefix="key" | ||
| pageOffset={sliceStart} | ||
| /> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
The 'No data' message for the model breakdown was removed. If there is no data for the selected period, the columns will now be completely empty. Consider restoring the empty state message and keeping the headers visible for consistency.
| {keyPageItems.length > 0 && ( | |
| <div className="space-y-2"> | |
| <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> | |
| {t("keyStats")} | |
| </p> | |
| <ModelBreakdownColumn | |
| pageItems={keyPageItems} | |
| currencyCode={currencyCode} | |
| totalCost={stats.totalCost} | |
| keyPrefix="key" | |
| pageOffset={sliceStart} | |
| /> | |
| </div> | |
| )} | |
| <div className="space-y-2"> | |
| <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide"> | |
| {t("keyStats")} | |
| </p> | |
| {keyPageItems.length > 0 ? ( | |
| <ModelBreakdownColumn | |
| pageItems={keyPageItems} | |
| currencyCode={currencyCode} | |
| totalCost={stats.totalCost} | |
| keyPrefix="key" | |
| pageOffset={sliceStart} | |
| /> | |
| ) : ( | |
| <p className="text-sm text-muted-foreground py-2">{t("noData")}</p> | |
| )} | |
| </div> |
| userAllowedModels.map((name) => ( | ||
| <Tooltip key={name}> | ||
| <TooltipTrigger asChild> | ||
| <span> | ||
| <Badge variant="outline" className="cursor-default font-mono text-xs"> | ||
| {abbreviateModel(name)} | ||
| </Badge> | ||
| </span> | ||
| </TooltipTrigger> | ||
| <TooltipContent>{name}</TooltipContent> | ||
| </Tooltip> | ||
| )) |
There was a problem hiding this comment.
Wrapping TooltipTrigger with an extra <span> is unnecessary. The asChild prop makes TooltipTrigger pass its props to its child, so you can directly wrap Badge.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/my-usage/_components/provider-group-info.tsx
Line: 133:144
Comment:
Wrapping `TooltipTrigger` with an extra `<span>` is unnecessary. The `asChild` prop makes `TooltipTrigger` pass its props to its child, so you can directly wrap `Badge`.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.| userAllowedClients.map((name) => ( | ||
| <Tooltip key={name}> | ||
| <TooltipTrigger asChild> | ||
| <span> | ||
| <Badge variant="outline" className="cursor-default font-mono text-xs"> | ||
| {abbreviateClient(name)} | ||
| </Badge> | ||
| </span> | ||
| </TooltipTrigger> | ||
| <TooltipContent>{name}</TooltipContent> | ||
| </Tooltip> | ||
| )) |
There was a problem hiding this comment.
Same issue - the extra <span> wrapper is unnecessary when using asChild prop on TooltipTrigger.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/my-usage/_components/provider-group-info.tsx
Line: 156:167
Comment:
Same issue - the extra `<span>` wrapper is unnecessary when using `asChild` prop on `TooltipTrigger`.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/my-usage/_components/provider-group-info.tsx:
- Around line 53-62: abbreviateClient can produce empty-string array elements
when the input starts with separators, causing w[0] to be undefined; update the
function (abbreviateClient) to filter out empty parts before slicing/mapping
(e.g., use parts.filter(Boolean) or equivalent) and ensure a safe fallback when
the resulting array is empty (return an empty string or a sensible default) so
the map and w[0] access cannot throw.
- Around line 9-51: abbreviateModel can crash when name.split("-") yields empty
strings (e.g., "model--v2"); filter out empty parts before processing or guard
against empty strings when building letterParts so you never call w[0] on an
empty string. Specifically, update the parts handling in abbreviateModel (the
parts array and the for loop that pushes into letterParts) to ignore "" entries
(or trim them) and ensure the prefix mapping (.slice(0,3).map(w =>
w[0].toUpperCase())) only runs on non-empty words; this prevents the TypeError
and preserves existing abbreviation logic (versionMixed/versionNums/prefix
assembly).
In `@src/app/`[locale]/my-usage/_components/quota-cards.tsx:
- Around line 182-183: The label span in quota-cards.tsx currently uses a fixed
width class ("w-8") which causes translations to overflow; in the <span
...>{label}</span> replace the fixed width with a responsive set such as "w-auto
min-w-8" or "max-w-fit" and add overflow handling classes like
"whitespace-nowrap overflow-hidden text-ellipsis" (keep existing "shrink-0
text-[11px] text-muted-foreground") so the label adapts to longer translations
without layout breakage.
In `@src/app/`[locale]/my-usage/_components/statistics-summary-card.tsx:
- Around line 278-302: The pagination controls lack accessibility and i18n: add
descriptive aria-labels to the icon Buttons (the ones rendering ChevronLeft and
ChevronRight within the pagination block using Button and handlers
setBreakdownPage) and replace the hardcoded page indicator "{breakdownPage} /
{breakdownTotalPages}" with an i18n string via t(), e.g. a translated pattern
like t('pagination.page_of', { page: breakdownPage, total: breakdownTotalPages
}); ensure the aria-labels also use t() (e.g. t('pagination.previous') /
t('pagination.next')) so all user-facing text is localized and screen-reader
friendly.
🧹 Nitpick comments (2)
src/app/[locale]/my-usage/_components/statistics-summary-card.tsx (1)
247-275: 分页列数不对称时布局可能跳动。当
keyModelBreakdown和userModelBreakdown长度不同时,较短一方在后续页面可能为空(pageItems.length > 0为 false),导致该列整体不渲染,grid 布局从双列变单列。如果需要保持布局稳定,可以在无数据时渲染一个占位空容器,或始终渲染列标题。src/app/[locale]/my-usage/_components/quota-cards.tsx (1)
3-3:Infinity导入遮蔽了全局Infinity常量从
lucide-react导入的Infinity组件遮蔽了 JavaScript 全局常量Infinity。当前代码中使用Number.isFinite因此不会出问题,但如果后续有人在此文件中写value === Infinity会产生难以排查的 bug。建议重命名导入以消除歧义。建议修改
-import { Infinity } from "lucide-react"; +import { Infinity as InfinityIcon } from "lucide-react";同时将 Line 198 的引用一并更新:
- {unlimited ? <Infinity className="inline h-3.5 w-3.5" /> : limitDisplay} + {unlimited ? <InfinityIcon className="inline h-3.5 w-3.5" /> : limitDisplay}
There was a problem hiding this comment.
[MEDIUM] TEST-MISSING-CRITICAL - The new abbreviateModel() (lines 10-48) and abbreviateClient() (lines 50-60) functions contain complex parsing logic with multiple regex patterns but have no unit test coverage. These functions handle various model name formats and edge cases (version numbers, date-like strings, mixed alphanumeric patterns). According to CLAUDE.md, new features require 80% test coverage.
There was a problem hiding this comment.
Code Review Summary
This PR introduces UX improvements for the My Usage page, including infinity icons for unlimited quotas, currency symbols instead of codes, Badge components for provider groups, and pagination for model breakdown. The changes are focused on three files in the my-usage components directory.
PR Size: S
- Lines changed: 495 (328 additions, 167 deletions)
- Files changed: 3
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 |
Critical Issues (Must Fix)
None.
High Priority Issues (Should Fix)
None.
Medium Priority Issues
- TEST-MISSING-CRITICAL: The new
abbreviateModel()andabbreviateClient()functions lack unit tests. These parsing functions contain complex regex logic handling edge cases (version numbers, date-like strings, mixed alphanumeric patterns) that should be tested per CLAUDE.md's 80% test coverage requirement.
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean
- Error handling - Clean
- Type safety - Clean
- Documentation accuracy - Clean
- Test coverage - 1 medium issue identified
- Code clarity - Good
Automated review by Claude AI
- Fix abbreviateModel/abbreviateClient crash on empty split parts - Fix pagination reset on auto-refresh by using dateRange deps - Restore noData fallback in model breakdown columns - Add i18n for pagination controls with aria-labels (5 langs) - Fix quota label overflow for long translations (w-8 -> w-auto) - Rename Infinity -> InfinityIcon to avoid shadowing global - Remove redundant span wrappers in TooltipTrigger asChild Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review Fixes AppliedAddressed all comments from Gemini, CodeRabbit, and Greptile reviews: Bugs Fixed
I18N
Code Quality
Verification
|
| ); | ||
| } | ||
|
|
||
| const MODEL_BREAKDOWN_PAGE_SIZE = 5; |
There was a problem hiding this comment.
Constant defined after usage
MODEL_BREAKDOWN_PAGE_SIZE is declared on line 322 but referenced on line 134. While this works at runtime (the module fully evaluates before React calls any component), declaring constants before their usage is conventional and improves readability. Consider moving this constant above the StatisticsSummaryCard component.
| const MODEL_BREAKDOWN_PAGE_SIZE = 5; | |
| const MODEL_BREAKDOWN_PAGE_SIZE = 5; |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/my-usage/_components/statistics-summary-card.tsx
Line: 322:322
Comment:
**Constant defined after usage**
`MODEL_BREAKDOWN_PAGE_SIZE` is declared on line 322 but referenced on line 134. While this works at runtime (the module fully evaluates before React calls any component), declaring constants before their usage is conventional and improves readability. Consider moving this constant above the `StatisticsSummaryCard` component.
```suggestion
const MODEL_BREAKDOWN_PAGE_SIZE = 5;
```
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/my-usage/_components/statistics-summary-card.tsx:
- Around line 119-138: The current breakdownPage can become larger than
breakdownTotalPages after an auto-refresh; add a clamp so the active page never
exceeds available pages. After computing breakdownTotalPages (using
MODEL_BREAKDOWN_PAGE_SIZE and stats), derive a clampedPage =
Math.min(Math.max(breakdownPage, 1), Math.max(1, breakdownTotalPages)) and use
that for sliceStart/sliceEnd and for pagination controls instead of raw
breakdownPage; also add a useEffect watching breakdownTotalPages (or stats) that
calls setBreakdownPage(clampedPage) when breakdownPage > breakdownTotalPages to
proactively reset the state. Update references that build keyPageItems and
userPageItems to slice using clampedPage.
* fix(proxy): extract model from Gemini Vertex AI publishers path for correct billing
When Gemini requests use the Vertex AI URL format
/v1/publishers/google/models/{model}:generateContent, the system
failed to extract the model name, falling back to a hardcoded
"gemini-2.5-flash" default and causing incorrect billing.
Add publishers path regex to extractModelFromPath() and
detectFormatByEndpoint() to handle this URL pattern.
* fix(proxy): correct Host header to match actual request target in standard path
buildHeaders() derives Host from provider.url, but the actual fetch target
(proxyUrl) may use a different host when activeEndpoint.baseUrl differs or
MCP passthrough overrides the base URL. This causes undici TLS certificate
validation failures. After proxyUrl is computed, re-derive Host from it.
* perf(logs): hide stats summary panel when no filters are active
Skip rendering UsageLogsStatsPanel and its aggregation query when all
filter conditions are empty, preventing full-table scans that cause
CPU overload.
* fix(proxy): remove deterministic session ID to prevent collision across conversations (#793)
generateDeterministicSessionId() hashes (UA, IP, API key prefix) with no time
dimension, producing identical session IDs for the same user hours apart. This
merges unrelated conversations into one session, polluting usage logs, session
tracking, and concurrent session limits.
The existing fallback in getOrCreateSessionId() (content hash -> random ID)
already provides correct session continuity without collision risk.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
* perf(logs): hide stats panel in virtualized view when no filters active
Apply the same hasStatsFilters guard from the old view to the
virtualized logs view, preventing an unconditional full-table
aggregation query on page load. Also remove the unused legacy
usage-logs-view.tsx which is no longer imported anywhere.
* fix(my-usage): UX improvements for quota and statistics cards (#794)
* style(my-usage): use Badge for provider group values
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(my-usage): use currency symbol instead of code in quota cards
Replace manual `${currency} ${num.toFixed(2)}` formatting with
`formatCurrency()` so quota values display "$3.50" instead of "USD 3.50",
consistent with all other currency displays in the app.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(my-usage): replace unlimited text with infinity icon in quota cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(my-usage): paginate model breakdown in statistics summary card
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore(my-usage): suppress biome exhaustive-deps for intentional stats reset
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(my-usage): address PR #794 review comments
- Fix abbreviateModel/abbreviateClient crash on empty split parts
- Fix pagination reset on auto-refresh by using dateRange deps
- Restore noData fallback in model breakdown columns
- Add i18n for pagination controls with aria-labels (5 langs)
- Fix quota label overflow for long translations (w-8 -> w-auto)
- Rename Infinity -> InfinityIcon to avoid shadowing global
- Remove redundant span wrappers in TooltipTrigger asChild
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: miraserver <20286838+miraserver@users.noreply.github.com>
Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Related Issues:
Test plan
bun run typecheckpassesbun run buildsucceedsDescription enhanced by Claude AI
Generated with Claude Code
Greptile Summary
This PR delivers several UX improvements to the my-usage page's quota and statistics cards:
Card-based grid layout with a more compact row-based design using inline progress bars, an infinity icon (InfinityIcon) for unlimited quotas, andformatCurrency()for proper currency symbol display (e.g.,$instead ofUSD).Badgecomponents with tooltip-enabled abbreviated model/client names, improving visual density and consistency.breakdownPrevPage,breakdownNextPage,breakdownPageIndicator) across all 5 locale files.Infinityimport toInfinityIconto avoid shadowing the global.The changes align with the existing codebase patterns (using the shared
formatCurrencyutility,Badge/Tooltipcomponents) and extend the currency symbol formatting from #717 to the quota cards.Confidence Score: 4/5
statistics-summary-card.tsxandprovider-group-info.tsxhave the most substantial changes but both are well-structured.Important Files Changed
ModelBreakdownColumncomponent.MODEL_BREAKDOWN_PAGE_SIZEis defined after usage but is safe at runtime.formatCurrencyfor currency symbols. Cleaner and more compact design.abbreviateModelandabbreviateClienthelper functions added.Infinityimport aliased toInfinityIconto avoid shadowing the globalInfinity. No logic changes.Flowchart
flowchart TD A[StatisticsSummaryCard] -->|date range change| B[Reset breakdownPage to 1] A -->|loadStats| C[Fetch MyStatsSummary] C --> D{stats available?} D -->|yes| E[Compute maxBreakdownLen] E --> F[Slice keyModelBreakdown & userModelBreakdown] F --> G[ModelBreakdownColumn key] F --> H[ModelBreakdownColumn user] G --> I[ModelBreakdownRow x N] H --> J[ModelBreakdownRow x N] D -->|no| K[Show noData] L[QuotaCards] --> M[QuotaBlock per quota item] M --> N[QuotaRow keyLevel] M --> O[QuotaRow userLevel] N --> P{unlimited?} P -->|yes| Q[InfinityIcon + muted bar] P -->|no| R[Progress bar + formatted value] O --> P S[ProviderGroupInfo] --> T[Badge for groups] S --> U{hasModels?} U -->|yes| V[Tooltip + Badge with abbreviateModel] U -->|no| W[noRestrictions text]Last reviewed commit: 3007012