feat: CNY localization, channel UX fixes, and JSON editor improvements - #4832
feat: CNY localization, channel UX fixes, and JSON editor improvements#4832lzcyyds0-afk wants to merge 15 commits into
Conversation
…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>
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis 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. ChangesCurrency & telemetry migration (CNY base + cache token metrics)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (10)
web/default/src/i18n/locales/en.json (1)
3032-3032: ⚡ Quick winUse 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 asdashboard.overview.titleand 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 winUse 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 asdashboard.overview.titleand 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
mergedToFieldstrusts unvalidated JSON shape at the type level.
JSON.parse(mergedJson)is cast toRecord<string, MergedModelEntry>without runtime checks, so if this helper is ever called from a code path that doesn't first runvalidateMergedJson(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 (handleApplyafter 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 invalidateMergedJson.🤖 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.lengthis read during render but a ref mutation alone won't trigger a re-render.The button's
disabledand the counter(${historyRef.current.length})only refresh becausehandleApplyandhandleUndohappen to also callsetApplied/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 staystrueuntil another state change occurs).Consider tracking history length in
useState(or replacing the entire ref with state). The undo stack is small and capped atMAX_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 tradeoffFile 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 undereditMode === 'merged'are self-contained and would naturally live in either:
- a sibling component
model-ratio-merged-editor.tsxconsumingformas 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 likesetRangeTextor value assignment break undo functionality. MDN explicitly documents this as a legitimate use case with no viable alternatives yet. The existingeslint-disable-next-line deprecation/deprecationcomment 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 winPin Redis version for reproducibility.
Using
redis:latestremoves 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-alpineOr 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 winReplace 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; useif-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 winUse 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; useif-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 winUse 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
⛔ Files ignored due to path filters (1)
web/default/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (30)
common/constants.gocontroller/billing.godocker-compose.dev.ymllogger/logger.gosetting/operation_setting/general_setting.gosetting/ratio_setting/model_ratio.goweb/classic/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.jsweb/default/rsbuild.config.tsweb/default/src/components/json-editor.tsxweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/components/model-mapping-editor.tsxweb/default/src/features/channels/lib/channel-type-config.tsweb/default/src/features/home/components/sections/features.tsxweb/default/src/features/home/constants.tsweb/default/src/features/models/components/drawers/model-mutate-drawer.tsxweb/default/src/features/pricing/components/dynamic-pricing-breakdown.tsxweb/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsxweb/default/src/features/system-settings/billing/index.tsxweb/default/src/features/system-settings/integrations/amount-discount-dialog.tsxweb/default/src/features/system-settings/integrations/amount-options-visual-editor.tsxweb/default/src/features/system-settings/integrations/payment-settings-section.tsxweb/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsxweb/default/src/features/system-settings/integrations/waffo-settings-section.tsxweb/default/src/features/system-settings/models/model-pricing-sheet.tsxweb/default/src/features/system-settings/models/model-ratio-form.tsxweb/default/src/features/system-settings/models/model-ratio-visual-editor.tsxweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/zh.jsonweb/default/src/lib/currency.tsweb/default/src/stores/system-config-store.ts
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 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=goRepository: 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 -100Repository: 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 -A3Repository: 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 -100Repository: 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=goRepository: 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 -50Repository: 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.goRepository: 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 -A2Repository: 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 -80Repository: 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.goRepository: 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.
| 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.
| default: // USD | ||
| return fmt.Sprintf("$%.6f 额度", q/common.QuotaPerUnit) | ||
| usd := q / common.QuotaPerUnit / operation_setting.USDExchangeRate | ||
| return fmt.Sprintf("$%.6f 额度", usd) | ||
| } |
There was a problem hiding this comment.
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.
| 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', | ||
| } |
There was a problem hiding this comment.
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.
| // Currency display | ||
| const currencyLabel = getCurrencyLabel() | ||
| const currencySymbol = | ||
| currencyLabel === 'CNY' ? '¥' : currencyLabel === 'USD' ? '$' : currencyLabel | ||
| const currencyName = currencyLabel === 'Tokens' ? 'Tokens' : currencyLabel |
There was a problem hiding this comment.
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).
| {t('Pricing mode')} ({currencySymbol}/1M tokens) | ||
| </Label> |
There was a problem hiding this comment.
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.
| const currencyLabel = getCurrencyLabel() | ||
| const currencySymbol = | ||
| currencyLabel === 'CNY' ? '¥' : currencyLabel === 'USD' ? '$' : currencyLabel | ||
| const [paying, setPaying] = useState(false) |
There was a problem hiding this comment.
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".
| 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)) |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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).
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>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
web/default/src/features/system-settings/models/model-ratio-form.tsx (2)
317-317: ⚡ Quick winReading
historyRef.current.lengthdirectly in JSX is fragile.The undo button's
disabledand counter label depend on a mutable ref read during render. It happens to update today only because every mutation ofhistoryRef.currentis 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
historytohandleUndo'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 liftExtract 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
MergedJsonEditorsubcomponent that takesform(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
ModelRatioFormand 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
📒 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>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
web/default/src/features/home/components/hero-terminal-demo.tsx (1)
285-285: ⚡ Quick winAvoid 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 winExtract a helper to reduce type-switch duplication.
The cache token extraction block is duplicated for both
cache_tokensandcache_creation_tokens. Both switch statements handleintandfloat64identically. 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
Otherwithintvalues via service functions likeGenerateTextOtherInfo), a helper could defensively handleint64as 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 winAdd 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
anytype in TypeScript; prefer specific types orunknown; 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 winUse 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.titleand 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
📒 Files selected for processing (11)
model/log.gomodel/usedata.goweb/default/src/features/dashboard/components/models/log-stat-cards.tsxweb/default/src/features/dashboard/components/overview/overview-dashboard.tsxweb/default/src/features/dashboard/hooks/use-dashboard-config.tsxweb/default/src/features/dashboard/lib/stats.tsweb/default/src/features/dashboard/types.tsweb/default/src/features/home/components/hero-terminal-demo.tsxweb/default/src/hooks/use-sidebar-data.tsweb/default/src/i18n/locales/en.jsonweb/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
| { | ||
| 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', | ||
| }, |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (6)
web/default/src/features/dashboard/components/users/user-charts.tsx (2)
286-314: 💤 Low valueSelected 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 winTop-user ranking duplicates ranking logic in
processUserChartData.
topUsershere re-implements (by-quota) ranking thatprocessUserChartDataalready computes internally from the sameuserData. The two can drift (e.g., one limits viatopUserLimit, the other vialimit). Consider exporting the ranked-users list as part ofProcessedUserChartData(or extracting a small helper inlib/) 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 winToken-bucket aggregation is duplicated between
processUserChartDataandprocessModelTokenChartData.Both blocks implement the same algorithm (compute
cacheHit / cacheMiss / output / totalper item with the prompt-vs-completion fallback tocache_creation_tokens+ remainder oftoken_used), differing only in the grouping key (uservsmodel). 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 valueColor palette and HSL helpers belong in a shared utility.
VIBRANT_TOKEN_COLORS,parseColorToHSL,rgbToHsl,hslToString, andgenerateTokenColorVariantsare general-purpose and self-contained. They'd be a natural fit under@/lib/color(or alib/colors.tshere 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 winHoist
sortedTopModelsout of the per-time loop.
sortedTopModels = sortedModels.filter((m) => topModels.has(m))is invariant acrosschartTimes, 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 winParameter-list growth is becoming unwieldy.
logQuotaDataCache,LogQuotaData, andincreaseQuotaDatanow each carry 10 positionalints in fixed order. This is a recipe for silent argument-order bugs at call sites (e.g., swappingcacheTokensandcacheCreationTokenswould 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
📒 Files selected for processing (9)
model/log.gomodel/usedata.goweb/default/src/features/dashboard/components/users/user-charts.tsxweb/default/src/features/dashboard/index.tsxweb/default/src/features/dashboard/lib/charts.tsweb/default/src/features/dashboard/lib/index.tsweb/default/src/features/dashboard/types.tsweb/default/src/i18n/locales/en.jsonweb/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
- 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>
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
Dockerfile.devweb/default/src/features/dashboard/components/models/model-token-chart.tsx
| # Skips frontend build, uses a placeholder for //go:embed web/dist | ||
|
|
||
| FROM golang:1.26.1-alpine AS builder | ||
| FROM golang:latest AS builder |
There was a problem hiding this comment.
🧩 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 builderor 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".
| 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) |
There was a problem hiding this comment.
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.
| 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.
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>
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
📡 Channel Management Fixes
constant/channel.go🧾 Model Pricing Editor
execCommand('insertText')so Ctrl+Z / native undo still works)Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Translations