Skip to content

feat: CNY localization, channel UX fixes, and JSON editor improvements - #4832

Open
lzcyyds0-afk wants to merge 15 commits into
QuantumNous:mainfrom
lzcyyds0-afk:feat/cny-localization-and-dev-docker
Open

feat: CNY localization, channel UX fixes, and JSON editor improvements#4832
lzcyyds0-afk wants to merge 15 commits into
QuantumNous:mainfrom
lzcyyds0-afk:feat/cny-localization-and-dev-docker

Conversation

@lzcyyds0-afk

@lzcyyds0-afk lzcyyds0-afk commented May 13, 2026

Copy link
Copy Markdown

Summary

This PR bundles a series of frontend improvements made over several sessions, focused on CNY/RMB localization, channel management UX, and the model pricing editor.

💴 CNY / RMB Localization

  • Default currency changed from USD to CNY throughout the UI
  • All quota and pricing displays use ¥ symbol and CNY label
  • Chinese AI provider logos added to the homepage provider grid

📡 Channel Management Fixes

  • Type selector: Combobox now shows the provider label (e.g. "DeepSeek") instead of the raw numeric ID ("43")
  • Base URL auto-fill: Switching channel type now correctly overwrites the URL if it is empty or matches any known default — custom URLs are preserved
  • Complete default URL table: All 40+ channel types now have correct default base URLs matching the backend constant/channel.go
  • Edit focus stability: Fixed focus loss when editing a channel caused unwanted data refresh

🧾 Model Pricing Editor

  • Merged JSON mode: New editing mode that shows all 8 pricing fields merged into a single per-model JSON object. Pre-populated from current form state; supports real-time JSON validation with field-level error messages
  • Undo stack: Every "Apply" action snapshots the previous state; up to 30 levels of undo via the "撤销" button
  • Sorted table: Models with a configured primary price (ratio / fixed price / expression) appear first; unconfigured models are pushed to the bottom
  • Tab = indent: Pressing Tab inside any JSON textarea inserts 2 spaces for indentation (using execCommand('insertText') so Ctrl+Z / native undo still works)

Test plan

  • Create a new channel, switch between provider types, verify base URL auto-fills correctly and custom URLs are not overwritten
  • Confirm channel type dropdown shows provider name, not numeric ID
  • Open Model Pricing → Visual mode: configured models appear before unconfigured ones
  • Switch to Merged JSON mode: current model prices pre-populate; edit JSON, press Apply, verify form fields update; Undo restores previous state
  • In any JSON editor (model mapping, header override, merged JSON): Tab inserts 2 spaces; Ctrl+Z undoes the insertion
  • All prices and quota values display in CNY (¥)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Tab key indentation in multiple JSON/text editors
    • Merged JSON edit mode with undo/apply for model ratio management
    • Token-trend charts and cache-token metrics added to reports and dashboard
    • New Model Token Trend chart component (lazy-loaded)
  • Improvements

    • System currency defaults to CNY (¥); dynamic currency symbols across pricing, billing, subscriptions, and top‑up UIs
    • Model/application listings, channel defaults, UI copy/icons, and dev/build/docker workflow tweaks
  • Translations

    • Added i18n entries for pricing, caching, and token analytics

Review Change Stack

lzc and others added 8 commits May 11, 2026 09:36
…omepage

- Change default QuotaDisplayType from USD to CNY in backend and frontend stores
- Update dynamic pricing breakdown fallback to CNY (¥) with 7x exchange rate
- Make currency symbol/label dynamic across all pricing, billing, and payment UI components using getCurrencyLabel()
- Add Chinese AI providers (Zhipu, Moonshot, Baidu, Spark, Volcengine, Minimax) and reorder homepage model list to prioritize domestic providers
- Add frontend container service to docker-compose.dev.yml for fully containerized dev workflow
- Add i18n keys for currency-neutral pricing labels in en/zh locales

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rice inputs

- Redefine ratio constants: RMB=500 as base, USD=RMB*7.3 (was USD=500)
- Update QuotaPerUnit comment and logger/billing to reflect CNY-first display
- Frontend currency.ts: CNY exchangeRate=1 (direct), USD=1/rate
- Default currency config: quotaDisplayType changed to CNY
- Fix toFixed(12) → toFixed(8) in price display (default + classic UI)
- Add formatRatio (toFixed(14)) for ratio storage to prevent round-trip
  floating-point loss (e.g. input 1 → stored as 0.33333333 → display 0.99999999)
- Enable Rsbuild polling watch for Docker on Windows (HMR support)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add a third editing mode alongside Visual and Field JSON
- Merged JSON allows configuring all fields of a model in one entry
- Live-sync between merged format and individual field JSONs
- Include Chinese field description table for quick reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- channel-mutate-drawer: add staleTime:Infinity + refetchOnWindowFocus:false
  to prevent form.reset() interrupting user input on focus events
- model-ratio-form: rewrite Merged JSON mode with explicit draft/apply pattern;
  add 追加应用 (merge) and 覆盖应用 (replace) buttons; decouple textarea from
  live form sync to prevent partial-JSON validation errors on keystroke

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- channel-type-config: expand CHANNEL_DEFAULT_BASE_URLS to cover all 40+
  channel types matching backend constant/channel.go ChannelBaseURLs
- channel-mutate-drawer: fix type combobox showing raw numeric ID instead of
  label (e.g. "43" → "DeepSeek"); maintain typeDisplayText state; search by
  both label and value; auto-fill base_url from CHANNEL_DEFAULT_BASE_URLS
  when type changes (not just type 45)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the 'only fill if empty' guard with 'fill if empty OR currently
a known default URL', so switching from OpenAI→DeepSeek correctly
replaces https://api.openai.com with https://api.deepseek.com.
Custom user-entered URLs are never overwritten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use document.execCommand('insertText') for Tab handling in all JSON
textarea fields (Merged JSON, JsonEditor, ModelMappingEditor, header
override). execCommand records the insertion in the browser's native
undo stack so Ctrl+Z works natively without any extra bookkeeping.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Models with a primary price (ratio, fixed price, or tiered expression)
now appear at the top of the table, sorted alphabetically within that
group. Models that only carry secondary fields or have no price are
pushed to the bottom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR makes CNY the system base currency, updates backend quota conversions and constants, updates frontend currency metadata and defaults, threads currency symbols across pricing UIs, adds cache-token telemetry and dashboard token charts, centralizes channel default base URLs, introduces merged JSON editing for model ratios, adds Tab-insert behavior in JSON editors, and adjusts dev/frontend build config.

Changes

Currency & telemetry migration (CNY base + cache token metrics)

Layer / File(s) Summary
Backend quota conversion, constants, logging and defaults
common/constants.go, controller/billing.go, logger/logger.go, setting/operation_setting/general_setting.go, setting/ratio_setting/model_ratio.go, model/log.go, model/usedata.go
QuotaPerUnit comment updated to CNY; GetSubscription/GetUsage/LogQuota/FormatQuota conversions changed (CNY = quota/QuotaPerUnit; default/USD = quota/QuotaPerUnit/USDExchangeRate; TOKENS unchanged); default quota display switched to CNY; model ratio constants re-based on RMB; RecordConsumeLog and LogQuotaData extended to include cache/prompt/completion token counters; DB updates/queries persist and aggregate new fields.
Frontend currency metadata & defaults
web/default/src/lib/currency.ts, web/default/src/stores/system-config-store.ts
getDisplayMeta() and frontend currency docs updated: CNY branch uses exchangeRate=1; USD/fallback uses reciprocal of usdExchangeRate (fallback 7.3); DEFAULT_CURRENCY_CONFIG now defaults to CNY and usdExchangeRate=7.3.
Pricing precision and model-pricing
web/classic/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js, web/default/src/features/system-settings/models/model-pricing-sheet.tsx, web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
Precision reduced (formatNumber → 8 dp), added formatRatio (14 dp), preview/lane derivation use formatRatio, and currencySymbol is threaded to pricing inputs and previews.
Frontend currency-aware UI texts
web/default/src/features/models/..., web/default/src/features/pricing/..., web/default/src/features/subscriptions/..., web/default/src/features/system-settings/...
Various components import getCurrencyLabel() and render currencySymbol/currencyName instead of hardcoded USD text across model pricing, subscription purchase, dynamic pricing breakdown, billing and multiple settings pages.
Payment integration and amount UIs
web/default/src/features/system-settings/integrations/*
Payment/integration UIs replace "USD/美元" with the dynamic currencyName/symbol in labels, descriptions, and amount option displays.
Channel default base URLs and drawer UX
web/default/src/features/channels/lib/channel-type-config.ts, web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
Introduced CHANNEL_DEFAULT_BASE_URLS; CHANNEL_TYPE_CONFIGS reference it; drawer uses typeDisplayText, prevents edit-mode refetch resets, defaults base_url from centralized lookup when appropriate, and improves type combobox resolution.
Tab insertion for JSON editors and header override
web/default/src/components/json-editor.tsx, web/default/src/features/channels/components/model-mapping-editor.tsx, web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
JSON-mode textareas and the header-override textarea intercept Tab to insert two spaces and prevent default focus changes.
Model ratio merged JSON editor
web/default/src/features/system-settings/models/model-ratio-form.tsx
Adds merged JSON edit mode with parsing, normalization, validation, apply/undo history, live validation, Tab handling, and merged-editor UI; removed USD-specific phrasing.
Dev infra and build config
docker-compose.dev.yml, web/default/rsbuild.config.ts, Dockerfile.dev
docker-compose.dev.yml adds/adjusts a Bun-based frontend service and volume; rsbuild.config.ts changes rspack config to a mutation callback and sets watchOptions for development; Dockerfile.dev updates builder base image to golang:latest.
Cache token counters and persistence
model/usedata.go, model/log.go
QuotaData extended with CacheTokens/CacheCreationTokens/PromptTokens/CompletionTokens; LogQuotaData and in-memory aggregation updated; DB increaseQuotaData persists new counters; aggregate queries include new sums.
Dashboard token charts, stats, types and UI
web/default/src/features/dashboard/lib/charts.ts, web/default/src/features/dashboard/*
Added token color utilities and processModelTokenChartData; processUserChartData extended with spec_user_token; dashboard stats accumulation, types, cards, user/model token charts and lazy chart components wired to UI.
Home lists, hero demo, sidebar, and i18n
web/default/src/features/home/*, web/default/src/hooks/use-sidebar-data.ts, web/default/src/i18n/locales/{en,zh}.json
AI_APPLICATIONS/AI_MODELS lists expanded; hero demo adds DeepSeek and placeholder handling; sidebar entries reordered; new i18n keys added for cache/token analytics and pricing labels.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • Calcium-Ion
  • seefs001

Poem

🐰 I hopped through yuan and token trails,

swapped symbols, charts, and merged-json tales.
Tabs now plant two spaces with a tap,
dashboards count cache reads on the map.
This rabbit claps for CNY on the lap!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the three main areas of change: CNY localization, channel UX improvements, and JSON editor enhancements. It is concise, specific, and clearly conveys the primary changes without being misleading or overly vague.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (10)
web/default/src/i18n/locales/en.json (1)

3032-3032: ⚡ Quick win

Use a hierarchical i18n key alias for new translations.

Line 3032 adds a flat key ("Prompt price"). Please also add a semantic hierarchical key (for example, pricing.prompt.price) to align new entries with structured naming and future maintainability.

Proposed minimal addition (non-breaking)
+    "pricing.prompt.price": "Prompt price",
     "Prompt price": "Prompt price",

As per coding guidelines web/default/src/i18n/**/*.{ts,tsx,json}: "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/en.json` at line 3032, The flat i18n key "Prompt
price" should be accompanied by a hierarchical alias for consistency; add a new
semantic key "pricing.prompt.price" with the same value "Prompt price" in the
JSON (keeping the existing flat key non-breaking), so both "Prompt price" and
"pricing.prompt.price" point to the same translation string to follow the
project's hierarchical naming convention.
web/default/src/i18n/locales/zh.json (1)

966-966: ⚡ Quick win

Use hierarchical i18n keys for the new entries.

The newly added translation keys are sentence literals; please switch them to semantic hierarchical keys (for example under a pricing namespace) and reference those keys in code to keep i18n naming consistent.

As per coding guidelines, web/default/src/i18n/**/*.{ts,tsx,json} should “Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency”.

Also applies to: 3033-3033, 4150-4151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/zh.json` at line 966, The translation was added
as a sentence literal ("Cost per request, regardless of tokens used.") instead
of a hierarchical key; change the JSON entry to a semantic key such as
"pricing.costPerRequest": "每请求的费用,不考虑使用的令牌数。", add the same hierarchical
key/value to all locale JSONs (including the English source), and update any
code references that currently use the literal string to use the new key (e.g.,
replace occurrences of the sentence literal in locale files and components with
the identifier pricing.costPerRequest). Ensure consistency by applying the same
change to the other similar literal entries found elsewhere in the locale files.
web/default/src/features/system-settings/models/model-ratio-form.tsx (4)

86-130: 💤 Low value

mergedToFields trusts unvalidated JSON shape at the type level.

JSON.parse(mergedJson) is cast to Record<string, MergedModelEntry> without runtime checks, so if this helper is ever called from a code path that doesn't first run validateMergedJson (e.g. future callers, tests), malformed input will silently produce empty/incorrect field maps rather than surface an error. Today it's safe because both call sites (handleApply after validation, and the literal '{}' in the empty branch) are controlled, but consider either inlining the validation here or asserting/narrowing via the same allowed-fields set used in validateMergedJson.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx` around
lines 86 - 130, mergedToFields currently trusts JSON.parse output as
Record<string, MergedModelEntry> which can silently accept malformed shapes;
update mergedToFields to validate/narrow the parsed value at runtime (use the
same allowed-fields set and field name checks as validateMergedJson or call
validateMergedJson directly) before iterating: ensure the parsed value is an
object, each key maps to an object whose allowed properties (ratio, price,
completion_ratio, etc.) are present only with numeric values; if validation
fails either throw or return {} so malformed input is not silently converted
into incorrect field JSONs.

416-419: ⚡ Quick win

historyRef.current.length is read during render but a ref mutation alone won't trigger a re-render.

The button's disabled and the counter (${historyRef.current.length}) only refresh because handleApply and handleUndo happen to also call setApplied/setMergedDraft, which causes a re-render. If those state updates are ever refactored away (or batched out), the button will go stale (e.g., after the first apply, disabled stays true until another state change occurs).

Consider tracking history length in useState (or replacing the entire ref with state). The undo stack is small and capped at MAX_HISTORY, so the re-render cost is negligible.

♻️ Suggested change
-  const historyRef = useRef<string[]>([])
+  const [history, setHistory] = useState<string[]>([])
@@
-        historyRef.current = [...historyRef.current, currentJson].slice(-MAX_HISTORY)
+        setHistory((h) => [...h, currentJson].slice(-MAX_HISTORY))
@@
-    historyRef.current = [...historyRef.current, currentJson || '{}'].slice(-MAX_HISTORY)
+    setHistory((h) => [...h, currentJson || '{}'].slice(-MAX_HISTORY))
@@
-    const history = historyRef.current
     if (history.length === 0) return
     const prev = history[history.length - 1]
-    historyRef.current = history.slice(0, -1)
+    setHistory((h) => h.slice(0, -1))
@@
-        disabled={historyRef.current.length === 0}
+        disabled={history.length === 0}
@@
-        撤销{historyRef.current.length > 0 ? ` (${historyRef.current.length})` : ''}
+        撤销{history.length > 0 ? ` (${history.length})` : ''}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx` around
lines 416 - 419, historyRef.current.length is read during render but mutations
to historyRef won't trigger re-renders, causing the Undo button's disabled state
and counter to go stale; change to track history length in React state (e.g.,
add historyLength via useState) or replace historyRef with stateful history so
updates cause renders. Update places that push/pop history (functions
handleApply and handleUndo and any other mutation sites) to update the new
historyLength state (or set the new history array) whenever history changes, and
remove reliance on historyRef.current.length in the render (use the state
variable instead); keep MAX_HISTORY logic intact when capping history. Ensure
setApplied/setMergedDraft calls can remain or be decoupled — the key is to
update the history state in the same mutation points so the button and counter
reflect the current history length.

1-689: ⚖️ Poor tradeoff

File now exceeds 200 lines — consider extracting the merged-mode editor.

With the new merged-JSON mode the file has grown to ~690 lines and bundles three editor variants plus all helper functions in a single component. The merged-mode helpers (MergedModelEntry, safeParseJson, fieldsToMerged, mergedToFields, validateMergedJson) and the JSX block under editMode === 'merged' are self-contained and would naturally live in either:

  • a sibling component model-ratio-merged-editor.tsx consuming form as a prop, or
  • a useMergedJsonEditor(form) custom hook returning { draft, error, applied, history, apply, undo, switchInto, onChange, onTab }.

This would also localize the i18n/currency fixes flagged separately.

As per coding guidelines: "Consider splitting components into smaller subcomponents or extracting logic to custom Hooks when a single file exceeds approximately 200 lines".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx` around
lines 1 - 689, This file is too large and the merged-JSON logic should be
extracted: move MergedModelEntry, safeParseJson, fieldsToMerged, mergedToFields,
validateMergedJson, MAX_HISTORY and all merged-mode state/handlers (mergedDraft,
mergedError, applied, historyRef, handleDraftChange, handleMergedJsonTab,
handleApply, handleUndo, switchMode logic for 'merged') plus the JSX under the
editMode === 'merged' branch into a new sibling component (e.g.
ModelRatioMergedEditor) or a hook (e.g. useMergedJsonEditor) that accepts the
form (UseFormReturn<ModelFormValues>) and any necessary callbacks/flags (onSave,
onReset, isSaving, isResetting, t) and exports the UI/handlers; then import and
use that component/hook from ModelRatioForm and remove the duplicated merged
logic from ModelRatioForm so the main file stays <~200 lines. Ensure you keep
the exact function/type names (MergedModelEntry, safeParseJson, fieldsToMerged,
mergedToFields, validateMergedJson, handleApply, handleUndo, handleDraftChange,
handleMergedJsonTab) so references are easy to update and preserve i18n usage
(t) in the extracted UI.

244-252: 💤 Low value

document.execCommand('insertText') — intentional use for native undo preservation.

This approach is valid and necessary. Verification confirms execCommand('insertText') remains supported in Chrome 149+ and Firefox 152+ as of 2026, with no official replacement that preserves the browser's native undo buffer. Direct alternatives like setRangeText or value assignment break undo functionality. MDN explicitly documents this as a legitimate use case with no viable alternatives yet. The existing eslint-disable-next-line deprecation/deprecation comment appropriately acknowledges the trade-off. Consider adding a TODO comment referencing this limitation if future browser behavior changes or alternatives emerge, but no action is required now.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx` around
lines 244 - 252, The use of the deprecated document.execCommand('insertText')
inside handleMergedJsonTab is intentional to preserve native undo; add a short
TODO comment above the execCommand call referencing this limitation and linking
MDN/notes that no viable replacement currently preserves the browser undo stack
(and that this should be revisited if browser behavior changes), and keep the
existing eslint-disable-next-line deprecation/deprecation annotation intact.
docker-compose.dev.yml (1)

60-60: ⚡ Quick win

Pin Redis version for reproducibility.

Using redis:latest removes version pinning and can lead to inconsistent behavior across different developer machines or over time. The previous version (redis:7-alpine) was pinned to major version 7.

📦 Recommended fix
-    image: redis:latest
+    image: redis:7-alpine

Or use the standard (non-Alpine) variant if Alpine compatibility is a concern:

-    image: redis:latest
+    image: redis:7
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.dev.yml` at line 60, The docker-compose service currently uses
an unpinned image tag ("image: redis:latest") which can cause inconsistent
environments; update the image tag for the Redis service to a pinned version
such as "redis:7-alpine" (or "redis:7" if you prefer non-Alpine), replacing the
"image: redis:latest" entry so all developers use the same Redis major/minor
release.
web/default/src/features/system-settings/models/model-pricing-sheet.tsx (1)

412-414: ⚡ Quick win

Replace the nested currency-symbol ternary with explicit branching.

Line 413 uses a 2-level nested ternary; switch/if-else is clearer and aligns with repo rules.

As per coding guidelines, web/default/**/*.{ts,tsx}: "Prohibit nested ternary expressions with 2 or more levels; use if-else, early returns, or extract functions instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-pricing-sheet.tsx`
around lines 412 - 414, The nested ternary assigning currencySymbol based on
getCurrencyLabel() is disallowed; replace it with explicit branching (if-else or
switch) to set currencySymbol from currencyLabel — locate the currencyLabel
constant and the currencySymbol assignment (currencyLabel, currencySymbol,
getCurrencyLabel) and change the expression into a clear branch that returns '¥'
for 'CNY', '' for 'Tokens', and '$' as the default.
web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx (1)

196-197: ⚡ Quick win

Use non-nested branching for currency symbol mapping.

Line 197 introduces a multi-level ternary; please convert it to if/else (or extracted helper) for maintainability and rule compliance.

As per coding guidelines, web/default/**/*.{ts,tsx}: "Prohibit nested ternary expressions with 2 or more levels; use if-else, early returns, or extract functions instead."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx`
around lines 196 - 197, Replace the nested ternary used to compute
currencySymbol (currently using currencyLabel and getCurrencyLabel) with a
non-nested branching approach: either an if/else chain or a small extracted
helper function (e.g., getCurrencySymbol(label: string)) that returns '¥' for
'CNY', '' for 'Tokens', and '$' otherwise; update the assignment to use that
helper or the if/else so the nested ternary is removed and the logic remains
identical.
web/default/src/features/system-settings/integrations/payment-settings-section.tsx (1)

570-570: ⚡ Quick win

Use i18n interpolation pattern instead of string replacement on translated text.

The string replacement approach .replace(/USD|美元/g, currencyName) applied to translated strings is not aligned with i18n best practices. Per i18next conventions and the project's i18n guidelines, currency values should be interpolated within the translation string itself.

Recommended approach:

t('Price ({{currency}})', { currency: getCurrencyLabel() })

This ensures proper localization handling and avoids post-processing translated strings. Current pattern applies string replacement after translation, which can create maintenance issues if translation keys or currency labels change.

Also applies to: lines 597, 610, 1035, 1049, 1061, 1074

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/system-settings/integrations/payment-settings-section.tsx`
at line 570, Replace the post-translation string.replace usage with i18n
interpolation: update the FormLabel calls that currently do t('Price (local
currency / USD)').replace(/USD|美元/g, currencyName) to use t with a placeholder
and pass the currency via options (e.g., t('Price ({{currency}})', { currency:
getCurrencyLabel() })) and do the same for the other occurrences; locate uses of
FormLabel and t in payment-settings-section.tsx (and the variable currencyName)
and change the translation keys to include {{currency}} and supply
getCurrencyLabel()/currencyName through the t call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/billing.go`:
- Around line 47-54: The default branch in controller.billing uses
operation_setting.USDExchangeRate to divide amount (and similar divisions exist
in logger code); add a guard that checks operation_setting.USDExchangeRate != 0
before performing the division (e.g., capture into a local safeRate :=
operation_setting.USDExchangeRate, if safeRate == 0 then log a warning and
either return/skip conversion or use a defined fallback rate), update the
default case in the switch around operation_setting.GetQuotaDisplayType() to use
safeRate for dividing amount, and apply the same validation pattern to any
logger functions that divide by operation_setting.USDExchangeRate so no
unguarded division can panic.

In `@logger/logger.go`:
- Around line 141-144: FormatQuota currently divides by
operation_setting.USDExchangeRate in the default (USD) branch without
validation; replicate the same validation used in the custom currency branch
(check that operation_setting.USDExchangeRate > 0) before doing the division in
the default/USD case inside FormatQuota, and if the rate is invalid return a
safe fallback message or handle the error similarly to the custom currency path
to avoid divide-by-zero or nonsensical results.

In `@web/default/src/features/channels/lib/channel-type-config.ts`:
- Around line 31-77: getDefaultBaseUrl currently only looks up
CHANNEL_TYPE_CONFIGS and can return an empty string for types that were added to
the new CHANNEL_DEFAULT_BASE_URLS map; update getDefaultBaseUrl to first try
CHANNEL_DEFAULT_BASE_URLS[type] (or fallback to
CHANNEL_TYPE_CONFIGS[type]?.baseUrl) so any entry present in
CHANNEL_DEFAULT_BASE_URLS is returned; ensure you reference
CHANNEL_DEFAULT_BASE_URLS and getDefaultBaseUrl (and CHANNEL_TYPE_CONFIGS) so
resolution prefers the centralized default URL table before falling back.

In `@web/default/src/features/models/components/drawers/model-mutate-drawer.tsx`:
- Around line 111-115: The currency mapping currently uses "Tokens" as a prefix
which yields outputs like "Tokens1.2345"; update the mapping in
model-mutate-drawer.tsx (where getCurrencyLabel(), currencyLabel,
currencySymbol, and currencyName are defined) so currencyName remains "Tokens"
for labels but currencySymbol is an actual symbol or an empty string for token
amounts (e.g., currencySymbol = '' when currencyLabel === 'Tokens', keep '¥' for
'CNY' and '$' for 'USD'); ensure consumers that concatenate currencySymbol +
amount rely on currencySymbol being empty for tokens (or include a trailing
space if your UI expects a separator).
- Around line 956-957: Several user-facing strings in model-mutate-drawer.tsx
(e.g., the Label showing "Pricing mode ({currencySymbol}/1M tokens)" and other
phrases like "per 1M tokens", "Calculated price", "Calculated ratio") are
hardcoded and must be wrapped with the component's i18n function; update these
occurrences to use the t() translation helper from useTranslation() and pass
dynamic parts via interpolation (for example t('Pricing mode
({{currencySymbol}}/1M tokens)', { currencySymbol }) or separate keys like
t('Pricing mode') + ' ' + t('({{currencySymbol}}/1M tokens)', { currencySymbol
})), and similarly replace "per 1M tokens", "Calculated price", "Calculated
ratio" and the other flagged lines (around the Label and the lines you noted)
with t(...) calls so all user-facing text is localizable.

In
`@web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx`:
- Around line 51-54: The Amount Due currently uses currencyLabel and sets
currencySymbol = currencyLabel which causes "Tokens99.00"; update the mapping
logic in the getCurrencyLabel/currencySymbol section (refer to getCurrencyLabel,
currencyLabel, currencySymbol) to handle the "Tokens" display type specially:
either map "Tokens" to a proper token symbol or return an empty prefix and use a
dedicated formatter (e.g., formatAmountDue(amount, currencyLabel)) that renders
tokens as a suffix or with a token-specific symbol; ensure any place that prints
currencySymbol (the Amount Due render path) uses the new formatter or mapped
symbol so token amounts render correctly (e.g., "99 Tokens" or "₮99.00") instead
of "Tokens99.00".

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx`:
- Around line 360-367: The table rows in model-ratio-form.tsx hardcode CNY
symbols/text for the "ratio" and "price" descriptions; replace those hardcoded
strings by calling the currency helper (getCurrencyLabel()) and interpolating
its result into the localized description text for the ratio and price rows so
the displayed symbol/label matches the configured currency (follow the same
approach used in model-pricing-sheet.tsx and model-mutate-drawer.tsx); update
the "ratio" and "price" description strings to use the dynamic label from
getCurrencyLabel() (keep other explanatory numbers the same).
- Around line 139-160: The file contains many hardcoded Chinese user-facing
strings (validator messages in the validation function, the Disclosure summary,
table headers, eight field-description rows, helper paragraph,
applied-confirmation banner, and action buttons like "应用到表单" and "撤销") which
bypass i18n; update the component to call useTranslation() and replace every
hardcoded string with t('...') references, add corresponding keys to the locale
JSON (group keys under a concise namespace such as modelRatioForm.* like
modelRatioForm.validator.topLevelMustBeObject,
modelRatioForm.validator.unknownField, modelRatioForm.summary,
modelRatioForm.table.headers.*, modelRatioForm.rows.* , modelRatioForm.helper,
modelRatioForm.banner.applied, modelRatioForm.button.apply,
modelRatioForm.button.revert), and ensure the validator returns translated
messages by invoking t(...) inside the same scope where the validator runs (or
pass t into the validator). Keep key names descriptive and update tests/usage
accordingly.

---

Nitpick comments:
In `@docker-compose.dev.yml`:
- Line 60: The docker-compose service currently uses an unpinned image tag
("image: redis:latest") which can cause inconsistent environments; update the
image tag for the Redis service to a pinned version such as "redis:7-alpine" (or
"redis:7" if you prefer non-Alpine), replacing the "image: redis:latest" entry
so all developers use the same Redis major/minor release.

In
`@web/default/src/features/system-settings/integrations/payment-settings-section.tsx`:
- Line 570: Replace the post-translation string.replace usage with i18n
interpolation: update the FormLabel calls that currently do t('Price (local
currency / USD)').replace(/USD|美元/g, currencyName) to use t with a placeholder
and pass the currency via options (e.g., t('Price ({{currency}})', { currency:
getCurrencyLabel() })) and do the same for the other occurrences; locate uses of
FormLabel and t in payment-settings-section.tsx (and the variable currencyName)
and change the translation keys to include {{currency}} and supply
getCurrencyLabel()/currencyName through the t call.

In `@web/default/src/features/system-settings/models/model-pricing-sheet.tsx`:
- Around line 412-414: The nested ternary assigning currencySymbol based on
getCurrencyLabel() is disallowed; replace it with explicit branching (if-else or
switch) to set currencySymbol from currencyLabel — locate the currencyLabel
constant and the currencySymbol assignment (currencyLabel, currencySymbol,
getCurrencyLabel) and change the expression into a clear branch that returns '¥'
for 'CNY', '' for 'Tokens', and '$' as the default.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx`:
- Around line 86-130: mergedToFields currently trusts JSON.parse output as
Record<string, MergedModelEntry> which can silently accept malformed shapes;
update mergedToFields to validate/narrow the parsed value at runtime (use the
same allowed-fields set and field name checks as validateMergedJson or call
validateMergedJson directly) before iterating: ensure the parsed value is an
object, each key maps to an object whose allowed properties (ratio, price,
completion_ratio, etc.) are present only with numeric values; if validation
fails either throw or return {} so malformed input is not silently converted
into incorrect field JSONs.
- Around line 416-419: historyRef.current.length is read during render but
mutations to historyRef won't trigger re-renders, causing the Undo button's
disabled state and counter to go stale; change to track history length in React
state (e.g., add historyLength via useState) or replace historyRef with stateful
history so updates cause renders. Update places that push/pop history (functions
handleApply and handleUndo and any other mutation sites) to update the new
historyLength state (or set the new history array) whenever history changes, and
remove reliance on historyRef.current.length in the render (use the state
variable instead); keep MAX_HISTORY logic intact when capping history. Ensure
setApplied/setMergedDraft calls can remain or be decoupled — the key is to
update the history state in the same mutation points so the button and counter
reflect the current history length.
- Around line 1-689: This file is too large and the merged-JSON logic should be
extracted: move MergedModelEntry, safeParseJson, fieldsToMerged, mergedToFields,
validateMergedJson, MAX_HISTORY and all merged-mode state/handlers (mergedDraft,
mergedError, applied, historyRef, handleDraftChange, handleMergedJsonTab,
handleApply, handleUndo, switchMode logic for 'merged') plus the JSX under the
editMode === 'merged' branch into a new sibling component (e.g.
ModelRatioMergedEditor) or a hook (e.g. useMergedJsonEditor) that accepts the
form (UseFormReturn<ModelFormValues>) and any necessary callbacks/flags (onSave,
onReset, isSaving, isResetting, t) and exports the UI/handlers; then import and
use that component/hook from ModelRatioForm and remove the duplicated merged
logic from ModelRatioForm so the main file stays <~200 lines. Ensure you keep
the exact function/type names (MergedModelEntry, safeParseJson, fieldsToMerged,
mergedToFields, validateMergedJson, handleApply, handleUndo, handleDraftChange,
handleMergedJsonTab) so references are easy to update and preserve i18n usage
(t) in the extracted UI.
- Around line 244-252: The use of the deprecated
document.execCommand('insertText') inside handleMergedJsonTab is intentional to
preserve native undo; add a short TODO comment above the execCommand call
referencing this limitation and linking MDN/notes that no viable replacement
currently preserves the browser undo stack (and that this should be revisited if
browser behavior changes), and keep the existing eslint-disable-next-line
deprecation/deprecation annotation intact.

In
`@web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx`:
- Around line 196-197: Replace the nested ternary used to compute currencySymbol
(currently using currencyLabel and getCurrencyLabel) with a non-nested branching
approach: either an if/else chain or a small extracted helper function (e.g.,
getCurrencySymbol(label: string)) that returns '¥' for 'CNY', '' for 'Tokens',
and '$' otherwise; update the assignment to use that helper or the if/else so
the nested ternary is removed and the logic remains identical.

In `@web/default/src/i18n/locales/en.json`:
- Line 3032: The flat i18n key "Prompt price" should be accompanied by a
hierarchical alias for consistency; add a new semantic key
"pricing.prompt.price" with the same value "Prompt price" in the JSON (keeping
the existing flat key non-breaking), so both "Prompt price" and
"pricing.prompt.price" point to the same translation string to follow the
project's hierarchical naming convention.

In `@web/default/src/i18n/locales/zh.json`:
- Line 966: The translation was added as a sentence literal ("Cost per request,
regardless of tokens used.") instead of a hierarchical key; change the JSON
entry to a semantic key such as "pricing.costPerRequest": "每请求的费用,不考虑使用的令牌数。",
add the same hierarchical key/value to all locale JSONs (including the English
source), and update any code references that currently use the literal string to
use the new key (e.g., replace occurrences of the sentence literal in locale
files and components with the identifier pricing.costPerRequest). Ensure
consistency by applying the same change to the other similar literal entries
found elsewhere in the locale files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bb803065-9c1d-422c-901f-b0d9148d4a03

📥 Commits

Reviewing files that changed from the base of the PR and between aa56667 and 06fed0e.

⛔ Files ignored due to path filters (1)
  • web/default/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (30)
  • common/constants.go
  • controller/billing.go
  • docker-compose.dev.yml
  • logger/logger.go
  • setting/operation_setting/general_setting.go
  • setting/ratio_setting/model_ratio.go
  • web/classic/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js
  • web/default/rsbuild.config.ts
  • web/default/src/components/json-editor.tsx
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/components/model-mapping-editor.tsx
  • web/default/src/features/channels/lib/channel-type-config.ts
  • web/default/src/features/home/components/sections/features.tsx
  • web/default/src/features/home/constants.ts
  • web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
  • web/default/src/features/pricing/components/dynamic-pricing-breakdown.tsx
  • web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx
  • web/default/src/features/system-settings/billing/index.tsx
  • web/default/src/features/system-settings/integrations/amount-discount-dialog.tsx
  • web/default/src/features/system-settings/integrations/amount-options-visual-editor.tsx
  • web/default/src/features/system-settings/integrations/payment-settings-section.tsx
  • web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx
  • web/default/src/features/system-settings/integrations/waffo-settings-section.tsx
  • web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/default/src/features/system-settings/models/model-ratio-form.tsx
  • web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/zh.json
  • web/default/src/lib/currency.ts
  • web/default/src/stores/system-config-store.ts

Comment thread controller/billing.go
Comment on lines 47 to 54
switch operation_setting.GetQuotaDisplayType() {
case operation_setting.QuotaDisplayTypeCNY:
amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
amount = amount / common.QuotaPerUnit
case operation_setting.QuotaDisplayTypeTokens:
// amount 保持 tokens 数值
default:
amount = amount / common.QuotaPerUnit
amount = amount / common.QuotaPerUnit / operation_setting.USDExchangeRate
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for USDExchangeRate validation in operation_setting

# Search for USDExchangeRate assignments and validation
rg -nP -C5 'USDExchangeRate\s*=' --type=go

# Search for validation patterns (> 0, != 0, etc.)
rg -nP 'USDExchangeRate.*[>!]=\s*0' --type=go

Repository: QuantumNous/new-api

Length of output: 860


🏁 Script executed:

# Check the operation_setting package structure
find . -name "*operation_setting*" -type f | grep -E "\.(go|proto)$"

Repository: QuantumNous/new-api

Length of output: 112


🏁 Script executed:

# Examine billing.go to see the actual code at lines 47-54 and around line 99
rg -n "GetQuotaDisplayType|GetUsage" --type=go -B2 -A10 | head -100

Repository: QuantumNous/new-api

Length of output: 6307


🏁 Script executed:

# Check how USDExchangeRate is initialized and used in operation_setting
rg -n "USDExchangeRate" --type=go -B3 -A3

Repository: QuantumNous/new-api

Length of output: 3586


🏁 Script executed:

# Read the operation_setting.go file to check its contents
cat -n setting/operation_setting/operation_setting.go | head -100

Repository: QuantumNous/new-api

Length of output: 1081


🏁 Script executed:

# Check if USDExchangeRate has any getter function with validation
rg -n "func.*USDExchangeRate|USDExchangeRate\s*>" setting/operation_setting/ --type=go

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check payment_setting_old.go and any other payment-related files
cat -n setting/operation_setting/payment_setting_old.go | head -50

Repository: QuantumNous/new-api

Length of output: 1474


🏁 Script executed:

# Check the context around the USDExchangeRate assignment in model/option.go
sed -n '365,375p' model/option.go

Repository: QuantumNous/new-api

Length of output: 469


🏁 Script executed:

# Search for any validation or guards around USDExchangeRate usage
rg -n "USDExchangeRate" --type=go -B2 -A2 | grep -E "(<=|>=|>|<|!=|==|if|validate|check|guard)" -B2 -A2

Repository: QuantumNous/new-api

Length of output: 305


🏁 Script executed:

# Check if there's any initialization or validation logic when loading settings
rg -n "LoadOptions|InitOptions|USDExchangeRate" --type=go -B5 -A5 | head -80

Repository: QuantumNous/new-api

Length of output: 4805


🏁 Script executed:

# Check if there are any guards around divisions by USDExchangeRate
rg -n "USDExchangeRate" controller/billing.go logger/logger.go --type=go -B5 | grep -E "(if|guard|validate|<=|>=)"

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Verify the entire context of controller/billing.go around the issue
sed -n '45,60p' controller/billing.go
sed -n '92,105p' controller/billing.go

Repository: QuantumNous/new-api

Length of output: 1027


Validate USDExchangeRate to prevent division by zero.

The default case divides by operation_setting.USDExchangeRate at lines 53 and 99 without validation. If the exchange rate is zero or becomes zero (e.g., from a failed strconv.ParseFloat in model/option.go:369), this will cause a runtime panic. Additional unguarded divisions exist in logger/logger.go at lines 142 and 168.

🛡️ Proposed fix to add validation
 	switch operation_setting.GetQuotaDisplayType() {
 	case operation_setting.QuotaDisplayTypeCNY:
 		amount = amount / common.QuotaPerUnit
 	case operation_setting.QuotaDisplayTypeTokens:
 		// amount 保持 tokens 数值
 	default:
+		if operation_setting.USDExchangeRate <= 0 {
+			operation_setting.USDExchangeRate = 1.0 // fallback to 1:1 if invalid
+		}
 		amount = amount / common.QuotaPerUnit / operation_setting.USDExchangeRate
 	}
📝 Committable suggestion

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

Suggested change
switch operation_setting.GetQuotaDisplayType() {
case operation_setting.QuotaDisplayTypeCNY:
amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
amount = amount / common.QuotaPerUnit
case operation_setting.QuotaDisplayTypeTokens:
// amount 保持 tokens 数值
default:
amount = amount / common.QuotaPerUnit
amount = amount / common.QuotaPerUnit / operation_setting.USDExchangeRate
}
switch operation_setting.GetQuotaDisplayType() {
case operation_setting.QuotaDisplayTypeCNY:
amount = amount / common.QuotaPerUnit
case operation_setting.QuotaDisplayTypeTokens:
// amount 保持 tokens 数值
default:
if operation_setting.USDExchangeRate <= 0 {
operation_setting.USDExchangeRate = 1.0 // fallback to 1:1 if invalid
}
amount = amount / common.QuotaPerUnit / operation_setting.USDExchangeRate
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/billing.go` around lines 47 - 54, The default branch in
controller.billing uses operation_setting.USDExchangeRate to divide amount (and
similar divisions exist in logger code); add a guard that checks
operation_setting.USDExchangeRate != 0 before performing the division (e.g.,
capture into a local safeRate := operation_setting.USDExchangeRate, if safeRate
== 0 then log a warning and either return/skip conversion or use a defined
fallback rate), update the default case in the switch around
operation_setting.GetQuotaDisplayType() to use safeRate for dividing amount, and
apply the same validation pattern to any logger functions that divide by
operation_setting.USDExchangeRate so no unguarded division can panic.

Comment thread logger/logger.go
Comment on lines 141 to 144
default: // USD
return fmt.Sprintf("$%.6f 额度", q/common.QuotaPerUnit)
usd := q / common.QuotaPerUnit / operation_setting.USDExchangeRate
return fmt.Sprintf("$%.6f 额度", usd)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate USDExchangeRate before division.

Line 142 (and line 168 in FormatQuota) divides by operation_setting.USDExchangeRate without validation. If the exchange rate is zero or negative, this will cause a runtime panic or produce nonsensical results. The custom currency case has proper validation (lines 134-136), but the USD case does not.

🛡️ Proposed fix to add validation
 	default: // USD
+		exchangeRate := operation_setting.USDExchangeRate
+		if exchangeRate <= 0 {
+			exchangeRate = 1.0
+		}
-		usd := q / common.QuotaPerUnit / operation_setting.USDExchangeRate
+		usd := q / common.QuotaPerUnit / exchangeRate
 		return fmt.Sprintf("$%.6f 额度", usd)
 	}

Apply a similar fix to FormatQuota at lines 167-169.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logger/logger.go` around lines 141 - 144, FormatQuota currently divides by
operation_setting.USDExchangeRate in the default (USD) branch without
validation; replicate the same validation used in the custom currency branch
(check that operation_setting.USDExchangeRate > 0) before doing the division in
the default/USD case inside FormatQuota, and if the rate is invalid return a
safe fallback message or handle the error similarly to the custom currency path
to avoid divide-by-zero or nonsensical results.

Comment on lines +31 to +77
export const CHANNEL_DEFAULT_BASE_URLS: Record<number, string> = {
1: 'https://api.openai.com',
2: 'https://oa.api2d.net',
4: 'http://localhost:11434',
5: 'https://api.openai-sb.com',
6: 'https://api.openaimax.com',
7: 'https://api.ohmygpt.com',
9: 'https://api.caipacity.com',
10: 'https://api.aiproxy.io',
12: 'https://api.api2gpt.com',
13: 'https://api.aigc2d.com',
14: 'https://api.anthropic.com',
15: 'https://aip.baidubce.com',
16: 'https://open.bigmodel.cn',
17: 'https://dashscope.aliyuncs.com',
19: 'https://api.360.cn',
20: 'https://openrouter.ai/api',
21: 'https://api.aiproxy.io',
22: 'https://fastgpt.run/api/openapi',
23: 'https://hunyuan.tencentcloudapi.com',
24: 'https://generativelanguage.googleapis.com',
25: 'https://api.moonshot.cn',
26: 'https://open.bigmodel.cn',
27: 'https://api.perplexity.ai',
31: 'https://api.lingyiwanwu.com',
34: 'https://api.cohere.ai',
35: 'https://api.minimax.chat',
37: 'https://api.dify.ai',
38: 'https://api.jina.ai',
39: 'https://api.cloudflare.com',
40: 'https://api.siliconflow.cn',
42: 'https://api.mistral.ai',
43: 'https://api.deepseek.com',
44: 'https://api.moka.ai',
45: 'https://ark.cn-beijing.volces.com',
46: 'https://qianfan.baidubce.com',
48: 'https://api.x.ai',
49: 'https://api.coze.cn',
50: 'https://api.klingai.com',
51: 'https://visual.volcengineapi.com',
52: 'https://api.vidu.cn',
53: 'https://llm.submodel.ai',
54: 'https://ark.cn-beijing.volces.com',
55: 'https://api.openai.com',
56: 'https://api.replicate.com',
57: 'https://chatgpt.com',
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

getDefaultBaseUrl does not use the new centralized default URL table

After introducing CHANNEL_DEFAULT_BASE_URLS, the helper still resolves from CHANNEL_TYPE_CONFIGS only (Line 309), so types that exist only in the map can still return ''.

Suggested fix
 export function getDefaultBaseUrl(type: number): string {
-  return CHANNEL_TYPE_CONFIGS[type]?.defaultBaseUrl || ''
+  return (
+    CHANNEL_DEFAULT_BASE_URLS[type] ??
+    CHANNEL_TYPE_CONFIGS[type]?.defaultBaseUrl ??
+    ''
+  )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/channels/lib/channel-type-config.ts` around lines 31
- 77, getDefaultBaseUrl currently only looks up CHANNEL_TYPE_CONFIGS and can
return an empty string for types that were added to the new
CHANNEL_DEFAULT_BASE_URLS map; update getDefaultBaseUrl to first try
CHANNEL_DEFAULT_BASE_URLS[type] (or fallback to
CHANNEL_TYPE_CONFIGS[type]?.baseUrl) so any entry present in
CHANNEL_DEFAULT_BASE_URLS is returned; ensure you reference
CHANNEL_DEFAULT_BASE_URLS and getDefaultBaseUrl (and CHANNEL_TYPE_CONFIGS) so
resolution prefers the centralized default URL table before falling back.

Comment on lines +111 to +115
// Currency display
const currencyLabel = getCurrencyLabel()
const currencySymbol =
currencyLabel === 'CNY' ? '¥' : currencyLabel === 'USD' ? '$' : currencyLabel
const currencyName = currencyLabel === 'Tokens' ? 'Tokens' : currencyLabel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tokens is being used as a currency prefix in price strings.

With the current mapping, token display type can produce outputs like Tokens1.2345, which is not a valid currency presentation for these pricing fields. Keep currencyName for labels, but map currencySymbol to an actual symbol (or empty string) for amount prefixes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/models/components/drawers/model-mutate-drawer.tsx`
around lines 111 - 115, The currency mapping currently uses "Tokens" as a prefix
which yields outputs like "Tokens1.2345"; update the mapping in
model-mutate-drawer.tsx (where getCurrencyLabel(), currencyLabel,
currencySymbol, and currencyName are defined) so currencyName remains "Tokens"
for labels but currencySymbol is an actual symbol or an empty string for token
amounts (e.g., currencySymbol = '' when currencyLabel === 'Tokens', keep '¥' for
'CNY' and '$' for 'USD'); ensure consumers that concatenate currencySymbol +
amount rely on currencySymbol being empty for tokens (or include a trailing
space if your UI expects a separator).

Comment on lines +956 to 957
{t('Pricing mode')} ({currencySymbol}/1M tokens)
</Label>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Several changed user-facing pricing strings are not fully i18n-safe.

These lines still include hardcoded English fragments (e.g., per 1M tokens, Calculated price, Calculated ratio) outside t(...), so they won’t localize correctly.

As per coding guidelines, web/default/**/*.{tsx,ts}: "All user-facing text content must support i18n using the t() function from useTranslation() in React components."

Also applies to: 992-993, 1034-1035, 1046-1047, 1063-1064

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/models/components/drawers/model-mutate-drawer.tsx`
around lines 956 - 957, Several user-facing strings in model-mutate-drawer.tsx
(e.g., the Label showing "Pricing mode ({currencySymbol}/1M tokens)" and other
phrases like "per 1M tokens", "Calculated price", "Calculated ratio") are
hardcoded and must be wrapped with the component's i18n function; update these
occurrences to use the t() translation helper from useTranslation() and pass
dynamic parts via interpolation (for example t('Pricing mode
({{currencySymbol}}/1M tokens)', { currencySymbol }) or separate keys like
t('Pricing mode') + ' ' + t('({{currencySymbol}}/1M tokens)', { currencySymbol
})), and similarly replace "per 1M tokens", "Calculated price", "Calculated
ratio" and the other flagged lines (around the Label and the lines you noted)
with t(...) calls so all user-facing text is localizable.

Comment on lines +51 to 54
const currencyLabel = getCurrencyLabel()
const currencySymbol =
currencyLabel === 'CNY' ? '¥' : currencyLabel === 'USD' ? '$' : currencyLabel
const [paying, setPaying] = useState(false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Amount Due can render invalid prefixes like Tokens99.00.

When display type is Tokens, Line 53 sets currencySymbol to "Tokens", and Line 233 prints it as a monetary prefix. Please map token mode to a real billing currency symbol (or dedicated billing formatter) instead of the literal label.

Also applies to: 233-233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx`
around lines 51 - 54, The Amount Due currently uses currencyLabel and sets
currencySymbol = currencyLabel which causes "Tokens99.00"; update the mapping
logic in the getCurrencyLabel/currencySymbol section (refer to getCurrencyLabel,
currencyLabel, currencySymbol) to handle the "Tokens" display type specially:
either map "Tokens" to a proper token symbol or return an empty prefix and use a
dedicated formatter (e.g., formatAmountDue(amount, currencyLabel)) that renders
tokens as a suffix or with a token-specific symbol; ensure any place that prints
currencySymbol (the Amount Due render path) uses the new formatter or mapped
symbol so token amounts render correctly (e.g., "99 Tokens" or "₮99.00") instead
of "Tokens99.00".

Comment on lines +139 to +160
return '顶层必须是对象 { "模型名": { ... } }'
}
const validFields = new Set([
'ratio', 'price', 'completion_ratio', 'cache_ratio',
'create_cache_ratio', 'image_ratio', 'audio_ratio', 'audio_completion_ratio',
])
for (const [model, entry] of Object.entries(parsed)) {
if (typeof entry !== 'object' || Array.isArray(entry) || entry === null) {
return `"${model}" 的值必须是对象,例如 { "ratio": 1.0 }`
}
for (const [field, val] of Object.entries(entry as Record<string, unknown>)) {
if (!validFields.has(field)) {
return `"${model}" 包含未知字段 "${field}",可用字段:${[...validFields].join(', ')}`
}
if (typeof val !== 'number') {
return `"${model}.${field}" 的值必须是数字,当前为 ${typeof val}`
}
}
}
return null
} catch (e) {
return 'JSON 格式错误:' + (e instanceof Error ? e.message : String(e))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Hardcoded user-facing strings bypass i18n.

The merged-mode UI introduces many hardcoded Chinese strings instead of routing them through t():

  • Validator error messages (lines 139, 147, 151, 154, 160).
  • Disclosure summary and table headers (lines 349, 354–356).
  • All eight field-description rows (lines 360–367).
  • Helper paragraph (line 374), applied-confirmation banner (line 389), action buttons "应用到表单" (line 410) and "撤销" (line 419).

This breaks language switching for non-Chinese users (the rest of the form, including the Visual/Field JSON tab labels, correctly uses t('Visual'), t('Merged JSON'), etc.). All of these should be moved to the i18n locale files and rendered via useTranslation.

As per coding guidelines: "All user-facing text content must support i18n using the t() function from useTranslation() in React components".

Also applies to: 349-419

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx` around
lines 139 - 160, The file contains many hardcoded Chinese user-facing strings
(validator messages in the validation function, the Disclosure summary, table
headers, eight field-description rows, helper paragraph, applied-confirmation
banner, and action buttons like "应用到表单" and "撤销") which bypass i18n; update the
component to call useTranslation() and replace every hardcoded string with
t('...') references, add corresponding keys to the locale JSON (group keys under
a concise namespace such as modelRatioForm.* like
modelRatioForm.validator.topLevelMustBeObject,
modelRatioForm.validator.unknownField, modelRatioForm.summary,
modelRatioForm.table.headers.*, modelRatioForm.rows.* , modelRatioForm.helper,
modelRatioForm.banner.applied, modelRatioForm.button.apply,
modelRatioForm.button.revert), and ensure the validator returns translated
messages by invoking t(...) inside the same scope where the validator runs (or
pass t into the validator). Keep key names descriptive and update tests/usage
accordingly.

Comment on lines +360 to +367
<tr><td className='px-3 py-2 font-mono'>ratio</td><td className='px-3 py-2'>number</td><td className='px-3 py-2'>输入 token 计费比率。1 = ¥0.002/1K tokens = ¥2/1M tokens</td></tr>
<tr><td className='px-3 py-2 font-mono'>price</td><td className='px-3 py-2'>number</td><td className='px-3 py-2'>每次请求固定价格(¥/次),优先于 ratio</td></tr>
<tr><td className='px-3 py-2 font-mono'>completion_ratio</td><td className='px-3 py-2'>number</td><td className='px-3 py-2'>输出/输入价格倍数。如 3.0 表示输出价格 = 输入 × 3</td></tr>
<tr><td className='px-3 py-2 font-mono'>cache_ratio</td><td className='px-3 py-2'>number</td><td className='px-3 py-2'>缓存读取折扣,通常 0.1~0.5</td></tr>
<tr><td className='px-3 py-2 font-mono'>create_cache_ratio</td><td className='px-3 py-2'>number</td><td className='px-3 py-2'>写入缓存的费用倍数,通常 1.25</td></tr>
<tr><td className='px-3 py-2 font-mono'>image_ratio</td><td className='px-3 py-2'>number</td><td className='px-3 py-2'>图像输入倍数</td></tr>
<tr><td className='px-3 py-2 font-mono'>audio_ratio</td><td className='px-3 py-2'>number</td><td className='px-3 py-2'>音频输入倍数</td></tr>
<tr><td className='px-3 py-2 font-mono'>audio_completion_ratio</td><td className='px-3 py-2'>number</td><td className='px-3 py-2'>音频输出倍数</td></tr>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Description hardcodes the ¥ symbol and CNY-denominated example pricing.

This PR's stated goal is to make currency configurable via getCurrencyLabel() (USD/CNY), but the ratio and price description rows hardcode ¥0.002/1K tokens, ¥2/1M tokens, and ¥/次. Users who configure the system to display USD will see CNY pricing in the field-description table, contradicting the per-component currency-aware rendering work in model-pricing-sheet.tsx and model-mutate-drawer.tsx.

Derive the symbol/label from the currency store (e.g. getCurrencyLabel()) and interpolate it into the localized strings instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx` around
lines 360 - 367, The table rows in model-ratio-form.tsx hardcode CNY
symbols/text for the "ratio" and "price" descriptions; replace those hardcoded
strings by calling the currency helper (getCurrencyLabel()) and interpolating
its result into the localized description text for the ratio and price rows so
the displayed symbol/label matches the configured currency (follow the same
approach used in model-pricing-sheet.tsx and model-mutate-drawer.tsx); update
the "ratio" and "price" description strings to use the dynamic label from
getCurrencyLabel() (keep other explanatory numbers the same).

lzc and others added 2 commits May 14, 2026 09:25
Merged JSON output is now structured into two named sections:
  "已设置价格" (configured): models with ratio or fixed price
  "未设置价格" (unconfigured): models with only secondary fields

The parser accepts both the new grouped format and the legacy flat
format so existing JSON pasted in still works. Format detection
inspects value shape (objects-of-objects vs objects-of-numbers) to
avoid misidentifying models that happen to be named the same as a
group section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ured)

Canonical group section names are now 'configured' and 'unconfigured'
instead of the Chinese variants. The Chinese names ('已设置价格' /
'未设置价格') stay in GROUP_KEYS so JSON saved before this change is
still accepted as input.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
web/default/src/features/system-settings/models/model-ratio-form.tsx (2)

317-317: ⚡ Quick win

Reading historyRef.current.length directly in JSX is fragile.

The undo button's disabled and counter label depend on a mutable ref read during render. It happens to update today only because every mutation of historyRef.current is paired with a state setter (setApplied/setMergedDraft/setEditMode) that triggers a re-render. Any future code path that grows/shrinks the history without an accompanying state update will leave the button stale. Promote history to component state so re-renders are driven by the data itself.

♻️ Suggested change
-  // 回滚历史栈:每次"应用"前把当前 draft 快照入栈
-  const historyRef = useRef<string[]>([])
+  // 回滚历史栈:每次"应用"前把当前 draft 快照入栈
+  const [history, setHistory] = useState<string[]>([])
@@
-        historyRef.current = [] // 清空历史
+        setHistory([]) // 清空历史
@@
-      if (currentJson) {
-        historyRef.current = [...historyRef.current, currentJson].slice(-MAX_HISTORY)
-      }
+      if (currentJson) {
+        setHistory((h) => [...h, currentJson].slice(-MAX_HISTORY))
+      }
@@
-    historyRef.current = [...historyRef.current, currentJson || '{}'].slice(-MAX_HISTORY)
+    setHistory((h) => [...h, currentJson || '{}'].slice(-MAX_HISTORY))
@@
-    const history = historyRef.current
-    if (history.length === 0) return
-    const prev = history[history.length - 1]
-    historyRef.current = history.slice(0, -1)
+    if (history.length === 0) return
+    const prev = history[history.length - 1]
+    setHistory((h) => h.slice(0, -1))
@@
-                disabled={historyRef.current.length === 0}
+                disabled={history.length === 0}
@@
-                撤销{historyRef.current.length > 0 ? ` (${historyRef.current.length})` : ''}
+                撤销{history.length > 0 ? ` (${history.length})` : ''}

Then add history to handleUndo's dependency array (or read via functional setter).

Also applies to: 537-540

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx` at line
317, Replace the mutable ref historyRef (const historyRef =
useRef<string[]>([])) with a reactive state variable (e.g., const [history,
setHistory] = useState<string[]>([])) and update all reads/writes that currently
use historyRef.current to use history and setHistory so component re-renders
whenever history changes; update functions that push/pop history (where
setApplied, setMergedDraft, setEditMode are called) to call setHistory (or use
functional updater) instead of mutating the ref; also update handleUndo to
include history in its dependency array (or read via functional setter) so the
undo button disabled state and counter label are driven by state rather than a
mutable ref.

19-278: 🏗️ Heavy lift

Extract merged-JSON helpers and merged-editor UI into separate modules.

This file is now ~810 lines and mixes pure parsing/validation utilities (lines 19–278) with a large merged-editor JSX block (lines 457–551) on top of the existing visual/field-JSON modes. Per the project guideline to split files exceeding ~200 lines, consider:

  • Move MergedModelEntry, safeParseJson, isGroupedFormat, normalizeToFlat, fieldsToMerged, mergedToFields, validateMergedJson, and the group/field constants into ./lib/merged-json.ts (pure, easily unit-testable).
  • Extract the merged-mode panel (toolbar buttons aside) into a MergedJsonEditor subcomponent that takes form (or value/onApply/onUndo handlers) as props, including its draft/error/applied/history state and the field-description table.

This both restores the single-responsibility shape of ModelRatioForm and isolates the helpers so they can be tested without rendering the form.

As per coding guidelines: "Consider splitting components into smaller subcomponents or extracting logic to custom Hooks when a single file exceeds approximately 200 lines".

Also applies to: 457-551

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/system-settings/models/model-ratio-form.tsx` around
lines 19 - 278, The current file mixes pure merged-JSON utilities and UI;
extract the parsing/validation/types/constants (MergedModelEntry, safeParseJson,
isGroupedFormat, normalizeToFlat, fieldsToMerged, mergedToFields,
validateMergedJson, GROUP_CONFIGURED, GROUP_UNCONFIGURED, GROUP_KEYS,
VALID_FIELDS, MAX_HISTORY) into a new module ./lib/merged-json.ts exporting
those symbols, and remove them from the component file; then extract the
merged-mode UI block into a new MergedJsonEditor React component that accepts
the form value/handlers (value, onApply, onUndo, onChange or a form object),
manages its own draft/error/applied/history state and renders the
field-description table and toolbar (keep toolbar buttons outside if intended),
and have ModelRatioForm import the helpers from ./lib/merged-json.ts and render
<MergedJsonEditor .../> to preserve behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@web/default/src/features/system-settings/models/model-ratio-form.tsx`:
- Line 317: Replace the mutable ref historyRef (const historyRef =
useRef<string[]>([])) with a reactive state variable (e.g., const [history,
setHistory] = useState<string[]>([])) and update all reads/writes that currently
use historyRef.current to use history and setHistory so component re-renders
whenever history changes; update functions that push/pop history (where
setApplied, setMergedDraft, setEditMode are called) to call setHistory (or use
functional updater) instead of mutating the ref; also update handleUndo to
include history in its dependency array (or read via functional setter) so the
undo button disabled state and counter label are driven by state rather than a
mutable ref.
- Around line 19-278: The current file mixes pure merged-JSON utilities and UI;
extract the parsing/validation/types/constants (MergedModelEntry, safeParseJson,
isGroupedFormat, normalizeToFlat, fieldsToMerged, mergedToFields,
validateMergedJson, GROUP_CONFIGURED, GROUP_UNCONFIGURED, GROUP_KEYS,
VALID_FIELDS, MAX_HISTORY) into a new module ./lib/merged-json.ts exporting
those symbols, and remove them from the component file; then extract the
merged-mode UI block into a new MergedJsonEditor React component that accepts
the form value/handlers (value, onApply, onUndo, onChange or a form object),
manages its own draft/error/applied/history state and renders the
field-description table and toolbar (keep toolbar buttons outside if intended),
and have ModelRatioForm import the helpers from ./lib/merged-json.ts and render
<MergedJsonEditor .../> to preserve behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a04d28d8-d97f-4bbc-abdd-8e3167148d8e

📥 Commits

Reviewing files that changed from the base of the PR and between 06fed0e and fd90731.

📒 Files selected for processing (1)
  • web/default/src/features/system-settings/models/model-ratio-form.tsx

- Swap Chat/Playground sidebar labels to match actual page content
- Dashboard overview: rename "Playground" quick action to "Chat"
- Home page: replace Gemini demo with DeepSeek (reasoning API), reorder tabs (DeepSeek before Claude), switch COST currency to CNY (¥)
- Add cache_tokens/cache_creation_tokens to quota_data table for dashboard cache hit tracking
- Add "Cache Hits" stat card to model dashboard (6-column layout)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
web/default/src/features/home/components/hero-terminal-demo.tsx (1)

285-285: ⚡ Quick win

Avoid hardcoding currency symbol/rate in UI rendering.

¥{(demo.tokens * 0.0002).toFixed(4)} bakes in display symbol + pricing multiplier locally. Prefer using the shared currency/quota formatter so this stays consistent with global conversion and future currency changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/home/components/hero-terminal-demo.tsx` at line 285,
Replace the hardcoded currency rendering "¥{(demo.tokens * 0.0002).toFixed(4)}"
in hero-terminal-demo.tsx with the shared currency/quota formatter used across
the app (e.g., the common formatCurrency or formatQuota util) so symbol and
conversion rate are not baked into the component; pass demo.tokens (or the
appropriate token-to-fiat value) into that formatter instead of multiplying by
0.0002 and concatenating the "¥" manually.
model/log.go (1)

248-271: ⚡ Quick win

Extract a helper to reduce type-switch duplication.

The cache token extraction block is duplicated for both cache_tokens and cache_creation_tokens. Both switch statements handle int and float64 identically. Extracting a small helper reduces duplication and makes the code more maintainable.

While the current switch covers the types seen in practice (all upstream callers populate Other with int values via service functions like GenerateTextOtherInfo), a helper could defensively handle int64 as well without additional cost.

♻️ Proposed refactor
 	if common.DataExportEnabled {
-		// Extract cache token counts from the Other map before spawning goroutine
-		cacheTokens := 0
-		cacheCreationTokens := 0
-		if v, ok := params.Other["cache_tokens"]; ok {
-			switch n := v.(type) {
-			case int:
-				cacheTokens = n
-			case float64:
-				cacheTokens = int(n)
-			}
-		}
-		if v, ok := params.Other["cache_creation_tokens"]; ok {
-			switch n := v.(type) {
-			case int:
-				cacheCreationTokens = n
-			case float64:
-				cacheCreationTokens = int(n)
-			}
-		}
+		// Extract cache token counts from the Other map before spawning goroutine
+		cacheTokens := intFromOther(params.Other, "cache_tokens")
+		cacheCreationTokens := intFromOther(params.Other, "cache_creation_tokens")
 		gopool.Go(func() {
 			LogQuotaData(userId, username, params.ModelName, params.Quota, common.GetTimestamp(), params.PromptTokens+params.CompletionTokens, cacheTokens, cacheCreationTokens)
 		})
 	}

Add the helper in model/log.go:

func intFromOther(m map[string]interface{}, key string) int {
	v, ok := m[key]
	if !ok {
		return 0
	}
	switch n := v.(type) {
	case int:
		return n
	case int64:
		return int(n)
	case float64:
		return int(n)
	}
	return 0
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/log.go` around lines 248 - 271, Extract the duplicated type-switch
logic that reads integer-like values from params.Other into a small helper
(e.g., intFromOther(map[string]interface{}, key string) int) and use it for both
"cache_tokens" and "cache_creation_tokens" before spawning the goroutine; update
the block around the gopool.Go call to call intFromOther(params.Other,
"cache_tokens") and intFromOther(params.Other, "cache_creation_tokens") when
calling LogQuotaData, and make the helper defensively handle int, int64, and
float64 returning 0 for missing/unsupported types.
web/default/src/features/dashboard/lib/stats.ts (1)

26-29: ⚡ Quick win

Add an explicit return/accumulator type for calculateDashboardStats.

The new fields make this object shape more important to lock down. Please type the reducer and function return explicitly to avoid silent shape drift.

Proposed refactor
+type DashboardStats = {
+  totalQuota: number
+  totalCount: number
+  totalTokens: number
+  totalCacheTokens: number
+  totalCacheCreationTokens: number
+}
+
-export function calculateDashboardStats(data: QuotaDataItem[]) {
-  return data.reduce(
+export function calculateDashboardStats(data: QuotaDataItem[]): DashboardStats {
+  return data.reduce<DashboardStats>(
     (acc, item) => ({
       totalQuota: acc.totalQuota + (Number(item.quota) || 0),
       totalCount: acc.totalCount + (Number(item.count) || 0),
       totalTokens: acc.totalTokens + (Number(item.token_used) || 0),
       totalCacheTokens: acc.totalCacheTokens + (Number(item.cache_tokens) || 0),
       totalCacheCreationTokens:
         acc.totalCacheCreationTokens +
         (Number(item.cache_creation_tokens) || 0),
     }),
     {
       totalQuota: 0,
       totalCount: 0,
       totalTokens: 0,
       totalCacheTokens: 0,
       totalCacheCreationTokens: 0,
     }
   )
 }

As per coding guidelines, "Avoid any type in TypeScript; prefer specific types or unknown; explicitly annotate parameter and return value types".

Also applies to: 31-37

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/dashboard/lib/stats.ts` around lines 26 - 29, The
reducer and function return types for calculateDashboardStats are not explicitly
annotated, risking silent shape drift after adding fields like totalCacheTokens
and totalCacheCreationTokens; add a dedicated typed interface (e.g.,
DashboardStats) describing all accumulator properties (totalCacheTokens,
totalCacheCreationTokens, plus the other totals present) and annotate the
reducer accumulator parameter and the calculateDashboardStats return type with
that interface, and also type the reducer function signature passed to
Array.prototype.reduce to ensure TypeScript enforces the object shape.
web/default/src/i18n/locales/en.json (1)

582-582: ⚡ Quick win

Use semantic hierarchical i18n keys for new entries.

These new translation keys are user-facing sentences/labels instead of stable semantic keys, which makes reuse and maintenance harder. Please switch these additions to namespaced keys (for example, dashboard.stats.cacheHits, pricing.promptPrice, etc.) and update corresponding call sites.

As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".

Also applies to: 3033-3033, 3801-3801, 3955-3956

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/i18n/locales/en.json` at line 582, Replace the flat
user-facing key "Cache Hits" in en.json with a semantic hierarchical key such as
"dashboard.stats.cacheHits" and update every call site that references the
literal "Cache Hits" to use the new key (e.g.,
i18n.t('dashboard.stats.cacheHits')). Do the same refactor for the other
mentioned entries (lines around 3033, 3801, 3955-3956) using consistent
namespaces like "pricing.promptPrice" or appropriate feature namespaces; ensure
keys are added to en.json and all components/services that used the old literal
keys are updated to use the new namespaced keys so runtime lookups still
resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/default/src/features/home/components/hero-terminal-demo.tsx`:
- Around line 96-120: The added user-facing strings like "DeepSeek", "Analyzing
the request...", and "DeepSeek request served." in hero-terminal-demo.tsx are
hardcoded; import and use the useTranslation hook (const { t } =
useTranslation()) in the component and replace these literals with t('...') keys
(e.g., t('hero.deepseek.label'), t('hero.deepseek.analyzing'),
t('hero.deepseek.served')) so the UI re-renders on language change; apply the
same replacement for the other occurrences noted (around lines referenced
395-401 and 434-440) and ensure you add matching i18n entries for the new keys
in the translation JSON files.

---

Nitpick comments:
In `@model/log.go`:
- Around line 248-271: Extract the duplicated type-switch logic that reads
integer-like values from params.Other into a small helper (e.g.,
intFromOther(map[string]interface{}, key string) int) and use it for both
"cache_tokens" and "cache_creation_tokens" before spawning the goroutine; update
the block around the gopool.Go call to call intFromOther(params.Other,
"cache_tokens") and intFromOther(params.Other, "cache_creation_tokens") when
calling LogQuotaData, and make the helper defensively handle int, int64, and
float64 returning 0 for missing/unsupported types.

In `@web/default/src/features/dashboard/lib/stats.ts`:
- Around line 26-29: The reducer and function return types for
calculateDashboardStats are not explicitly annotated, risking silent shape drift
after adding fields like totalCacheTokens and totalCacheCreationTokens; add a
dedicated typed interface (e.g., DashboardStats) describing all accumulator
properties (totalCacheTokens, totalCacheCreationTokens, plus the other totals
present) and annotate the reducer accumulator parameter and the
calculateDashboardStats return type with that interface, and also type the
reducer function signature passed to Array.prototype.reduce to ensure TypeScript
enforces the object shape.

In `@web/default/src/features/home/components/hero-terminal-demo.tsx`:
- Line 285: Replace the hardcoded currency rendering "¥{(demo.tokens *
0.0002).toFixed(4)}" in hero-terminal-demo.tsx with the shared currency/quota
formatter used across the app (e.g., the common formatCurrency or formatQuota
util) so symbol and conversion rate are not baked into the component; pass
demo.tokens (or the appropriate token-to-fiat value) into that formatter instead
of multiplying by 0.0002 and concatenating the "¥" manually.

In `@web/default/src/i18n/locales/en.json`:
- Line 582: Replace the flat user-facing key "Cache Hits" in en.json with a
semantic hierarchical key such as "dashboard.stats.cacheHits" and update every
call site that references the literal "Cache Hits" to use the new key (e.g.,
i18n.t('dashboard.stats.cacheHits')). Do the same refactor for the other
mentioned entries (lines around 3033, 3801, 3955-3956) using consistent
namespaces like "pricing.promptPrice" or appropriate feature namespaces; ensure
keys are added to en.json and all components/services that used the old literal
keys are updated to use the new namespaced keys so runtime lookups still
resolve.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9d7b13a-4dc9-44c5-9702-8540e4589086

📥 Commits

Reviewing files that changed from the base of the PR and between fd90731 and bfeaa8d.

📒 Files selected for processing (11)
  • model/log.go
  • model/usedata.go
  • web/default/src/features/dashboard/components/models/log-stat-cards.tsx
  • web/default/src/features/dashboard/components/overview/overview-dashboard.tsx
  • web/default/src/features/dashboard/hooks/use-dashboard-config.tsx
  • web/default/src/features/dashboard/lib/stats.ts
  • web/default/src/features/dashboard/types.ts
  • web/default/src/features/home/components/hero-terminal-demo.tsx
  • web/default/src/hooks/use-sidebar-data.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (3)
  • web/default/src/features/dashboard/types.ts
  • web/default/src/features/dashboard/hooks/use-dashboard-config.tsx
  • web/default/src/i18n/locales/zh.json

Comment on lines +96 to +120
{
id: 'deepseek',
label: 'DeepSeek',
method: 'POST',
endpoint: '/v1/chat/completions',
headers: ['"Authorization: Bearer sk-••••"'],
request: [
'"model": "deepseek-reasoner",',
'"messages": [',
' { "role": "user", "content": "..." }',
']',
],
response: [
'{',
' "choices": [{ "message": {',
' "reasoning_content": <think>,',
' "content": <text> } }],',
' "usage": { "total_tokens": <tokens> }',
'}',
],
responseHighlights: ['<think>', '<text>', '<tokens>'],
tokens: 34,
latency: 178,
accent: 'violet',
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

New user-facing strings are not i18n-enabled.

The newly added visible text (DeepSeek, Analyzing the request..., DeepSeek request served.) is hardcoded. Please wire this component to useTranslation() and render these via t(...) so language switching updates correctly.

As per coding guidelines, "web/default/**/*.{tsx,ts}: All user-facing text content must support i18n using the t() function from useTranslation() in React components" and "web/default/**/*.tsx: In React components, use const { t } = useTranslation() hook to ensure components re-render when language changes".

Also applies to: 395-401, 434-440

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/home/components/hero-terminal-demo.tsx` around lines
96 - 120, The added user-facing strings like "DeepSeek", "Analyzing the
request...", and "DeepSeek request served." in hero-terminal-demo.tsx are
hardcoded; import and use the useTranslation hook (const { t } =
useTranslation()) in the component and replace these literals with t('...') keys
(e.g., t('hero.deepseek.label'), t('hero.deepseek.analyzing'),
t('hero.deepseek.served')) so the UI re-renders on language change; apply the
same replacement for the other occurrences noted (around lines referenced
395-401 and 434-440) and ensure you add matching i18n entries for the new keys
in the translation JSON files.

Backend:
- Add PromptTokens and CompletionTokens fields to QuotaData struct
- Store prompt/completion token split from RecordConsumeLog
- Update all aggregation queries to include new fields

Frontend:
- Add prompt_tokens/completion_tokens to QuotaDataItem type
- Implement processModelTokenChartData with warm color stacked bar chart
  (shows per-model token trends: cache hit / cache miss / output breakdown)
- Add ModelTokenChart component in model call analytics section
- Update user charts: add user selector for token trend, shows per-model
  breakdown for the selected user via admin API
- Proper 3-segment coloring: cache hit (light), cache miss (medium), output (dark)
- Fallback for historical data without prompt/completion split

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
web/default/src/features/dashboard/components/users/user-charts.tsx (2)

286-314: 💤 Low value

Selected state on user buttons is not exposed to assistive tech.

The buttons in the user selector are toggled visually via class only; consider adding aria-pressed={selectedUser === user} so screen-reader users can perceive the active selection. Same pattern is reused throughout this file's pill button groups, so this is essentially a follow-up note for the entire control style.

♿ Proposed minimal change for the new selector
                   {topUsers.map((user) => (
                     <button
                       key={user}
                       type='button'
                       onClick={() => setSelectedUser(user)}
+                      aria-pressed={selectedUser === user}
                       className={`rounded px-2 py-0.5 text-xs font-medium transition-colors ${
                         selectedUser === user
                           ? 'bg-primary text-primary-foreground shadow-sm'
                           : 'text-muted-foreground hover:bg-muted hover:text-foreground'
                       }`}
                     >
                       {user}
                     </button>
                   ))}

As per coding guidelines: "Ensure keyboard operability and logical focus order; use ARIA attributes when necessary".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/dashboard/components/users/user-charts.tsx` around
lines 286 - 314, The user selector's pill buttons are only visually styled and
lack an accessible pressed state; update the button elements rendered inside the
topUsers.map in the User Token Trend block to include aria-pressed={selectedUser
=== user} (using the existing selectedUser and setSelectedUser logic) so screen
readers announce the active selection, and apply the same change to the other
pill-style button groups in this file (where similar patterns exist) to ensure
consistent ARIA exposure across components.

120-138: ⚡ Quick win

Top-user ranking duplicates ranking logic in processUserChartData.

topUsers here re-implements (by-quota) ranking that processUserChartData already computes internally from the same userData. The two can drift (e.g., one limits via topUserLimit, the other via limit). Consider exporting the ranked-users list as part of ProcessedUserChartData (or extracting a small helper in lib/) and consuming it here, so the selector and chart legend stay in lockstep.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/dashboard/components/users/user-charts.tsx` around
lines 120 - 138, The topUsers computed in this component duplicates the by-quota
ranking done inside processUserChartData and can drift; replace the local
ranking by consuming the ranked users produced by processUserChartData (or move
the ranking logic into a shared helper in lib/ and import it here).
Specifically, extend processUserChartData to return the ranked user list (e.g.,
add rankedUsers to ProcessedUserChartData) or create/export a
getRankedUsers(userData, limit) helper, then use that ranked list instead of the
local topUsers calculation and keep the auto-select logic
(setSelectedUser/selectedUser) but fed from the shared ranked result, honoring
topUserLimit/limit consistently.
web/default/src/features/dashboard/lib/charts.ts (3)

937-968: ⚡ Quick win

Token-bucket aggregation is duplicated between processUserChartData and processModelTokenChartData.

Both blocks implement the same algorithm (compute cacheHit / cacheMiss / output / total per item with the prompt-vs-completion fallback to cache_creation_tokens + remainder of token_used), differing only in the grouping key (user vs model). Any future fix to the fallback math has to be applied in two places.

Consider extracting a helper like:

type TokenBuckets = { cacheHit: number; cacheMiss: number; output: number; total: number }

function computeItemTokenBuckets(item: QuotaDataItem): TokenBuckets {
  const promptTokens = Number(item.prompt_tokens) || 0
  const completionTokens = Number(item.completion_tokens) || 0
  const cacheHit = Number(item.cache_tokens) || 0
  const tokenUsed = Number(item.token_used) || 0
  if (promptTokens > 0 || completionTokens > 0) {
    return {
      cacheHit,
      cacheMiss: Math.max(0, promptTokens - cacheHit),
      output: completionTokens,
      total: tokenUsed,
    }
  }
  const cacheCreation = Number(item.cache_creation_tokens) || 0
  return {
    cacheHit,
    cacheMiss: cacheCreation,
    output: Math.max(0, tokenUsed - cacheHit - cacheCreation),
    total: tokenUsed,
  }
}

…and use it in both aggregation loops.

Also applies to: 1208-1247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/dashboard/lib/charts.ts` around lines 937 - 968, The
token-bucket aggregation logic is duplicated in processUserChartData and
processModelTokenChartData; extract a shared helper (e.g., function
computeItemTokenBuckets(item: QuotaDataItem): TokenBuckets) that returns
{cacheHit, cacheMiss, output, total} using the existing rules (use
prompt_tokens/completion_tokens path when present, otherwise use
cache_creation_tokens + remainder), then replace the inline calculations in both
aggregation loops to call computeItemTokenBuckets(item) and add the returned
values to the existing per-key accumulators (the Map updates in
processUserChartData and processModelTokenChartData).

32-109: 💤 Low value

Color palette and HSL helpers belong in a shared utility.

VIBRANT_TOKEN_COLORS, parseColorToHSL, rgbToHsl, hslToString, and generateTokenColorVariants are general-purpose and self-contained. They'd be a natural fit under @/lib/color (or a lib/colors.ts here in the feature) so other charts/components can reuse them without importing from a chart-processing module. No behavior change needed in this PR — flagging as good-to-have.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/dashboard/lib/charts.ts` around lines 32 - 109, Move
the general-purpose color utilities into a shared module (e.g., create a new
module at "@/lib/color" or lib/colors.ts) by extracting VIBRANT_TOKEN_COLORS,
parseColorToHSL, rgbToHsl, hslToString, and generateTokenColorVariants into that
file, export them, and update this file to import those symbols instead of
defining them inline; ensure type HSL is exported or re-declared and that
generateTokenColorVariants still returns [string, string, string] with identical
logic and no behavior changes.

1309-1337: ⚡ Quick win

Hoist sortedTopModels out of the per-time loop.

sortedTopModels = sortedModels.filter((m) => topModels.has(m)) is invariant across chartTimes, but is currently recomputed for every time bucket — O(timePoints · totalModels). Compute it once before the loop.

♻️ Proposed hoist
+  const sortedTopModels = sortedModels.filter((m) => topModels.has(m))
+
   chartTimes.forEach((time) => {
     const modelMap = timeModelTokenMap.get(time)
-    const sortedTopModels = sortedModels.filter((m) => topModels.has(m))
-
     const otherBuckets = { cacheHit: 0, cacheMiss: 0, output: 0, total: 0 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/dashboard/lib/charts.ts` around lines 1309 - 1337,
sortedTopModels is recomputed inside the chartTimes.forEach loop even though it
only depends on sortedModels and topModels; move the computation const
sortedTopModels = sortedModels.filter((m) => topModels.has(m)) so it sits before
the chartTimes.forEach(...) begins so it's computed once, then use that hoisted
sortedTopModels inside the loop where it's currently referenced; ensure no other
time-dependent variables are moved and keep other logic (otherBuckets
aggregation and per-model pushes) unchanged.
model/usedata.go (1)

41-77: ⚡ Quick win

Parameter-list growth is becoming unwieldy.

logQuotaDataCache, LogQuotaData, and increaseQuotaData now each carry 10 positional ints in fixed order. This is a recipe for silent argument-order bugs at call sites (e.g., swapping cacheTokens and cacheCreationTokens would compile cleanly). Consider folding the token counters into a small struct (e.g., TokenBreakdown{Used, CacheTokens, CacheCreationTokens, PromptTokens, CompletionTokens int}) and threading that through.

Not blocking, but as more counters arrive this surface will keep growing.

Also applies to: 104-118

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/usedata.go` around lines 41 - 77, The functions logQuotaDataCache,
LogQuotaData, and increaseQuotaData take too many positional ints which risks
silent argument-order bugs; introduce a small struct TokenBreakdown { Used,
CacheTokens, CacheCreationTokens, PromptTokens, CompletionTokens int } and
replace the five token-related int parameters with a single TokenBreakdown
parameter in logQuotaDataCache, LogQuotaData, and increaseQuotaData, update the
QuotaData struct to embed or reference TokenBreakdown, change all places that
construct or update QuotaData to assign/increment via the TokenBreakdown fields
(e.g., quotaData.TokenBreakdown.Used += tb.Used), and update callers to build
and pass a TokenBreakdown instead of passing five separate ints while keeping
the existing createdAt handling and CacheQuotaData locking logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/default/src/features/dashboard/lib/charts.ts`:
- Around line 1300-1305: The "Other" bucket currently computes its base color
using VIBRANT_TOKEN_COLORS[sortedModels.filter(...).length %
VIBRANT_TOKEN_COLORS.length], which can collide with the top model; change this
to use a dedicated neutral base color (e.g., a new OTHER_NEUTRAL_BASE or the
same neutral used by processChartData's otherColor) before calling
generateTokenColorVariants, and keep pushing its light/medium/dark into
tokenColorRange while leaving tokenDomain logic unchanged (update references in
this block around topModels, sortedModels, VIBRANT_TOKEN_COLORS,
generateTokenColorVariants, tokenDomain, and tokenColorRange).

---

Nitpick comments:
In `@model/usedata.go`:
- Around line 41-77: The functions logQuotaDataCache, LogQuotaData, and
increaseQuotaData take too many positional ints which risks silent
argument-order bugs; introduce a small struct TokenBreakdown { Used,
CacheTokens, CacheCreationTokens, PromptTokens, CompletionTokens int } and
replace the five token-related int parameters with a single TokenBreakdown
parameter in logQuotaDataCache, LogQuotaData, and increaseQuotaData, update the
QuotaData struct to embed or reference TokenBreakdown, change all places that
construct or update QuotaData to assign/increment via the TokenBreakdown fields
(e.g., quotaData.TokenBreakdown.Used += tb.Used), and update callers to build
and pass a TokenBreakdown instead of passing five separate ints while keeping
the existing createdAt handling and CacheQuotaData locking logic.

In `@web/default/src/features/dashboard/components/users/user-charts.tsx`:
- Around line 286-314: The user selector's pill buttons are only visually styled
and lack an accessible pressed state; update the button elements rendered inside
the topUsers.map in the User Token Trend block to include
aria-pressed={selectedUser === user} (using the existing selectedUser and
setSelectedUser logic) so screen readers announce the active selection, and
apply the same change to the other pill-style button groups in this file (where
similar patterns exist) to ensure consistent ARIA exposure across components.
- Around line 120-138: The topUsers computed in this component duplicates the
by-quota ranking done inside processUserChartData and can drift; replace the
local ranking by consuming the ranked users produced by processUserChartData (or
move the ranking logic into a shared helper in lib/ and import it here).
Specifically, extend processUserChartData to return the ranked user list (e.g.,
add rankedUsers to ProcessedUserChartData) or create/export a
getRankedUsers(userData, limit) helper, then use that ranked list instead of the
local topUsers calculation and keep the auto-select logic
(setSelectedUser/selectedUser) but fed from the shared ranked result, honoring
topUserLimit/limit consistently.

In `@web/default/src/features/dashboard/lib/charts.ts`:
- Around line 937-968: The token-bucket aggregation logic is duplicated in
processUserChartData and processModelTokenChartData; extract a shared helper
(e.g., function computeItemTokenBuckets(item: QuotaDataItem): TokenBuckets) that
returns {cacheHit, cacheMiss, output, total} using the existing rules (use
prompt_tokens/completion_tokens path when present, otherwise use
cache_creation_tokens + remainder), then replace the inline calculations in both
aggregation loops to call computeItemTokenBuckets(item) and add the returned
values to the existing per-key accumulators (the Map updates in
processUserChartData and processModelTokenChartData).
- Around line 32-109: Move the general-purpose color utilities into a shared
module (e.g., create a new module at "@/lib/color" or lib/colors.ts) by
extracting VIBRANT_TOKEN_COLORS, parseColorToHSL, rgbToHsl, hslToString, and
generateTokenColorVariants into that file, export them, and update this file to
import those symbols instead of defining them inline; ensure type HSL is
exported or re-declared and that generateTokenColorVariants still returns
[string, string, string] with identical logic and no behavior changes.
- Around line 1309-1337: sortedTopModels is recomputed inside the
chartTimes.forEach loop even though it only depends on sortedModels and
topModels; move the computation const sortedTopModels = sortedModels.filter((m)
=> topModels.has(m)) so it sits before the chartTimes.forEach(...) begins so
it's computed once, then use that hoisted sortedTopModels inside the loop where
it's currently referenced; ensure no other time-dependent variables are moved
and keep other logic (otherBuckets aggregation and per-model pushes) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec362d3f-5b93-4dbd-906a-849fc6160274

📥 Commits

Reviewing files that changed from the base of the PR and between bfeaa8d and 9e1db04.

📒 Files selected for processing (9)
  • model/log.go
  • model/usedata.go
  • web/default/src/features/dashboard/components/users/user-charts.tsx
  • web/default/src/features/dashboard/index.tsx
  • web/default/src/features/dashboard/lib/charts.ts
  • web/default/src/features/dashboard/lib/index.ts
  • web/default/src/features/dashboard/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (2)
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/default/src/features/dashboard/types.ts
  • model/log.go

Comment thread web/default/src/features/dashboard/lib/charts.ts
- Add model-token-chart.tsx: stacked bar chart component for model token
  trends, showing cache-hit / cache-miss / output breakdown per model
  with warm color palette
- Switch Dockerfile.dev builder from golang:1.26.1-alpine to golang:latest
  to allow local builds when specific alpine tag is unavailable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Dockerfile.dev`:
- Line 4: Replace the floating base image in the Dockerfile.dev (the FROM
golang:latest line) with a pinned, valid Go release to ensure reproducible
builds (for example use a specific tag like golang:1.26.3 or
golang:1.26.3-alpine depending on whether you need Debian or Alpine); update the
FROM instruction accordingly so it references an explicit version instead of
"latest".

In `@web/default/src/features/dashboard/components/models/model-token-chart.tsx`:
- Around line 37-46: The dynamic import of '`@visactor/vchart`' should catch
failures so a rejected promise isn't cached in themeManagerPromise; wrap the
import call in a try/catch (or append .catch) around the await for
themeManagerPromise in the block that sets ThemeManager,
ThemeManager.setCurrentTheme and setThemeReady, and on failure reset
themeManagerPromise = undefined (so future attempts can retry), log or surface
the error, and ensure setThemeReady(false) (or an appropriate error state) is
set so the UI doesn't hang; refer to themeManagerPromise, ThemeManager,
themeManagerRef, ThemeManager.setCurrentTheme and setThemeReady when
implementing this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0c84b4a-6f1e-4033-b2bf-9efd8aff6c1b

📥 Commits

Reviewing files that changed from the base of the PR and between 9e1db04 and 6357a5b.

📒 Files selected for processing (2)
  • Dockerfile.dev
  • web/default/src/features/dashboard/components/models/model-token-chart.tsx

Comment thread Dockerfile.dev
# Skips frontend build, uses a placeholder for //go:embed web/dist

FROM golang:1.26.1-alpine AS builder
FROM golang:latest AS builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest stable version of Go (golang)?

💡 Result:

Latest stable version of Go (as of today, 2026-05-19): Go 1.26.3 [1]. This is shown on the official Go download page under “Stable versions”, listing “go1.26.3” as the latest stable release [1].

Citations:


Pin the Go version instead of using latest.

Using golang:latest breaks build reproducibility and can cause unexpected failures when a new Go version is released. While this solves the immediate problem of an unavailable tag (golang:1.26.1-alpine doesn't exist), the proper fix is to use a valid pinned version.

📌 Suggested fix

Use a recent pinned version (Go 1.26.3 is the current stable):

-FROM golang:latest AS builder
+FROM golang:1.26-alpine AS builder

or with the full Debian image:

-FROM golang:latest AS builder
+FROM golang:1.26 AS builder
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile.dev` at line 4, Replace the floating base image in the
Dockerfile.dev (the FROM golang:latest line) with a pinned, valid Go release to
ensure reproducible builds (for example use a specific tag like golang:1.26.3 or
golang:1.26.3-alpine depending on whether you need Debian or Alpine); update the
FROM instruction accordingly so it references an explicit version instead of
"latest".

Comment on lines +37 to +46
if (!themeManagerPromise) {
themeManagerPromise = import('@visactor/vchart').then(
(m) => m.ThemeManager
)
}

const ThemeManager = await themeManagerPromise
themeManagerRef.current = ThemeManager
ThemeManager.setCurrentTheme(resolvedTheme === 'dark' ? 'dark' : 'light')
setThemeReady(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add error handling for the dynamic import.

If the import('@visactor/vchart') fails, the rejected promise is cached forever in themeManagerPromise, causing all subsequent theme updates to fail silently and preventing the chart from ever rendering.

🛡️ Proposed fix to add error handling
 useEffect(() => {
   const updateTheme = async () => {
     setThemeReady(false)

-    if (!themeManagerPromise) {
-      themeManagerPromise = import('`@visactor/vchart`').then(
-        (m) => m.ThemeManager
-      )
-    }
-
-    const ThemeManager = await themeManagerPromise
-    themeManagerRef.current = ThemeManager
-    ThemeManager.setCurrentTheme(resolvedTheme === 'dark' ? 'dark' : 'light')
-    setThemeReady(true)
+    try {
+      if (!themeManagerPromise) {
+        themeManagerPromise = import('`@visactor/vchart`').then(
+          (m) => m.ThemeManager
+        )
+      }
+
+      const ThemeManager = await themeManagerPromise
+      themeManagerRef.current = ThemeManager
+      ThemeManager.setCurrentTheme(resolvedTheme === 'dark' ? 'dark' : 'light')
+      setThemeReady(true)
+    } catch (error) {
+      console.error('Failed to load VChart ThemeManager:', error)
+      themeManagerPromise = null // Reset cache to allow retry
+      setThemeReady(false)
+    }
   }

   updateTheme()
 }, [resolvedTheme])
📝 Committable suggestion

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

Suggested change
if (!themeManagerPromise) {
themeManagerPromise = import('@visactor/vchart').then(
(m) => m.ThemeManager
)
}
const ThemeManager = await themeManagerPromise
themeManagerRef.current = ThemeManager
ThemeManager.setCurrentTheme(resolvedTheme === 'dark' ? 'dark' : 'light')
setThemeReady(true)
try {
if (!themeManagerPromise) {
themeManagerPromise = import('`@visactor/vchart`').then(
(m) => m.ThemeManager
)
}
const ThemeManager = await themeManagerPromise
themeManagerRef.current = ThemeManager
ThemeManager.setCurrentTheme(resolvedTheme === 'dark' ? 'dark' : 'light')
setThemeReady(true)
} catch (error) {
console.error('Failed to load VChart ThemeManager:', error)
themeManagerPromise = null // Reset cache to allow retry
setThemeReady(false)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/dashboard/components/models/model-token-chart.tsx`
around lines 37 - 46, The dynamic import of '`@visactor/vchart`' should catch
failures so a rejected promise isn't cached in themeManagerPromise; wrap the
import call in a try/catch (or append .catch) around the await for
themeManagerPromise in the block that sets ThemeManager,
ThemeManager.setCurrentTheme and setThemeReady, and on failure reset
themeManagerPromise = undefined (so future attempts can retry), log or surface
the error, and ensure setThemeReady(false) (or an appropriate error state) is
set so the UI doesn't hang; refer to themeManagerPromise, ThemeManager,
themeManagerRef, ThemeManager.setCurrentTheme and setThemeReady when
implementing this change.

lzc and others added 2 commits May 20, 2026 20:42
Replace theme-variable + positional color assignment with a stable
hash function (getModelBaseColor) so the same model/user always gets
the same color family regardless of which chart or sort order it appears in.

- Remove getThemeChartColors / getVChartDefaultColors / USER_COLOR_FALLBACKS
- Remove vchartDefaultDataScheme import (no longer needed)
- processChartData: use specified color map (hash) instead of ordinal domain/range
- processModelTokenChartData: use getModelBaseColor instead of positional index
- processUserChartData: user rank/trend/token charts all use the same hash color

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…delTokenChart

- Add ModelColorEntry type and extend ProcessedModelTokenChartData with
  models[] and modelColorMap (ordered list + per-model color variants)
- processModelTokenChartData: disable built-in legend, expose color info
- ModelTokenChart: render custom legend that groups the 3 segments (cache
  hit / cache miss / output) under each model name with three adjacent
  color swatches; clicking a model toggles its visibility by filtering
  the data before passing to VChart
- Add dimension tooltip with total row for the token trend chart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant