Skip to content

Conversation

@weiruchenai1
Copy link
Owner

@weiruchenai1 weiruchenai1 commented Oct 8, 2025

Summary by CodeRabbit

  • New Features
    • Detailed logs toggle with persisted setting.
    • Revamped Modal (confirm/alert/custom), new Card, StatCard, StatusBadge, IconButton; Button supports icons.
    • Key list: show/hide key and one-click copy.
  • UI/UX
    • Results panel redesigned with persistent stats, Cards, and improved Empty States.
    • Horizontal key item layout with clearer status indicators.
    • Simplified Concurrency/Retry controls (slider + number input).
    • Advanced Settings, Paid Detection, and Model selector refreshed with Cards and clearer statuses.
  • Bug Fixes
    • Mobile outside-click handling for sidebar.
    • Surface HTTP status codes in status updates.
  • Tests
    • Updated virtualization height tests across viewports.

@vercel
Copy link

vercel bot commented Oct 8, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
api-key-tester Ready Ready Preview Comment Oct 10, 2025 1:57pm

@coderabbitai
Copy link

coderabbitai bot commented Oct 8, 2025

Walkthrough

Adds statusCode propagation in worker updates; introduces reusable UI components (Card, EmptyState, FormField, IconButton, StatCard, StatusBadge); refactors Modal; reworks Results and Logs UIs; adds detailed-logs toggle with state persistence; simplifies virtualization to viewport-based heights with tests; broad CSS consolidation and removals; multiple feature UIs updated to new components.

Changes

Cohort / File(s) Summary of Changes
Worker status updates
public/worker.js
Include statusCode in KEY_STATUS_UPDATE payloads; extend extractStatusCode to parse parentheses-wrapped codes; surface statusCode alongside cacheApiStatus across paths.
App shell
src/App.jsx
Replace feature sections with Card components; simplify rightPanel to <Results />; rename settings button class to btn--settings.
Button enhancements
src/components/common/Button/index.jsx
Add optional icon prop; render spinner only when loading; otherwise render icon if provided.
New UI primitives
src/components/common/Card/index.jsx, src/components/common/EmptyState/index.jsx, src/components/common/FormField/index.jsx, src/components/common/IconButton/index.jsx, src/components/common/StatCard/index.jsx, src/components/common/StatusBadge/index.jsx
Add reusable Card, EmptyState, FormField (+ Input), IconButton, StatCard, StatusBadge components with prop-driven variants and classes.
Modal refactor
src/components/common/Modal/index.jsx, src/components/common/Modal/Modal.module.css
Rebuild Modal using overlay + Card, header/body/footer; support custom/confirm/alert types, new props (maxWidth, padding, zIndex, closeOnOverlayClick); remove old CSS module.
Advanced settings & logs toggle
src/components/features/AdvancedSettings/index.jsx, src/components/features/LogDisplayControl/index.jsx, src/contexts/AppStateContext.jsx, src/locales/en.json, src/locales/zh.json
Add LogDisplayControl; persist showDetailedLogs in context/localStorage; gate logs and clear when disabled; add i18n keys; update AdvancedSettings UI using Card/IconButton and insert LogDisplayControl.
Concurrency & Retry controls
src/components/features/ConcurrencyControl/index.jsx, src/components/features/RetryControl/index.jsx, src/components/features/ConcurrencyControl/ConcurrencyControl.module.css, src/components/features/RetryControl/RetryControl.module.css
Wrap controls in FormField; add inline number inputs; remove presets; add ARIA; migrate CSS to theme variables and reduce hover/transition effects.
Paid detection UI
src/components/features/PaidDetectionControl/index.jsx, src/components/features/PaidDetectionControl/PaidDetectionControl.module.css, src/components/features/PaidDetectionPrompt/PaidDetectionPrompt.module.css
Replace buttons with Button; wrap sections in Card; disable controls during testing; set Modal type="confirm"; remove CSS modules and hover/animation styles.
Results overhaul
src/components/features/Results/index.jsx, src/components/features/Results/StatsCards.jsx, src/components/features/Results/VirtualizedList.jsx, src/components/features/Results/Results.module.css
Always show StatsCards; use primary results Card and EmptyState fallback; rework VirtualizedList item layout with masking, copy, visibility toggle, StatusBadge; remove Results CSS module.
Logs preview
src/components/features/LogsPreview/KeyLogModal.jsx, src/components/features/LogsPreview/LogsPreviewPanel.jsx
Replace custom markup with Modal/Card/EmptyState; gate modal by isLogModalOpen && showDetailedLogs; restructure sections and event cards.
Key input controls
src/components/features/KeyInput/FileImport.jsx, src/components/features/KeyInput/PasteButton.jsx, src/components/features/KeyInput/KeyInput.module.css
Use IconButton for file import; convert PasteButton to clickable SVG with hover visuals; remove KeyInput CSS module.
API config UI tweaks
src/components/features/ApiConfig/ModelSelector.jsx, src/components/features/ApiConfig/ProxySettings.jsx, src/components/features/ApiConfig/ApiConfig.module.css
Move paid-detection status into label row; remove help texts; delete ApiConfig CSS module.
Layout and nav
src/components/layout/AppLayout/index.jsx, src/components/layout/AppLayout/AppLayout.module.css, src/components/layout/NavBar/NavBar.module.css, src/components/layout/Sidebar/Sidebar.module.css, src/components/layout/ThemeToggle/index.jsx, src/components/layout/ThemeToggle/ThemeToggle.module.css
Add touchstart to outside-click handler; switch to flex two-column layout; reduce transitions/animations in NavBar/Sidebar; adjust collapsed behavior; remove ThemeToggle component and its styles.
Virtualization logic & tests
src/hooks/useVirtualization.js, src/__tests__/hooks/useVirtualization.test.js
Simplify getItemHeight to viewport-based fixed values (62/66/74); update tests for matchMedia scenarios, consistency, and no-arg signature.
Global/component styles
src/styles/components.css, src/styles/globals.css
Remove many transitions/animations; add new card/button classes (card--primary/info/results/empty-state, btn--settings, results-content); introduce extensive horizontal key-item layout styles and status variants; minor selector tweaks.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant AdvancedSettings as AdvancedSettings
  participant LogDisplay as LogDisplayControl
  participant AppState as AppStateContext
  participant Storage as localStorage
  participant UI as Logs UI (LogsPreview/KeyLogModal)

  User->>AdvancedSettings: Open Advanced Settings
  AdvancedSettings->>LogDisplay: Render toggle
  User->>LogDisplay: Toggle "Show detailed logs"
  LogDisplay->>AppState: dispatch(SET_SHOW_DETAILED_LOGS, value)
  AppState->>Storage: Persist showDetailedLogs
  alt value = true
    AppState->>UI: Enable log collector/render
  else value = false
    AppState->>UI: CLEAR_LOGS and hide gated views
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

Hop hop, I tidy the UI lanes,
Cards and badges, neat as trains.
Logs now toggle, secrets hide,
Keys parade in horizontal stride.
Status codes whisper, clear and bright—
Thump! says the rabbit: shipped just right. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title emphasizes only refactoring key card styles and removing unused code and hover effects, but the pull request also introduces shared UI components (Card, FormField, IconButton, etc.), restructures core UIs (modals, results panel, controls), enhances state management for detailed logs, updates hooks and global styles, and adds tests, so it does not accurately capture the main scope of changes. Revise the title to reflect the broader architectural refactor and key additions—for example, “refactor: introduce shared Card/FormField components, overhaul UI layouts, and remove hover/transitions”—so it succinctly summarizes the primary changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch experimental

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (4)
src/components/features/Results/StatsCards.jsx (1)

52-63: Fix misaligned testing stat label translation

t('testing') resolves to the sentence “Testing API keys…”, so the new StatCard now renders a long progress message where a short status label is expected. Use the existing testingLabel translation (and fall back to stat.key otherwise) so we keep the concise label in all locales.

   const testingStats = [
     {
       key: 'testing',
+      labelKey: 'testingLabel',
       value: testingCount,
       className: 'testing'
     },
     {
       key: 'retrying',
       value: retryingCount,
       className: 'retrying'
     }
   ];
 …
-            label={t(stat.key)}
+            label={t(stat.labelKey || stat.key)}

Also applies to: 86-91

src/components/features/LogsPreview/LogsPreviewPanel.jsx (2)

41-47: Prevent runtime errors in search filtering when fields are missing.

log.keyId or log.apiType can be undefined, causing toLowerCase to throw.

-      filtered = filtered.filter(log => 
-        log.keyId.toLowerCase().includes(searchTerm.toLowerCase()) ||
-        log.apiType.toLowerCase().includes(searchTerm.toLowerCase()) ||
-        (log.error?.message || '').toLowerCase().includes(searchTerm.toLowerCase())
-      );
+      const term = searchTerm.toLowerCase();
+      filtered = filtered.filter(log => 
+        (log.keyId || '').toLowerCase().includes(term) ||
+        (log.apiType || '').toLowerCase().includes(term) ||
+        (log.error?.message || '').toLowerCase().includes(term)
+      );

26-29: Mask API keys in UI to avoid leaking secrets.

maskApiKey currently returns the full key. This risks exposing sensitive keys in logs.

-  const maskApiKey = (key) => {
-    if (!key || typeof key !== 'string') return 'Unknown';
-    return key;
-  };
+  const maskApiKey = (key) => {
+    if (!key || typeof key !== 'string') return 'Unknown';
+    if (key.length <= 8) return key;
+    return `${key.slice(0, 4)}...${key.slice(-4)}`;
+  };
src/components/features/Results/VirtualizedList.jsx (1)

265-279: Fix per-row state leakage with react-window by setting itemKey.

KeyItem holds local state (isKeyVisible). Without itemKey, react-window reuses rows and state can “jump” between items.

       <List
         ref={listRef}
         height={listHeight}
         itemCount={filteredKeys.length}
         itemSize={getItemSize}
         itemData={filteredKeys}
+        itemKey={(index, data) => (data[index] && (data[index].key || data[index].id || index))}
         overscanCount={3}
         width="100%"
         className="virtualized-list"
       >
🧹 Nitpick comments (11)
public/worker.js (1)

501-519: LGTM! Clear status code extraction logic.

The dual-format detection (parentheses and "HTTP " prefix) correctly handles various error message formats. The regex /\((\d{3})\)$/ properly matches trailing status codes like "认证失败 (401)".

Consider making the comment on line 504 more explicit about the format it matches:

-  // 匹配括号中的3位数字状态码,如 "认证失败 (401)" 或 "权限不足 (403)"
+  // 匹配字符串末尾括号中的3位数字状态码,如 "认证失败 (401)" 或 "权限不足 (403)"
src/components/features/LogDisplayControl/index.jsx (1)

16-30: Consider adding explicit label association for better accessibility.

The outer label at line 16 displays the translated text but isn't explicitly associated with the checkbox input. Consider wrapping the entire switch control (lines 21-30) with a label element or adding an htmlFor attribute pointing to a unique id on the checkbox input.

Apply this diff to improve accessibility:

-      <label className="text-sm font-medium text-primary">
+      <label htmlFor="detailed-logs-toggle" className="text-sm font-medium text-primary">
         {t('showDetailedLogs')}
       </label>
       <div className="space-y-sm">
         <div className="flex items-center gap-sm">
           <label className="switch-base">
             <input
+              id="detailed-logs-toggle"
               type="checkbox"
               checked={state.showDetailedLogs}
               onChange={handleToggleDetailedLogs}
               disabled={state.isTesting}
               className="switch-input"
             />
             <span className="switch-slider"></span>
           </label>
src/components/features/ConcurrencyControl/index.jsx (2)

31-48: Consider UX inconsistency between slider and numeric input ranges.

The slider is capped at 50 (line 35), but the numeric input allows values up to 100 (line 45). When a user enters a value greater than 50 via the numeric input, the slider will be maxed out at 50, which could be confusing. Consider either:

  1. Aligning both to the same max value (e.g., both 100)
  2. Updating the slider's max dynamically based on the current value
  3. Adding a clear indicator that values above 50 require manual entry

13-19: Direct DOM mutation in event handler is unusual.

Lines 14-18 directly mutate e.target.value after validation. While this works, it's not idiomatic in React. The controlled input pattern typically lets React manage the input value through state. The current approach could cause confusion during debugging.

Consider this alternative pattern:

 const handleInputChange = (e) => {
   let value = parseInt(e.target.value);
   if (isNaN(value) || value < 1) value = 1;
   if (value > 100) value = 100;
-  e.target.value = value;
   updateConcurrency(value);
 };

The input will reflect the correct value through the controlled value prop at line 43.

src/components/common/EmptyState/index.jsx (1)

21-25: Redundant conditional expression.

Line 23 has a no-op ternary: typeof icon === 'string' ? icon : icon always returns icon regardless of the type check. This was likely intended to handle different icon types differently, but currently serves no purpose.

If the intent is to simply render the icon (whether string emoji or JSX), simplify to:

       {icon && (
         <div className="empty-icon">
-          {typeof icon === 'string' ? icon : icon}
+          {icon}
         </div>
       )}

If you intended to wrap JSX icons differently from string emojis, clarify the intended behavior.

src/components/features/ConcurrencyControl/ConcurrencyControl.module.css (1)

71-71: Consider completing the CSS variable migration.

While many colors have been migrated to CSS variables, some hardcoded colors remain:

  • Lines 71, 81: Slider thumb uses #495057
  • Lines 179, 184: Dark theme slider thumb uses #ffffff

For full theming consistency, consider replacing these with CSS variables like var(--slider-thumb-color) or similar.

Also applies to: 81-81, 179-179, 184-184

src/components/features/AdvancedSettings/index.jsx (1)

45-61: Add dialog semantics for accessibility (role and aria-modal).

The modal root lacks dialog attributes. Add role="dialog" and aria-modal="true" to improve a11y and announce the modal to assistive tech.

-    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center" style={{zIndex: 9999}} onClick={handleOverlayClick}>
+    <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"
+         style={{ zIndex: 9999 }}
+         role="dialog"
+         aria-modal="true"
+         onClick={handleOverlayClick}>
src/components/common/FormField/index.jsx (1)

41-77: Unused prop and a11y hints in Input subcomponent.

  • The variant prop isn’t used (only textarea class toggles). Either implement variants or remove the prop to avoid confusion.
  • Consider mirroring aria-invalid/aria-describedby handling at the Input level when used standalone (outside FormField).
src/components/features/LogsPreview/LogsPreviewPanel.jsx (1)

116-119: PR objective alignment: remove transition class.

The container still uses transition-all while the PR aims to remove transitions.

-    <div className={`card-base mb-md transition-all overflow-hidden ${
+    <div className={`card-base mb-md overflow-hidden ${
       isExpanded ? 'max-h-96' : 'max-h-32'
     }`}>
src/components/common/StatusBadge/index.jsx (1)

10-19: Align default status class with rest of code.

VirtualizedList maps unknown statuses to 'status-unknown'. Use the same here for consistency.

-      default: return 'status-testing';
+      default: return 'status-unknown';
src/components/features/LogsPreview/KeyLogModal.jsx (1)

194-194: Misleading use of EmptyState for loading.

EmptyState is semantically intended for "no data" scenarios, not loading states. A loading spinner or dedicated loading component would communicate intent more clearly.

Consider introducing a Loading component or using a spinner:

-            <EmptyState message={t('loading') || '加载中...'} />
+            <div className="flex items-center justify-center p-lg">
+              <div className="loading-spinner" />
+              <span className="ml-2 text-secondary">{t('loading') || '加载中...'}</span>
+            </div>

Or reuse the existing Loading component if it supports inline rendering.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dad6d91 and 914860a.

📒 Files selected for processing (43)
  • public/worker.js (5 hunks)
  • src/App.jsx (4 hunks)
  • src/components/common/Button/index.jsx (2 hunks)
  • src/components/common/Card/index.jsx (1 hunks)
  • src/components/common/EmptyState/index.jsx (1 hunks)
  • src/components/common/FormField/index.jsx (1 hunks)
  • src/components/common/IconButton/index.jsx (1 hunks)
  • src/components/common/Modal/Modal.module.css (0 hunks)
  • src/components/common/Modal/index.jsx (3 hunks)
  • src/components/common/StatCard/index.jsx (1 hunks)
  • src/components/common/StatusBadge/index.jsx (1 hunks)
  • src/components/features/AdvancedSettings/index.jsx (3 hunks)
  • src/components/features/ApiConfig/ApiConfig.module.css (0 hunks)
  • src/components/features/ConcurrencyControl/ConcurrencyControl.module.css (8 hunks)
  • src/components/features/ConcurrencyControl/index.jsx (3 hunks)
  • src/components/features/Controls/index.jsx (3 hunks)
  • src/components/features/KeyInput/FileImport.jsx (2 hunks)
  • src/components/features/KeyInput/KeyInput.module.css (0 hunks)
  • src/components/features/KeyInput/PasteButton.jsx (2 hunks)
  • src/components/features/LogDisplayControl/index.jsx (1 hunks)
  • src/components/features/LogsPreview/KeyLogModal.jsx (6 hunks)
  • src/components/features/LogsPreview/LogsPreviewPanel.jsx (4 hunks)
  • src/components/features/PaidDetectionControl/PaidDetectionControl.module.css (0 hunks)
  • src/components/features/PaidDetectionControl/index.jsx (6 hunks)
  • src/components/features/PaidDetectionPrompt/PaidDetectionPrompt.module.css (0 hunks)
  • src/components/features/Results/Results.module.css (0 hunks)
  • src/components/features/Results/StatsCards.jsx (2 hunks)
  • src/components/features/Results/VirtualizedList.jsx (5 hunks)
  • src/components/features/Results/index.jsx (1 hunks)
  • src/components/features/RetryControl/RetryControl.module.css (7 hunks)
  • src/components/features/RetryControl/index.jsx (3 hunks)
  • src/components/layout/AppLayout/AppLayout.module.css (3 hunks)
  • src/components/layout/AppLayout/index.jsx (1 hunks)
  • src/components/layout/NavBar/NavBar.module.css (0 hunks)
  • src/components/layout/Sidebar/Sidebar.module.css (3 hunks)
  • src/components/layout/ThemeToggle/ThemeToggle.module.css (0 hunks)
  • src/components/layout/ThemeToggle/index.jsx (0 hunks)
  • src/contexts/AppStateContext.jsx (6 hunks)
  • src/hooks/useVirtualization.js (1 hunks)
  • src/locales/en.json (1 hunks)
  • src/locales/zh.json (1 hunks)
  • src/styles/components.css (2 hunks)
  • src/styles/globals.css (5 hunks)
💤 Files with no reviewable changes (9)
  • src/components/features/PaidDetectionControl/PaidDetectionControl.module.css
  • src/components/layout/NavBar/NavBar.module.css
  • src/components/layout/ThemeToggle/ThemeToggle.module.css
  • src/components/features/PaidDetectionPrompt/PaidDetectionPrompt.module.css
  • src/components/common/Modal/Modal.module.css
  • src/components/features/KeyInput/KeyInput.module.css
  • src/components/features/Results/Results.module.css
  • src/components/layout/ThemeToggle/index.jsx
  • src/components/features/ApiConfig/ApiConfig.module.css
🧰 Additional context used
🧬 Code graph analysis (22)
src/components/features/RetryControl/index.jsx (1)
src/components/common/FormField/index.jsx (1)
  • FormField (3-39)
src/components/features/AdvancedSettings/index.jsx (3)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/common/IconButton/index.jsx (1)
  • IconButton (4-27)
src/components/features/LogDisplayControl/index.jsx (1)
  • LogDisplayControl (5-41)
src/components/features/LogsPreview/KeyLogModal.jsx (3)
src/components/common/Modal/index.jsx (1)
  • Modal (7-129)
src/components/common/EmptyState/index.jsx (1)
  • EmptyState (3-39)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/hooks/useVirtualization.js (1)
src/components/features/Results/VirtualizedList.jsx (1)
  • keyData (12-12)
src/components/features/LogDisplayControl/index.jsx (2)
src/contexts/AppStateContext.jsx (2)
  • useAppState (524-530)
  • useAppState (524-530)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/features/ConcurrencyControl/index.jsx (1)
src/components/common/FormField/index.jsx (1)
  • FormField (3-39)
src/App.jsx (4)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/features/ApiConfig/index.jsx (1)
  • ApiConfig (5-14)
src/components/features/KeyInput/index.jsx (1)
  • KeyInput (7-36)
src/components/features/Results/index.jsx (1)
  • Results (11-83)
src/components/common/Modal/index.jsx (5)
src/components/features/AdvancedSettings/index.jsx (1)
  • handleOverlayClick (38-42)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/common/IconButton/index.jsx (1)
  • IconButton (4-27)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/Button/index.jsx (1)
  • Button (3-40)
src/components/features/Controls/index.jsx (1)
src/components/common/Button/index.jsx (1)
  • Button (3-40)
src/components/features/KeyInput/FileImport.jsx (1)
src/components/common/IconButton/index.jsx (1)
  • IconButton (4-27)
src/components/features/Results/index.jsx (4)
src/components/features/Results/StatsCards.jsx (1)
  • StatsCards (6-97)
src/components/features/Results/ResultTabs.jsx (1)
  • ResultTabs (5-39)
src/components/features/Results/VirtualizedList.jsx (1)
  • VirtualizedList (210-281)
src/components/features/Results/CopyButtons.jsx (1)
  • CopyButtons (6-148)
src/components/features/KeyInput/PasteButton.jsx (1)
src/components/common/IconButton/index.jsx (1)
  • IconButton (4-27)
src/components/common/StatCard/index.jsx (4)
src/components/common/Card/index.jsx (2)
  • baseClass (42-42)
  • classes (47-53)
src/components/common/FormField/index.jsx (3)
  • baseClass (49-49)
  • sizeClass (52-53)
  • classes (55-57)
src/components/common/StatusBadge/index.jsx (3)
  • baseClass (22-22)
  • sizeClass (24-25)
  • classes (27-29)
src/components/common/EmptyState/index.jsx (2)
  • sizeClass (12-13)
  • classes (15-17)
src/components/features/Results/VirtualizedList.jsx (4)
src/components/features/BalanceDisplay/KeyBalanceDisplay.jsx (3)
  • useLanguage (7-7)
  • useAppState (8-8)
  • KeyBalanceDisplay (6-77)
src/contexts/AppStateContext.jsx (5)
  • useAppState (524-530)
  • useAppState (524-530)
  • key (25-25)
  • key (50-50)
  • state (370-370)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/StatusBadge/index.jsx (1)
  • getStatusClass (10-20)
src/components/features/PaidDetectionControl/index.jsx (3)
src/contexts/AppStateContext.jsx (1)
  • state (370-370)
src/components/common/Button/index.jsx (1)
  • Button (3-40)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/common/IconButton/index.jsx (1)
src/components/common/Button/index.jsx (1)
  • Button (3-40)
src/components/common/StatusBadge/index.jsx (1)
src/components/features/Results/VirtualizedList.jsx (1)
  • getStatusClass (19-29)
src/components/common/EmptyState/index.jsx (5)
src/components/features/Results/VirtualizedList.jsx (1)
  • EmptyState (203-208)
src/components/common/FormField/index.jsx (2)
  • sizeClass (52-53)
  • classes (55-57)
src/components/common/StatCard/index.jsx (2)
  • sizeClass (13-13)
  • classes (14-16)
src/components/common/StatusBadge/index.jsx (2)
  • sizeClass (24-25)
  • classes (27-29)
src/components/common/Card/index.jsx (1)
  • classes (47-53)
src/components/features/Results/StatsCards.jsx (2)
src/components/common/StatCard/index.jsx (1)
  • StatCard (3-36)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/Card/index.jsx (5)
src/components/common/Button/index.jsx (2)
  • baseClass (14-14)
  • classes (21-23)
src/components/common/FormField/index.jsx (2)
  • baseClass (49-49)
  • classes (55-57)
src/components/common/StatCard/index.jsx (2)
  • baseClass (12-12)
  • classes (14-16)
src/components/common/StatusBadge/index.jsx (2)
  • baseClass (22-22)
  • classes (27-29)
src/components/common/EmptyState/index.jsx (1)
  • classes (15-17)
src/components/common/FormField/index.jsx (2)
src/components/common/Card/index.jsx (2)
  • baseClass (42-42)
  • classes (47-53)
src/components/common/StatusBadge/index.jsx (3)
  • baseClass (22-22)
  • sizeClass (24-25)
  • classes (27-29)
src/components/features/LogsPreview/LogsPreviewPanel.jsx (2)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/common/EmptyState/index.jsx (1)
  • EmptyState (3-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (22)
public/worker.js (1)

353-356: LGTM! Consistent statusCode surfacing across error paths.

The statusCode is now consistently extracted and included in KEY_STATUS_UPDATE payloads across all finalization paths (success, max retries, non-retry, and exceptions). This provides better observability for downstream status-driven UI components.

Also applies to: 384-387, 412-415, 459-462

src/locales/zh.json (1)

209-210: LGTM! Clear locale additions for detailed logs feature.

The new locale keys follow existing naming conventions and provide clear descriptions for the detailed logs toggle feature.

src/components/features/RetryControl/RetryControl.module.css (1)

1-236: LGTM! Comprehensive migration to theme-aware CSS variables.

The refactor consistently replaces hard-coded colors with CSS variables (--bg-card, --border-color, --bg-input, etc.) across both light and dark themes. This enables consistent theming while maintaining visual hierarchy.

src/styles/components.css (2)

231-234: LGTM! Simplified switch toggle positioning.

The change from transform-based animation to fixed positioning aligns with the PR's objective of removing transitions and hover effects.


351-518: LGTM! Well-structured card variant system.

The new card variants provide a consistent design language with:

  • Proper semantic naming (card--primary, card--info, etc.)
  • Complete dark theme support
  • Responsive mobile adaptations
  • Backward compatibility aliases

Note: The fixed heights (392px for results cards) work well with the fixed-height virtualization changes in useVirtualization.js.

src/styles/globals.css (2)

296-839: LGTM! Comprehensive horizontal key item styling system.

The new .key-item-horizontal layout introduces a sophisticated status-driven design with:

  • Clear visual hierarchy (status indicator → key capsule → actions)
  • Complete theming for all statuses (valid, invalid, rate-limited, paid, testing, retrying)
  • Proper light/dark theme support using CSS variables
  • Horizontal scrolling capsule for long keys

The structure is complex but well-organized with clear sections for each status variant.


1183-1343: LGTM! Well-considered mobile responsiveness.

The responsive adjustments for horizontal key items appropriately:

  • Reduce component sizes while maintaining touch-friendly targets (36-40px buttons)
  • Hide non-critical status info on mobile to save space (line 1216)
  • Use flexible capsule widths for better adaptation
  • Apply progressive enhancement across breakpoints (768px → 480px)
src/components/features/KeyInput/FileImport.jsx (1)

28-43: LGTM! Clean migration to IconButton component.

The refactor properly replaces the inline button with the IconButton component, maintaining all functionality while improving consistency with the broader component system. The variant="ghost" and size="small" props are appropriate for an icon-only action button.

src/App.jsx (2)

21-41: LGTM! Consistent Card component adoption.

The migration to the Card component properly:

  • Uses semantic variants (function, usage) that map to CSS definitions in components.css
  • Maintains existing layout structure and content
  • Improves UI consistency across the application

The Card variants align with the new card system (.card--primary, .card--info) introduced in src/styles/components.css.


44-44: LGTM! UI simplification aligns with CSS refactor.

The settings button class update to btn--settings correctly uses the new CSS definition from components.css (line 398). The right panel simplification removes unnecessary wrapping, as the Results component handles its own layout (seen in relevant snippets).

Also applies to: 61-61

src/components/features/KeyInput/PasteButton.jsx (1)

4-4: LGTM! Clean refactor to shared IconButton.

The migration from a native button to the shared IconButton component is well-executed. All necessary props (icon, onClick, disabled, title, variant, size, className) are correctly passed, and the inline positioning style is preserved.

Also applies to: 40-59

src/components/layout/AppLayout/index.jsx (1)

39-46: LGTM! Touch support added for mobile compatibility.

The addition of a touchstart event listener alongside mousedown ensures the outside-click behavior works on touch devices. The cleanup correctly removes both listeners, preventing memory leaks.

src/components/features/Controls/index.jsx (1)

8-8: LGTM! Clean migration to shared Button component.

All three buttons have been successfully refactored to use the shared Button component while preserving their original behavior, icons, and labels. The dynamic variant switching for the start/cancel button based on state.isTesting is well-implemented.

Also applies to: 92-143

src/components/common/Card/index.jsx (1)

47-69: LGTM; consistent class composition and optional header.

Implementation is straightforward and consistent with other components.

src/components/features/PaidDetectionControl/index.jsx (1)

73-81: LGTM; consistent with shared Button/Card and confirm Modal.

Disables interactions during testing and wraps status in Card. Modal additions look correct.

Also applies to: 89-121, 132-133

src/components/features/LogsPreview/KeyLogModal.jsx (2)

198-228: LGTM: Card usage is consistent.

The refactor to wrap summary sections in Card components aligns with the PR's goal to standardize UI building blocks.


236-272: LGTM: Event rendering with Card.

Each event is correctly wrapped in a Card, maintaining a consistent visual hierarchy.

src/components/features/Results/index.jsx (1)

26-38: LGTM: Conditional tooltip rendering.

The tooltip is correctly shown based on state.showLogTooltip with proper dismiss handling.

src/contexts/AppStateContext.jsx (2)

504-509: Verify user expectations for auto-clearing logs.

When showDetailedLogs toggles to false, all logs are immediately cleared. Users might expect logs to persist after toggling, especially if they want to temporarily disable collection while preserving existing data.

Consider whether this behavior aligns with user expectations. If logs should persist, remove the auto-clear effect. If this is intentional, add a confirmation dialog or tooltip explaining that disabling will clear existing logs.

If you decide to keep auto-clear, document this behavior in the UI (e.g., in a tooltip or help text for the toggle control).


112-113: LGTM: showDetailedLogs implementation is correct.

The new showDetailedLogs state is properly initialized from localStorage, persisted on changes, and integrated with the log collector's enable/disable mechanism.

Also applies to: 208-212, 419-419, 492-492

src/components/common/Modal/index.jsx (2)

27-37: LGTM: Modal interaction handling is solid.

Body scroll locking, Escape key handling, and overlay click handling are all correctly implemented with proper cleanup.

Also applies to: 39-48, 52-56


66-72: LGTM: Card-based modal structure is well-designed.

The refactor to use Card as the modal content container, with support for confirm/alert layouts and custom children, provides a flexible and consistent foundation.

Also applies to: 91-125

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 10, 2025

Deploying api-key-tester with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0c68747
Status: ✅  Deploy successful!
Preview URL: https://ecba547f.api-key-tester-81w.pages.dev
Branch Preview URL: https://experimental.api-key-tester-81w.pages.dev

View logs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/components/features/Results/VirtualizedList.jsx (1)

13-129: Visibility toggle leaks to other keys

isKeyVisible is stored per rendered index. React Window recycles row components, so switching tabs or filtering reuses the same index with a different key while the previous true state sticks. The new key then renders unmasked without user intent, accidentally exposing secrets. Reset the flag whenever the key identity changes (or hoist visibility state keyed by the actual API key) so every key defaults to masked until explicitly revealed.

-  const [isKeyVisible, setIsKeyVisible] = useState(false);
+  const [isKeyVisible, setIsKeyVisible] = useState(false);
+
+  useEffect(() => {
+    setIsKeyVisible(false);
+  }, [keyData.key]);
@@
-    setIsKeyVisible(!isKeyVisible);
+    setIsKeyVisible(prev => !prev);
♻️ Duplicate comments (3)
src/components/common/Modal/index.jsx (1)

73-95: Past issue resolved: Title rendering now handles complex elements.

The typeof title === 'string' check (lines 77-81) correctly implements the fix suggested in the previous review. String titles are wrapped in an h3 tag for semantic HTML, while React elements are rendered directly, preventing invalid HTML when block-level elements are passed as titles (as in KeyLogModal.jsx lines 184-187).

src/components/features/LogsPreview/KeyLogModal.jsx (1)

184-187: Past issue resolved: Title structure is now valid HTML.

The title now uses a React fragment containing two div elements. This is valid because the Modal component (lines 77-81 in Modal/index.jsx) checks typeof title === 'string' and only wraps string titles in an h3 tag. React elements are rendered directly without the h3 wrapper, avoiding the previous "div inside h3" HTML validation error.

src/components/features/ConcurrencyControl/ConcurrencyControl.module.css (1)

139-143: Active state styling is now correct.

The past review comment flagged that line 140 had background: #ffffff (white) for the active state, which was incorrect. The current code shows background: #667eea (primary blue), which is the appropriate color for an active state. This issue has been resolved.

Note: The past review comment identified this issue, and it has since been fixed. The active button now uses the primary color #667eea instead of white.

🧹 Nitpick comments (5)
src/styles/globals.css (2)

314-353: Consider extracting status indicator styles to reduce duplication.

The status indicator styling is repeated for each of the 6 status types (valid, invalid, rate-limited, paid, testing, retrying) with only border and background colors changing. This creates ~240 lines of highly duplicated code.

Consider using CSS custom properties or a more DRY approach:

/* Base status indicator */
.key-status-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 80px;
  padding: 4px 8px;
  border-radius: 20px;
  border: 1px solid var(--status-border-color, #28a745);
  background: var(--status-bg-color, rgba(40, 167, 69, 0.1));
}

/* Status-specific colors */
.key-item-horizontal.status-valid {
  --status-border-color: #28a745;
  --status-bg-color: rgba(40, 167, 69, 0.1);
}

.key-item-horizontal.status-invalid {
  --status-border-color: #dc3545;
  --status-bg-color: rgba(220, 53, 69, 0.1);
}

/* etc. */

This would reduce the 6 status indicator blocks (~42 lines each) to a single base + 6 compact overrides.


1193-1237: Mobile optimization looks good with minor accessibility concern.

The mobile responsive styles are well-structured with appropriate breakpoints. However, line 1226 completely hides .key-status-info-external on mobile with display: none. This removes potentially important status information from mobile users.

Consider alternatives to complete removal:

  1. Truncate instead of hide:
.key-status-info-external {
  font-size: 9px;
  max-width: 40px;
  overflow: hidden;
  text-overflow: ellipsis;
}
  1. Show on tap/focus:
.key-status-info-external {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.key-item-horizontal:active .key-status-info-external,
.key-item-horizontal:focus-within .key-status-info-external {
  opacity: 1;
  pointer-events: auto;
}
  1. Move to tooltip if status code info is important enough.
src/components/common/FormField/index.jsx (2)

12-17: ID generation uses Math.random which is unstable across renders.

While useRef is correctly used to persist the ID reference, the initial value uses Math.random().toString(36) which can still cause issues:

  1. Server-side rendering (SSR) mismatch: If this component is rendered server-side, the ID generated on the server will differ from the ID generated during client-side hydration, causing React hydration errors.

  2. Snapshot testing: Tests that capture component output will have non-deterministic IDs, making snapshots fragile.

Use a more deterministic approach for ID generation:

+let fieldIdCounter = 0;
+
 const FormField = ({
   label,
   children,
   error,
   help,
   required = false,
   className = '',
   ...props
 }) => {
-  const idRef = useRef(props.id || `field-${Math.random().toString(36).substr(2, 9)}`);
+  const idRef = useRef(props.id || `field-${++fieldIdCounter}`);
   const fieldId = idRef.current;

Or use a more robust approach with React's useId hook (React 18+):

import React, { useId, useRef } from 'react';

const FormField = ({ label, children, error, help, required = false, className = '', ...props }) => {
  const generatedId = useId();
  const fieldId = props.id || generatedId;
  // ... rest of component

27-35: Form control detection could miss custom components.

The isFormControl check (line 31) only detects intrinsic HTML elements and components with displayName: 'Input'. Custom form libraries (Material-UI, Chakra, etc.) use different naming conventions and won't receive proper ARIA attributes.

Consider a more flexible detection approach:

-        const isFormControl = ['input', 'textarea', 'select', 'Input'].includes(tag);
+        // Check if element has typical form control props
+        const isFormControl = (
+          ['input', 'textarea', 'select', 'Input'].includes(tag) ||
+          child.props.onChange ||
+          child.props.value !== undefined ||
+          child.props.name
+        );

Or add a prop to explicitly mark controls:

<FormField.Input isFormControl />
<CustomSelect isFormControl />
src/components/features/ConcurrencyControl/ConcurrencyControl.module.css (1)

55-63: Consider adding visual feedback for disabled sliders.

The slider's thumb color is #495057 (dark gray) in light mode (line 71) and #ffffff (white) in dark mode (line 179), but there's no visual distinction when the slider is disabled beyond the opacity: 0.6 at line 88. Users may not clearly recognize that the control is inactive.

Add distinct disabled thumb styling:

.concurrencySlider:disabled::-webkit-slider-thumb {
  cursor: not-allowed;
  background: #adb5bd; /* Lighter gray */
  box-shadow: none;
}

.concurrencySlider:disabled::-moz-range-thumb {
  cursor: not-allowed;
  background: #adb5bd;
  box-shadow: none;
}

:global(.dark-theme) .concurrencySlider:disabled::-webkit-slider-thumb {
  background: #6c757d; /* Darker gray */
}

:global(.dark-theme) .concurrencySlider:disabled::-moz-range-thumb {
  background: #6c757d;
}

Also applies to: 174-176

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 914860a and e0211d8.

📒 Files selected for processing (13)
  • src/components/common/FormField/index.jsx (1 hunks)
  • src/components/common/Modal/index.jsx (3 hunks)
  • src/components/common/StatCard/index.jsx (1 hunks)
  • src/components/features/ConcurrencyControl/ConcurrencyControl.module.css (6 hunks)
  • src/components/features/ConcurrencyControl/index.jsx (2 hunks)
  • src/components/features/LogsPreview/KeyLogModal.jsx (3 hunks)
  • src/components/features/Results/VirtualizedList.jsx (5 hunks)
  • src/components/features/Results/index.jsx (2 hunks)
  • src/components/features/RetryControl/index.jsx (2 hunks)
  • src/components/layout/Sidebar/Sidebar.module.css (4 hunks)
  • src/hooks/useVirtualization.js (1 hunks)
  • src/styles/components.css (2 hunks)
  • src/styles/globals.css (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/features/RetryControl/index.jsx
  • src/styles/components.css
🧰 Additional context used
🧬 Code graph analysis (7)
src/components/common/Modal/index.jsx (5)
src/components/features/AdvancedSettings/index.jsx (1)
  • handleOverlayClick (38-42)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/common/IconButton/index.jsx (1)
  • IconButton (4-27)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/Button/index.jsx (1)
  • Button (3-40)
src/components/common/FormField/index.jsx (4)
src/components/common/StatCard/index.jsx (3)
  • baseClass (12-12)
  • sizeClass (13-13)
  • classes (14-16)
src/components/common/Card/index.jsx (2)
  • baseClass (42-42)
  • classes (47-53)
src/components/common/StatusBadge/index.jsx (3)
  • baseClass (22-22)
  • sizeClass (24-25)
  • classes (27-29)
src/components/common/EmptyState/index.jsx (2)
  • sizeClass (12-13)
  • classes (15-17)
src/components/features/ConcurrencyControl/index.jsx (3)
src/components/common/FormField/index.jsx (1)
  • FormField (3-48)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/features/RetryControl/index.jsx (2)
  • handleSliderChange (19-22)
  • handleInputChange (11-17)
src/components/features/Results/index.jsx (4)
src/components/features/Results/StatsCards.jsx (1)
  • StatsCards (6-97)
src/components/features/Results/ResultTabs.jsx (1)
  • ResultTabs (5-39)
src/components/features/Results/VirtualizedList.jsx (2)
  • VirtualizedList (209-280)
  • EmptyState (202-207)
src/components/features/Results/CopyButtons.jsx (1)
  • CopyButtons (6-148)
src/components/common/StatCard/index.jsx (4)
src/components/common/FormField/index.jsx (3)
  • baseClass (58-58)
  • sizeClass (61-62)
  • classes (64-66)
src/components/common/Card/index.jsx (2)
  • baseClass (42-42)
  • classes (47-53)
src/components/common/StatusBadge/index.jsx (3)
  • baseClass (22-22)
  • sizeClass (24-25)
  • classes (27-29)
src/components/common/EmptyState/index.jsx (2)
  • sizeClass (12-13)
  • classes (15-17)
src/components/features/Results/VirtualizedList.jsx (2)
src/components/features/BalanceDisplay/KeyBalanceDisplay.jsx (3)
  • useLanguage (7-7)
  • useAppState (8-8)
  • KeyBalanceDisplay (6-77)
src/components/common/StatusBadge/index.jsx (1)
  • getStatusClass (10-20)
src/components/features/LogsPreview/KeyLogModal.jsx (3)
src/components/common/Modal/index.jsx (1)
  • Modal (7-135)
src/components/common/EmptyState/index.jsx (1)
  • EmptyState (3-39)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (17)
src/components/common/Modal/index.jsx (6)

2-3: LGTM!

The new imports for Card and IconButton enable the refactored modal structure with better component composition.


8-24: LGTM!

The comprehensive props with sensible defaults provide flexibility while maintaining ease of use. The addition of closeOnOverlayClick, showCloseButton, and zIndex covers common modal requirements.


27-37: LGTM!

The body scroll lock effect correctly prevents background scrolling when the modal is open and properly cleans up on unmount.


39-48: LGTM!

The Escape key handler uses optional chaining for onClose?.(), safely handling cases where onClose might not be provided.


52-56: LGTM!

The overlay click handler correctly checks closeOnOverlayClick and verifies e.target === e.currentTarget to ensure clicks on the overlay itself (not on child elements) trigger the close action.


97-131: LGTM!

The conditional rendering based on modal type (confirm, alert, or custom) provides appropriate layouts for different use cases. The confirm/alert types render predefined button layouts with localized labels, while custom types allow full flexibility.

src/components/features/LogsPreview/KeyLogModal.jsx (4)

5-7: LGTM!

The new imports for Modal, Card, and EmptyState enable the refactored UI with better component composition and consistent styling.


192-229: LGTM!

The refactored layout uses Card components consistently for status, context, and metadata sections. The grid layout provides a clean two-column structure on medium+ screens. Loading and empty states are handled with the EmptyState component for consistency.


232-275: LGTM!

Each event is wrapped in a Card component with consistent padding. The details/summary elements provide collapsible request/response data, improving readability for large payloads. The empty state for no events uses EmptyState for consistency.


115-115: Verify modal visibility condition
The isOpen flag now requires both state.isLogModalOpen and state.showDetailedLogs, so turning off detailed logs will prevent the modal from opening even when it’s requested. Confirm this stricter gating is intentional.

src/styles/globals.css (1)

295-311: LGTM! New horizontal layout is well-structured.

The new .key-item-horizontal class introduces a modern card-based layout with proper theming support. The explicit transition: none at line 310 correctly implements the PR objective to remove hover effects and transitions.

src/components/features/ConcurrencyControl/index.jsx (2)

13-23: LGTM! Input validation is correct.

The handleInputChange and handleSliderChange functions properly validate and constrain the concurrency value. The slider is capped at 50 (line 39) while the numeric input allows up to 100 (line 16), which aligns with the different UX needs of each control type.


27-59: Fix duplicate ID attribute.

Lines 35 and 46 both assign id="concurrency-count" to different controls (the slider and the number input). This violates the HTML specification requirement that IDs must be unique within a document and breaks accessibility features like label associations and screen reader navigation.

Apply this diff to fix the duplicate ID:

           <input
             id="concurrency-count"
             type="range"
             className={styles.concurrencySlider}
             min="1"
             max="50"
             value={Math.min(state.concurrency, 50)}
             onChange={handleSliderChange}
             disabled={state.isTesting}
             aria-label={t('concurrencyControl') || 'Concurrency slider'}
           />
           <input
-            id="concurrency-count-number"
+            id="concurrency-number-input"
             type="number"
             className={styles.concurrencySliderValue}
             value={state.concurrency}
             min="1"
             max="100"
             onChange={handleInputChange}
             disabled={state.isTesting}
             aria-label={t('concurrencyControl') + ' ' + (t('value') || 'value')}
           />

Also verify the FormField's id prop (line 28) should match the primary control (the slider):

     <FormField
-      id="concurrency-count"
+      id="concurrency-slider"
       label={t('concurrencyControl')}
     >

Likely an incorrect or invalid review comment.

src/components/common/FormField/index.jsx (1)

50-86: LGTM! Input component is well-structured.

The Input subcomponent correctly uses forwardRef to expose refs, properly handles textarea vs input rendering, and computes CSS classes cleanly. The displayName is set for debugging, and the pattern of attaching it to FormField as a subcomponent (FormField.Input) is a good practice.

src/components/features/ConcurrencyControl/ConcurrencyControl.module.css (1)

1-12: LGTM! Container now uses CSS variables for theming.

Lines 9-10 correctly replace hard-coded color values with CSS variables (var(--bg-card) and var(--border-color)), enabling proper light/dark theme support. This aligns with the broader theming refactor across the codebase.

src/components/layout/Sidebar/Sidebar.module.css (2)

156-162: Mobile collapse behavior preserved

Keeping the transform/transition pairing inside the mobile breakpoint keeps the drawer sliding fully off‑canvas when collapsed, which matches the existing UX while the reduced-motion override below handles the accessibility side. Looks good.


200-204: Reduced-motion fallback restored

Thanks for reinstating the prefers-reduced-motion guard—this cleanly neuters the sidebar transition for people who opt out of animations.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0211d8 and 47002f9.

📒 Files selected for processing (12)
  • src/__tests__/hooks/useVirtualization.test.js (2 hunks)
  • src/components/features/ApiConfig/ModelSelector.jsx (1 hunks)
  • src/components/features/ApiConfig/ProxySettings.jsx (0 hunks)
  • src/components/features/KeyInput/PasteButton.jsx (2 hunks)
  • src/components/features/LogsPreview/KeyLogModal.jsx (3 hunks)
  • src/components/features/Results/VirtualizedList.jsx (6 hunks)
  • src/components/layout/Sidebar/Sidebar.module.css (4 hunks)
  • src/hooks/useVirtualization.js (1 hunks)
  • src/locales/en.json (1 hunks)
  • src/locales/zh.json (1 hunks)
  • src/styles/components.css (4 hunks)
  • src/styles/globals.css (6 hunks)
💤 Files with no reviewable changes (1)
  • src/components/features/ApiConfig/ProxySettings.jsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/layout/Sidebar/Sidebar.module.css
🧰 Additional context used
🧬 Code graph analysis (5)
src/components/features/ApiConfig/ModelSelector.jsx (2)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/contexts/AppStateContext.jsx (1)
  • state (370-370)
src/components/features/LogsPreview/KeyLogModal.jsx (3)
src/components/common/Modal/index.jsx (1)
  • Modal (7-135)
src/components/common/EmptyState/index.jsx (1)
  • EmptyState (3-39)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/features/KeyInput/PasteButton.jsx (2)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/contexts/AppStateContext.jsx (1)
  • state (370-370)
src/__tests__/hooks/useVirtualization.test.js (2)
src/components/features/Results/VirtualizedList.jsx (1)
  • useVirtualization (250-250)
src/hooks/useVirtualization.js (2)
  • useVirtualization (3-19)
  • useVirtualization (3-19)
src/components/features/Results/VirtualizedList.jsx (5)
src/components/features/BalanceDisplay/KeyBalanceDisplay.jsx (3)
  • useLanguage (7-7)
  • useAppState (8-8)
  • KeyBalanceDisplay (6-77)
src/contexts/AppStateContext.jsx (5)
  • useAppState (524-530)
  • useAppState (524-530)
  • key (25-25)
  • key (50-50)
  • state (370-370)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/StatusBadge/index.jsx (1)
  • getStatusClass (10-20)
src/hooks/useVirtualization.js (1)
  • getItemHeight (4-13)
🔇 Additional comments (14)
src/components/features/ApiConfig/ModelSelector.jsx (1)

123-139: LGTM! Visual layout improvement.

The restructured flex layout groups the label and paid-detection status inline, improving visual cohesion. The change is purely presentational and does not alter functionality or accessibility.

src/hooks/useVirtualization.js (1)

4-13: LGTM! Simplified API addresses past review.

The refactor removes unused parameters (keyData, apiType) and returns viewport-based heights only. This aligns with the resolution of the past review comment and simplifies the hook's API surface.

Verified: Call sites in VirtualizedList.jsx (line 276) have been updated to call getItemHeight() without parameters.

src/__tests__/hooks/useVirtualization.test.js (1)

9-145: LGTM! Comprehensive test coverage.

The updated tests properly mock window.matchMedia for different viewport scenarios and include cleanup in afterEach. The tests verify:

  • Desktop (74px), mobile (66px), and extra-small (62px) heights
  • Consistent behavior across multiple calls
  • Graceful handling when window.matchMedia is undefined
  • Parameters are correctly ignored

The test coverage aligns well with the simplified hook API.

src/locales/en.json (1)

206-209: LGTM! Locale keys added for detailed logs feature.

The new localization keys showDetailedLogs and showDetailedLogsDescription support the detailed logs visibility feature introduced in this PR. The JSON structure is valid.

src/locales/zh.json (1)

206-209: LGTM! Chinese translations for detailed logs feature.

The Chinese translations mirror the English locale additions, maintaining consistency across both supported languages.

src/components/features/Results/VirtualizedList.jsx (4)

107-117: LGTM! Key masking logic is sound.

The formatKey function correctly masks keys by showing the first 4 and last 4 characters with 10 asterisks in between. The edge case check (key.length <= 8) prevents masking very short keys, which is appropriate.


119-156: LGTM! Clipboard fallback properly implemented.

The clipboard copy implementation correctly:

  • Prioritizes the modern Clipboard API
  • Falls back to execCommand('copy') for unsupported browsers
  • Handles errors gracefully with try/catch
  • Cleans up the temporary textarea in finally block

This addresses the past review comment about clipboard access guarding.


166-234: LGTM! Improved horizontal layout with proper accessibility.

The restructured KeyItem layout provides:

  • Clear status indicator on the left
  • Semi Design-inspired input wrapper for the key display
  • Action buttons with proper ARIA labels and titles
  • Clickable info row for opening logs (addressing past nested interactive controls concern)
  • SVG icons for visibility toggle and copy actions

The accessibility attributes (aria-label, title) ensure screen reader compatibility.


275-277: LGTM! Call site updated for simplified hook API.

The getItemSize function correctly calls getItemHeight() without parameters, aligning with the simplified virtualization hook API.

src/components/features/LogsPreview/KeyLogModal.jsx (2)

180-191: LGTM! Modal component properly integrated.

The migration to the dedicated Modal component is well-structured:

  • isOpen state includes state.showDetailedLogs check, gating visibility appropriately
  • Title uses a React fragment to avoid invalid HTML (addresses past review comment at e0211d8)
  • Props correctly passed: onClose, maxWidth, className

192-280: LGTM! Card and EmptyState components improve structure.

The modal body refactor enhances maintainability:

  • Loading state uses EmptyState with translated message
  • Two-column grid layout with Card components for summary and context
  • Event list wrapped in Card boundaries with proper conditional rendering
  • Consistent use of EmptyState for no-data scenarios

The refactor aligns with the broader Card/EmptyState component introduction across the PR.

src/styles/components.css (3)

222-234: LGTM! Adjacent sibling combinator is more precise.

The change from ~ (general sibling) to + (adjacent sibling) combinator is correct for switch styling. Since .switch-slider is the immediate sibling of .switch-input, + is semantically more precise.


351-529: LGTM! Comprehensive card variant system with dark theme support.

The extensive card variant additions provide:

  • Unified naming convention (card--primary, card--info, card--results, card--empty-state)
  • Consistent button variant (btn--settings)
  • Dark theme adaptations for all variants
  • Responsive adjustments for mobile viewports
  • Compatibility aliases for gradual migration (function-card, usage-card, etc.)

The CSS architecture supports the component refactoring seen throughout this PR, with proper fallbacks and progressive enhancement.


119-124: Verify .form-field.textarea applies to all textareas
Ensure every <textarea class="form-field"> also has the textarea class; otherwise this rule won’t match.

const { t } = useLanguage();
const { state, dispatch } = useAppState();
const [isPasting, setIsPasting] = useState(false);
const [isHovered, setIsHovered] = useState(false);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistency with PR objectives: hover effects and transitions added, not removed.

The PR title states "移除悬停效果和过渡" (remove hover effects and transitions), but this code introduces:

  • Hover state tracking (isHovered)
  • Mouse enter/leave handlers
  • Scale transform on hover
  • Transition effects for transform and opacity

Apply this diff to align with the PR objectives:

-  const [isHovered, setIsHovered] = useState(false);
 
   const handlePaste = async () => {
     <svg
       width="16"
       height="16"
       viewBox="0 0 24 24"
       fill="currentColor"
       onClick={handlePaste}
-      onMouseEnter={() => setIsHovered(true)}
-      onMouseLeave={() => setIsHovered(false)}
       title={t('paste')}
       style={{
         position: 'absolute',
         top: '8px',
         right: '16px',
         zIndex: 10,
         cursor: state.isTesting ? 'not-allowed' : 'pointer',
-        opacity: state.isTesting ? 0.5 : 1,
-        transform: isHovered && !state.isTesting ? 'scale(1.1)' : 'scale(1)',
-        transition: 'transform 0.2s ease, opacity 0.2s ease'
+        opacity: state.isTesting ? 0.5 : 1
       }}
     >

Also applies to: 46-47, 56-57

🤖 Prompt for AI Agents
In src/components/features/KeyInput/PasteButton.jsx around line 9 (and also
affecting lines 46-47 and 56-57), the PR is supposed to remove hover effects and
transitions but instead adds hover state and related handlers; remove the hover
implementation by deleting the useState hook for isHovered, removing any
onMouseEnter/onMouseLeave handlers, and eliminating any conditional usage of
isHovered (including scale transform logic); also remove CSS
transitions/transform/opacity changes tied to hover (classNames or inline
styles) so the button has no hover scaling/transition behavior.

Comment on lines +40 to +61
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
onClick={handlePaste}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
title={t('paste')}
style={{
position: 'absolute',
top: '8px',
right: '8px',
zIndex: 10
right: '16px',
zIndex: 10,
cursor: state.isTesting ? 'not-allowed' : 'pointer',
opacity: state.isTesting ? 0.5 : 1,
transform: isHovered && !state.isTesting ? 'scale(1.1)' : 'scale(1)',
transition: 'transform 0.2s ease, opacity 0.2s ease'
}}
onClick={handlePaste}
disabled={state.isTesting}
title={t('paste')}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
<path d="m16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
</svg>
</button>
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path>
</svg>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add keyboard accessibility and ARIA attributes.

The SVG lacks keyboard navigation and proper accessibility attributes. Interactive SVGs must be keyboard-accessible and announce their purpose to screen readers.

Apply this diff to add accessibility support:

+  const handleKeyDown = (e) => {
+    if ((e.key === 'Enter' || e.key === ' ') && !state.isTesting && !isPasting) {
+      e.preventDefault();
+      handlePaste();
+    }
+  };
+
   return (
     <svg
       width="16"
       height="16"
       viewBox="0 0 24 24"
       fill="currentColor"
+      role="button"
+      tabIndex={state.isTesting ? -1 : 0}
+      aria-label={t('paste')}
       onClick={handlePaste}
-      onMouseEnter={() => setIsHovered(true)}
-      onMouseLeave={() => setIsHovered(false)}
+      onKeyDown={handleKeyDown}
       title={t('paste')}
       style={{

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/features/KeyInput/PasteButton.jsx around lines 40 to 61, the
SVG is not keyboard-accessible or announced to screen readers; make it an
accessible interactive element by adding role="button",
tabIndex={state.isTesting ? -1 : 0}, and aria-disabled={state.isTesting} (or
aria-label={t('paste')} if not using title), implement an onKeyDown handler that
invokes handlePaste when Enter or Space is pressed (and ignores input when
state.isTesting), and keep existing visual disabled cues (cursor/opacity) so
keyboard users get the same affordance; ensure title or aria-label is present
and remove reliance on title alone for screen readers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (2)
src/components/features/Results/VirtualizedList.jsx (2)

304-315: Prevent row-state bleed with a stable itemKey (possible secret exposure).

With VariableSizeList keyed by index, reordering/filtered updates can reuse row instances and carry isKeyVisible to the wrong item. A key’s visibility may appear unmasked for another key after list changes.

Add a stable itemKey based on the key value.

       <List
         ref={listRef}
         height={listHeight}
         itemCount={filteredKeys.length}
         itemSize={getItemSize}
         itemData={filteredKeys}
+        itemKey={(index, data) => data[index]?.key ?? index}
         overscanCount={3}
         width="100%"
         className="virtualized-list"
       >

100-105: Avoid double activation on a real button; add type="button".

  • handleKeyDown on a native can double-trigger with onClick (Enter/Space). Remove it and rely on button semantics.
  • Add type="button" to all action buttons to prevent unintended form submits.
-  const handleKeyDown = (event) => {
-    if (event.key === 'Enter' || event.key === ' ') {
-      event.preventDefault();
-      handleOpenLogs();
-    }
-  };
               <button
+                type="button"
                 className="semi-button semi-button-borderless semi-button-small semi-button-tertiary"
                 onClick={handleToggleKeyVisibility}
               <button
+                type="button"
                 className="semi-button semi-button-borderless semi-button-small semi-button-tertiary"
                 onClick={handleCopyKey}
-          <button
+          <button
+            type="button"
             className="key-info-row key-info-clickable"
             onClick={handleOpenLogs}
-            onKeyDown={handleKeyDown}
             aria-label={t('openLogs') || 'Open logs for this key'}

Also applies to: 183-200, 201-211, 216-221

♻️ Duplicate comments (3)
src/styles/globals.css (1)

625-656: Restore a visible scrollbar (duplicate of earlier feedback).

The capsule still hides scrollbars entirely (display: none / scrollbar-width: none). That leaves keyboard and pointer users without any affordance for horizontal overflow, even with the new gradient. Please reinstate a thin, low-contrast scrollbar (or an equivalent accessible indicator) so the scroll state remains discoverable.

src/components/features/Results/VirtualizedList.jsx (2)

119-139: Clipboard fallback added — previous issue resolved.

Defensive Clipboard API usage with textarea fallback is implemented. Looks good.

Also applies to: 141-156


215-234: A11y: nested-interactive issue fixed.

Switching the middle region to a proper resolves the prior role="button" nesting problem. Good fix.

🧹 Nitpick comments (7)
src/components/features/ApiConfig/ModelSelector.jsx (1)

134-137: Consider using the StatusBadge component for semantic consistency.

Since the PR introduces a new StatusBadge component, consider using it here instead of a <small> tag for better semantic markup and consistency across the application.

Example refactor:

+import StatusBadge from '../../common/StatusBadge';
+
 // ... rest of imports

-      <small className="form-info enabled text-xs">
-        {t('paidDetectionEnabled')}
-      </small>
+      <StatusBadge status="enabled" size="sm">
+        {t('paidDetectionEnabled')}
+      </StatusBadge>

Note: Adjust the StatusBadge props based on the actual component API.

src/styles/components.css (4)

119-124: Scope monospace more safely for textareas

Use a tokenized monospace stack to avoid platform-specific “Courier New” quirks.

-.form-field.textarea,
-textarea.form-field {
+.form-field.textarea,
+textarea.form-field {
   resize: vertical;
   min-height: 120px;
-  font-family: 'Courier New', monospace;
+  font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace);
 }

366-371: Prefer variables over hardcoded info colors

Use theme tokens with fallbacks to keep light/dark/parity consistent.

-.card--info {
-  background: #e3f2fd;
-  border-color: #bbdefb;
-  color: #1976d2;
+.card--info {
+  background: var(--info-bg, #e3f2fd);
+  border-color: var(--info-border, #bbdefb);
+  color: var(--info-fg, #1976d2);
   padding: var(--spacing-sm) var(--spacing-md);
 }

409-428: .btn--settings should reuse base button and add a visible focus state

Consider composing with .btn-base in markup to avoid duplication, and add a focus-visible ring for accessibility.

 .btn--settings {
   display: flex;
   align-items: center;
   justify-content: center;
   gap: 8px;
   width: 100%;
   min-height: 48px;
   padding: var(--spacing-md) var(--spacing-xl);
   background: var(--color-secondary);
   color: white;
   border: none;
   border-radius: var(--border-radius);
   font-size: var(--font-size-base);
   font-weight: 600;
   cursor: pointer;
   text-decoration: none;
   box-sizing: border-box;
   margin-bottom: 0;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 }
+
+.btn--settings:focus-visible {
+  outline: 2px solid var(--focus-ring, var(--primary-color));
+  outline-offset: 2px;
+}

Also verify that --color-secondary is defined in the theme.


441-444: Dark theme parity for results alias

If keeping aliases, ensure dark theme rules also target .results-card to avoid mismatched borders/backgrounds.

-.dark-theme .card--results {
+.dark-theme .card--results,
+.dark-theme .results-card {
   background: #2a2a2a;
   border-color: #444444;
 }
src/components/features/Results/VirtualizedList.jsx (2)

19-29: De-duplicate status class mapping to avoid drift.

This mapping duplicates StatusBadge logic and has a different default ('status-unknown' vs StatusBadge’s 'status-testing'). Prefer centralizing via StatusBadge or a shared util.


43-53: Remove unused helper.

getLocalizedError isn’t used; drop it to reduce bundle size.

-  const getLocalizedError = (error) => {
-    if (!error) return '';
-
-    // 如果错误是翻译键,直接使用翻译
-    if (error.startsWith('errorMessages.')) {
-      return t(error) || error;
-    }
-
-    // 直接翻译显示
-    return error;
-  };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0211d8 and 0759c67.

📒 Files selected for processing (13)
  • new-api (1 hunks)
  • src/__tests__/hooks/useVirtualization.test.js (2 hunks)
  • src/components/features/ApiConfig/ModelSelector.jsx (1 hunks)
  • src/components/features/ApiConfig/ProxySettings.jsx (0 hunks)
  • src/components/features/KeyInput/PasteButton.jsx (2 hunks)
  • src/components/features/LogsPreview/KeyLogModal.jsx (3 hunks)
  • src/components/features/Results/VirtualizedList.jsx (6 hunks)
  • src/components/layout/Sidebar/Sidebar.module.css (4 hunks)
  • src/hooks/useVirtualization.js (1 hunks)
  • src/locales/en.json (1 hunks)
  • src/locales/zh.json (1 hunks)
  • src/styles/components.css (4 hunks)
  • src/styles/globals.css (6 hunks)
💤 Files with no reviewable changes (1)
  • src/components/features/ApiConfig/ProxySettings.jsx
✅ Files skipped from review due to trivial changes (1)
  • new-api
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/features/KeyInput/PasteButton.jsx
  • src/hooks/useVirtualization.js
🧰 Additional context used
🧬 Code graph analysis (4)
src/__tests__/hooks/useVirtualization.test.js (2)
src/components/features/Results/VirtualizedList.jsx (1)
  • useVirtualization (250-250)
src/hooks/useVirtualization.js (2)
  • useVirtualization (3-19)
  • useVirtualization (3-19)
src/components/features/ApiConfig/ModelSelector.jsx (1)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/features/LogsPreview/KeyLogModal.jsx (3)
src/components/common/Modal/index.jsx (1)
  • Modal (7-135)
src/components/common/EmptyState/index.jsx (1)
  • EmptyState (3-39)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/features/Results/VirtualizedList.jsx (5)
src/components/features/BalanceDisplay/KeyBalanceDisplay.jsx (3)
  • useLanguage (7-7)
  • useAppState (8-8)
  • KeyBalanceDisplay (6-77)
src/contexts/AppStateContext.jsx (5)
  • useAppState (524-530)
  • useAppState (524-530)
  • key (25-25)
  • key (50-50)
  • state (370-370)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/StatusBadge/index.jsx (1)
  • getStatusClass (10-20)
src/hooks/useVirtualization.js (1)
  • getItemHeight (4-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
src/components/layout/Sidebar/Sidebar.module.css (1)

200-205: Reduced-motion fallback reinstated.

Thanks for restoring the reduced-motion override; nice job closing the accessibility gap.

src/components/features/ApiConfig/ModelSelector.jsx (1)

123-131: LGTM! Clean layout refactor.

The flex container provides a clear horizontal layout for the label and detected models badge. The conditional rendering and styling are appropriate.

src/styles/components.css (1)

223-225: Unify CSS variable naming for primary color tokens

Both --color-primary and --primary-color are used. Pick one scheme to avoid theme bugs and fallback inconsistencies.

Please confirm which is canonical (e.g., --color-primary and --color-primary-alpha) and standardize across this file.

Also applies to: 339-341

</label>

{/* 付费检测状态信息 */}
{state.apiType === 'gemini' && state.enablePaidDetection && (
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Extract duplicated paid detection condition.

The condition state.apiType === 'gemini' && state.enablePaidDetection (or its reversed form) appears 7 times throughout this file (lines 134, 143, 145, 148, 159, 161, 169). Extract it to a memoized computed value to improve maintainability and reduce duplication.

Apply this refactor near the top of the component:

 const ModelSelector = () => {
   const { t } = useLanguage();
   const { state, dispatch } = useAppState();
   const { detectModels, isDetecting } = useApiTester();
   const [showPaidDetectionPrompt, setShowPaidDetectionPrompt] = useState(false);
   const [isCustomModel, setIsCustomModel] = useState(false);
+
+  // Memoize paid detection condition to avoid duplication
+  const isGeminiPaidDetection = useMemo(
+    () => state.apiType === 'gemini' && state.enablePaidDetection,
+    [state.apiType, state.enablePaidDetection]
+  );

   const currentModels = useMemo(() => MODEL_OPTIONS[state.apiType] || [], [state.apiType]);

Then replace all 7 occurrences with isGeminiPaidDetection.

📝 Committable suggestion

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

Suggested change
{state.apiType === 'gemini' && state.enablePaidDetection && (
// File: src/components/features/ApiConfig/ModelSelector.jsx
const ModelSelector = () => {
const { t } = useLanguage();
const { state, dispatch } = useAppState();
const { detectModels, isDetecting } = useApiTester();
const [showPaidDetectionPrompt, setShowPaidDetectionPrompt] = useState(false);
const [isCustomModel, setIsCustomModel] = useState(false);
// Memoize paid detection condition to avoid duplication
const isGeminiPaidDetection = useMemo(
() => state.apiType === 'gemini' && state.enablePaidDetection,
[state.apiType, state.enablePaidDetection]
);
const currentModels = useMemo(
() => MODEL_OPTIONS[state.apiType] || [],
[state.apiType]
);
// …rest of your hooks and handlers…
return (
<>
{/* …other JSX… */}
{/* Example replacement at line 134 */}
{isGeminiPaidDetection && (
/* …paid-detection UI… */
)}
{/* Make the same replacement for the other 6 occurrences */}
</>
);
};
🤖 Prompt for AI Agents
In src/components/features/ApiConfig/ModelSelector.jsx around lines 134–169, the
condition state.apiType === 'gemini' && state.enablePaidDetection is duplicated
seven times; create a single memoized boolean near the top of the component
(e.g. const isGeminiPaidDetection = useMemo(() => state.apiType === 'gemini' &&
state.enablePaidDetection, [state.apiType, state.enablePaidDetection]) — add
useMemo to the imports if missing — then replace all seven occurrences with
isGeminiPaidDetection to remove duplication and improve maintainability.

Comment on lines +107 to +117
// 格式化密钥显示 - 参考 new-api 的遮罩规则
const formatKey = (key, isVisible) => {
if (isVisible) {
// 显示完整密钥
return key;
} else {
// 显示遮罩版本:前4位 + 10个星号 + 后4位
if (key.length <= 8) return key;
return key.substring(0, 4) + '**********' + key.substring(key.length - 4);
}
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mask short keys too (don’t reveal ≤8‑char keys when hidden).

Current logic returns the full key when length ≤ 8, leaking secrets. Always mask when not visible.

-  const formatKey = (key, isVisible) => {
-    if (isVisible) {
-      // 显示完整密钥
-      return key;
-    } else {
-      // 显示遮罩版本:前4位 + 10个星号 + 后4位
-      if (key.length <= 8) return key;
-      return key.substring(0, 4) + '**********' + key.substring(key.length - 4);
-    }
-  };
+  const formatKey = (key, isVisible) => {
+    if (!key) return '';
+    if (isVisible) return key;
+    const len = key.length;
+    if (len <= 8) {
+      // 短密钥也需遮罩,至少保留前后各2位
+      return `${key.slice(0, 2)}****${key.slice(-2)}`;
+    }
+    return `${key.slice(0, 4)}**********${key.slice(-4)}`;
+  };
📝 Committable suggestion

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

Suggested change
// 格式化密钥显示 - 参考 new-api 的遮罩规则
const formatKey = (key, isVisible) => {
if (isVisible) {
// 显示完整密钥
return key;
} else {
// 显示遮罩版本:前4位 + 10个星号 + 后4位
if (key.length <= 8) return key;
return key.substring(0, 4) + '**********' + key.substring(key.length - 4);
}
};
// 格式化密钥显示 - 参考 new-api 的遮罩规则
const formatKey = (key, isVisible) => {
if (!key) return '';
if (isVisible) return key;
const len = key.length;
if (len <= 8) {
// 短密钥也需遮罩,至少保留前后各2位
return `${key.slice(0, 2)}****${key.slice(-2)}`;
}
return `${key.slice(0, 4)}**********${key.slice(-4)}`;
};
🤖 Prompt for AI Agents
In src/components/features/Results/VirtualizedList.jsx around lines 107 to 117,
the formatKey function currently returns the full key when key.length <= 8 which
leaks secrets; change the logic so that when isVisible is false you never return
the original key — for keys longer than 8 keep the existing pattern (first 4 +
10 stars + last 4), and for keys of length 8 or less return a masked string
(e.g., repeat '*' for the key length) so short keys are fully masked.

Comment on lines +206 to 209
},
"showDetailedLogs": "Show Detailed Logs",
"showDetailedLogsDescription": "Control whether to show detailed API call logs"
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add missing strings used by UI (prevents raw key labels).

VirtualizedList uses t('copyKey' | 'showKey' | 'hideKey' | 'openLogs'); without entries, labels show literal keys.

   },
   "showDetailedLogs": "Show Detailed Logs",
-  "showDetailedLogsDescription": "Control whether to show detailed API call logs"
+  "showDetailedLogsDescription": "Control whether to show detailed API call logs",
+  "copyKey": "Copy key",
+  "showKey": "Show key",
+  "hideKey": "Hide key",
+  "openLogs": "Open logs for this key"
 }
📝 Committable suggestion

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

Suggested change
},
"showDetailedLogs": "Show Detailed Logs",
"showDetailedLogsDescription": "Control whether to show detailed API call logs"
}
},
"showDetailedLogs": "Show Detailed Logs",
"showDetailedLogsDescription": "Control whether to show detailed API call logs",
"copyKey": "Copy key",
"showKey": "Show key",
"hideKey": "Hide key",
"openLogs": "Open logs for this key"
}
🤖 Prompt for AI Agents
In src/locales/en.json around lines 206 to 209, the UI is missing translation
keys used by VirtualizedList (copyKey, showKey, hideKey, openLogs), causing raw
key labels to appear; add these string entries to the JSON with appropriate
English text (e.g., "copyKey": "Copy", "showKey": "Show key", "hideKey": "Hide
key", "openLogs": "Open logs") ensuring proper JSON comma placement and
consistent style with existing keys.

Comment on lines +206 to 209
},
"showDetailedLogs": "显示详细日志",
"showDetailedLogsDescription": "控制是否显示API调用的详细日志信息"
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

补齐缺失的文案键,避免显示原始键名。

VirtualizedList 依赖 t('copyKey' | 'showKey' | 'hideKey' | 'openLogs'),目前未定义。

   },
   "showDetailedLogs": "显示详细日志",
-  "showDetailedLogsDescription": "控制是否显示API调用的详细日志信息"
+  "showDetailedLogsDescription": "控制是否显示API调用的详细日志信息",
+  "copyKey": "复制密钥",
+  "showKey": "显示密钥",
+  "hideKey": "隐藏密钥",
+  "openLogs": "打开该密钥的日志"
 }
📝 Committable suggestion

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

Suggested change
},
"showDetailedLogs": "显示详细日志",
"showDetailedLogsDescription": "控制是否显示API调用的详细日志信息"
}
},
"showDetailedLogs": "显示详细日志",
"showDetailedLogsDescription": "控制是否显示API调用的详细日志信息",
"copyKey": "复制密钥",
"showKey": "显示密钥",
"hideKey": "隐藏密钥",
"openLogs": "打开该密钥的日志"
}
🤖 Prompt for AI Agents
In src/locales/zh.json around lines 206 to 209, the VirtualizedList expects
translation keys copyKey, showKey, hideKey and openLogs which are missing; add
these keys with appropriate Chinese translations (e.g., "复制键", "显示键", "隐藏键",
"打开日志") into the JSON near the existing entries so t(...) resolves to localized
strings instead of falling back to raw keys.

Comment on lines +231 to 234
.switch-input:checked+.switch-slider:before {
left: 22px;
background: white;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Switch knob final position likely off by 2px

For a 44px track and 18px knob, the right position should be left: 24px (symmetrical 2px gutters).

-.switch-input:checked+.switch-slider:before {
-  left: 22px;
+.switch-input:checked+.switch-slider:before {
+  left: 24px;
   background: white;
 }
🤖 Prompt for AI Agents
In src/styles/components.css around lines 231 to 234, the .switch-input:checked
+ .switch-slider:before rule moves the 18px knob to left: 22px which is 2px
short for a 44px track; change the value to left: 24px so the knob is centered
with symmetrical 2px gutters (update the CSS rule accordingly).

Comment on lines +374 to +385
.card--results {
display: flex;
flex-direction: column;
flex: 1;
margin-bottom: 0;
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 16px;
max-height: 340px;
min-height: 340px;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Alias divergence: .results-card and .card--results are not equivalent (heights differ, responsive not synced)

This will cause inconsistent layouts depending on which alias is used. Unify definitions and responsive rules.

-/* 结果展示卡片 */
-.card--results {
+/* 结果展示卡片(主类与别名保持一致) */
+.card--results,
+.results-card {
   display: flex;
   flex-direction: column;
   flex: 1;
   margin-bottom: 0;
   background: #f8f9fa;
   border: 2px solid #e9ecef;
   border-radius: 12px;
   padding: 16px;
-  max-height: 340px;
-  min-height: 340px;
+  max-height: 340px;
+  min-height: 340px;
 }
@@
-  .card--results {
+  .card--results,
+  .results-card {
     max-height: 400px;
     min-height: 400px;
     padding: 12px;
   }
@@
-  .card--results {
+  .card--results,
+  .results-card {
     max-height: 350px;
     min-height: 350px;
     padding: 10px;
   }
@@
-.results-card {
-  display: flex;
-  flex-direction: column;
-  flex: 1;
-  margin-bottom: 0;
-  background: #f8f9fa;
-  border: 2px solid #e9ecef;
-  border-radius: 12px;
-  padding: 16px;
-  max-height: 392px;
-  min-height: 392px;
-}
+/* 删除独立定义,已与 .card--results 合并以保持一致 */

Also applies to: 477-481, 485-489, 509-520

Comment on lines +297 to +313
display: flex;
align-items: center;
padding: 16px 20px;
background: var(--bg-input);
/* 使用主题变量 */
border: 2px solid var(--border-color);
border-radius: 12px;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 1px;
height: 72px;
width: 100%;
box-sizing: border-box;
cursor: pointer;
transition: none;
/* 移除过渡效果 */
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hard-coding the card height.

height: 72px; (and the mobile overrides) will clip or overflow longer status texts, error messages, or multi-line metadata inside .key-display-container. Use min-height (or remove the explicit height) so the card can expand with its content.

🤖 Prompt for AI Agents
In src/styles/globals.css around lines 297 to 313, the rule for
.key-display-container sets a fixed height (height: 72px) which can clip
multi-line content; replace the fixed height with a flexible constraint such as
min-height: 72px (or remove the height declaration entirely) and ensure any
mobile overrides also use min-height or no explicit height so the card can grow
with its content; keep existing padding, box-sizing and alignment to preserve
visual layout while allowing expansion.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (2)
src/components/features/RetryControl/RetryControl.module.css (1)

139-199: Active preset contrast regressed

The active preset state now renders white text on #e8eaed, giving a contrast ratio of ~1.2:1. That fails WCAG 2.1 AA (4.5:1) and makes the label nearly unreadable in both light (Lines 139-143) and dark (Lines 195-199) themes. Please restore a high-contrast pairing—e.g., keep the light background but switch the text to a dark tone such as #1f2933 (or your text token), or revert to the accent background that supported white text.

public/worker.js (1)

32-37: Redact secrets in logs before posting to main thread

Request logs currently include Authorization/x‑api keys and may expose query keys (e.g., Gemini). Mask these before self.postMessage.

-const createRequestLog = ({ url, method, headers, body }) => ({
-  url,
-  method,
-  headers: headersToObject(headers),
-  body: truncateForLog(body)
-});
+const createRequestLog = ({ url, method, headers, body }) => {
+  const h = headersToObject(headers) || {};
+  if (h.Authorization) h.Authorization = 'Bearer ****';
+  if (h['x-api-key']) h['x-api-key'] = '****';
+  if (h['x-goog-api-key']) h['x-goog-api-key'] = '****';
+  // Redact key in URL query
+  const safeUrl = typeof url === 'string' ? url.replace(/([?&](?:key|api_key|apiKey))=[^&#]*/gi, '$1=****') : url;
+  return {
+    url: safeUrl,
+    method,
+    headers: h,
+    body: truncateForLog(body)
+  };
+};
♻️ Duplicate comments (7)
src/components/features/KeyInput/PasteButton.jsx (2)

9-9: Duplicate concern: hover state contradicts PR objectives.

This concern was already raised in a previous review. The PR title indicates removing hover effects and transitions, but this code introduces them.


40-62: Duplicate concern: keyboard accessibility missing.

A previous review already noted that the SVG lacks keyboard accessibility (role, tabIndex, onKeyDown handler). This remains unaddressed.

src/locales/zh.json (1)

206-209: Duplicate concern: VirtualizedList translation keys still missing.

A previous review noted that VirtualizedList expects keys like copyKey, showKey, hideKey, and openLogs which are not present in the localization file.

src/styles/components.css (2)

231-234: Switch knob final position off by 2px.

For a 44px track with an 18px knob, the checked position should be left: 24px to maintain symmetrical 2px gutters (44 - 18 - 2 = 24), not left: 22px.

Apply this diff:

 .switch-input:checked+.switch-slider:before {
-  left: 22px;
+  left: 24px;
   background: white;
 }

374-385: Height mismatch between card variants creates layout inconsistency.

.card--results at lines 374-385 has max-height: 340px; min-height: 340px, while its alias .results-card at lines 509-520 has max-height: 392px; min-height: 392px. This 52px difference will cause inconsistent layouts depending on which class is used.

Unify the heights. If 340px is the intended height, apply this diff:

 .results-card {
   display: flex;
   flex-direction: column;
   flex: 1;
   margin-bottom: 0;
   background: #f8f9fa;
   border: 2px solid #e9ecef;
   border-radius: 12px;
   padding: 16px;
-  max-height: 392px;
-  min-height: 392px;
+  max-height: 340px;
+  min-height: 340px;
 }

Alternatively, merge the selectors to ensure they always stay in sync:

-/* 结果展示卡片 */
-.card--results {
+/* 结果展示卡片(主类与别名保持一致) */
+.card--results,
+.results-card {
   display: flex;
   flex-direction: column;
   flex: 1;
   margin-bottom: 0;
   background: #f8f9fa;
   border: 2px solid #e9ecef;
   border-radius: 12px;
   padding: 16px;
   max-height: 340px;
   min-height: 340px;
 }

Then remove the duplicate .results-card definition at lines 509-520.

src/styles/globals.css (1)

771-954: Critical: All status variants use identical colors, losing visual distinction.

Lines 771-796 define the same --status-* custom property values for all six status variants (valid, invalid, rate-limited, paid, testing, retrying). As a result, every card renders with identical neutral colors, eliminating the visual cues that differentiate statuses—a significant UX regression.

Restore per-status color palettes. For example:

 .key-item-horizontal.status-valid {
-  --status-bg-light: #ffffff;
-  --status-bg-dark: #2c2c2c;
-  --status-border-light: #e0e0e0;
-  --status-border-dark: #404040;
-  --status-capsule-light: rgba(108, 117, 125, 0.2);
-  --status-capsule-dark: rgba(33, 37, 41, 0.9);
-  --status-text-light: #343a40;
-  --status-text-dark: white;
+  --status-bg-light: rgba(40, 167, 69, 0.08);
+  --status-bg-dark: rgba(40, 167, 69, 0.18);
+  --status-border-light: rgba(40, 167, 69, 0.45);
+  --status-border-dark: rgba(56, 189, 105, 0.65);
+  --status-capsule-light: rgba(40, 167, 69, 0.12);
+  --status-capsule-dark: rgba(40, 167, 69, 0.35);
+  --status-text-light: #166534;
+  --status-text-dark: #d1f7dc;
   /* ... remaining properties with green palette */
 }

 .key-item-horizontal.status-invalid {
-  --status-bg-light: #ffffff;
+  --status-bg-light: rgba(220, 53, 69, 0.08);
   /* ... remaining properties with red palette */
 }

 .key-item-horizontal.status-rate-limited {
-  --status-bg-light: #ffffff;
+  --status-bg-light: rgba(255, 193, 7, 0.08);
   /* ... remaining properties with yellow palette */
 }

 .key-item-horizontal.status-paid {
-  --status-bg-light: #ffffff;
+  --status-bg-light: rgba(13, 110, 253, 0.08);
   /* ... remaining properties with blue palette */
 }

 .key-item-horizontal.status-testing,
 .key-item-horizontal.status-retrying {
   /* Keep neutral gray for in-progress states */
 }

Apply distinct color palettes to each status variant so users can immediately identify the state of each key card.

src/components/features/Results/VirtualizedList.jsx (1)

107-117: Don’t reveal short keys when hidden (mask ≤8 chars too).

Always mask when not visible to avoid secret leakage.

-  const formatKey = (key, isVisible) => {
-    if (isVisible) {
-      // 显示完整密钥
-      return key;
-    } else {
-      // 显示遮罩版本:前4位 + 10个星号 + 后4位
-      if (key.length <= 8) return key;
-      return key.substring(0, 4) + '**********' + key.substring(key.length - 4);
-    }
-  };
+  const formatKey = (key, isVisible) => {
+    if (!key) return '';
+    if (isVisible) return key;
+    const len = key.length;
+    if (len <= 8) {
+      // 短密钥也需遮罩,至少保留前后各2位
+      return `${key.slice(0, 2)}****${key.slice(-2)}`;
+    }
+    return `${key.slice(0, 4)}**********${key.slice(-4)}`;
+  };
🧹 Nitpick comments (15)
src/components/features/Controls/index.jsx (2)

92-114: Consider using the Button component's icon prop.

The Button component provides a dedicated icon prop that handles spacing automatically (see src/components/common/Button/index.jsx lines 30-34). Currently, SVG icons are embedded in children, which bypasses this built-in handling and may conflict with the gap-xs utility class.

Apply this diff to leverage the icon prop:

-        <Button
-          variant={state.isTesting ? 'danger' : 'primary'}
-          size="large"
+        <Button
+          variant={state.isTesting ? 'danger' : 'primary'}
+          size="large"
+          icon={
+            state.isTesting ? (
+              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                <rect x="6" y="4" width="4" height="16" />
+                <rect x="14" y="4" width="4" height="16" />
+              </svg>
+            ) : (
+              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+                <polygon points="5,3 19,12 5,21" />
+              </svg>
+            )
+          }
           onClick={handleStartTest}
-          className="btn-fixed-lg flex items-center gap-xs"
+          className="btn-fixed-lg"
         >
-          {state.isTesting ? (
-            <>
-              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
-                <rect x="6" y="4" width="4" height="16" />
-                <rect x="14" y="4" width="4" height="16" />
-              </svg>
-              {t('cancelTest')}
-            </>
-          ) : (
-            <>
-              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
-                <polygon points="5,3 19,12 5,21" />
-              </svg>
-              {t('startTest')}
-            </>
-          )}
+          {state.isTesting ? t('cancelTest') : t('startTest')}
         </Button>

116-129: Consider using the Button component's icon prop.

Similar to the start button, these buttons embed SVG icons in children instead of using the Button component's dedicated icon prop. This bypasses the built-in spacing logic and may conflict with the gap-xs utility class.

Apply this pattern to both buttons:

For the Dedupe button:

-        <Button
-          variant="secondary"
-          size="large"
+        <Button
+          variant="secondary"
+          size="large"
+          icon={
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+              <polyline points="23 4 23 10 17 10" />
+              <polyline points="1 20 1 14 7 14" />
+              <path d="m3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
+            </svg>
+          }
           onClick={handleDedupeKeys}
           disabled={state.isTesting}
-          className="btn-fixed-lg flex items-center gap-xs"
+          className="btn-fixed-lg"
         >
-          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
-            <polyline points="23 4 23 10 17 10" />
-            <polyline points="1 20 1 14 7 14" />
-            <path d="m3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
-          </svg>
           {t('dedupeKeys')}
         </Button>

For the Clear button:

-        <Button
-          variant="secondary"
-          size="large"
+        <Button
+          variant="secondary"
+          size="large"
+          icon={
+            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+              <polyline points="3,6 5,6 21,6" />
+              <path d="m19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
+            </svg>
+          }
           onClick={handleClear}
           disabled={state.isTesting}
-          className="btn-fixed-lg flex items-center gap-xs"
+          className="btn-fixed-lg"
         >
-          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
-            <polyline points="3,6 5,6 21,6" />
-            <path d="m19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
-          </svg>
           {t('clear')}
         </Button>

Also applies to: 131-143

public/worker.js (3)

353-355: Status code propagation looks good; consider deriving once per attempt

LGTM. Minor: compute const statusCode = extractStatusCode(result.error) right after testApiKey() returns and reuse in all KEY_STATUS_UPDATEs for this attempt to avoid repeated parsing. Also consider including statusCode in the final postLogEvent.extra for consistency; ensure the UI handles null.

Also applies to: 384-386, 412-414, 459-461


504-506: Make extractStatusCode more robust

Tighten matching:

  • Allow trailing spaces after the closing parenthesis.
  • Match “HTTP 4xx/5xx” case‑insensitively and without the pre‑check.

Apply this minimal tweak first:

-  const match = error.match(/\((\d{3})\)$/);
+  const match = error.match(/\((\d{3})\)\s*$/);

Optionally simplify the HTTP branch and add radix=10:

// outside selected lines; illustrative full replacement
function extractStatusCode(error) {
  if (!error || typeof error !== 'string') return null;

  const paren = error.match(/\((\d{3})\)\s*$/);
  if (paren) return parseInt(paren[1], 10);

  const http = error.match(/HTTP\s+(\d{3})/i);
  if (http) return parseInt(http[1], 10);

  return null;
}

Also applies to: 510-510


483-487: Avoid retrying on 403; consider 408 instead

403 is typically non‑transient. Recommend removing 403 from the retryable list and optionally adding 408 (Request Timeout).

-  if ([403, 502, 503, 504].includes(statusCode)) {
+  if ([408, 502, 503, 504].includes(statusCode)) {
     return true;
   }
src/components/features/LogDisplayControl/index.jsx (1)

16-30: Improve switch accessibility: associate label and use switch semantics.

Link the visible label to the input and add role/aria for better a11y.

-      <label className="text-sm font-medium text-primary">
+      <label className="text-sm font-medium text-primary" htmlFor="toggle-detailed-logs">
         {t('showDetailedLogs')}
       </label>
       <div className="space-y-sm">
         <div className="flex items-center gap-sm">
           <label className="switch-base">
             <input
               type="checkbox"
+              id="toggle-detailed-logs"
               checked={state.showDetailedLogs}
               onChange={handleToggleDetailedLogs}
               disabled={state.isTesting}
               className="switch-input"
+              role="switch"
+              aria-checked={state.showDetailedLogs}
             />
             <span className="switch-slider"></span>
           </label>
src/components/features/Results/StatsCards.jsx (1)

11-46: Compute counts in one pass to avoid repeated filters.

Single reduce over keyResults improves readability and performance.

-  const mainStats = [
-    {
-      key: 'total',
-      value: state.keyResults.length,
-      className: 'total'
-    },
-    {
-      key: 'valid',
-      value: state.enablePaidDetection ?
-        state.keyResults.filter(k => k.status === 'valid').length :
-        state.keyResults.filter(k => k.status === 'valid' || k.status === 'paid').length,
-      className: 'valid'
-    },
-    {
-      key: 'invalid',
-      value: state.keyResults.filter(k => k.status === 'invalid').length,
-      className: 'invalid'
-    },
-    {
-      key: 'rateLimited',
-      value: state.keyResults.filter(k => k.status === 'rate-limited').length,
-      className: 'rate-limited'
-    }
-  ];
-
-  const hasPaidDetection = state.apiType === 'gemini' && state.enablePaidDetection;
-
-  if (hasPaidDetection) {
-    mainStats.push({
-      key: 'paidKeys',
-      value: state.keyResults.filter(k => k.status === 'paid').length,
-      className: 'paid'
-    });
-  }
+  const counts = state.keyResults.reduce((acc, k) => {
+    acc.total += 1;
+    acc[k.status] = (acc[k.status] || 0) + 1;
+    return acc;
+  }, { total: 0 });
+
+  const hasPaidDetection = state.apiType === 'gemini' && state.enablePaidDetection;
+  const validCount = hasPaidDetection
+    ? (counts['valid'] || 0)
+    : (counts['valid'] || 0) + (counts['paid'] || 0);
+
+  const mainStats = [
+    { key: 'total', value: counts.total, className: 'total' },
+    { key: 'valid', value: validCount, className: 'valid' },
+    { key: 'invalid', value: counts['invalid'] || 0, className: 'invalid' },
+    { key: 'rateLimited', value: counts['rate-limited'] || 0, className: 'rate-limited' },
+    ...(hasPaidDetection
+      ? [{ key: 'paidKeys', value: counts['paid'] || 0, className: 'paid' }]
+      : [])
+  ];
src/components/features/AdvancedSettings/index.jsx (1)

46-61: Add dialog semantics for the modal container.

Improve accessibility with role="dialog", aria-modal, and label association.

-      <Card 
-        variant="base" 
-        padding="none"
-        className="w-full max-w-2xl max-h-90vh overflow-y-auto m-md"
-      >
-        <div className="flex items-center justify-between p-lg border-b">
-          <h3 className="text-lg font-semibold text-primary">{t('advancedSettings')}</h3>
+      <Card 
+        variant="base" 
+        padding="none"
+        className="w-full max-w-2xl max-h-90vh overflow-y-auto m-md"
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="advancedSettingsTitle"
+      >
+        <div className="flex items-center justify-between p-lg border-b">
+          <h3 id="advancedSettingsTitle" className="text-lg font-semibold text-primary">{t('advancedSettings')}</h3>

Additionally, consider moving the inline z-index on the overlay into a CSS class for consistency.

src/components/common/Card/index.jsx (2)

27-39: Verify padding class naming consistency.

'lg' returns 'p-lg' while others use 'card-padding*'. Ensure 'p-lg' exists or align naming (e.g., 'card-padding-lg').


9-11: Drop hover prop to match “remove hover effects and transitions”.

To reflect the PR goal, remove hover prop and card-hover class.

-  hover = false,
   className = '',
   onClick,
   ...props
 }) => {
@@
-  const hoverClass = hover ? 'card-hover' : '';
   const clickableClass = onClick ? 'cursor-pointer' : '';
   
   const classes = [
     baseClass,
     paddingClass,
-    hoverClass,
     clickableClass,
     className
   ].filter(Boolean).join(' ');

Also applies to: 44-53

src/components/features/Results/VirtualizedList.jsx (2)

158-162: Use functional state update when toggling visibility.

Avoid stale closures; use prev => !prev.

-  const handleToggleKeyVisibility = (e) => {
+  const handleToggleKeyVisibility = (e) => {
     e.stopPropagation();
-    setIsKeyVisible(!isKeyVisible);
+    setIsKeyVisible(prev => !prev);
   };

216-221: Redundant keyboard handler on a real button.

Buttons already handle Enter/Space. Remove onKeyDown to prevent double-invoke.

-          <button
-            className="key-info-row key-info-clickable"
-            onClick={handleOpenLogs}
-            onKeyDown={handleKeyDown}
-            aria-label={t('openLogs') || 'Open logs for this key'}
-          >
+          <button
+            className="key-info-row key-info-clickable"
+            onClick={handleOpenLogs}
+            aria-label={t('openLogs') || 'Open logs for this key'}
+          >
src/contexts/AppStateContext.jsx (1)

504-509: Clear logs when disabling detailed logs: good. Also guard opening modal when disabled.

To keep state consistent, ignore OPEN_LOG_MODAL when showDetailedLogs is false.

Add a guard in the reducer:

// Inside appReducer, before returning in OPEN_LOG_MODAL:
case 'OPEN_LOG_MODAL':
  if (!state.showDetailedLogs) {
    return state; // ignore when detailed logs are off
  }
  return {
    ...state,
    activeLogKey: action.payload,
    isLogModalOpen: true
  };
src/components/common/Modal/index.jsx (2)

69-69: Consider renaming 'log-modal-content' to a more generic class name.

The class name log-modal-content appears to be a remnant from domain-specific usage. For a reusable Modal component, consider using a more generic name like modal-content.

Apply this diff to use a more generic class name:

-        className={`log-modal-content ${maxWidth} m-md ${className}`} 
+        className={`modal-content ${maxWidth} m-md ${className}`} 

111-116: Add optional chaining for onConfirm to prevent runtime errors.

Line 113 calls onConfirm directly without checking if it exists. If a confirm-type modal is rendered without providing onConfirm, clicking the Confirm button will throw a runtime error.

Apply this diff to add defensive optional chaining:

                   <Button
                     variant="primary"
-                    onClick={onConfirm}
+                    onClick={onConfirm ? () => onConfirm() : undefined}
                   >
                     {confirmText || t('confirm')}
                   </Button>

Note: This matches the optional chaining pattern used for onClose at lines 42 and 54.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0759c67 and 0c68747.

📒 Files selected for processing (46)
  • public/worker.js (5 hunks)
  • src/App.jsx (4 hunks)
  • src/__tests__/hooks/useVirtualization.test.js (2 hunks)
  • src/components/common/Button/index.jsx (2 hunks)
  • src/components/common/Card/index.jsx (1 hunks)
  • src/components/common/EmptyState/index.jsx (1 hunks)
  • src/components/common/FormField/index.jsx (1 hunks)
  • src/components/common/IconButton/index.jsx (1 hunks)
  • src/components/common/Modal/Modal.module.css (0 hunks)
  • src/components/common/Modal/index.jsx (3 hunks)
  • src/components/common/StatCard/index.jsx (1 hunks)
  • src/components/common/StatusBadge/index.jsx (1 hunks)
  • src/components/features/AdvancedSettings/index.jsx (3 hunks)
  • src/components/features/ApiConfig/ApiConfig.module.css (0 hunks)
  • src/components/features/ApiConfig/ModelSelector.jsx (1 hunks)
  • src/components/features/ApiConfig/ProxySettings.jsx (0 hunks)
  • src/components/features/ConcurrencyControl/ConcurrencyControl.module.css (6 hunks)
  • src/components/features/ConcurrencyControl/index.jsx (2 hunks)
  • src/components/features/Controls/index.jsx (3 hunks)
  • src/components/features/KeyInput/FileImport.jsx (2 hunks)
  • src/components/features/KeyInput/KeyInput.module.css (0 hunks)
  • src/components/features/KeyInput/PasteButton.jsx (2 hunks)
  • src/components/features/LogDisplayControl/index.jsx (1 hunks)
  • src/components/features/LogsPreview/KeyLogModal.jsx (3 hunks)
  • src/components/features/LogsPreview/LogsPreviewPanel.jsx (4 hunks)
  • src/components/features/PaidDetectionControl/PaidDetectionControl.module.css (0 hunks)
  • src/components/features/PaidDetectionControl/index.jsx (6 hunks)
  • src/components/features/PaidDetectionPrompt/PaidDetectionPrompt.module.css (0 hunks)
  • src/components/features/Results/Results.module.css (0 hunks)
  • src/components/features/Results/StatsCards.jsx (2 hunks)
  • src/components/features/Results/VirtualizedList.jsx (6 hunks)
  • src/components/features/Results/index.jsx (2 hunks)
  • src/components/features/RetryControl/RetryControl.module.css (7 hunks)
  • src/components/features/RetryControl/index.jsx (2 hunks)
  • src/components/layout/AppLayout/AppLayout.module.css (3 hunks)
  • src/components/layout/AppLayout/index.jsx (1 hunks)
  • src/components/layout/NavBar/NavBar.module.css (0 hunks)
  • src/components/layout/Sidebar/Sidebar.module.css (4 hunks)
  • src/components/layout/ThemeToggle/ThemeToggle.module.css (0 hunks)
  • src/components/layout/ThemeToggle/index.jsx (0 hunks)
  • src/contexts/AppStateContext.jsx (6 hunks)
  • src/hooks/useVirtualization.js (1 hunks)
  • src/locales/en.json (1 hunks)
  • src/locales/zh.json (1 hunks)
  • src/styles/components.css (4 hunks)
  • src/styles/globals.css (6 hunks)
💤 Files with no reviewable changes (10)
  • src/components/features/ApiConfig/ProxySettings.jsx
  • src/components/features/ApiConfig/ApiConfig.module.css
  • src/components/common/Modal/Modal.module.css
  • src/components/features/PaidDetectionControl/PaidDetectionControl.module.css
  • src/components/features/KeyInput/KeyInput.module.css
  • src/components/layout/NavBar/NavBar.module.css
  • src/components/layout/ThemeToggle/index.jsx
  • src/components/features/PaidDetectionPrompt/PaidDetectionPrompt.module.css
  • src/components/features/Results/Results.module.css
  • src/components/layout/ThemeToggle/ThemeToggle.module.css
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/components/common/EmptyState/index.jsx
  • src/components/features/ApiConfig/ModelSelector.jsx
  • src/components/layout/AppLayout/index.jsx
  • src/App.jsx
  • src/components/common/StatCard/index.jsx
  • src/components/common/StatusBadge/index.jsx
  • src/components/features/RetryControl/index.jsx
  • src/locales/en.json
  • src/components/common/Button/index.jsx
  • src/components/features/PaidDetectionControl/index.jsx
  • src/components/common/IconButton/index.jsx
  • src/components/features/KeyInput/FileImport.jsx
  • src/components/features/LogsPreview/LogsPreviewPanel.jsx
🧰 Additional context used
🧬 Code graph analysis (13)
src/components/features/LogDisplayControl/index.jsx (2)
src/contexts/AppStateContext.jsx (3)
  • useAppState (524-530)
  • useAppState (524-530)
  • state (370-370)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/features/ConcurrencyControl/index.jsx (2)
src/components/common/FormField/index.jsx (1)
  • FormField (3-48)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/features/Results/index.jsx (5)
src/components/features/Results/StatsCards.jsx (1)
  • StatsCards (6-97)
src/components/features/Results/ResultTabs.jsx (1)
  • ResultTabs (5-39)
src/components/features/Results/VirtualizedList.jsx (2)
  • VirtualizedList (247-318)
  • EmptyState (240-245)
src/components/common/EmptyState/index.jsx (1)
  • EmptyState (3-39)
src/components/features/Results/CopyButtons.jsx (1)
  • CopyButtons (6-148)
src/components/features/KeyInput/PasteButton.jsx (2)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/contexts/AppStateContext.jsx (1)
  • state (370-370)
src/components/features/LogsPreview/KeyLogModal.jsx (5)
src/contexts/AppStateContext.jsx (1)
  • state (370-370)
src/components/common/Modal/index.jsx (1)
  • Modal (7-135)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/EmptyState/index.jsx (1)
  • EmptyState (3-39)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/features/Results/StatsCards.jsx (2)
src/components/common/StatCard/index.jsx (1)
  • StatCard (3-36)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/features/AdvancedSettings/index.jsx (3)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/common/IconButton/index.jsx (1)
  • IconButton (4-27)
src/components/features/LogDisplayControl/index.jsx (1)
  • LogDisplayControl (5-41)
src/components/common/Modal/index.jsx (5)
src/components/features/AdvancedSettings/index.jsx (1)
  • handleOverlayClick (38-42)
src/components/common/Card/index.jsx (1)
  • Card (3-70)
src/components/common/IconButton/index.jsx (1)
  • IconButton (4-27)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/Button/index.jsx (1)
  • Button (3-40)
src/components/common/Card/index.jsx (5)
src/components/common/Button/index.jsx (2)
  • baseClass (14-14)
  • classes (21-23)
src/components/common/FormField/index.jsx (2)
  • baseClass (58-58)
  • classes (64-66)
src/components/common/StatCard/index.jsx (2)
  • baseClass (12-12)
  • classes (14-16)
src/components/common/StatusBadge/index.jsx (2)
  • baseClass (22-22)
  • classes (27-29)
src/components/common/EmptyState/index.jsx (1)
  • classes (15-17)
src/components/features/Controls/index.jsx (1)
src/components/common/Button/index.jsx (1)
  • Button (3-40)
src/components/common/FormField/index.jsx (2)
src/components/common/Button/index.jsx (4)
  • baseClass (14-14)
  • variantClass (15-18)
  • sizeClass (19-20)
  • classes (21-23)
src/components/common/Card/index.jsx (2)
  • baseClass (42-42)
  • classes (47-53)
src/__tests__/hooks/useVirtualization.test.js (2)
src/components/features/Results/VirtualizedList.jsx (1)
  • useVirtualization (250-250)
src/hooks/useVirtualization.js (2)
  • useVirtualization (3-19)
  • useVirtualization (3-19)
src/components/features/Results/VirtualizedList.jsx (4)
src/components/features/BalanceDisplay/KeyBalanceDisplay.jsx (3)
  • useLanguage (7-7)
  • useAppState (8-8)
  • KeyBalanceDisplay (6-77)
src/contexts/LanguageContext.jsx (1)
  • t (81-121)
src/components/common/StatusBadge/index.jsx (1)
  • getStatusClass (10-20)
src/hooks/useVirtualization.js (1)
  • getItemHeight (4-13)
🔇 Additional comments (21)
src/components/features/Controls/index.jsx (1)

8-8: LGTM!

The import is correct and supports the refactoring to use the shared Button component.

src/components/layout/Sidebar/Sidebar.module.css (1)

200-205: Reduced-motion override restored — thanks. Instant collapses without animation now respect users’ motion preferences.

src/components/layout/AppLayout/AppLayout.module.css (1)

41-65: LGTM! Flex-based layout correctly implements equal-height columns.

The transition from grid to flex is well-executed. The align-items: stretch combined with flex: 1 on the columns ensures equal heights, and the responsive breakpoint correctly switches to a vertical stack on smaller screens.

src/locales/zh.json (1)

206-209: LGTM! New localization keys support detailed logs feature.

The added keys showDetailedLogs and showDetailedLogsDescription properly support the new log display control feature introduced in this PR.

src/components/common/FormField/index.jsx (1)

1-92: LGTM! Well-implemented accessible form field component.

The FormField component correctly implements accessibility features:

  • Stable ID generation via useRef prevents regeneration on each render
  • Proper aria-invalid and aria-describedby applied to form controls
  • Error and help blocks use stable IDs for ARIA association
  • Input subcomponent uses forwardRef for proper ref forwarding
src/components/features/ConcurrencyControl/index.jsx (1)

27-59: LGTM! FormField integration and accessibility improvements are well-executed.

The component correctly wraps controls in FormField and adds proper ARIA labels to both the slider and numeric input for screen reader support.

src/components/features/ConcurrencyControl/ConcurrencyControl.module.css (1)

1-242: LGTM! Comprehensive theming refactor with CSS variables.

The file has been successfully migrated to use CSS variables throughout, replacing hard-coded colors with theme-aware tokens like var(--bg-card), var(--border-color), and var(--bg-input). Dark theme adaptations are properly implemented.

src/styles/components.css (2)

119-124: LGTM! Textarea styling correctly scoped.

The .form-field.textarea selector properly applies special styling only to textarea-type form fields.


351-453: LGTM! New card variants and settings button properly implemented.

The new card variants (.card--primary, .card--info, .card--results, .card--empty-state) and .btn--settings are well-structured with proper dark theme adaptations.

src/styles/globals.css (2)

406-541: LGTM! Semi Design-inspired input/button components well-implemented.

The semi-input-wrapper and semi-button components are properly styled with focus states, hover effects, and size variants that align with the Semi Design aesthetic.


625-659: LGTM! Scrollbar accessibility addressed with gradient fade indicator.

The capsule scrolling now includes a right-edge gradient fade to indicate overflow while hiding the scrollbar, addressing the previous accessibility concern about hidden scrollbars.

src/components/features/LogDisplayControl/index.jsx (1)

9-12: Reducer action and translation keys validated - SET_SHOW_DETAILED_LOGS is implemented and all related i18n keys (showDetailedLogs, showDetailedLogsDescription, enabled, disabled) are defined.

src/components/features/Results/StatsCards.jsx (1)

73-91: All translation keys are defined. The keys total, valid, invalid, rateLimited, paidKeys, testing, and retrying exist in both src/locales/en.json and zh.json.

src/__tests__/hooks/useVirtualization.test.js (1)

9-15: Solid coverage and isolation of matchMedia mocks.

Scenarios and restoration look correct; tests align with the new no-arg API. LGTM.

Also applies to: 32-49, 51-68, 70-87, 89-111, 113-122, 124-145

src/components/features/LogsPreview/KeyLogModal.jsx (1)

179-191: Modal usage and title rendering look correct.

Fragment title avoids invalid h3 nesting; maxWidth/className wiring is fine.

src/components/features/Results/VirtualizedList.jsx (1)

274-277: LGTM: itemSize delegates to viewport-based height.

Matches the simplified hook API.

src/contexts/AppStateContext.jsx (4)

111-113: Add showDetailedLogs to initial state: good.

Defaults to false with localStorage hydration.


208-213: Reducer action for showDetailedLogs is correct.

State update is straightforward.


419-425: Persistence wiring includes showDetailedLogs: good.

Saved with other global settings.


492-503: Collector enablement tied to showDetailedLogs: good.

Effect dependency is correct; hydration guarded.

src/components/common/Modal/index.jsx (1)

73-95: Title rendering implementation correctly addresses past feedback.

The type check typeof title === 'string' properly handles both string and React element titles, resolving the invalid HTML concern raised in the previous review. The implementation maintains accessibility with appropriate heading semantics for strings while allowing flexible custom content.

Comment on lines +61 to +66
{(title || action) && (
<div className="card-header flex items-center justify-between mb-4">
{title && <h3 className="card-title text-lg font-semibold">{title}</h3>}
{action}
</div>
)}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid invalid HTML by not wrapping non-string titles in h3.

Render string titles inside h3; render React nodes as-is.

-      {(title || action) && (
-        <div className="card-header flex items-center justify-between mb-4">
-          {title && <h3 className="card-title text-lg font-semibold">{title}</h3>}
-          {action}
-        </div>
-      )}
+      {(title || action) && (
+        <div className="card-header flex items-center justify-between mb-4">
+          {title && (
+            typeof title === 'string'
+              ? <h3 className="card-title text-lg font-semibold">{title}</h3>
+              : title
+          )}
+          {action}
+        </div>
+      )}
📝 Committable suggestion

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

Suggested change
{(title || action) && (
<div className="card-header flex items-center justify-between mb-4">
{title && <h3 className="card-title text-lg font-semibold">{title}</h3>}
{action}
</div>
)}
{(title || action) && (
<div className="card-header flex items-center justify-between mb-4">
{title && (
typeof title === 'string'
? <h3 className="card-title text-lg font-semibold">{title}</h3>
: title
)}
{action}
</div>
)}
🤖 Prompt for AI Agents
In src/components/common/Card/index.jsx around lines 61 to 66, the current code
always wraps title in an <h3>, which can produce invalid HTML when title is a
React node; change the render so that if title is a string (typeof title ===
"string" or use React.isValidElement to detect nodes) you output the <h3
className="card-title...">{title}</h3>, otherwise render the title value
directly (no h3 wrapper). Keep the surrounding header markup and ensure
null/undefined titles are still skipped.

max="100"
onChange={handleInputChange}
disabled={state.isTesting}
aria-label={t('concurrencyControl') + ' ' + (t('value') || 'value')}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify translation key 'value' exists.

Line 54 constructs an aria-label using t('value'). Ensure this key exists in the localization files (zh.json and en.json) to avoid displaying a raw key name.

Run the following script to check if the 'value' translation key exists:


🏁 Script executed:

#!/bin/bash
# Check if 'value' key exists in localization files
rg -n '"value"' src/locales/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# List locale JSON files and check for 'value' key
find src/locales -type f -name '*.json' -print -exec grep -n '"value"' {} +

Length of output: 39


Add missing translation key ‘value’
The key 'value' isn’t defined in src/locales/en.json or src/locales/zh.json, so t('value') will fall back to the raw key. Add appropriate entries for both locales.

🤖 Prompt for AI Agents
In src/components/features/ConcurrencyControl/index.jsx around line 54, the call
t('value') references a missing translation key; add a "value" entry to both
locale files (src/locales/en.json and src/locales/zh.json) — e.g. add "value":
"value" to en.json and "value": "值" (or the preferred Chinese term) to zh.json —
then save and reload the app or rebuild translations so t('value') returns the
localized string instead of the raw key.

Comment on lines 21 to +62
<>
{(state.showResults || state.isTesting || state.keyResults.length > 0) ? (
<>
<StatsCards />

<div className="function-card results-card">
{state.showLogTooltip && (
<div className="results-tooltip" role="note">
<div className="results-tooltip__text">{t('results.logTooltip')}</div>
<button
type="button"
className="results-tooltip__close"
onClick={handleTooltipDismiss}
aria-label={t('close') || 'Close'}
>
&times;
</button>
</div>
)}
<ResultTabs />
<div className="results-content">
<VirtualizedList />
</div>
<CopyButtons />
</div>
</>
) : (
<div className="function-card initial-empty-state-card">
<div className="empty-state">
<div className="empty-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
</div>
<div className="empty-text">{t('resultsWillShow')}</div>
{/* 检测结果统计 - 始终显示,但在没有结果时显示0 */}
<StatsCards />

{/* 密钥列表区域 - 始终显示结果卡片 */}
<div className="card--primary card--results">
{state.showLogTooltip && (
<div className="results-tooltip" role="note">
<div className="results-tooltip__text">{t('results.logTooltip')}</div>
<button
type="button"
className="results-tooltip__close"
onClick={handleTooltipDismiss}
aria-label={t('close') || 'Close'}
>
&times;
</button>
</div>
)}

{/* Tab列表始终显示 */}
<ResultTabs />

<div className="results-content">
{(state.showResults || state.isTesting || state.keyResults.length > 0) ? (
<VirtualizedList />
) : (
<EmptyState
icon={
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
}
message={t('resultsWillShow')}
/>
)}
</div>
)}

{/* 复制按钮始终显示在底部 */}
<CopyButtons />
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Results layout consolidation looks good.

Consistent EmptyState use and always-visible sections improve UX. No functional issues spotted.

Optionally confirm translations exist for results.logTooltip and resultsWillShow:


🏁 Script executed:

#!/bin/bash
rg -n "results\\.logTooltip|resultsWillShow" -S src/locales || true

Length of output: 147


Add missing translation for results.logTooltip
The key results.logTooltip isn’t defined in your locale files—add it to src/locales/zh.json and en.json alongside resultsWillShow.

🤖 Prompt for AI Agents
In src/components/features/Results/index.jsx around lines 21 to 62, the UI
references a missing translation key results.logTooltip; add this key to both
locale files (src/locales/en.json and src/locales/zh.json) alongside the
existing resultsWillShow entry, providing appropriate English and Chinese
strings (e.g., English: "Hover to view log details" and Chinese: "将鼠标悬停以查看日志详情"
or your preferred copy), then run the localization/test script to ensure the new
key is picked up and no i18n warnings remain.

Comment on lines +4 to 13
const getItemHeight = useCallback(() => {
// 根据屏幕大小返回固定高度
if (window.matchMedia && window.matchMedia('(max-width: 480px)').matches) {
return 62; // 超小屏幕:60px + wrapper padding(2px)
} else if (window.matchMedia && window.matchMedia('(max-width: 768px)').matches) {
return 66; // 移动端:64px + wrapper padding(2px)
} else {
return 74; // 桌面端:72px + wrapper padding(2px)
}

// 如果有重试信息,增加一行
if (keyData.retryCount > 0) {
baseHeight += lineHeight;
}

// 如果有状态信息(有效密钥信息),增加一行
if (keyData.status === 'valid' || keyData.status === 'paid') {
baseHeight += lineHeight;
}

// 如果是硅基流动且是有效密钥,为余额显示增加额外高度
if (apiType === 'siliconcloud' && (keyData.status === 'valid' || keyData.status === 'paid')) {
baseHeight += lineHeight; // 为余额显示增加一行高度
}

return Math.max(baseHeight, 68); // 设置最小高度为68px
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against undefined window to avoid SSR/test crashes.

Accessing window.matchMedia can throw outside the browser.

-  const getItemHeight = useCallback(() => {
-    // 根据屏幕大小返回固定高度
-    if (window.matchMedia && window.matchMedia('(max-width: 480px)').matches) {
+  const getItemHeight = useCallback(() => {
+    // 根据屏幕大小返回固定高度(在无 window 环境下安全回退)
+    const canMatch = typeof window !== 'undefined' && typeof window.matchMedia === 'function';
+    if (canMatch && window.matchMedia('(max-width: 480px)').matches) {
       return 62; // 超小屏幕:60px + wrapper padding(2px)
-    } else if (window.matchMedia && window.matchMedia('(max-width: 768px)').matches) {
+    } else if (canMatch && window.matchMedia('(max-width: 768px)').matches) {
       return 66; // 移动端:64px + wrapper padding(2px)
     } else {
       return 74; // 桌面端:72px + wrapper padding(2px)
     }
   }, []);

Optional: re-compute/reset list on viewport change (matchMedia 'change' or resize) to keep item heights in sync.

📝 Committable suggestion

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

Suggested change
const getItemHeight = useCallback(() => {
// 根据屏幕大小返回固定高度
if (window.matchMedia && window.matchMedia('(max-width: 480px)').matches) {
return 62; // 超小屏幕:60px + wrapper padding(2px)
} else if (window.matchMedia && window.matchMedia('(max-width: 768px)').matches) {
return 66; // 移动端:64px + wrapper padding(2px)
} else {
return 74; // 桌面端:72px + wrapper padding(2px)
}
// 如果有重试信息,增加一行
if (keyData.retryCount > 0) {
baseHeight += lineHeight;
}
// 如果有状态信息(有效密钥信息),增加一行
if (keyData.status === 'valid' || keyData.status === 'paid') {
baseHeight += lineHeight;
}
// 如果是硅基流动且是有效密钥,为余额显示增加额外高度
if (apiType === 'siliconcloud' && (keyData.status === 'valid' || keyData.status === 'paid')) {
baseHeight += lineHeight; // 为余额显示增加一行高度
}
return Math.max(baseHeight, 68); // 设置最小高度为68px
}, []);
const getItemHeight = useCallback(() => {
// 根据屏幕大小返回固定高度(在无 window 环境下安全回退)
const canMatch = typeof window !== 'undefined' && typeof window.matchMedia === 'function';
if (canMatch && window.matchMedia('(max-width: 480px)').matches) {
return 62; // 超小屏幕:60px + wrapper padding(2px)
} else if (canMatch && window.matchMedia('(max-width: 768px)').matches) {
return 66; // 移动端:64px + wrapper padding(2px)
} else {
return 74; // 桌面端:72px + wrapper padding(2px)
}
}, []);
🤖 Prompt for AI Agents
In src/hooks/useVirtualization.js around lines 4 to 13, the useCallback directly
calls window.matchMedia which will throw in SSR/test environments; guard against
undefined window by checking typeof window !== 'undefined' before accessing
matchMedia and return a sensible default height when window is unavailable. Also
wire up (optionally) a matchMedia 'change' listener or window resize listener to
recompute/reset the list item height when the viewport changes, update the
stored height via state or ref, and ensure you remove the listener in cleanup to
avoid leaks.

Comment on lines +295 to +313
/* 新的横向布局密钥项 */
.key-item-horizontal {
display: flex;
align-items: center;
padding: 16px 20px;
background: var(--bg-input);
/* 使用主题变量 */
border: 2px solid var(--border-color);
border-radius: 12px;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 1px;
height: 72px;
width: 100%;
box-sizing: border-box;
cursor: pointer;
transition: none;
/* 移除过渡效果 */
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid hard-coding card height.

Line 307 sets a fixed height: 72px which will clip longer status texts, error messages, or multi-line metadata. Use min-height instead to allow the card to expand with its content.

Apply this diff:

 .key-item-horizontal {
   display: flex;
   align-items: center;
   padding: 16px 20px;
   background: var(--bg-input);
   border: 2px solid var(--border-color);
   border-radius: 12px;
   gap: 16px;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
   margin-bottom: 1px;
-  height: 72px;
+  min-height: 72px;
   width: 100%;
   box-sizing: border-box;
   cursor: pointer;
   transition: none;
 }
📝 Committable suggestion

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

Suggested change
/* 新的横向布局密钥项 */
.key-item-horizontal {
display: flex;
align-items: center;
padding: 16px 20px;
background: var(--bg-input);
/* 使用主题变量 */
border: 2px solid var(--border-color);
border-radius: 12px;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 1px;
height: 72px;
width: 100%;
box-sizing: border-box;
cursor: pointer;
transition: none;
/* 移除过渡效果 */
}
/* 新的横向布局密钥项 */
.key-item-horizontal {
display: flex;
align-items: center;
padding: 16px 20px;
background: var(--bg-input);
border: 2px solid var(--border-color);
border-radius: 12px;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 1px;
min-height: 72px;
width: 100%;
box-sizing: border-box;
cursor: pointer;
transition: none;
}
🤖 Prompt for AI Agents
In src/styles/globals.css around lines 295 to 313, the .key-item-horizontal rule
currently uses a fixed height: 72px which can clip multi-line content; replace
the fixed height with min-height: 72px (and remove height) so the card keeps the
intended baseline size but can expand for longer text, keeping display:flex and
align-items as-is (adjust vertical alignment to start if needed for multiline
layouts).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants