🤓feat: the model management module - #1452
Conversation
* Re-architected model-pricing page into modular components: * PricingPage / PricingSidebar / PricingContent * Removed obsolete `ModelPricing*` components and column defs * Introduced reusable `SelectableButtonGroup` in `common/ui` * Supports Row/Col grid (3 per row) * Optional collapsible mode with gradient mask & toggle * Rebuilt filter panels with the new button-group: * Model categories, token groups, and quota types * Added dynamic `tagCount` badges to display item totals * Extended `useModelPricingData` hook * Added `filterGroup` and `filterQuotaType` state and logic * Updated PricingTable columns & sidebar reset logic to respect new states * Ensured backward compatibility via re-export in `index.jsx` * Polished styling, icons and i18n keys
…oup, fixed column, scroll tweaks (#1365) • SelectableButtonGroup • Added optional collapsible support with gradient mask & toggle • Dynamic tagCount badge support for groups / quota types • Switched to responsive Row/Col (`xs 24`, `sm 24`, `lg 12`, `xl 8`) for fluid layout • Shows expand button only when item count exceeds visible rows • Sidebar filters • PricingGroups & PricingQuotaTypes now pass tag counts to button-group • Counts derived from current models & quota_type • PricingTableColumns • Moved “Availability” column to far right; fixed via `fixed: 'right'` • Re-ordered columns and preserved ratio / price logic • PricingTable • Added `compactMode` prop; strips fixed columns and sets `scroll={compactMode ? undefined : { x: 'max-content' }}` • Processes columns to remove `fixed` in compact mode • PricingPage & index.css • Added `.pricing-scroll-hide` utility to hide Y-axis scrollbar for `Sider` & `Content` • Responsive / style refinements • Sidebar width adjusted to 460px • Scrollbars hidden uniformly across pricing modules These changes complete the model-pricing UI refactor, ensuring clean scrolling, responsive filters, and fixed availability column for better usability.
…1365) * Added `position: sticky; top: 0; z-index: 5;` to search bar container – keeps the bar fixed while the table body scrolls * Preserves previous padding, border and background styles * Improves usability by ensuring quick access to search & actions during long list navigation • PricingTable • Added `compactMode` prop; strips fixed columns and sets `scroll={compactMode ? undefined : { x: 'max-content' }}` • Processes columns to remove `fixed` in compact mode • PricingPage & index.css • Added `.pricing-scroll-hide` utility to hide Y-axis scrollbar for `Sider` & `Content` • Responsive / style refinements • Sidebar width adjusted to 460px • Scrollbars hidden uniformly across pricing modules These changes complete the model-pricing UI refactor, ensuring clean scrolling, responsive filters, and fixed availability column for better usability.
…e layout (#1365) * **PricingDisplaySettings.jsx** • Extracted display settings (recharge price, currency, ratio toggle) from PricingSidebar • Maintains complete styling and functionality as standalone component * **SelectableButtonGroup.jsx** • Added isMobile detection with conditional Col spans • Mobile: `span={12}` (2 buttons per row) for better touch experience • Desktop: preserved responsive grid `xs={24} sm={24} md={24} lg={12} xl={8}` * **PricingSidebar.jsx** • Updated imports to use new PricingDisplaySettings component • Simplified component structure while preserving reset logic These changes enhance code modularity and provide optimized mobile UX for filter button groups across the pricing interface.
… eliminate duplication (#1365) Centralize filter-reset logic to improve maintainability and consistency. - Add `resetPricingFilters` helper to `web/src/helpers/utils.js`, encapsulating all reset actions (search, category, currency, ratio, group, quota type, etc.). - Update `PricingFilterModal.jsx` and `PricingSidebar.jsx` to import and use the new utility instead of keeping their own duplicate `handleResetFilters`. - Removes repeated code, ensures future changes to reset behavior require modification in only one place, and keeps components lean.
…eColumns (#1365) Summary • Swapped out the old availability UI for clearer icon-based feedback. • Users now see a green check icon when their group can use a model and a red × icon (with tooltip) when it cannot. Details 1. Imports • Removed deprecated `IconVerify`. • Added `IconCheckCircleStroked` ✅ and `IconClose` ❌ for new states. 2. Availability column • `renderAvailable` now – Shows a green `IconCheckCircleStroked` inside a popover (“Your group can use this model”). – Shows a red `IconClose` inside a popover (“你的分组无权使用该模型”) when the model is inaccessible. – Eliminates the empty cell/grey tag fallback. 3. Group tag • Updated selected-group tag to use `IconCheckCircleStroked` for visual consistency. Result Improves UX by providing explicit visual cues for model availability and removes ambiguous blank cells.
…tor pricing display settings (#1365) - Add withCheckbox prop to SelectableButtonGroup component for checkbox-prefixed buttons - Support both single value and array activeValue for multi-selection scenarios - Refactor PricingDisplaySettings to use consistent SelectableButtonGroup styling - Replace Switch components with checkbox-enabled SelectableButtonGroup - Replace Select dropdown with SelectableButtonGroup for currency selection - Maintain unified UI/UX across all pricing filter components - Add proper JSDoc documentation for new withCheckbox functionality This improves visual consistency and provides a more cohesive user experience in the model pricing filter interface.
…nent (#1365) Add comprehensive loading state support with skeleton animations for the SelectableButtonGroup component, improving user experience during data loading. Key Changes: - Add loading prop to SelectableButtonGroup with minimum 500ms display duration - Implement skeleton buttons with proper Semi-UI Skeleton wrapper and active animation - Use fixed skeleton count (6 items) to prevent visual jumping during load transitions - Pass loading state through all pricing filter components hierarchy: - PricingSidebar and PricingFilterModal as container components - PricingDisplaySettings, PricingCategories, PricingGroups, PricingQuotaTypes as filter components Technical Details: - Reference CardTable.js implementation for consistent skeleton UI patterns - Add useEffect hook for 500ms minimum loading duration control - Support both checkbox and regular button skeleton modes - Maintain responsive layout compatibility (mobile/desktop) - Add proper JSDoc parameter documentation for loading prop Fixes: - Prevent skeleton count sudden changes that caused visual discontinuity - Ensure proper skeleton animation with Semi-UI active parameter - Maintain consistent loading experience across all filter components
) Filter out the special empty string group ("": "用户分组") from the usable groups in PricingGroups component. This empty group represents "user's current group" but contains no data and should not be displayed in the group filter options. - Add filter condition to exclude empty string keys from usableGroup - Prevents displaying invalid empty group option in UI - Improves user experience by showing only valid selectable groups
…ure (#1365) - Replace model count with group ratio display (x2.2, x1) in group filter - Remove redundant "Available Groups" column from pricing table - Remove "Availability" column and related logic completely - Move "Supported Endpoint Types" column to fixed right position - Clean up unused parameters and variables in PricingTableColumns.js - Optimize variable declarations (let → const) and simplify render logic - Improve code readability and reduce memory allocations This refactor enhances user experience by: - Providing clearer group ratio information in filters - Simplifying table layout while maintaining essential functionality - Improving performance through better code organization Breaking changes: None
#1365) - Increase skeleton card count from 6 to 10 for better visual coverage - Extend minimum skeleton display duration from 500ms to 1000ms for smoother UX - Add circle shape to all pricing tags for consistent rounded design - Apply circle styling to billing type, popularity, endpoint, and context tags This commit improves the visual consistency and user experience of the pricing card view by standardizing tag appearance and optimizing skeleton loading timing.
- Create PricingEndpointTypes.jsx component for endpoint type filtering - Add filterEndpointType state management in useModelPricingData hook - Integrate endpoint type filtering logic in filteredModels computation - Update PricingSidebar.jsx to include endpoint type filter component - Update PricingFilterModal.jsx to support endpoint type filtering on mobile - Extend resetPricingFilters utility function to include endpoint type reset - Support filtering models by endpoint types (OpenAI, Anthropic, Gemini, etc.) - Display model count for each endpoint type with localized labels - Ensure filter state resets to first page when endpoint type changes This enhancement allows users to filter models by their supported endpoint types, providing more granular control over model selection in the pricing interface.
- Remove K/M switch from model price column header in pricing table - Add "Display in K units" option to pricing display settings panel - Update parameter passing for tokenUnit and setTokenUnit across components: - PricingDisplaySettings: Add tokenUnit toggle functionality - PricingSidebar: Pass tokenUnit props to display settings - PricingFilterModal: Include tokenUnit in mobile filter modal - Enhance resetPricingFilters utility to reset token unit to default 'M' - Clean up PricingTableColumns by removing unused setTokenUnit parameter - Add English translation for "按K显示单位" as "Display in K units" This change improves UX by consolidating all display-related controls in the filter settings panel, making the interface more organized and the token unit setting more discoverable alongside other display options. Affected components: - PricingTableColumns.js - PricingDisplaySettings.jsx - PricingSidebar.jsx - PricingFilterModal.jsx - PricingTable.jsx - utils.js (resetPricingFilters) - en.json (translations)
…maintainability (#1365) - Extract default values to DEFAULT_PRICING_FILTERS constant for centralized configuration - Replace verbose type checks with optional chaining operator (?.) for cleaner code - Eliminate redundant function type validations and comments - Reduce code lines by ~50% (from 60 to 25 lines) while maintaining full functionality - Improve code readability and follow modern JavaScript best practices This refactoring enhances code quality without changing the function's behavior, making it easier to maintain and modify default filter values in the future.
…maintainability (#1365) - Extract default values to DEFAULT_PRICING_FILTERS constant for centralized configuration - Replace verbose type checks with optional chaining operator (?.) for cleaner code - Eliminate redundant function type validations and comments - Reduce code lines by ~50% (from 60 to 25 lines) while maintaining full functionality - Improve code readability and follow modern JavaScript best practices This refactoring enhances code quality without changing the function's behavior, making it easier to maintain and modify default filter values in the future.
Optimize grid column breakpoints to account for 460px sidebar width: - Change from sm:grid-cols-2 lg:grid-cols-3 to xl:grid-cols-2 2xl:grid-cols-3 - Ensures adequate space for card display after subtracting sidebar width - Improves layout on medium-sized screens where previous breakpoints caused cramped display Breakpoint calculation: - 1280px screen - 460px sidebar = 820px → 2 columns - 1536px screen - 460px sidebar = 1076px → 3 columns
…stency - **Fix SideSheet double-click issue**: Remove early return for null modelData to prevent rendering blockage during async state updates - **Component modularization**: - Split ModelDetailSideSheet into focused sub-components (ModelHeader, ModelBasicInfo, ModelEndpoints, ModelPricingTable) - Refactor PricingFilterModal with FilterModalContent and FilterModalFooter components - Remove unnecessary FilterSection wrapper for cleaner interface - **Improve visual consistency**: - Unify avatar/icon logic between ModelHeader and PricingCardView components - Standardize tag colors across all pricing components (violet/teal for billing types) - Apply consistent dashed border styling using Semi UI theme colors - **Enhance data accuracy**: - Display raw endpoint type names (e.g., "openai", "anthropic") instead of translated descriptions - Remove text alignment classes for better responsive layout - Add proper null checks to prevent runtime errors - **Code quality improvements**: - Reduce component complexity by 52-74% through modularization - Improve maintainability with single responsibility principle - Add comprehensive error handling for edge cases This refactoring improves component reusability, reduces bundle size, and provides a more consistent user experience across the model pricing interface.
This commit introduces a unified, maintainable solution for all model-pricing filter buttons and removes redundant code.
Key points
• Added `usePricingFilterCounts` hook
- Centralises filtering logic and returns:
- `quotaTypeModels`, `endpointTypeModels`, `dynamicCategoryCounts`, `groupCountModels`
- Keeps internal helpers private (removed public `modelsAfterCategory`).
• Updated components to consume the new hook
- `PricingSidebar.jsx`
- `FilterModalContent.jsx`
• Improved button UI/UX
- `SelectableButtonGroup.jsx` now respects `item.disabled` and auto-disables when `tagCount === 0`.
- `PricingGroups.jsx` counts models per group (after all other filters) and disables groups with zero matches.
- `PricingEndpointTypes.jsx` enumerates all endpoint types, computes filtered counts, and disables entries with zero matches.
• Removed obsolete / duplicate calculations and comments to keep components lean.
The result is consistent, real-time tag counts across all filter groups, automatic disabling of unavailable options, and a single source of truth for filter computations, making future extensions straightforward.
|
Warning Rate limit exceeded@t0ng7u has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 12 minutes and 8 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
WalkthroughThis update introduces a comprehensive model and vendor management system to both the frontend and backend. On the backend, new RESTful APIs and database models are added for managing models, vendors, and prefill groups, along with associated metadata and advanced querying. The frontend implements a full-featured UI for model and vendor management, model pricing, filtering, editing, and batch operations, including reusable UI components and hooks. Supporting utilities, hooks, and styles are also added or refactored for consistency and maintainability. Changes
Sequence Diagram(s)Model Management: Create/Edit FlowsequenceDiagram
participant AdminUser
participant ModelsPage
participant EditModelModal
participant BackendAPI
participant DB
AdminUser->>ModelsPage: Click "Add/Edit Model"
ModelsPage->>EditModelModal: Open modal
AdminUser->>EditModelModal: Fill form & submit
EditModelModal->>BackendAPI: POST/PUT /api/models
BackendAPI->>DB: Create/Update Model
DB-->>BackendAPI: Success/Error
BackendAPI-->>EditModelModal: Response
EditModelModal-->>ModelsPage: Close modal, refresh list
Model Pricing: Filtering and Detail ModalsequenceDiagram
participant User
participant PricingPage
participant PricingSidebar
participant PricingContent
participant BackendAPI
User->>PricingSidebar: Set filters (group, vendor, etc.)
PricingSidebar->>PricingPage: Update filter state
PricingPage->>PricingContent: Pass filtered data
User->>PricingContent: Click model card/table row
PricingContent->>PricingPage: openModelDetail(model)
PricingPage->>ModelDetailSideSheet: Show model details
ModelDetailSideSheet->>BackendAPI: (if needed) Fetch more info
Estimated code review effort🎯 5 (Critical) | ⏱️ ~90+ minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (24)
web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx (1)
26-26: Add PropTypes for better type safety.The component is missing prop validation which would help catch bugs and improve maintainability.
Add PropTypes at the end of the file:
+import PropTypes from 'prop-types'; // ... existing code ... +ModelEndpoints.propTypes = { + modelData: PropTypes.shape({ + supported_endpoint_types: PropTypes.arrayOf(PropTypes.string) + }), + t: PropTypes.func.isRequired +}; + export default ModelEndpoints;web/src/components/table/model-pricing/filter/PricingQuotaTypes.jsx (1)
23-50: LGTM! Well-documented component with proper filtering logic.The component correctly implements quota type filtering with clear JSDoc documentation. The
qtyCountfunction properly filters models based onquota_typevalues.Consider extracting the hardcoded quota type definitions to constants for better maintainability:
+const QUOTA_TYPES = { + ALL: 'all', + PAY_AS_YOU_GO: 0, + PAY_PER_USE: 1, +}; const items = [ - { value: 'all', label: t('全部类型'), tagCount: qtyCount('all') }, - { value: 0, label: t('按量计费'), tagCount: qtyCount(0) }, - { value: 1, label: t('按次计费'), tagCount: qtyCount(1) }, + { value: QUOTA_TYPES.ALL, label: t('全部类型'), tagCount: qtyCount(QUOTA_TYPES.ALL) }, + { value: QUOTA_TYPES.PAY_AS_YOU_GO, label: t('按量计费'), tagCount: qtyCount(QUOTA_TYPES.PAY_AS_YOU_GO) }, + { value: QUOTA_TYPES.PAY_PER_USE, label: t('按次计费'), tagCount: qtyCount(QUOTA_TYPES.PAY_PER_USE) }, ];web/src/components/table/model-pricing/layout/PricingPage.jsx (1)
28-40: LGTM! Clean component structure with good separation of concerns.The component properly uses hooks for data management and responsive behavior. The local state management for UI options is well-placed.
Consider making prop passing more explicit to improve debugging and maintainability:
- const allProps = { - ...pricingData, - showRatio, - setShowRatio, - viewMode, - setViewMode - }; + const sidebarProps = { + ...pricingData, + showRatio, + setShowRatio, + viewMode, + setViewMode, + }; + + const contentProps = { + ...pricingData, + showRatio, + viewMode, + };web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx (1)
28-66: LGTM! Well-organized component composition with proper hook integration.The component effectively centralizes filter modal content with clean prop destructuring and proper integration of the
usePricingFilterCountshook for dynamic filtering.Consider grouping related props to improve readability:
const FilterModalContent = ({ sidebarProps, t }) => { const { + // Display settings showWithRecharge, setShowWithRecharge, currency, setCurrency, showRatio, setShowRatio, viewMode, setViewMode, tokenUnit, setTokenUnit, + // Filter states handleChange, setActiveKey, filterGroup, setFilterGroup, filterQuotaType, setFilterQuotaType, filterEndpointType, setFilterEndpointType, + // Other props loading, ...categoryProps } = sidebarProps;web/src/components/table/model-pricing/layout/header/PricingCategoryIntroSkeleton.jsx (1)
70-72: Consider removing redundant Skeleton wrapper.The
Skeletonwrapper withloading={true}is redundant since the individual skeleton components already handle the loading state internally.- return ( - <Skeleton loading={true} active placeholder={placeholder}></Skeleton> - ); + return placeholder;web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx (1)
56-59: Consider removing unused label transformation function.The
getEndpointTypeLabelfunction currently just returns the input unchanged. If no transformation is needed, consider removing this function for simplicity.- // 端点类型显示名称映射 - const getEndpointTypeLabel = (endpointType) => { - return endpointType; - };And update the usage:
value: endpointType, - label: getEndpointTypeLabel(endpointType), + label: endpointType,web/src/components/table/model-pricing/modal/PricingFilterModal.jsx (1)
67-68: Add WebKit scrollbar hiding for broader browser support.The current scrollbar hiding implementation covers Firefox and IE/Edge but misses WebKit-based browsers.
scrollbarWidth: 'none', - msOverflowStyle: 'none' + msOverflowStyle: 'none', + WebkitScrollbar: { display: 'none' }Or better yet, use CSS-in-JS or a CSS class for this styling.
web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx (1)
52-82: Simplify data preparation logic and improve error handling.The data preparation logic has some areas for improvement:
- Simplify price data extraction:
- outputPrice: modelData?.quota_type === 0 ? (priceData.completionPrice || priceData.outputPrice) : '-', + outputPrice: modelData?.quota_type === 0 ? (priceData.completionPrice ?? priceData.outputPrice ?? '-') : '-',
- Consider better fallback for empty groups:
const availableGroups = Object.keys(usableGroup || {}).filter(g => g !== ''); if (availableGroups.length === 0) { - availableGroups.push('default'); + // Consider logging a warning or returning early instead of assuming 'default' exists + console.warn('No available groups found for pricing display'); + return <div className="text-gray-500">{t('无可用分组')}</div>; }
- Extract billing type logic:
+ const getBillingType = (quotaType) => quotaType === 0 ? t('按量计费') : t('按次计费'); + return { // ... - billingType: modelData?.quota_type === 0 ? t('按量计费') : t('按次计费'), + billingType: getBillingType(modelData?.quota_type),web/src/components/table/model-pricing/view/card/PricingCardSkeleton.jsx (2)
50-64: Extract magic numbers for better maintainability.The dynamic width calculations use magic numbers that reduce code readability. Consider extracting these values as constants.
+const SKELETON_WIDTHS = { + MODEL_NAME_BASE: 120, + MODEL_NAME_VARIATION: 30, + PRICE_BASE: 160, + PRICE_VARIATION: 20 +}; + /* 模型名称骨架 */ <Skeleton.Title style={{ - width: `${120 + (index % 3) * 30}px`, + width: `${SKELETON_WIDTHS.MODEL_NAME_BASE + (index % 3) * SKELETON_WIDTHS.MODEL_NAME_VARIATION}px`, height: 20, marginBottom: 8 }} /> /* 价格信息骨架 */ <Skeleton.Title style={{ - width: `${160 + (index % 4) * 20}px`, + width: `${SKELETON_WIDTHS.PRICE_BASE + (index % 4) * SKELETON_WIDTHS.PRICE_VARIATION}px`, height: 20, marginBottom: 0 }} />
31-36: Consider performance optimization for large skeleton countsThe current implementation generates skeleton cards without any performance optimization. For large
skeletonCountvalues, this could impact rendering performance.Consider memoizing the skeleton card structure:
+const SkeletonCard = React.memo(({ index, rowSelection, showRatio }) => ( + <Card + key={index} + className="!rounded-2xl border border-gray-200" + bodyStyle={{ padding: '24px' }} + > + {/* ... card content ... */} + </Card> +)); const placeholder = ( <div className="p-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> {Array.from({ length: skeletonCount }).map((_, index) => ( - <Card - key={index} - className="!rounded-2xl border border-gray-200" - bodyStyle={{ padding: '24px' }} - > + <SkeletonCard + key={index} + index={index} + rowSelection={rowSelection} + showRatio={showRatio} + />web/src/components/table/model-pricing/layout/header/PricingCategoryIntro.jsx (4)
58-84: Consider extracting category descriptions for better maintainability.The category descriptions are comprehensive but hardcoded within the function. Consider extracting them to a configuration object for easier maintenance and potential localization.
+const CATEGORY_DESCRIPTIONS = { + all: '查看所有可用的AI模型,包括文本生成、图像处理、音频转换等多种类型的模型。', + openai: '令牌分发介绍:SSVIP 为纯OpenAI官方。SVIP 为纯Azure。Default 为Azure 消费。VIP为近似的复数。VVIP为近似的书发。', + // ... other descriptions +}; + const getCategoryDescription = (categoryKey) => { - const descriptions = { - all: t('查看所有可用的AI模型,包括文本生成、图像处理、音频转换等多种类型的模型。'), - // ... other descriptions - }; - return descriptions[categoryKey] || t('该分类包含多种AI模型,适用于不同的应用场景。'); + return t(CATEGORY_DESCRIPTIONS[categoryKey] || '该分类包含多种AI模型,适用于不同的应用场景。'); };
50-84: Category descriptions are comprehensive but consider localization impact.The extensive category descriptions provide good user context, but some contain very specific details that might be challenging to localize effectively across different languages.
Consider if some of the longer, more detailed descriptions could be simplified for better localization and user scanning, while keeping essential information.
87-122: Complex avatar rendering logic needs refactoringThe
renderAllModelsAvatarfunction is quite complex with nested conditions and array manipulations. Consider breaking it down into smaller, more testable functions.Break down the complex rendering logic:
+ const createFallbackCategories = () => { + return Object.entries(modelCategories) + .filter(([key]) => key !== 'all') + .slice(0, 3) + .map(([key, category]) => ({ + key, + label: category.label, + text: category.label.slice(0, 2) || key.slice(0, 2).toUpperCase() + })); + }; + + const renderFallbackAvatars = () => { + const fallbackCategories = createFallbackCategories(); + return ( + <div className="min-w-16 h-16 rounded-2xl bg-white shadow-md flex items-center justify-center px-2"> + <AvatarGroup size="default" overlapFrom='end'> + {fallbackCategories.map((item) => ( + <Avatar key={item.key} size="default" color="transparent" alt={item.label}> + {item.text} + </Avatar> + ))} + </AvatarGroup> + </div> + ); + }; + const renderAllModelsAvatar = () => { const rotatedCategories = validCategories.length > 3 ? [ ...validCategories.slice(currentOffset), ...validCategories.slice(0, currentOffset) ] : validCategories; - if (validCategories.length === 0) { - // ... complex fallback logic - return (/* fallback JSX */); - } + if (validCategories.length === 0) { + return renderFallbackAvatars(); + }
183-187: Consider responsive font sizing consistencyThe responsive text sizing (
text-lg sm:text-xl) is good, but ensure it's consistent across similar components in the application.Verify consistent responsive typography patterns across the pricing components.
web/src/helpers/utils.js (1)
705-730: Consider adding JSDoc documentation for better maintainability.The
resetPricingFiltersfunction is well-implemented with proper null safety, but would benefit from JSDoc documentation describing the expected parameter structure.+/** + * Resets all pricing filter states to their default values + * @param {Object} options - Object containing setter functions and data + * @param {Function} options.handleChange - Handler for search input changes + * @param {Function} options.setActiveKey - Setter for active category key + * @param {Array} options.availableCategories - Array of available category keys + * @param {Function} options.setShowWithRecharge - Setter for recharge display toggle + * @param {Function} options.setCurrency - Setter for currency selection + * @param {Function} options.setShowRatio - Setter for ratio display toggle + * @param {Function} options.setViewMode - Setter for view mode (card/table) + * @param {Function} options.setFilterGroup - Setter for group filter + * @param {Function} options.setFilterQuotaType - Setter for quota type filter + * @param {Function} options.setFilterEndpointType - Setter for endpoint type filter + * @param {Function} options.setCurrentPage - Setter for current page + * @param {Function} options.setTokenUnit - Setter for token unit (K/M) + */ export const resetPricingFilters = ({web/src/components/common/ui/SelectableButtonGroup.jsx (2)
52-58: Consider memoizing expensive calculations.The
maxVisibleRowscalculation andneedCollapseboolean are recalculated on every render. Consider wrapping them inuseMemofor better performance.+ const maxVisibleRows = useMemo(() => Math.max(1, Math.floor(collapseHeight / 32)), [collapseHeight]); + const needCollapse = useMemo(() => collapsible && items.length > perRow * maxVisibleRows, [collapsible, items.length, perRow, maxVisibleRows]); - const maxVisibleRows = Math.max(1, Math.floor(collapseHeight / 32)); // Approx row height 32 - const needCollapse = collapsible && items.length > perRow * maxVisibleRows;
89-131: Improve skeleton rendering maintainability.The skeleton rendering logic is complex and could benefit from extraction into a separate component or hook for reusability.
Consider extracting the skeleton logic:
+ const SkeletonButton = ({ withCheckbox, index }) => ( + <div style={{ + width: '100%', + height: '32px', + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', + border: '1px solid var(--semi-color-border)', + borderRadius: 'var(--semi-border-radius-medium)', + padding: '0 12px', + gap: '8px' + }}> + {withCheckbox && ( + <Skeleton.Title active style={{ width: 14, height: 14 }} /> + )} + <Skeleton.Title + active + style={{ + width: `${60 + (index % 3) * 20}px`, + height: 14 + }} + /> + </div> + );web/src/hooks/model-pricing/useModelPricingData.js (2)
81-116: Optimize filtering performance with better memoization.The filtering logic performs multiple sequential filters which could be expensive. Consider optimizing the dependency array and filtering logic.
The current implementation is correct but could be optimized:
const filteredModels = useMemo(() => { let result = models; + // Early return if no models + if (!models.length) return []; // Apply all filters in a single pass for better performance + return models.filter(model => { + // Category filter + if (activeKey !== 'all' && !modelCategories[activeKey].filter(model)) { + return false; + } + + // Group filter + if (filterGroup !== 'all' && !model.enable_groups.includes(filterGroup)) { + return false; + } + + // Quota type filter + if (filterQuotaType !== 'all' && model.quota_type !== filterQuotaType) { + return false; + } + + // Endpoint type filter + if (filterEndpointType !== 'all' && + (!model.supported_endpoint_types || !model.supported_endpoint_types.includes(filterEndpointType))) { + return false; + } + + // Search filter + if (searchValue.length > 0) { + const searchTerm = searchValue.toLowerCase(); + if (!model.model_name.toLowerCase().includes(searchTerm)) { + return false; + } + } + + return true; + }); - // Sequential filtering logic... }, [activeKey, models, searchValue, filterGroup, filterQuotaType, filterEndpointType, modelCategories]);
243-311: Consider grouping related return values for better organization.The return object is quite large and could benefit from logical grouping to improve maintainability.
Consider organizing the return object:
return { - // 状态 - searchValue, - setSearchValue, - // ... many individual items + // Search and filters + search: { + searchValue, + setSearchValue, + handleChange, + handleCompositionStart, + handleCompositionEnd, + }, + + // Filters + filters: { + activeKey, + setActiveKey, + filterGroup, + setFilterGroup, + filterQuotaType, + setFilterQuotaType, + filterEndpointType, + setFilterEndpointType, + }, + + // Pagination + pagination: { + currentPage, + setCurrentPage, + pageSize, + setPageSize, + }, + + // Selection and modals + selection: { + selectedRowKeys, + setSelectedRowKeys, + rowSelection, + }, + + // ... other groups };However, this would be a breaking change, so consider it for future refactoring.
web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx (3)
20-41: Component setup is functional but has high prop dependency.The component accepts many props which creates tight coupling. Consider if some of these props could be grouped into objects or if the component could be broken down further to reduce complexity.
58-67: Review copy button styling and accessibilityThe copy button has custom styling that overrides theme colors and lacks accessibility considerations for disabled state.
Improve accessibility and styling:
<Button theme='outline' type='primary' icon={<IconCopy />} onClick={() => copyText(selectedRowKeys)} disabled={selectedRowKeys.length === 0} - className="!bg-blue-500 hover:!bg-blue-600 text-white" + className="!bg-blue-500 hover:!bg-blue-600 text-white disabled:!bg-gray-300 disabled:!text-gray-500" + aria-label={t('复制选中的模型')} + title={selectedRowKeys.length === 0 ? t('请先选择要复制的模型') : t('复制选中的模型')} > {t('复制')} </Button>
99-106: Consider prop drilling optimizationThe component passes
sidebarPropsdirectly to the modal, which may contain many properties. Consider if this creates unnecessary coupling.Consider extracting only the needed props:
{isMobile && ( <PricingFilterModal visible={showFilterModal} onClose={() => setShowFilterModal(false)} - sidebarProps={sidebarProps} + filterProps={{ + handleChange: sidebarProps.handleChange, + setActiveKey: sidebarProps.setActiveKey, + // ... other specific props needed by modal + }} t={t} /> )}web/src/components/table/model-pricing/view/table/PricingTableColumns.js (2)
63-74: Consider destructuring validation for complex parameter objectThe function accepts a large parameter object with many properties. Consider adding parameter validation or at least documenting the expected structure.
Add parameter validation or use destructuring with defaults:
-export const getPricingTableColumns = ({ - t, - selectedGroup, - groupRatio, - copyText, - setModalImageUrl, - setIsModalOpenurl, - currency, - tokenUnit, - displayPrice, - showRatio, -}) => { +export const getPricingTableColumns = (params) => { + const { + t, + selectedGroup, + groupRatio = {}, + copyText, + setModalImageUrl, + setIsModalOpenurl, + currency = 'USD', + tokenUnit = 'K', + displayPrice = true, + showRatio = false, + } = params || {}; + + if (!t || !copyText) { + throw new Error('Required parameters t and copyText are missing'); + }
94-96: Review filter performance for large datasetsThe
onFilterfunction usestoLowerCase()andincludes()which could be inefficient for large datasets. Consider if this filtering should be moved to the backend or optimized.Consider optimizing the filter function:
- onFilter: (value, record) => - record.model_name.toLowerCase().includes(value.toLowerCase()), + onFilter: (value, record) => { + if (!value) return true; + const searchTerm = value.toLowerCase(); + const modelName = record.model_name?.toLowerCase() || ''; + return modelName.includes(searchTerm); + },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (43)
web/src/components/common/ui/CardTable.js(2 hunks)web/src/components/common/ui/SelectableButtonGroup.jsx(1 hunks)web/src/components/layout/HeaderBar.js(3 hunks)web/src/components/layout/PageLayout.js(1 hunks)web/src/components/table/model-pricing/ModelPricingColumnDefs.js(0 hunks)web/src/components/table/model-pricing/ModelPricingFilters.jsx(0 hunks)web/src/components/table/model-pricing/ModelPricingHeader.jsx(0 hunks)web/src/components/table/model-pricing/ModelPricingTabs.jsx(0 hunks)web/src/components/table/model-pricing/filter/PricingCategories.jsx(1 hunks)web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx(1 hunks)web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx(1 hunks)web/src/components/table/model-pricing/filter/PricingGroups.jsx(1 hunks)web/src/components/table/model-pricing/filter/PricingQuotaTypes.jsx(1 hunks)web/src/components/table/model-pricing/index.jsx(0 hunks)web/src/components/table/model-pricing/layout/PricingPage.jsx(1 hunks)web/src/components/table/model-pricing/layout/PricingSidebar.jsx(1 hunks)web/src/components/table/model-pricing/layout/content/PricingContent.jsx(1 hunks)web/src/components/table/model-pricing/layout/content/PricingView.jsx(1 hunks)web/src/components/table/model-pricing/layout/header/PricingCategoryIntro.jsx(1 hunks)web/src/components/table/model-pricing/layout/header/PricingCategoryIntroSkeleton.jsx(1 hunks)web/src/components/table/model-pricing/layout/header/PricingCategoryIntroWithSkeleton.jsx(1 hunks)web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx(1 hunks)web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx(1 hunks)web/src/components/table/model-pricing/modal/PricingFilterModal.jsx(1 hunks)web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx(1 hunks)web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx(1 hunks)web/src/components/table/model-pricing/modal/components/ModelBasicInfo.jsx(1 hunks)web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx(1 hunks)web/src/components/table/model-pricing/modal/components/ModelHeader.jsx(1 hunks)web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx(1 hunks)web/src/components/table/model-pricing/view/card/PricingCardSkeleton.jsx(1 hunks)web/src/components/table/model-pricing/view/card/PricingCardView.jsx(1 hunks)web/src/components/table/model-pricing/view/table/PricingTable.jsx(2 hunks)web/src/components/table/model-pricing/view/table/PricingTableColumns.js(1 hunks)web/src/components/table/usage-logs/UsageLogsActions.jsx(2 hunks)web/src/helpers/utils.js(2 hunks)web/src/hooks/common/useMinimumLoadingTime.js(1 hunks)web/src/hooks/dashboard/useDashboardData.js(4 hunks)web/src/hooks/model-pricing/useModelPricingData.js(7 hunks)web/src/hooks/model-pricing/usePricingFilterCounts.js(1 hunks)web/src/i18n/locales/en.json(2 hunks)web/src/index.css(3 hunks)web/src/pages/Pricing/index.js(1 hunks)
💤 Files with no reviewable changes (5)
- web/src/components/table/model-pricing/index.jsx
- web/src/components/table/model-pricing/ModelPricingFilters.jsx
- web/src/components/table/model-pricing/ModelPricingTabs.jsx
- web/src/components/table/model-pricing/ModelPricingHeader.jsx
- web/src/components/table/model-pricing/ModelPricingColumnDefs.js
🧰 Additional context used
🧬 Code Graph Analysis (16)
web/src/components/common/ui/CardTable.js (1)
web/src/hooks/common/useMinimumLoadingTime.js (3)
showSkeleton(29-29)useMinimumLoadingTime(28-50)useMinimumLoadingTime(28-50)
web/src/components/table/model-pricing/layout/content/PricingView.jsx (3)
web/src/components/table/model-pricing/layout/PricingPage.jsx (1)
viewMode(33-33)web/src/components/table/model-pricing/view/card/PricingCardView.jsx (1)
PricingCardView(35-331)web/src/components/table/model-pricing/view/table/PricingTable.jsx (1)
PricingTable(28-128)
web/src/components/table/model-pricing/layout/PricingPage.jsx (4)
web/src/hooks/model-pricing/useModelPricingData.js (2)
useModelPricingData(27-312)useModelPricingData(27-312)web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx (2)
isMobile(51-51)ModelDetailSideSheet(38-101)web/src/components/table/model-pricing/layout/PricingSidebar.jsx (1)
PricingSidebar(30-154)web/src/components/table/model-pricing/layout/content/PricingContent.jsx (1)
PricingContent(24-38)
web/src/hooks/common/useMinimumLoadingTime.js (8)
web/src/hooks/model-pricing/useModelPricingData.js (1)
loading(47-47)web/src/components/layout/HeaderBar.js (1)
loading(71-71)web/src/hooks/dashboard/useDashboardData.js (1)
loading(36-36)web/src/components/common/ui/SelectableButtonGroup.jsx (1)
showSkeleton(58-58)web/src/components/table/model-pricing/view/card/PricingCardView.jsx (1)
showSkeleton(59-59)web/src/components/common/ui/CardTable.js (1)
showSkeleton(45-45)web/src/components/table/model-pricing/layout/header/PricingCategoryIntroWithSkeleton.jsx (1)
showSkeleton(33-33)web/src/components/table/usage-logs/UsageLogsActions.jsx (1)
showSkeleton(34-34)
web/src/components/table/model-pricing/filter/PricingQuotaTypes.jsx (2)
web/src/components/common/ui/SelectableButtonGroup.jsx (1)
SelectableButtonGroup(40-255)web/src/hooks/model-pricing/useModelPricingData.js (1)
filterQuotaType(38-38)
web/src/components/table/model-pricing/filter/PricingCategories.jsx (5)
web/src/components/table/model-pricing/filter/PricingGroups.jsx (1)
items(36-57)web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx (1)
items(39-66)web/src/components/table/model-pricing/filter/PricingQuotaTypes.jsx (1)
items(34-38)web/src/hooks/model-pricing/useModelPricingData.js (3)
modelCategories(58-58)availableCategories(73-79)categoryCounts(60-71)web/src/components/common/ui/SelectableButtonGroup.jsx (1)
SelectableButtonGroup(40-255)
web/src/components/table/model-pricing/modal/components/ModelHeader.jsx (1)
web/src/components/table/model-pricing/view/card/PricingCardView.jsx (2)
CARD_STYLES(28-33)getModelIcon(78-118)
web/src/components/table/model-pricing/view/card/PricingCardSkeleton.jsx (3)
web/src/components/table/model-pricing/layout/header/PricingCategoryIntroSkeleton.jsx (1)
placeholder(26-68)web/src/hooks/model-pricing/useModelPricingData.js (1)
rowSelection(118-126)web/src/components/table/model-pricing/layout/PricingPage.jsx (1)
showRatio(32-32)
web/src/components/table/model-pricing/modal/PricingFilterModal.jsx (4)
web/src/components/table/model-pricing/layout/PricingSidebar.jsx (1)
handleResetFilters(71-85)web/src/helpers/utils.js (2)
resetPricingFilters(705-730)resetPricingFilters(705-730)web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx (1)
FilterModalFooter(23-42)web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx (1)
FilterModalContent(28-120)
web/src/components/table/model-pricing/filter/PricingGroups.jsx (2)
web/src/hooks/model-pricing/useModelPricingData.js (3)
usableGroup(49-49)groupRatio(48-48)filterGroup(37-37)web/src/components/common/ui/SelectableButtonGroup.jsx (1)
SelectableButtonGroup(40-255)
web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx (6)
web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx (1)
Typography(36-36)web/src/components/table/model-pricing/modal/components/ModelBasicInfo.jsx (1)
Typography(24-24)web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx (1)
Typography(24-24)web/src/hooks/model-pricing/useModelPricingData.js (3)
usableGroup(49-49)groupRatio(48-48)tokenUnit(45-45)web/src/components/table/model-pricing/view/table/PricingTable.jsx (1)
columns(50-74)web/src/components/table/model-pricing/layout/PricingPage.jsx (1)
showRatio(32-32)
web/src/helpers/utils.js (2)
web/src/hooks/model-pricing/useModelPricingData.js (7)
groupRatio(48-48)selectedGroup(34-34)tokenUnit(45-45)displayPrice(128-138)currency(43-43)handleChange(193-199)availableCategories(73-79)web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx (1)
handleChange(73-88)
web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx (3)
web/src/hooks/model-pricing/useModelPricingData.js (10)
handleCompositionStart(201-203)handleCompositionEnd(205-210)handleChange(193-199)copyText(185-191)selectedRowKeys(31-31)loading(47-47)activeKey(39-39)modelCategories(58-58)categoryCounts(60-71)availableCategories(73-79)web/src/components/table/model-pricing/layout/header/PricingCategoryIntroWithSkeleton.jsx (1)
PricingCategoryIntroWithSkeleton(25-52)web/src/components/table/model-pricing/modal/PricingFilterModal.jsx (1)
PricingFilterModal(26-74)
web/src/components/table/model-pricing/layout/content/PricingContent.jsx (2)
web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx (1)
PricingTopSection(26-109)web/src/components/table/model-pricing/layout/content/PricingView.jsx (1)
PricingView(24-31)
web/src/components/common/ui/SelectableButtonGroup.jsx (1)
web/src/hooks/common/useMinimumLoadingTime.js (3)
showSkeleton(29-29)useMinimumLoadingTime(28-50)useMinimumLoadingTime(28-50)
web/src/hooks/model-pricing/usePricingFilterCounts.js (3)
web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx (1)
usePricingFilterCounts(52-65)web/src/components/table/model-pricing/layout/PricingSidebar.jsx (1)
usePricingFilterCounts(56-69)web/src/hooks/model-pricing/useModelPricingData.js (5)
modelCategories(58-58)filterGroup(37-37)filterQuotaType(38-38)filterEndpointType(40-40)searchValue(29-29)
🔇 Additional comments (98)
web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx (2)
44-46: Verify the hardcoded URL path assumption.The code assumes all endpoint types use the same URL path
/v1/chat/completions, but different endpoint types typically have different paths (e.g.,/v1/embeddings,/v1/completions,/v1/chat/completions).Please verify that all supported endpoint types actually use the same URL path, or consider making the path dynamic based on the endpoint type:
const getEndpointPath = (endpointType) => { const pathMap = { 'chat': '/v1/chat/completions', 'completion': '/v1/completions', 'embedding': '/v1/embeddings', // add other mappings as needed }; return pathMap[endpointType] || '/v1/chat/completions'; };
53-67: LGTM! Clean component structure with proper internationalization.The component structure is well-organized with:
- Proper use of Semi-UI components
- Clean CSS styling with responsive design
- Good internationalization implementation
- Semantic HTML structure
web/src/hooks/model-pricing/usePricingFilterCounts.js (6)
25-42: LGTM! Well-structured parameter handling and category filtering.The parameter destructuring with sensible defaults and the category filtering logic are well-implemented:
- Proper handling of the 'all' category case
- Safe function checking before calling
category.filter- Correct memoization dependencies
45-63: LGTM! Comprehensive filtering logic with proper data structure handling.The filtering implementation correctly handles different data types:
- Array inclusion checks for
enable_groupsandsupported_endpoint_types- Direct comparison for
quota_type- Case-insensitive search for model names
The chaining approach ensures all filters are applied correctly.
66-77: LGTM! Smart dynamic counting logic for category filters.The dynamic category counting implementation is well-designed:
- Properly excludes the 'all' category from iteration
- Safe function checking before applying category filters
- Provides meaningful fallback (0) when filter function is missing
This enables responsive UI counts that update based on other active filters.
80-103: LGTM! Correct complementary filtering logic for button counts.Both
quotaTypeModelsandendpointTypeModelsimplement the right logic for filter button counts:
- Start with category-filtered models
- Apply all filters except the one being counted
- This provides accurate counts for what users would see if they selected each option
The parallel structure makes the code maintainable and consistent.
106-123: LGTM! Consistent filtering logic with clear documentation.The
groupCountModelslogic correctly implements the complementary filtering pattern:
- Properly excludes the group filter itself
- Applies all other relevant filters
- Clear comments explaining the logic
Consistent with the other count model implementations.
125-131: LGTM! Well-designed hook with clear return values.The hook returns all the necessary filtered datasets with descriptive names that align with their usage in consuming components. The overall design follows React hook best practices with proper memoization throughout.
web/src/components/layout/PageLayout.js (1)
45-45: LGTM! Appropriate footer hiding for the new pricing page layout.The addition of the exact
/pricingpath to the footer hiding condition aligns well with the new pricing page design that likely requires full-screen layout, similar to console pages.web/src/pages/Pricing/index.js (2)
21-21: LGTM! Updated import path reflects the new modular architecture.The import path change from the old monolithic component to the new
layout/PricingPagestructure aligns with the comprehensive refactoring mentioned in the PR objectives.
24-26: LGTM! Simplified wrapper reflects improved component design.The removal of the styled
divwrapper in favor of a React fragment suggests the newPricingPagecomponent properly handles its own layout and spacing internally, which is a good architectural improvement.web/src/components/common/ui/CardTable.js (2)
26-26: LGTM! Good refactoring to use centralized loading state management.The import of
useMinimumLoadingTimehook aligns with the broader effort to standardize minimum loading duration behavior across components.
45-45: LGTM! Simplified loading state management with custom hook.The replacement of manual minimum loading time logic with the
useMinimumLoadingTimehook improves:
- Code reusability across components
- Consistent loading behavior
- Reduced duplication of timing logic
The hook maintains the same 1000ms default minimum time while providing better abstraction.
web/src/components/table/model-pricing/layout/content/PricingView.jsx (1)
24-31: Clean implementation of view switcher pattern.The component follows React best practices with proper prop destructuring, default parameters, and conditional rendering. The fallback to table view when
viewModeis not 'card' provides good defensive programming.web/src/i18n/locales/en.json (2)
953-953: Key rename aligns with UI refactor.The renaming of the Chinese key from "定价" to "模型广场" while maintaining the English translation "Pricing" is consistent with the broader model pricing UI refactor mentioned in the PR objectives.
1198-1198: New translation key supports token unit display feature.The addition of "按K显示单位": "Display in K units" provides localization support for the new token unit display toggle functionality in the pricing display settings.
web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx (1)
23-42: Well-structured modal footer with proper button hierarchy.The component effectively implements a modal footer with appropriate visual hierarchy:
- Reset button uses outline theme (secondary action)
- Confirm button uses solid primary theme (primary action)
- Proper internationalization support
- Clean flexbox layout with right alignment
web/src/components/table/model-pricing/layout/content/PricingContent.jsx (1)
24-38: Well-structured responsive layout component.The component effectively implements a responsive layout with:
- Conditional CSS classes for mobile/desktop differences
- Clear separation between fixed header and scrollable content areas
- Proper prop extraction and forwarding (
viewModefromsidebarProps)- Good component composition with
PricingTopSectionandPricingViewThe layout structure supports the intended UX of having a fixed search/filter header with scrollable pricing data below.
web/src/components/table/model-pricing/layout/header/PricingCategoryIntroWithSkeleton.jsx (1)
25-52: Effective loading wrapper with consistent skeleton timing.The component implements a clean loading wrapper pattern:
- Uses the custom
useMinimumLoadingTimehook for standardized skeleton display timing- Proper conditional rendering between skeleton and actual content
- Correct prop mapping (
activeKey === 'all'toisAllModels)- Clean separation of concerns between loading logic and content rendering
This pattern provides a consistent loading experience across the application.
web/src/components/table/usage-logs/UsageLogsActions.jsx (1)
20-34: LGTM! Clean refactor to centralize loading skeleton logic.The replacement of manual loading timing logic with the
useMinimumLoadingTimehook improves code maintainability and reusability while preserving the existing functionality.web/src/components/table/model-pricing/filter/PricingCategories.jsx (1)
23-42: LGTM! Well-structured filter component with clean data transformation.The component properly filters available categories and transforms the data structure to match the
SelectableButtonGroupcomponent's expectations. The implementation follows good React patterns with appropriate prop passing and data mapping.web/src/components/table/model-pricing/layout/PricingPage.jsx (1)
42-84: Validation complete: modal props and CSS classes are correctly mapped.– Confirmed that CSS selectors
.pricing-layout,.pricing-scroll-hide,.pricing-sidebar, and.pricing-contentare defined inweb/src/index.css.
– Verified thatuseModelPricingDatareturnsmodalImageUrl,isModalOpenurl, andsetIsModalOpenurl, which are used assrc,visible, andonVisibleChangeon<ImagePreview>.No changes required.
web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx (1)
67-119: Excellent component composition with consistent prop passing.The component properly renders all filter components with appropriate props and maintains consistency in prop passing patterns across all child components.
web/src/hooks/common/useMinimumLoadingTime.js (1)
22-50: LGTM! Well-implemented custom hook with proper React patterns.The hook correctly manages minimum loading time with:
- Proper use of useRef to track loading start time
- Appropriate useEffect cleanup for timeouts
- Sound logic for calculating remaining display time
- Good default parameter (1000ms)
The implementation handles edge cases well and follows React best practices.
web/src/index.css (2)
394-416: LGTM! Consistent scrollbar hiding extension.The addition of
.pricing-scroll-hidefollows the established pattern for hiding scrollbars across the application. The implementation is consistent with existing scroll hiding utilities.
622-675: LGTM! Comprehensive and well-structured pricing layout styles.The new pricing layout CSS provides:
- Proper fixed sidebar (460px) with responsive mobile handling
- Flexible content area with appropriate overflow management
- Sticky header positioning with correct z-index layering
- Consistent theming using CSS custom properties
- Good mobile responsiveness with dedicated classes
The implementation follows modern CSS practices and supports the new modular pricing page architecture.
web/src/components/layout/HeaderBar.js (4)
55-55: LGTM! Proper integration of the centralized loading hook.The import of
useMinimumLoadingTimealigns with the broader refactoring to standardize loading skeleton behavior across components.
71-72: LGTM! Simplified loading state management.The refactoring to use
useMinimumLoadingTimeimproves the component by:
- Centralizing loading time logic in a reusable hook
- Reducing local state complexity
- Maintaining consistent UX with simplified code
The loading derivation from
statusState?.status === undefinedis appropriate.
133-133: LGTM! Navigation text update aligns with the pricing page refactor.The change from
t('定价')tot('模型广场')reflects the evolution from a simple pricing page to a more comprehensive model marketplace/square interface, which aligns with the broader refactoring objectives.
461-461: LGTM! Appropriate header styling enhancement.The addition of a bottom border using
var(--semi-color-border)provides visual separation and maintains theming consistency. The inline style is acceptable for this simple styling enhancement.web/src/components/table/model-pricing/modal/components/ModelBasicInfo.jsx (1)
26-53: LGTM! Well-structured component with good UX considerations.The
ModelBasicInfocomponent demonstrates:
- Proper use of semi-ui components for consistent styling
- Good conditional logic for model-specific descriptions (gpt-4o-image handling)
- Appropriate fallback handling when modelData is missing
- Clean internationalization support
- Semantic HTML structure with accessible styling
The special case handling for gpt-4o-image models provides valuable user guidance about the model's limitations and costs.
web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx (1)
38-101: LGTM! Well-architected modal component with excellent composition.The
ModelDetailSideSheetcomponent demonstrates:
- Excellent responsive design with mobile-first approach
- Clean component composition separating concerns (BasicInfo, Endpoints, PricingTable)
- Proper loading state handling with user-friendly feedback
- Appropriate prop passing and state management
- Good use of semi-ui components for consistent styling
- Proper accessibility with close button and keyboard handling
The responsive width handling (
isMobile ? '100%' : 600) and component structure provide a solid foundation for the model detail view.web/src/hooks/dashboard/useDashboardData.js (1)
27-27: LGTM! Clean refactor to centralize loading state management.The integration of
useMinimumLoadingTimehook successfully replaces manual loading timing logic while maintaining the same API interface. This standardization improves consistency across components and reduces code duplication.Also applies to: 39-39, 246-246
web/src/components/table/model-pricing/layout/header/PricingCategoryIntroSkeleton.jsx (1)
32-50: Well-implemented conditional avatar skeleton logic.The conditional rendering for
isAllModelsprovides appropriate skeleton patterns for both single and multiple avatar contexts. The overlapping avatar effect with negative margins creates a realistic loading placeholder.web/src/components/table/model-pricing/view/table/PricingTable.jsx (2)
26-26: Clean component and import renaming.The renaming from
ModelPricingTabletoPricingTableand corresponding column import aligns well with the new pricing architecture.Also applies to: 28-28
88-93: Effective compact mode implementation for responsive design.The logic to remove
fixedproperties and disable horizontal scrolling in compact mode improves mobile usability. The destructuring approach cleanly removes the fixed property while preserving other column properties.web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx (2)
35-42: Robust endpoint type extraction with proper fallback.The logic correctly handles the fallback from
allModelstomodelsand safely checks for array properties before processing. The sorting ensures consistent display order.
46-54: Efficient count calculation with proper filtering.The counting logic correctly handles the 'all' case and properly filters models based on supported endpoint types. The null safety checks prevent runtime errors.
web/src/components/table/model-pricing/modal/PricingFilterModal.jsx (2)
32-46: Well-structured filter reset implementation.The
handleResetFiltersfunction properly maps all the relevant sidebarProps to theresetPricingFiltersutility, ensuring comprehensive filter state reset. The parameter mapping aligns with the utility function signature shown in the relevant code snippets.
62-69: Effective full-screen modal styling for mobile use.The modal styling properly implements full-viewport dimensions with appropriate height calculations and scrollbar hiding. The scrollable content area ensures accessibility on various screen sizes.
web/src/components/table/model-pricing/modal/components/ModelHeader.jsx (1)
106-133: Component render logic looks well-structured.The JSX render logic is clean and follows React best practices:
- Proper use of conditional rendering with optional chaining
- Good accessibility with copyable text and success toast
- Clean styling with Tailwind classes
- Proper key usage in map operations
web/src/components/table/model-pricing/filter/PricingGroups.jsx (3)
20-32: Excellent documentation and clean imports.The JSDoc documentation clearly describes all parameters and their types, making the component interface easy to understand.
59-69: Clean component render with proper prop delegation.The component render is well-structured, properly delegating to the reusable
SelectableButtonGroupcomponent with all necessary props.
34-57: Add null safety to prevent runtime errors.The current implementation has potential null safety issues that could cause runtime errors:
- Line 34: The filter
key !== ''doesn't handle null/undefined keys- Line 39:
m.enable_groups.includes(g)could throw ifenable_groupsis undefinedApply these improvements for better null safety:
-const groups = ['all', ...Object.keys(usableGroup).filter(key => key !== '')]; +const groups = ['all', ...Object.keys(usableGroup).filter(key => key && key.trim() !== '')]; const items = groups.map((g) => { const modelCount = g === 'all' ? models.length - : models.filter(m => m.enable_groups && m.enable_groups.includes(g)).length; + : models.filter(m => Array.isArray(m.enable_groups) && m.enable_groups.includes(g)).length;This ensures:
- Keys are truthy and not just empty strings
enable_groupsis actually an array before callingincludes()Likely an incorrect or invalid review comment.
web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx (2)
84-171: Well-structured column definition logic.The dynamic column generation handles different pricing models appropriately and uses proper render functions. The conditional logic for showing ratios and different pricing types is clean.
Minor suggestion for consistency:
columns.push({ title: t('计费类型'), dataIndex: 'billingType', render: (text) => ( - <Tag color={text === t('按量计费') ? 'violet' : 'teal'} size="small" shape="circle"> + <Tag color={text.includes('按量') ? 'violet' : 'teal'} size="small" shape="circle"> {text} </Tag> ), });This makes the color logic more resilient to translation changes.
174-187: Clean and well-structured component render.The main render uses appropriate UI components with good visual hierarchy. The card layout and header provide clear context for the pricing table.
web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx (3)
25-71: Well-structured component interface with helpful tooltips.The component interface clearly separates different types of settings, and the tooltip explanation for the ratio setting is particularly helpful for user understanding.
73-97: Clean state management logic.The single change handler effectively manages multiple toggle types, and the active values calculation correctly maps the current state to the expected format.
99-124: Clean component render with appropriate conditional logic.The component render effectively uses the reusable
SelectableButtonGroupand properly conditionally renders the currency selection based on the recharge display setting.web/src/components/table/model-pricing/layout/PricingSidebar.jsx (3)
30-69: Well-structured component coordination with effective hook usage.The component effectively orchestrates multiple filter states and uses the
usePricingFilterCountshook appropriately to provide filtered data to each filter component. The extensive prop interface is appropriate for a coordinating component.
71-85: Proper delegation of reset logic to utility function.The reset handler correctly delegates to the
resetPricingFiltersutility function with all necessary state setters, promoting code reusability and maintainability.
87-153: Clean and logical component render structure.The sidebar layout is well-organized with appropriate visual hierarchy. Filter components are rendered in logical order and receive properly filtered data and state handlers.
web/src/components/table/model-pricing/view/card/PricingCardSkeleton.jsx (13)
23-27: Props structure looks good!The component props are well-defined with sensible defaults. The destructuring pattern and default values follow React best practices.
87-100: Tag skeleton generation is well-implemented.The dynamic tag count (2-4 tags per card) and consistent sizing create a realistic loading appearance.
132-134: Skeleton wrapper is correctly configured.The Skeleton component is properly configured with
loading={true}andactiveprops to display the skeleton UI.
1-21: License header and imports look good.The GPL license header is comprehensive and the imports are clean with no unused dependencies.
23-27: Component signature is well-structured.The props are clearly named with appropriate default values that make sense for the skeleton loading use case.
28-76: Grid layout and card structure are well-implemented.The responsive grid layout and dynamic width calculations for skeleton elements create a realistic loading experience. The card structure effectively mimics the actual content layout.
78-100: Description and tags sections are properly implemented.The variable tag count (2 + index % 3) creates realistic variety in the skeleton layout, and the description paragraph skeleton is appropriately sized.
102-120: Ratio section conditional rendering is well-structured.The ratio section properly uses conditional rendering and includes appropriate layout for title and 3-column ratio display when needed.
124-135: Pagination and wrapper implementation are correct.The pagination skeleton is appropriately positioned and the wrapper Skeleton component uses the correct props for active loading state.
23-27: LGTM: Well-structured component propsThe component props are properly typed with sensible defaults. The naming is clear and the default values are appropriate for typical usage scenarios.
50-64: Good use of dynamic widths for realistic skeleton effectThe varying widths based on card index (
120 + (index % 3) * 30and160 + (index % 4) * 20) create a more realistic loading appearance by simulating different content lengths.
89-100: Excellent tag count variationThe dynamic tag count (
2 + (index % 3)) effectively simulates realistic variation in the number of tags per model, enhancing the skeleton's authenticity.
132-134: Ignore unnecessary wrapper warning
The outer<Skeleton loading active placeholder={…}>is required to provide theloadingandactivecontext for all nestedSkeleton.Avatar,Skeleton.Title,Skeleton.Paragraph, andSkeleton.Buttoncomponents in yourplaceholder. If you simply returnplaceholderon its own, those inner skeletons won’t receive the context props and won’t render or animate correctly.No changes needed.
Likely an incorrect or invalid review comment.
web/src/components/table/model-pricing/view/table/PricingTableColumns.js (8)
25-61: Helper functions are well-implemented.Both
renderQuotaTypeandrenderSupportedEndpointshandle edge cases properly and use appropriate UI components with consistent styling.
63-86: Function structure and column assembly look good.The
getPricingTableColumnsfunction is well-organized with clear parameter destructuring and logical column assembly. The conditional inclusion of the ratio column is handled correctly.
1-23: Imports and license header are appropriate.Clean imports with necessary UI components and helper functions for table column functionality.
25-61: Helper functions are well-implemented.Both
renderQuotaTypeandrenderSupportedEndpointshandle edge cases properly and use appropriate UI components with consistent styling.
63-107: Basic column definitions are comprehensive.The function signature accommodates necessary customization parameters, and the basic columns (endpoint, model name, quota) are well-structured with appropriate handlers and features.
109-143: Ratio column implementation is comprehensive.The column properly handles different ratio types and quota conditions, with helpful tooltip and modal integration for user guidance.
145-186: Price column and final assembly are well-structured.The price column properly handles different pricing models, and the conditional column assembly logic ensures the correct columns are included based on configuration.
179-185: Good modular column constructionThe conditional addition of the ratio column and the systematic building of the columns array demonstrates good modularity and maintainability.
web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx (7)
41-81: Memoization and state management are well-implemented.The use of
useMemoforSearchAndActionsis appropriate to prevent unnecessary re-renders. The dependency array correctly includes all used props, and the local state for modal visibility is properly managed.
47-54: Excellent search input implementation with IME support.The search input correctly handles composition events for better support of international input methods (IME), while also providing a clear button and search icon for enhanced UX.
86-106: Component integration is well-designed.The integration with
PricingCategoryIntroWithSkeletonand conditional rendering ofPricingFilterModalfor mobile devices follows good responsive design patterns. Props are correctly passed through to child components.
83-108: Component render structure is well-organized.The JSX structure is clean with proper separation of concerns between category introduction, search/actions, and mobile filter modal.
111-111: Standard component export.
43-81: Excellent use of useMemo for performance optimizationThe memoization of the SearchAndActions component with proper dependencies prevents unnecessary re-renders while maintaining reactivity to the required props.
47-54: Good composition event handling for search inputThe proper handling of composition events (
onCompositionStartandonCompositionEnd) ensures correct behavior for CJK (Chinese, Japanese, Korean) input methods.web/src/components/table/model-pricing/layout/header/PricingCategoryIntro.jsx (9)
37-48: Carousel logic is well-implemented.The carousel effect is properly controlled with appropriate conditions, interval management, and cleanup. The logic only activates for the "all" category with sufficient items, and properly cleans up the interval to prevent memory leaks.
87-168: Avatar rendering logic is comprehensive and well-implemented.The carousel rotation logic, fallback handling, and AvatarGroup configuration are all well-designed. The use of
key={currentOffset}ensures proper re-rendering for the carousel animation.
50-230: Component render structure is well-organized.The conditional rendering logic, early returns for invalid states, and consistent Card styling create a robust and maintainable component structure. The responsive design and internationalization are properly implemented.
20-34: Component setup and initial state management are appropriate.The component properly filters valid categories and initializes carousel state for animation purposes.
36-53: Animation useEffect is properly implemented.The carousel animation logic correctly handles conditions, timing, and cleanup with an appropriate dependency array.
86-161: Complex avatar rendering logic is well-implemented.The function handles multiple scenarios including array rotation for carousel effects, fallback cases, and proper overflow handling with AvatarGroup. The logic is complex but appears correct.
163-196: Single category avatar and all models rendering are well-structured.The render functions use appropriate responsive design and maintain consistent layout patterns throughout the component.
199-232: Specific category rendering and export are appropriate.The component properly handles specific category cases with null checking and maintains layout consistency. Standard export pattern is used.
37-48: Good interval cleanup and conditional logicThe useEffect properly manages the carousel interval with cleanup and conditional logic to prevent unnecessary intervals when not needed.
web/src/helpers/utils.js (2)
620-639: formatPriceInfo function is well-implemented.The function properly handles both pricing types, uses theme-consistent colors, and integrates well with the translation system. The JSX structure is clean and semantic.
691-702: DEFAULT_PRICING_FILTERS constant is well-defined.The default filter values are sensible and comprehensive, covering all the filter dimensions with appropriate initial states.
web/src/components/common/ui/SelectableButtonGroup.jsx (3)
40-51: LGTM: Well-structured props with good defaults.The component props are well-defined with appropriate defaults and TypeScript-style JSDoc comments. The prop structure supports flexible usage patterns.
227-250: Verify collapsible behavior edge cases.The collapsible logic has complex conditional rendering that could lead to layout shifts. Test with edge cases like exactly
perRow * maxVisibleRowsitems.The collapse/expand logic should be tested with boundary conditions to ensure smooth transitions and proper state management.
136-139: Disabled logic is intentional and correct.All usages of SelectableButtonGroup that supply a numeric tagCount (e.g. quota types, endpoint types) deliberately disable items with zero associated models or counts, and components that shouldn’t ever be disabled (categories, display settings, currencies) omit tagCount and disabled so they remain selectable. No changes are required unless you specifically need to allow a zero-count item to be selectable—in that case you’d need to omit its tagCount or adjust the component logic to let an explicit disabled=false override the zero-count check.
web/src/components/table/model-pricing/view/card/PricingCardView.jsx (1)
320-321: Verify pagination options are appropriate.The page size options include 100, which might be too large for card view and could impact performance.
Consider whether 100 cards per page provides a good user experience, especially on mobile devices.
web/src/hooks/model-pricing/useModelPricingData.js (3)
28-42: LGTM: Well-structured state management.The state variables are clearly named and properly initialized. The addition of filter states and modal management enhances the hook's functionality.
239-241: LGTM: Proper pagination reset on filter changes.The effect properly resets pagination when any filter changes, preventing users from being stuck on empty pages.
212-222: Review automatic filter synchronization behaviorIt’s clear from the inline comment (“同时将分组过滤设置为该分组”) that handleGroupClick in web/src/hooks/model-pricing/useModelPricingData.js (lines 212-222) intentionally does two things at once:
- setSelectedGroup(group)
- setFilterGroup(group)
Please verify whether this coupling aligns with the desired UX:
- If selecting a group should only change pricing context, leave as-is.
- If filtering the table should remain an independent control, remove the setFilterGroup call from handleGroupClick and implement filtering via a separate UI action.
| <Button | ||
| onClick={() => { /* disabled */ }} | ||
| theme={isActive ? 'light' : 'outline'} | ||
| type={isActive ? 'primary' : 'tertiary'} | ||
| disabled={isDisabled} | ||
| icon={ | ||
| <Checkbox | ||
| checked={isActive} | ||
| onChange={() => onChange(item.value)} | ||
| disabled={isDisabled} | ||
| style={{ pointerEvents: 'auto' }} | ||
| /> | ||
| } | ||
| style={{ width: '100%', cursor: 'default' }} |
There was a problem hiding this comment.
Fix accessibility issue with disabled button click handler.
The button with checkbox has an empty onClick handler, which creates an accessibility issue. Users might expect the button to be clickable or it should be properly disabled.
- onClick={() => { /* disabled */ }}
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }}Or better yet, remove the onClick entirely if the button shouldn't be clickable:
- onClick={() => { /* disabled */ }}🤖 Prompt for AI Agents
In web/src/components/common/ui/SelectableButtonGroup.jsx around lines 150 to
163, the Button component has an empty onClick handler which causes
accessibility issues. Remove the onClick prop entirely if the button is disabled
or not meant to be clickable, and ensure the disabled prop is set correctly to
prevent user interaction and improve accessibility.
| const getCategoryDescription = (categoryKey) => { | ||
| const descriptions = { | ||
| all: t('查看所有可用的AI模型,包括文本生成、图像处理、音频转换等多种类型的模型。'), | ||
| openai: t('令牌分发介绍:SSVIP 为纯OpenAI官方。SVIP 为纯Azure。Default 为Azure 消费。VIP为近似的复数。VVIP为近似的书发。'), | ||
| anthropic: t('Anthropic Claude系列模型,以安全性和可靠性著称,擅长对话、分析和创作任务。'), | ||
| gemini: t('Google Gemini系列模型,具备强大的多模态能力,支持文本、图像和代码理解。'), | ||
| moonshot: t('月之暗面Moonshot系列模型,专注于长文本处理和深度理解能力。'), | ||
| zhipu: t('智谱AI ChatGLM系列模型,在中文理解和生成方面表现优秀。'), | ||
| qwen: t('阿里云通义千问系列模型,覆盖多个领域的智能问答和内容生成。'), | ||
| deepseek: t('DeepSeek系列模型,在代码生成和数学推理方面具有出色表现。'), | ||
| minimax: t('MiniMax ABAB系列模型,专注于对话和内容创作的AI助手。'), | ||
| baidu: t('百度文心一言系列模型,在中文自然语言处理方面具有强大能力。'), | ||
| xunfei: t('科大讯飞星火系列模型,在语音识别和自然语言理解方面领先。'), | ||
| midjourney: t('Midjourney图像生成模型,专业的AI艺术创作和图像生成服务。'), | ||
| tencent: t('腾讯混元系列模型,提供全面的AI能力和企业级服务。'), | ||
| cohere: t('Cohere Command系列模型,专注于企业级自然语言处理应用。'), | ||
| cloudflare: t('Cloudflare Workers AI模型,提供边缘计算和高性能AI服务。'), | ||
| ai360: t('360智脑系列模型,在安全和智能助手方面具有独特优势。'), | ||
| yi: t('零一万物Yi系列模型,提供高质量的多语言理解和生成能力。'), | ||
| jina: t('Jina AI模型,专注于嵌入和向量搜索的AI解决方案。'), | ||
| mistral: t('Mistral AI系列模型,欧洲领先的开源大语言模型。'), | ||
| xai: t('xAI Grok系列模型,具有独特的幽默感和实时信息处理能力。'), | ||
| llama: t('Meta Llama系列模型,开源的大语言模型,在各种任务中表现优秀。'), | ||
| doubao: t('字节跳动豆包系列模型,在内容创作和智能对话方面表现出色。'), | ||
| }; | ||
| return descriptions[categoryKey] || t('该分类包含多种AI模型,适用于不同的应用场景。'); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider extracting category descriptions to external configuration
The hardcoded category descriptions make the component less maintainable and harder to internationalize. Consider moving these to a separate configuration file or using a more structured approach.
Extract descriptions to a separate configuration:
+// Create a new file: categoryDescriptions.js
+export const getCategoryDescriptions = (t) => ({
+ all: t('查看所有可用的AI模型,包括文本生成、图像处理、音频转换等多种类型的模型。'),
+ openai: t('令牌分发介绍:SSVIP 为纯OpenAI官方。SVIP 为纯Azure。Default 为Azure 消费。VIP为近似的复数。VVIP为近似的书发。'),
+ // ... other descriptions
+});
- // 获取分类描述信息
- const getCategoryDescription = (categoryKey) => {
- const descriptions = {
- all: t('查看所有可用的AI模型...'),
- // ... all hardcoded descriptions
- };
- return descriptions[categoryKey] || t('该分类包含多种AI模型,适用于不同的应用场景。');
- };
+ const getCategoryDescription = (categoryKey) => {
+ const descriptions = getCategoryDescriptions(t);
+ return descriptions[categoryKey] || t('该分类包含多种AI模型,适用于不同的应用场景。');
+ };Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In web/src/components/table/model-pricing/layout/header/PricingCategoryIntro.jsx
around lines 58 to 84, the category descriptions are hardcoded inside the
component, which reduces maintainability and complicates internationalization.
To fix this, extract the descriptions object into a separate configuration file
or module that exports these mappings. Then import this configuration into the
component and use it to retrieve descriptions, enabling easier updates and
better support for localization.
| <AvatarGroup | ||
| maxCount={4} | ||
| size="default" | ||
| overlapFrom='end' | ||
| key={currentOffset} | ||
| renderMore={(restNumber) => ( | ||
| <Avatar | ||
| size="default" | ||
| style={{ backgroundColor: 'transparent', color: 'var(--semi-color-text-0)' }} | ||
| alt={`${restNumber} more categories`} | ||
| > | ||
| {`+${restNumber}`} | ||
| </Avatar> | ||
| )} | ||
| > |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Accessibility improvement needed for carousel
The carousel functionality lacks accessibility features such as pause on hover, keyboard navigation, or screen reader announcements of changes.
Add accessibility features:
return (
<div className="min-w-16 h-16 rounded-2xl bg-white shadow-md flex items-center justify-center px-2">
<AvatarGroup
maxCount={4}
size="default"
overlapFrom='end'
key={currentOffset}
+ role="img"
+ aria-label={t('模型分类头像轮播')}
+ aria-live="polite"
renderMore={(restNumber) => (
<Avatar
size="default"
style={{ backgroundColor: 'transparent', color: 'var(--semi-color-text-0)' }}
alt={`${restNumber} more categories`}
+ aria-label={t('还有{{count}}个分类', { count: restNumber })}
>
{`+${restNumber}`}
</Avatar>
)}
>Also consider adding pause on hover:
useEffect(() => {
if (activeKey !== 'all' || validCategories.length <= 3) {
setCurrentOffset(0);
return;
}
+ let interval;
+ const startInterval = () => {
+ interval = setInterval(() => {
+ setCurrentOffset(prev => (prev + 1) % validCategories.length);
+ }, 2000);
+ };
+
+ startInterval();
- const interval = setInterval(() => {
- setCurrentOffset(prev => (prev + 1) % validCategories.length);
- }, 2000);
return () => clearInterval(interval);
}, [activeKey, validCategories.length]);🤖 Prompt for AI Agents
In web/src/components/table/model-pricing/layout/header/PricingCategoryIntro.jsx
around lines 126 to 140, the AvatarGroup carousel lacks accessibility features
like pause on hover, keyboard navigation, and screen reader announcements. To
fix this, implement keyboard event handlers to allow navigation with arrow keys,
add aria-live regions or similar for screen reader updates when the carousel
changes, and add event handlers to pause the carousel animation or auto-scroll
when the user hovers over it. Ensure all interactive elements have appropriate
ARIA roles and labels.
| const SearchAndActions = useMemo(() => ( | ||
| <div className="flex items-center gap-4 w-full"> | ||
| {/* 搜索框 */} | ||
| <div className="flex-1"> | ||
| <Input | ||
| prefix={<IconSearch />} | ||
| placeholder={t('模糊搜索模型名称')} | ||
| onCompositionStart={handleCompositionStart} | ||
| onCompositionEnd={handleCompositionEnd} | ||
| onChange={handleChange} | ||
| showClear | ||
| /> | ||
| </div> | ||
|
|
||
| {/* 操作按钮 */} | ||
| <Button | ||
| theme='outline' | ||
| type='primary' | ||
| icon={<IconCopy />} | ||
| onClick={() => copyText(selectedRowKeys)} | ||
| disabled={selectedRowKeys.length === 0} | ||
| className="!bg-blue-500 hover:!bg-blue-600 text-white" | ||
| > | ||
| {t('复制')} | ||
| </Button> | ||
|
|
||
| {/* 移动端筛选按钮 */} | ||
| {isMobile && ( | ||
| <Button | ||
| theme="outline" | ||
| type='tertiary' | ||
| icon={<IconFilter />} | ||
| onClick={() => setShowFilterModal(true)} | ||
| > | ||
| {t('筛选')} | ||
| </Button> | ||
| )} | ||
| </div> | ||
| ), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText, isMobile]); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Optimize useMemo implementation and fix button styling conflict.
The useMemo optimization is well-implemented with correct dependencies. However, there's a styling conflict in the copy button:
<Button
- theme='outline'
- type='primary'
+ theme='solid'
+ type='primary'
icon={<IconCopy />}
onClick={() => copyText(selectedRowKeys)}
disabled={selectedRowKeys.length === 0}
- className="!bg-blue-500 hover:!bg-blue-600 text-white"
>The theme='outline' conflicts with the forced background colors. Use theme='solid' instead or remove the custom className and rely on the theme styling.
📝 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.
| const SearchAndActions = useMemo(() => ( | |
| <div className="flex items-center gap-4 w-full"> | |
| {/* 搜索框 */} | |
| <div className="flex-1"> | |
| <Input | |
| prefix={<IconSearch />} | |
| placeholder={t('模糊搜索模型名称')} | |
| onCompositionStart={handleCompositionStart} | |
| onCompositionEnd={handleCompositionEnd} | |
| onChange={handleChange} | |
| showClear | |
| /> | |
| </div> | |
| {/* 操作按钮 */} | |
| <Button | |
| theme='outline' | |
| type='primary' | |
| icon={<IconCopy />} | |
| onClick={() => copyText(selectedRowKeys)} | |
| disabled={selectedRowKeys.length === 0} | |
| className="!bg-blue-500 hover:!bg-blue-600 text-white" | |
| > | |
| {t('复制')} | |
| </Button> | |
| {/* 移动端筛选按钮 */} | |
| {isMobile && ( | |
| <Button | |
| theme="outline" | |
| type='tertiary' | |
| icon={<IconFilter />} | |
| onClick={() => setShowFilterModal(true)} | |
| > | |
| {t('筛选')} | |
| </Button> | |
| )} | |
| </div> | |
| ), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText, isMobile]); | |
| const SearchAndActions = useMemo(() => ( | |
| <div className="flex items-center gap-4 w-full"> | |
| {/* 搜索框 */} | |
| <div className="flex-1"> | |
| <Input | |
| prefix={<IconSearch />} | |
| placeholder={t('模糊搜索模型名称')} | |
| onCompositionStart={handleCompositionStart} | |
| onCompositionEnd={handleCompositionEnd} | |
| onChange={handleChange} | |
| showClear | |
| /> | |
| </div> | |
| {/* 操作按钮 */} | |
| <Button | |
| theme='solid' | |
| type='primary' | |
| icon={<IconCopy />} | |
| onClick={() => copyText(selectedRowKeys)} | |
| disabled={selectedRowKeys.length === 0} | |
| > | |
| {t('复制')} | |
| </Button> | |
| {/* 移动端筛选按钮 */} | |
| {isMobile && ( | |
| <Button | |
| theme="outline" | |
| type='tertiary' | |
| icon={<IconFilter />} | |
| onClick={() => setShowFilterModal(true)} | |
| > | |
| {t('筛选')} | |
| </Button> | |
| )} | |
| </div> | |
| ), [selectedRowKeys, t, handleCompositionStart, handleCompositionEnd, handleChange, copyText, isMobile]); |
🤖 Prompt for AI Agents
In web/src/components/table/model-pricing/layout/header/PricingTopSection.jsx
around lines 43 to 81, the copy button uses theme='outline' but also applies
forced background colors via className, causing a styling conflict. To fix this,
change the button's theme from 'outline' to 'solid' to align with the custom
background colors, or alternatively remove the custom className and rely solely
on the theme styling for consistent appearance.
| onRow={(record) => ({ | ||
| onClick: () => openModelDetail && openModelDetail(record), | ||
| style: { cursor: 'pointer' } | ||
| })} |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Verify click handler doesn't interfere with row selection.
The row click handler for opening model details might conflict with row selection functionality if both are active simultaneously.
🏁 Script executed:
#!/bin/bash
# Check if this table is used with row selection and click handlers simultaneously
ast-grep --pattern 'rowSelection={$_}' | head -10
rg -A 5 -B 5 "PricingTable.*rowSelection"Length of output: 95
I wasn’t able to find any occurrences of rowSelection or its use with PricingTable. Let’s search the entire repo for both the prop and component usages to be sure:
🏁 Script executed:
#!/bin/bash
# 1) Find every usage of rowSelection prop
rg --color=auto -n "rowSelection"
# 2) Find all imports and JSX usages of PricingTable
rg --color=auto -n "<PricingTable"
rg --color=auto -n "import .*PricingTable" Length of output: 3636
🏁 Script executed:
#!/bin/bash
# Show context around rowSelection in PricingTable
rg -n -A 5 -B 5 "rowSelection" web/src/components/table/model-pricing/view/table/PricingTable.jsx
# Show context around onRow in PricingTable
rg -n -A 5 -B 5 "onRow" web/src/components/table/model-pricing/view/table/PricingTable.jsxLength of output: 1697
Prevent detail view opening when selecting a row
Because you’re passing both rowSelection and a blanket onRow.onClick, clicking the selection checkbox will also trigger openModelDetail. To avoid this, scope the click handler so it only fires when clicking non‐selection areas:
• File: web/src/components/table/model-pricing/view/table/PricingTable.jsx
Lines: 104–107
Proposed change:
- onRow={(record) => ({
- onClick: () => openModelDetail && openModelDetail(record),
- style: { cursor: 'pointer' }
- })}
+ onRow={(record) => ({
+ onClick: (e) => {
+ // ignore clicks on the selection checkbox column
+ if (e.target.closest('.ant-checkbox-wrapper, .ant-table-selection-column')) {
+ return;
+ }
+ openModelDetail?.(record);
+ },
+ style: { cursor: 'pointer' }
+ })}Verify in the browser that selecting rows still works without opening the detail pane.
📝 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.
| onRow={(record) => ({ | |
| onClick: () => openModelDetail && openModelDetail(record), | |
| style: { cursor: 'pointer' } | |
| })} | |
| onRow={(record) => ({ | |
| onClick: (e) => { | |
| // ignore clicks on the selection checkbox column | |
| if (e.target.closest('.ant-checkbox-wrapper, .ant-table-selection-column')) { | |
| return; | |
| } | |
| openModelDetail?.(record); | |
| }, | |
| style: { cursor: 'pointer' } | |
| })} |
🤖 Prompt for AI Agents
In web/src/components/table/model-pricing/view/table/PricingTable.jsx lines 104
to 107, the onRow onClick handler triggers openModelDetail even when clicking
the row selection checkbox. Modify the onClick handler to check the event target
and only call openModelDetail if the click is outside the selection checkbox
area. This prevents the detail view from opening when selecting rows. After the
change, verify in the browser that row selection still works correctly without
opening the detail pane.
| const ratioColumn = { | ||
| title: () => ( | ||
| <div className="flex items-center space-x-1"> | ||
| <span>{t('倍率')}</span> | ||
| <Tooltip content={t('倍率是为了方便换算不同价格的模型')}> | ||
| <IconHelpCircle | ||
| className="text-blue-500 cursor-pointer" | ||
| onClick={() => { | ||
| setModalImageUrl('/ratio.png'); | ||
| setIsModalOpenurl(true); | ||
| }} | ||
| /> | ||
| </Tooltip> | ||
| </div> | ||
| ), | ||
| dataIndex: 'model_ratio', | ||
| render: (text, record, index) => { | ||
| const completionRatio = parseFloat(record.completion_ratio.toFixed(3)); | ||
| const content = ( | ||
| <div className="space-y-1"> | ||
| <div className="text-gray-700"> | ||
| {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')} | ||
| </div> | ||
| <div className="text-gray-700"> | ||
| {t('补全倍率')}: | ||
| {record.quota_type === 0 ? completionRatio : t('无')} | ||
| </div> | ||
| <div className="text-gray-700"> | ||
| {t('分组倍率')}:{groupRatio[selectedGroup]} | ||
| </div> | ||
| </div> | ||
| ); | ||
| return content; | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add null safety for completion_ratio access.
The ratio column accesses record.completion_ratio.toFixed(3) without checking if completion_ratio exists, which could cause runtime errors.
const ratioColumn = {
// ... title definition
render: (text, record, index) => {
- const completionRatio = parseFloat(record.completion_ratio.toFixed(3));
+ const completionRatio = record.completion_ratio
+ ? parseFloat(record.completion_ratio.toFixed(3))
+ : 0;
const content = (
<div className="space-y-1">
<div className="text-gray-700">
{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
</div>
<div className="text-gray-700">
{t('补全倍率')}:
- {record.quota_type === 0 ? completionRatio : t('无')}
+ {record.quota_type === 0 && record.completion_ratio ? completionRatio : t('无')}
</div>
<div className="text-gray-700">
{t('分组倍率')}:{groupRatio[selectedGroup]}
</div>
</div>
);
return content;
},
};📝 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.
| const ratioColumn = { | |
| title: () => ( | |
| <div className="flex items-center space-x-1"> | |
| <span>{t('倍率')}</span> | |
| <Tooltip content={t('倍率是为了方便换算不同价格的模型')}> | |
| <IconHelpCircle | |
| className="text-blue-500 cursor-pointer" | |
| onClick={() => { | |
| setModalImageUrl('/ratio.png'); | |
| setIsModalOpenurl(true); | |
| }} | |
| /> | |
| </Tooltip> | |
| </div> | |
| ), | |
| dataIndex: 'model_ratio', | |
| render: (text, record, index) => { | |
| const completionRatio = parseFloat(record.completion_ratio.toFixed(3)); | |
| const content = ( | |
| <div className="space-y-1"> | |
| <div className="text-gray-700"> | |
| {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')} | |
| </div> | |
| <div className="text-gray-700"> | |
| {t('补全倍率')}: | |
| {record.quota_type === 0 ? completionRatio : t('无')} | |
| </div> | |
| <div className="text-gray-700"> | |
| {t('分组倍率')}:{groupRatio[selectedGroup]} | |
| </div> | |
| </div> | |
| ); | |
| return content; | |
| }, | |
| }; | |
| const ratioColumn = { | |
| title: () => ( | |
| <div className="flex items-center space-x-1"> | |
| <span>{t('倍率')}</span> | |
| <Tooltip content={t('倍率是为了方便换算不同价格的模型')}> | |
| <IconHelpCircle | |
| className="text-blue-500 cursor-pointer" | |
| onClick={() => { | |
| setModalImageUrl('/ratio.png'); | |
| setIsModalOpenurl(true); | |
| }} | |
| /> | |
| </Tooltip> | |
| </div> | |
| ), | |
| dataIndex: 'model_ratio', | |
| render: (text, record, index) => { | |
| - const completionRatio = parseFloat(record.completion_ratio.toFixed(3)); | |
| + const completionRatio = record.completion_ratio | |
| + ? parseFloat(record.completion_ratio.toFixed(3)) | |
| + : 0; | |
| const content = ( | |
| <div className="space-y-1"> | |
| <div className="text-gray-700"> | |
| {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')} | |
| </div> | |
| <div className="text-gray-700"> | |
| {t('补全倍率')}: | |
| - {record.quota_type === 0 ? completionRatio : t('无')} | |
| + {record.quota_type === 0 && record.completion_ratio ? completionRatio : t('无')} | |
| </div> | |
| <div className="text-gray-700"> | |
| {t('分组倍率')}:{groupRatio[selectedGroup]} | |
| </div> | |
| </div> | |
| ); | |
| return content; | |
| }, | |
| }; |
🤖 Prompt for AI Agents
In web/src/components/table/model-pricing/view/table/PricingTableColumns.js
between lines 109 and 143, the code accesses record.completion_ratio.toFixed(3)
without verifying if completion_ratio is defined, risking runtime errors. Add a
null check to ensure completion_ratio exists and is a number before calling
toFixed; if not, provide a safe fallback value like 0 or an empty string to
prevent errors.
| render: (text, record, index) => { | ||
| const completionRatio = parseFloat(record.completion_ratio.toFixed(3)); | ||
| const content = ( | ||
| <div className="space-y-1"> | ||
| <div className="text-gray-700"> | ||
| {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')} | ||
| </div> | ||
| <div className="text-gray-700"> | ||
| {t('补全倍率')}: | ||
| {record.quota_type === 0 ? completionRatio : t('无')} | ||
| </div> | ||
| <div className="text-gray-700"> | ||
| {t('分组倍率')}:{groupRatio[selectedGroup]} | ||
| </div> | ||
| </div> | ||
| ); | ||
| return content; | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Potential null reference error in ratio calculation
The code accesses record.completion_ratio.toFixed(3) without null checking, which could cause runtime errors if the property is undefined.
Add null safety checks:
render: (text, record, index) => {
- const completionRatio = parseFloat(record.completion_ratio.toFixed(3));
+ const completionRatio = record.completion_ratio
+ ? parseFloat(record.completion_ratio.toFixed(3))
+ : 0;
const content = (
<div className="space-y-1">
<div className="text-gray-700">
{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
</div>
<div className="text-gray-700">
{t('补全倍率')}:
- {record.quota_type === 0 ? completionRatio : t('无')}
+ {record.quota_type === 0 && record.completion_ratio ? completionRatio : t('无')}
</div>
<div className="text-gray-700">
- {t('分组倍率')}:{groupRatio[selectedGroup]}
+ {t('分组倍率')}:{groupRatio?.[selectedGroup] || t('无')}
</div>
</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.
| render: (text, record, index) => { | |
| const completionRatio = parseFloat(record.completion_ratio.toFixed(3)); | |
| const content = ( | |
| <div className="space-y-1"> | |
| <div className="text-gray-700"> | |
| {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')} | |
| </div> | |
| <div className="text-gray-700"> | |
| {t('补全倍率')}: | |
| {record.quota_type === 0 ? completionRatio : t('无')} | |
| </div> | |
| <div className="text-gray-700"> | |
| {t('分组倍率')}:{groupRatio[selectedGroup]} | |
| </div> | |
| </div> | |
| ); | |
| return content; | |
| }, | |
| }; | |
| render: (text, record, index) => { | |
| const completionRatio = record.completion_ratio | |
| ? parseFloat(record.completion_ratio.toFixed(3)) | |
| : 0; | |
| const content = ( | |
| <div className="space-y-1"> | |
| <div className="text-gray-700"> | |
| {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')} | |
| </div> | |
| <div className="text-gray-700"> | |
| {t('补全倍率')}: | |
| {record.quota_type === 0 && record.completion_ratio | |
| ? completionRatio | |
| : t('无')} | |
| </div> | |
| <div className="text-gray-700"> | |
| {t('分组倍率')}:{groupRatio?.[selectedGroup] || t('无')} | |
| </div> | |
| </div> | |
| ); | |
| return content; | |
| }, | |
| }; |
🤖 Prompt for AI Agents
In web/src/components/table/model-pricing/view/table/PricingTableColumns.js
around lines 125 to 143, the code calls toFixed(3) on record.completion_ratio
without checking if completion_ratio is null or undefined, risking runtime
errors. Fix this by adding a null safety check before calling toFixed, for
example, verify that record.completion_ratio is a valid number before formatting
it, and provide a fallback value if it is null or undefined.
| export const calculateModelPrice = ({ | ||
| record, | ||
| selectedGroup, | ||
| groupRatio, | ||
| tokenUnit, | ||
| displayPrice, | ||
| currency, | ||
| precision = 4 | ||
| }) => { | ||
| if (record.quota_type === 0) { | ||
| // 按量计费 | ||
| const inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup]; | ||
| const completionRatioPriceUSD = | ||
| record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup]; | ||
|
|
||
| const unitDivisor = tokenUnit === 'K' ? 1000 : 1; | ||
| const unitLabel = tokenUnit === 'K' ? 'K' : 'M'; | ||
|
|
||
| const rawDisplayInput = displayPrice(inputRatioPriceUSD); | ||
| const rawDisplayCompletion = displayPrice(completionRatioPriceUSD); | ||
|
|
||
| const numInput = parseFloat(rawDisplayInput.replace(/[^0-9.]/g, '')) / unitDivisor; | ||
| const numCompletion = parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor; | ||
|
|
||
| return { | ||
| inputPrice: `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(precision)}`, | ||
| completionPrice: `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(precision)}`, | ||
| unitLabel, | ||
| isPerToken: true | ||
| }; | ||
| } else { | ||
| // 按次计费 | ||
| const priceUSD = parseFloat(record.model_price) * groupRatio[selectedGroup]; | ||
| const displayVal = displayPrice(priceUSD); | ||
|
|
||
| return { | ||
| price: displayVal, | ||
| isPerToken: false | ||
| }; | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Improve numeric value extraction robustness.
The numeric extraction using /[^0-9.]/g regex might not handle all currency formats correctly (e.g., numbers with commas, different decimal separators).
- const numInput = parseFloat(rawDisplayInput.replace(/[^0-9.]/g, '')) / unitDivisor;
- const numCompletion = parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor;
+ const extractNumericValue = (priceString) => {
+ // Remove currency symbols and spaces, handle both comma and dot as decimal separators
+ const cleaned = priceString.replace(/[^\d.,]/g, '').replace(/,/g, '.');
+ return parseFloat(cleaned) || 0;
+ };
+
+ const numInput = extractNumericValue(rawDisplayInput) / unitDivisor;
+ const numCompletion = extractNumericValue(rawDisplayCompletion) / unitDivisor;📝 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.
| export const calculateModelPrice = ({ | |
| record, | |
| selectedGroup, | |
| groupRatio, | |
| tokenUnit, | |
| displayPrice, | |
| currency, | |
| precision = 4 | |
| }) => { | |
| if (record.quota_type === 0) { | |
| // 按量计费 | |
| const inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup]; | |
| const completionRatioPriceUSD = | |
| record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup]; | |
| const unitDivisor = tokenUnit === 'K' ? 1000 : 1; | |
| const unitLabel = tokenUnit === 'K' ? 'K' : 'M'; | |
| const rawDisplayInput = displayPrice(inputRatioPriceUSD); | |
| const rawDisplayCompletion = displayPrice(completionRatioPriceUSD); | |
| const numInput = parseFloat(rawDisplayInput.replace(/[^0-9.]/g, '')) / unitDivisor; | |
| const numCompletion = parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor; | |
| return { | |
| inputPrice: `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(precision)}`, | |
| completionPrice: `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(precision)}`, | |
| unitLabel, | |
| isPerToken: true | |
| }; | |
| } else { | |
| // 按次计费 | |
| const priceUSD = parseFloat(record.model_price) * groupRatio[selectedGroup]; | |
| const displayVal = displayPrice(priceUSD); | |
| return { | |
| price: displayVal, | |
| isPerToken: false | |
| }; | |
| } | |
| }; | |
| export const calculateModelPrice = ({ | |
| record, | |
| selectedGroup, | |
| groupRatio, | |
| tokenUnit, | |
| displayPrice, | |
| currency, | |
| precision = 4 | |
| }) => { | |
| if (record.quota_type === 0) { | |
| // 按量计费 | |
| const inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup]; | |
| const completionRatioPriceUSD = | |
| record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup]; | |
| const unitDivisor = tokenUnit === 'K' ? 1000 : 1; | |
| const unitLabel = tokenUnit === 'K' ? 'K' : 'M'; | |
| const rawDisplayInput = displayPrice(inputRatioPriceUSD); | |
| const rawDisplayCompletion = displayPrice(completionRatioPriceUSD); | |
| const extractNumericValue = (priceString) => { | |
| // Remove currency symbols and spaces, handle both comma and dot as decimal separators | |
| const cleaned = priceString.replace(/[^\d.,]/g, '').replace(/,/g, '.'); | |
| return parseFloat(cleaned) || 0; | |
| }; | |
| const numInput = extractNumericValue(rawDisplayInput) / unitDivisor; | |
| const numCompletion = extractNumericValue(rawDisplayCompletion) / unitDivisor; | |
| return { | |
| inputPrice: `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(precision)}`, | |
| completionPrice: `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(precision)}`, | |
| unitLabel, | |
| isPerToken: true | |
| }; | |
| } else { | |
| // 按次计费 | |
| const priceUSD = parseFloat(record.model_price) * groupRatio[selectedGroup]; | |
| const displayVal = displayPrice(priceUSD); | |
| return { | |
| price: displayVal, | |
| isPerToken: false | |
| }; | |
| } | |
| }; |
🤖 Prompt for AI Agents
In web/src/helpers/utils.js around lines 577 to 617, the numeric extraction from
the displayPrice output uses a regex that removes all non-digit and non-dot
characters, which may fail for numbers with commas or different decimal
separators. To fix this, update the extraction logic to handle commas by
removing them before parsing and ensure the decimal separator is correctly
interpreted, possibly by normalizing the string to a standard format before
parsing with parseFloat.
…ntend) and UI refinements Backend • Add `model/model_meta.go` and `model/vendor_meta.go` defining Model & Vendor entities with CRUD helpers, soft-delete and time stamps • Create corresponding controllers `controller/model_meta.go`, `controller/vendor_meta.go` and register routes in `router/api-router.go` • Auto-migrate new tables in DB startup logic Frontend • Build complete “Model Management” module under `/console/models` - New pages, tables, filters, actions, hooks (`useModelsData`) and dynamic vendor tabs - Modals `EditModelModal.jsx` & unified `EditVendorModal.jsx`; latter now uses default confirm/cancel footer and mobile-friendly modal sizing (`full-width` / `small`) via `useIsMobile` • Update sidebar (`SiderBar.js`) and routing (`App.js`) to surface the feature • Add helper updates (`render.js`) incl. `stringToColor`, dynamic LobeHub icon retrieval, and tag color palettes Table UX improvements • Replace separate status column with inline Enable / Disable buttons in operation column (matching channel table style) • Limit visible tags to max 3; overflow represented as “+x” tag with padded `Popover` showing remaining tags • Color all tags deterministically using `stringToColor` for consistent theming • Change vendor column tag color to white for better contrast Misc • Minor layout tweaks, compact-mode toggle relocation, lint fixes and TypeScript/ESLint clean-up These changes collectively deliver end-to-end model & vendor administration while unifying visual language across management tables.
Add visual distinction for enabled/disabled models by applying different background colors to table rows based on model status. This implementation follows the same pattern used in ChannelsTable for consistent user experience. Changes: - Modified handleRow function in useModelsData.js to include row styling - Disabled models (status !== 1) now display with gray background using --semi-color-disabled-border CSS variable - Enabled models (status === 1) maintain normal background color - Preserved existing row click selection functionality This enhancement improves the visual feedback for users to quickly identify which models are active vs inactive in the models management interface.
… and overhaul visual JSON editor
Backend (Go)
- Include custom endpoints in each model’s SupportedEndpointTypes by parsing Model.Endpoints (JSON) and appending keys alongside native endpoint types.
- Build a global supportedEndpointMap map[string]EndpointInfo{path, method} by:
- Seeding with native defaults.
- Overriding/adding from models.endpoints (accepts string path → default POST, or {path, method}).
- Expose supported_endpoint at the top level of /api/pricing (vendors-like), removing per-model duplication.
- Fix default path for EndpointTypeOpenAIResponse to /v1/responses.
- Keep concurrency/caching for pricing retrieval intact.
Frontend (React)
- Fetch supported_endpoint in useModelPricingData and propagate to PricingPage → ModelDetailSideSheet → ModelEndpoints.
- ModelEndpoints
- Resolve path+method via endpointMap; replace {model} with actual model name.
- Fix mobile visibility; always show path and HTTP method.
- JSONEditor
- Wrap with Form.Slot to inherit form layout; simplify visual styles.
- Use Tabs for “Visual” / “Manual” modes.
- Unify editors: key-value editor now supports nested JSON:
- “+” to convert a primitive into an object and add nested fields.
- Add “Convert to value” for two‑way toggle back from object.
- Stable key rename without reordering rows; new rows append at bottom.
- Use Row/Col grid for clean alignment; region editor uses Form.Slot + grid.
- Editing flows
- EditModelModal / EditPrefillGroupModal use JSONEditor (editorType='object') for endpoint mappings.
- PrefillGroupManagement renders endpoint group items by JSON keys.
Data expectations / compatibility
- models.endpoints should be a JSON object mapping endpoint type → string path or {path, method}. Strings default to POST.
- No schema changes; existing TEXT field continues to store JSON.
QA
- /api/pricing now returns custom endpoint types and global supported_endpoint.
- UI shows both native and custom endpoints; paths/methods render on mobile; nested editing works and preserves order.
There was a problem hiding this comment.
Actionable comments posted: 10
🔭 Outside diff range comments (1)
web/src/hooks/model-pricing/useModelPricingData.js (1)
159-182: Missing error handling in loadPricing function.The API call lacks comprehensive error handling for network failures or malformed responses.
const loadPricing = async () => { setLoading(true); - let url = '/api/pricing'; - const res = await API.get(url); - const { success, message, data, vendors, group_ratio, usable_group, supported_endpoint } = res.data; - if (success) { - setGroupRatio(group_ratio); - setUsableGroup(usable_group); - setSelectedGroup(userState.user ? userState.user.group : 'default'); - // 构建供应商 Map 方便查找 - const vendorMap = {}; - if (Array.isArray(vendors)) { - vendors.forEach(v => { - vendorMap[v.id] = v; - }); - } - setVendorsMap(vendorMap); - setEndpointMap(supported_endpoint || {}); - setModelsFormat(data, group_ratio, vendorMap); - } else { - showError(message); - } - setLoading(false); + try { + let url = '/api/pricing'; + const res = await API.get(url); + const { success, message, data, vendors, group_ratio, usable_group, supported_endpoint } = res.data; + if (success) { + setGroupRatio(group_ratio); + setUsableGroup(usable_group); + setSelectedGroup(userState.user ? userState.user.group : 'default'); + // 构建供应商 Map 方便查找 + const vendorMap = {}; + if (Array.isArray(vendors)) { + vendors.forEach(v => { + vendorMap[v.id] = v; + }); + } + setVendorsMap(vendorMap); + setEndpointMap(supported_endpoint || {}); + setModelsFormat(data || [], group_ratio, vendorMap); + } else { + showError(message || t('加载定价数据失败')); + } + } catch (error) { + console.error('Failed to load pricing:', error); + showError(t('网络错误,请重试')); + } finally { + setLoading(false); + } };
♻️ Duplicate comments (2)
model/pricing.go (2)
98-99: Database error handling still needs improvement.The database operations continue to ignore errors using
_ = DB.Find(&...).Errorpattern. This can lead to silent failures and data inconsistency issues.-_ = DB.Find(&allMeta).Error +if err := DB.Find(&allMeta).Error; err != nil { + common.SysError(fmt.Sprintf("Failed to load model metadata: %v", err)) + return +}
150-151: Database error handling still needs improvement.Similar to the previous issue, vendor loading also ignores database errors.
-_ = DB.Find(&vendors).Error +if err := DB.Find(&vendors).Error; err != nil { + common.SysError(fmt.Sprintf("Failed to load vendor data: %v", err)) + return +}
🧹 Nitpick comments (5)
web/src/hooks/model-pricing/useModelPricingData.js (2)
22-22: Import includes unused functions.The import statement includes
showError,showInfo, andshowSuccessfrom helpers, but onlyshowErrorandshowSuccessappear to be used in the code. Consider removingshowInfoto keep imports clean.-import { API, copy, showError, showInfo, showSuccess } from '../../helpers'; +import { API, copy, showError, showSuccess } from '../../helpers';
60-102: Optimize filtering logic with early returns.The filtering logic correctly implements multi-dimensional filtering. However, it can be optimized with early returns to avoid unnecessary iterations when the result set is empty.
const filteredModels = useMemo(() => { let result = models; + + // Early return if no models + if (result.length === 0) { + return result; + } // 分组筛选 if (filterGroup !== 'all') { result = result.filter(model => model.enable_groups.includes(filterGroup)); + if (result.length === 0) return result; } // 计费类型筛选 if (filterQuotaType !== 'all') { result = result.filter(model => model.quota_type === filterQuotaType); + if (result.length === 0) return result; } // 端点类型筛选 if (filterEndpointType !== 'all') { result = result.filter(model => model.supported_endpoint_types && model.supported_endpoint_types.includes(filterEndpointType) ); + if (result.length === 0) return result; } // 供应商筛选 if (filterVendor !== 'all') { if (filterVendor === 'unknown') { result = result.filter(model => !model.vendor_name); } else { result = result.filter(model => model.vendor_name === filterVendor); } + if (result.length === 0) return result; } // 搜索筛选 if (searchValue.length > 0) { const searchTerm = searchValue.toLowerCase(); result = result.filter(model => (model.model_name && model.model_name.toLowerCase().includes(searchTerm)) || (model.description && model.description.toLowerCase().includes(searchTerm)) || (model.tags && model.tags.toLowerCase().includes(searchTerm)) || (model.vendor_name && model.vendor_name.toLowerCase().includes(searchTerm)) ); } return result; }, [models, searchValue, filterGroup, filterQuotaType, filterEndpointType, filterVendor]);web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx (1)
37-40: Consider fallback for model name extraction.The model name extraction has good fallback logic but could benefit from an additional fallback to avoid empty replacements.
if (path.includes('{model}')) { - const modelName = modelData.model_name || modelData.modelName || ''; + const modelName = modelData.model_name || modelData.modelName || 'unknown-model'; path = path.replaceAll('{model}', modelName); }web/src/components/common/ui/JSONEditor.js (2)
27-41: Consider adding PropTypes for type safetyThe component accepts many props but lacks type definitions. This could lead to runtime errors and makes the component harder to use correctly.
Add PropTypes after the component definition:
import PropTypes from 'prop-types'; // After the component definition JSONEditor.propTypes = { value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), onChange: PropTypes.func, field: PropTypes.string, label: PropTypes.string, placeholder: PropTypes.string, extraText: PropTypes.node, showClear: PropTypes.bool, template: PropTypes.object, templateLabel: PropTypes.string, editorType: PropTypes.oneOf(['keyValue', 'region', 'object']), rules: PropTypes.array, formApi: PropTypes.object, };
217-217: Translate comment to English- // 渲染键值对编辑器 + // Render key-value editor
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
common/endpoint_defaults.go(1 hunks)controller/pricing.go(1 hunks)model/pricing.go(5 hunks)web/src/components/common/ui/JSONEditor.js(1 hunks)web/src/components/table/model-pricing/layout/PricingPage.jsx(1 hunks)web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx(1 hunks)web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx(1 hunks)web/src/components/table/models/modals/EditModelModal.jsx(1 hunks)web/src/components/table/models/modals/EditPrefillGroupModal.jsx(1 hunks)web/src/components/table/models/modals/PrefillGroupManagement.jsx(1 hunks)web/src/hooks/model-pricing/useModelPricingData.js(10 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- controller/pricing.go
- web/src/components/table/models/modals/EditPrefillGroupModal.jsx
- web/src/components/table/model-pricing/layout/PricingPage.jsx
- web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx
- web/src/components/table/models/modals/PrefillGroupManagement.jsx
- web/src/components/table/models/modals/EditModelModal.jsx
🔇 Additional comments (10)
web/src/hooks/model-pricing/useModelPricingData.js (2)
126-157: LGTM! Well-implemented vendor enrichment logic.The
setModelsFormatfunction correctly enriches model data with vendor information and implements proper sorting. The dual sorting approach (quota type first, then model name with GPT priority) provides a good user experience.
242-244: LGTM! Proper pagination reset on filter changes.The effect correctly resets pagination to page 1 when any filter changes, ensuring consistent user experience.
common/endpoint_defaults.go (3)
13-16: LGTM! Well-defined struct with appropriate JSON tags.The
EndpointInfostruct is well-designed with clear field names and proper JSON tags for API serialization.
29-32: LGTM! Clean and efficient getter function.The
GetDefaultEndpointInfofunction follows Go best practices with the comma ok idiom for safe map access.
19-26: Endpoint mapping coverage confirmed — verify path accuracy against API specsAll
EndpointTypeconstants inconstant/endpoint_type.goare present in thedefaultEndpointInfoMap(common/endpoint_defaults.go) and used consistently. Please double-check that each path matches the official API documentation:
- common/endpoint_defaults.go
• Gemini:/v1beta/models/{model}:generateContent
– Ensure{model}placeholder aligns with the spec and substitution logic.
• Anthropic:/v1/messages
• Jina Rerank:/rerank
• Image Generation:/v1/images/generationsweb/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx (1)
27-65: LGTM! Significant improvement over previous implementation.The
renderAPIEndpointsfunction has been substantially improved from the previous version:
- ✅ Eliminated hardcoded values: Now uses dynamic
endpointMapinstead of hardcoded URLs and methods- ✅ Removed inefficient processing: Direct map operation instead of forEach + push pattern
- ✅ Smart placeholder replacement: Properly handles
{model}placeholders in paths- ✅ Better conditional rendering: Only shows path and method when available
- ✅ Improved responsive design: Better mobile layout with text wrapping
The implementation effectively addresses all the concerns raised in the previous review.
model/pricing.go (4)
104-147: LGTM! Well-implemented model matching logic.The model metadata matching logic correctly handles different name rules (exact, prefix, suffix, contains) with proper precedence. The use of separate slices for non-exact rules and the existence check prevents overwrites, ensuring exact matches take priority.
221-258: Robust endpoint mapping construction.The endpoint mapping construction logic effectively combines default and custom endpoints. The JSON parsing with type assertions and fallback values (defaulting to "POST" method) makes the system resilient to malformed data.
270-277: Good security practice with status filtering.The status check (
meta.Status != 1) before including models in the pricing response is a good security practice that prevents disabled models from being exposed to the frontend.
292-299: Proper concurrent access protection.The cache refresh logic correctly uses write locks to protect the shared maps (
modelEnableGroupsandmodelQuotaTypeMap) during updates, ensuring thread safety.
| // 初始化JSON数据 | ||
| const [jsonData, setJsonData] = useState(() => { | ||
| // 初始化时解析JSON数据 | ||
| if (typeof value === 'string' && value.trim()) { | ||
| try { | ||
| const parsed = JSON.parse(value); | ||
| return parsed; | ||
| } catch (error) { | ||
| return {}; | ||
| } | ||
| } | ||
| if (typeof value === 'object' && value !== null) { | ||
| return value; | ||
| } | ||
| return {}; | ||
| }); | ||
|
|
||
| // 根据键数量决定默认编辑模式 | ||
| const [editMode, setEditMode] = useState(() => { | ||
| // 如果初始JSON数据的键数量大于10个,则默认使用手动模式 | ||
| if (typeof value === 'string' && value.trim()) { | ||
| try { | ||
| const parsed = JSON.parse(value); | ||
| const keyCount = Object.keys(parsed).length; | ||
| return keyCount > 10 ? 'manual' : 'visual'; | ||
| } catch (error) { | ||
| // JSON无效时默认显示手动编辑模式 | ||
| return 'manual'; | ||
| } | ||
| } | ||
| return 'visual'; | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Use English comments for consistency and optimize duplicate JSON parsing
The code contains Chinese comments and parses the same JSON twice during initialization.
- // 初始化JSON数据
+ // Initialize JSON data
const [jsonData, setJsonData] = useState(() => {
- // 初始化时解析JSON数据
+ // Parse JSON data on initialization
if (typeof value === 'string' && value.trim()) {
try {
const parsed = JSON.parse(value);
return parsed;
} catch (error) {
return {};
}
}
if (typeof value === 'object' && value !== null) {
return value;
}
return {};
});
- // 根据键数量决定默认编辑模式
+ // Determine default edit mode based on key count
const [editMode, setEditMode] = useState(() => {
- // 如果初始JSON数据的键数量大于10个,则默认使用手动模式
- if (typeof value === 'string' && value.trim()) {
- try {
- const parsed = JSON.parse(value);
- const keyCount = Object.keys(parsed).length;
- return keyCount > 10 ? 'manual' : 'visual';
- } catch (error) {
- // JSON无效时默认显示手动编辑模式
- return 'manual';
- }
- }
- return 'visual';
+ // Reuse the parsed jsonData to avoid duplicate parsing
+ const keyCount = Object.keys(jsonData).length;
+ return keyCount > 10 ? 'manual' : 'visual';
});Note: You'll need to restructure the initialization to avoid the circular dependency. Consider using a single initialization function that returns both values.
📝 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.
| // 初始化JSON数据 | |
| const [jsonData, setJsonData] = useState(() => { | |
| // 初始化时解析JSON数据 | |
| if (typeof value === 'string' && value.trim()) { | |
| try { | |
| const parsed = JSON.parse(value); | |
| return parsed; | |
| } catch (error) { | |
| return {}; | |
| } | |
| } | |
| if (typeof value === 'object' && value !== null) { | |
| return value; | |
| } | |
| return {}; | |
| }); | |
| // 根据键数量决定默认编辑模式 | |
| const [editMode, setEditMode] = useState(() => { | |
| // 如果初始JSON数据的键数量大于10个,则默认使用手动模式 | |
| if (typeof value === 'string' && value.trim()) { | |
| try { | |
| const parsed = JSON.parse(value); | |
| const keyCount = Object.keys(parsed).length; | |
| return keyCount > 10 ? 'manual' : 'visual'; | |
| } catch (error) { | |
| // JSON无效时默认显示手动编辑模式 | |
| return 'manual'; | |
| } | |
| } | |
| return 'visual'; | |
| }); | |
| // Initialize JSON data | |
| const [jsonData, setJsonData] = useState(() => { | |
| // Parse JSON data on initialization | |
| if (typeof value === 'string' && value.trim()) { | |
| try { | |
| const parsed = JSON.parse(value); | |
| return parsed; | |
| } catch (error) { | |
| return {}; | |
| } | |
| } | |
| if (typeof value === 'object' && value !== null) { | |
| return value; | |
| } | |
| return {}; | |
| }); | |
| // Determine default edit mode based on key count | |
| const [editMode, setEditMode] = useState(() => { | |
| // Reuse the parsed jsonData to avoid duplicate parsing | |
| const keyCount = Object.keys(jsonData).length; | |
| return keyCount > 10 ? 'manual' : 'visual'; | |
| }); |
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js around lines 44 to 75, the code
has duplicate JSON parsing and uses Chinese comments. Refactor by creating a
single initialization function that parses the JSON once and returns both the
initial jsonData and editMode based on the key count. Replace all Chinese
comments with English ones for consistency.
| // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效) | ||
| useEffect(() => { | ||
| try { | ||
| let parsed = {}; | ||
| if (typeof value === 'string' && value.trim()) { | ||
| parsed = JSON.parse(value); | ||
| } else if (typeof value === 'object' && value !== null) { | ||
| parsed = value; | ||
| } | ||
| setJsonData(parsed); | ||
| setJsonError(''); | ||
| } catch (error) { | ||
| console.log('JSON解析失败:', error.message); | ||
| setJsonError(error.message); | ||
| // JSON格式错误时不更新jsonData | ||
| } | ||
| }, [value]); |
There was a problem hiding this comment.
Remove console.log and use English comments
Production code should not contain console.log statements, and comments should be in English.
- // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效)
+ // Data sync - always update jsonData when value changes (if JSON is valid)
useEffect(() => {
try {
let parsed = {};
if (typeof value === 'string' && value.trim()) {
parsed = JSON.parse(value);
} else if (typeof value === 'object' && value !== null) {
parsed = value;
}
setJsonData(parsed);
setJsonError('');
} catch (error) {
- console.log('JSON解析失败:', error.message);
+ // JSON parsing failed - handled by setting error state
setJsonError(error.message);
- // JSON格式错误时不更新jsonData
+ // Don't update jsonData when JSON format is invalid
}
}, [value]);📝 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.
| // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效) | |
| useEffect(() => { | |
| try { | |
| let parsed = {}; | |
| if (typeof value === 'string' && value.trim()) { | |
| parsed = JSON.parse(value); | |
| } else if (typeof value === 'object' && value !== null) { | |
| parsed = value; | |
| } | |
| setJsonData(parsed); | |
| setJsonError(''); | |
| } catch (error) { | |
| console.log('JSON解析失败:', error.message); | |
| setJsonError(error.message); | |
| // JSON格式错误时不更新jsonData | |
| } | |
| }, [value]); | |
| // Data sync - always update jsonData when value changes (if JSON is valid) | |
| useEffect(() => { | |
| try { | |
| let parsed = {}; | |
| if (typeof value === 'string' && value.trim()) { | |
| parsed = JSON.parse(value); | |
| } else if (typeof value === 'object' && value !== null) { | |
| parsed = value; | |
| } | |
| setJsonData(parsed); | |
| setJsonError(''); | |
| } catch (error) { | |
| // JSON parsing failed - handled by setting error state | |
| setJsonError(error.message); | |
| // Don't update jsonData when JSON format is invalid | |
| } | |
| }, [value]); |
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js around lines 78 to 94, remove the
console.log statement used for logging JSON parse errors and replace all
comments with English text. Instead of console.log, rely on setting the
jsonError state to handle error reporting. Ensure comments clearly describe the
logic in English for maintainability.
| // 处理可视化编辑的数据变化 | ||
| const handleVisualChange = useCallback((newData) => { | ||
| setJsonData(newData); | ||
| setJsonError(''); | ||
| const jsonString = Object.keys(newData).length === 0 ? '' : JSON.stringify(newData, null, 2); | ||
|
|
||
| // 通过formApi设置值(如果提供的话) | ||
| if (formApi && field) { | ||
| formApi.setValue(field, jsonString); | ||
| } | ||
|
|
||
| onChange?.(jsonString); | ||
| }, [onChange, formApi, field]); | ||
|
|
||
| // 处理手动编辑的数据变化 | ||
| const handleManualChange = useCallback((newValue) => { | ||
| onChange?.(newValue); | ||
| // 验证JSON格式 | ||
| if (newValue && newValue.trim()) { | ||
| try { | ||
| const parsed = JSON.parse(newValue); | ||
| setJsonError(''); | ||
| // 预先准备可视化数据,但不立即应用 | ||
| // 这样切换到可视化模式时数据已经准备好了 | ||
| } catch (error) { | ||
| setJsonError(error.message); | ||
| } | ||
| } else { | ||
| setJsonError(''); | ||
| } | ||
| }, [onChange]); | ||
|
|
||
| // 切换编辑模式 | ||
| const toggleEditMode = useCallback(() => { | ||
| if (editMode === 'visual') { | ||
| // 从可视化模式切换到手动模式 | ||
| setEditMode('manual'); | ||
| } else { | ||
| // 从手动模式切换到可视化模式,需要验证JSON | ||
| try { | ||
| let parsed = {}; | ||
| if (typeof value === 'string' && value.trim()) { | ||
| parsed = JSON.parse(value); | ||
| } else if (typeof value === 'object' && value !== null) { | ||
| parsed = value; | ||
| } | ||
| setJsonData(parsed); | ||
| setJsonError(''); | ||
| setEditMode('visual'); | ||
| } catch (error) { | ||
| setJsonError(error.message); | ||
| // JSON格式错误时不切换模式 | ||
| return; | ||
| } | ||
| } | ||
| }, [editMode, value]); | ||
|
|
||
| // 添加键值对 | ||
| const addKeyValue = useCallback(() => { | ||
| const newData = { ...jsonData }; | ||
| const keys = Object.keys(newData); | ||
| let counter = 1; | ||
| let newKey = `field_${counter}`; | ||
| while (newData.hasOwnProperty(newKey)) { | ||
| counter += 1; | ||
| newKey = `field_${counter}`; | ||
| } | ||
| newData[newKey] = ''; | ||
| handleVisualChange(newData); | ||
| }, [jsonData, handleVisualChange]); | ||
|
|
||
| // 删除键值对 | ||
| const removeKeyValue = useCallback((keyToRemove) => { | ||
| const newData = { ...jsonData }; | ||
| delete newData[keyToRemove]; | ||
| handleVisualChange(newData); | ||
| }, [jsonData, handleVisualChange]); | ||
|
|
||
| // 更新键名 | ||
| const updateKey = useCallback((oldKey, newKey) => { | ||
| if (oldKey === newKey || !newKey) return; | ||
| const newData = {}; | ||
| Object.entries(jsonData).forEach(([k, v]) => { | ||
| if (k === oldKey) { | ||
| newData[newKey] = v; | ||
| } else { | ||
| newData[k] = v; | ||
| } | ||
| }); | ||
| handleVisualChange(newData); | ||
| }, [jsonData, handleVisualChange]); | ||
|
|
||
| // 更新值 | ||
| const updateValue = useCallback((key, newValue) => { | ||
| const newData = { ...jsonData }; | ||
| newData[key] = newValue; | ||
| handleVisualChange(newData); | ||
| }, [jsonData, handleVisualChange]); | ||
|
|
||
| // 填入模板 | ||
| const fillTemplate = useCallback(() => { | ||
| if (template) { | ||
| const templateString = JSON.stringify(template, null, 2); | ||
|
|
||
| // 通过formApi设置值(如果提供的话) | ||
| if (formApi && field) { | ||
| formApi.setValue(field, templateString); | ||
| } | ||
|
|
||
| // 无论哪种模式都要更新值 | ||
| onChange?.(templateString); | ||
|
|
||
| // 如果是可视化模式,同时更新jsonData | ||
| if (editMode === 'visual') { | ||
| setJsonData(template); | ||
| } | ||
|
|
||
| // 清除错误状态 | ||
| setJsonError(''); | ||
| } | ||
| }, [template, onChange, editMode, formApi, field]); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Translate Chinese comments to English
Multiple handler functions contain Chinese comments that should be in English for consistency.
Replace all Chinese comments with English translations throughout these handler functions. For example:
- Line 96:
// 处理可视化编辑的数据变化→// Handle visual editing data changes - Line 110:
// 处理手动编辑的数据变化→// Handle manual editing data changes - Line 113:
// 验证JSON格式→// Validate JSON format - Line 128:
// 切换编辑模式→// Toggle edit mode - Line 153:
// 添加键值对→// Add key-value pair - Line 167:
// 删除键值对→// Remove key-value pair - Line 174:
// 更新键名→// Update key name - Line 188:
// 更新值→// Update value - Line 195:
// 填入模板→// Fill template
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js from lines 96 to 216, replace all
Chinese comments with their English translations to maintain consistency. For
example, change comments like "// 处理可视化编辑的数据变化" to "// Handle visual editing
data changes" and similarly translate all other Chinese comments in the handler
functions as indicated in the review comment.
| const keys = Object.keys(newData); | ||
| let counter = 1; | ||
| let newKey = `field_${counter}`; | ||
| while (newData.hasOwnProperty(newKey)) { |
There was a problem hiding this comment.
Use Object.prototype.hasOwnProperty for safety
Direct use of hasOwnProperty can fail if the object has a property with the same name.
- while (newData.hasOwnProperty(newKey)) {
+ while (Object.prototype.hasOwnProperty.call(newData, newKey)) {📝 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.
| while (newData.hasOwnProperty(newKey)) { | |
| while (Object.prototype.hasOwnProperty.call(newData, newKey)) { |
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js at line 159, replace the direct
call to newData.hasOwnProperty(newKey) with
Object.prototype.hasOwnProperty.call(newData, newKey) to safely check property
existence and avoid issues if newData has a hasOwnProperty property.
| // 添加嵌套对象 | ||
| const flattenObject = useCallback((parentKey) => { | ||
| const newData = { ...jsonData }; | ||
| let primitive = ''; | ||
| const obj = newData[parentKey]; | ||
| if (obj && typeof obj === 'object') { | ||
| const firstKey = Object.keys(obj)[0]; | ||
| if (firstKey !== undefined) { | ||
| const firstVal = obj[firstKey]; | ||
| if (typeof firstVal !== 'object') primitive = firstVal; | ||
| } | ||
| } | ||
| newData[parentKey] = primitive; | ||
| handleVisualChange(newData); | ||
| }, [jsonData, handleVisualChange]); | ||
|
|
||
| const addNestedObject = useCallback((parentKey) => { | ||
| const newData = { ...jsonData }; | ||
| if (typeof newData[parentKey] !== 'object' || newData[parentKey] === null) { | ||
| newData[parentKey] = {}; | ||
| } | ||
| const existingKeys = Object.keys(newData[parentKey]); | ||
| let counter = 1; | ||
| let newKey = `field_${counter}`; | ||
| while (newData[parentKey].hasOwnProperty(newKey)) { | ||
| counter += 1; | ||
| newKey = `field_${counter}`; | ||
| } | ||
| newData[parentKey][newKey] = ''; | ||
| handleVisualChange(newData); | ||
| }, [jsonData, handleVisualChange]); | ||
|
|
||
| // 渲染参数值输入控件(支持嵌套) | ||
| const renderValueInput = (key, value) => { | ||
| const valueType = typeof value; | ||
|
|
||
| if (valueType === 'boolean') { | ||
| return ( | ||
| <div className="flex items-center"> | ||
| <Switch | ||
| checked={value} | ||
| onChange={(newValue) => updateValue(key, newValue)} | ||
| /> | ||
| <Text type="tertiary" className="ml-2"> | ||
| {value ? t('true') : t('false')} | ||
| </Text> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (valueType === 'number') { | ||
| return ( | ||
| <InputNumber | ||
| value={value} | ||
| onChange={(newValue) => updateValue(key, newValue)} | ||
| style={{ width: '100%' }} | ||
| step={key === 'temperature' ? 0.1 : 1} | ||
| precision={key === 'temperature' ? 2 : 0} | ||
| placeholder={t('输入数字')} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| if (valueType === 'object' && value !== null) { | ||
| // 渲染嵌套对象 | ||
| const entries = Object.entries(value); | ||
| return ( | ||
| <Card className="!rounded-2xl"> | ||
| {entries.length === 0 && ( | ||
| <Text type="tertiary" className="text-gray-500 text-xs"> | ||
| {t('空对象,点击下方加号添加字段')} | ||
| </Text> | ||
| )} | ||
|
|
||
| {entries.map(([nestedKey, nestedValue], index) => ( | ||
| <Row key={index} gutter={4} align="middle" className="mb-1"> | ||
| <Col span={8}> | ||
| <Input | ||
| size="small" | ||
| placeholder={t('键名')} | ||
| value={nestedKey} | ||
| onChange={(newKey) => { | ||
| const newData = { ...jsonData }; | ||
| const oldValue = newData[key][nestedKey]; | ||
| delete newData[key][nestedKey]; | ||
| newData[key][newKey] = oldValue; | ||
| handleVisualChange(newData); | ||
| }} | ||
| /> | ||
| </Col> | ||
| <Col span={14}> | ||
| {typeof nestedValue === 'object' && nestedValue !== null ? ( | ||
| <TextArea | ||
| size="small" | ||
| rows={2} | ||
| value={JSON.stringify(nestedValue, null, 2)} | ||
| onChange={(txt) => { | ||
| try { | ||
| const obj = txt.trim() ? JSON.parse(txt) : {}; | ||
| const newData = { ...jsonData }; | ||
| newData[key][nestedKey] = obj; | ||
| handleVisualChange(newData); | ||
| } catch { | ||
| // ignore parse error | ||
| } | ||
| }} | ||
| /> | ||
| ) : ( | ||
| <Input | ||
| size="small" | ||
| placeholder={t('值')} | ||
| value={String(nestedValue)} | ||
| onChange={(newValue) => { | ||
| const newData = { ...jsonData }; | ||
| let convertedValue = newValue; | ||
| if (newValue === 'true') convertedValue = true; | ||
| else if (newValue === 'false') convertedValue = false; | ||
| else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { | ||
| convertedValue = Number(newValue); | ||
| } | ||
| newData[key][nestedKey] = convertedValue; | ||
| handleVisualChange(newData); | ||
| }} | ||
| /> | ||
| )} | ||
| </Col> | ||
| <Col span={2}> | ||
| <Button | ||
| size="small" | ||
| icon={<IconDelete />} | ||
| type="danger" | ||
| theme="borderless" | ||
| onClick={() => { | ||
| const newData = { ...jsonData }; | ||
| delete newData[key][nestedKey]; | ||
| handleVisualChange(newData); | ||
| }} | ||
| style={{ width: '100%' }} | ||
| /> | ||
| </Col> | ||
| </Row> | ||
| ))} | ||
|
|
||
| <div className="flex justify-center mt-1 gap-2"> | ||
| <Button | ||
| size="small" | ||
| icon={<IconPlus />} | ||
| type="tertiary" | ||
| onClick={() => addNestedObject(key)} | ||
| > | ||
| {t('添加字段')} | ||
| </Button> | ||
| <Button | ||
| size="small" | ||
| icon={<IconRefresh />} | ||
| type="tertiary" | ||
| onClick={() => flattenObject(key)} | ||
| > | ||
| {t('转换为值')} | ||
| </Button> | ||
| </div> | ||
| </Card> | ||
| ); | ||
| } | ||
|
|
||
| // 字符串或其他原始类型 | ||
| return ( | ||
| <div className="flex items-center gap-1"> | ||
| <Input | ||
| placeholder={t('参数值')} | ||
| value={String(value)} | ||
| onChange={(newValue) => { | ||
| let convertedValue = newValue; | ||
| if (newValue === 'true') convertedValue = true; | ||
| else if (newValue === 'false') convertedValue = false; | ||
| else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { | ||
| convertedValue = Number(newValue); | ||
| } | ||
| updateValue(key, convertedValue); | ||
| }} | ||
| /> | ||
| <Button | ||
| icon={<IconPlus />} | ||
| type="tertiary" | ||
| onClick={() => { | ||
| // 将当前值转换为对象 | ||
| const newData = { ...jsonData }; | ||
| newData[key] = { '1': value }; | ||
| handleVisualChange(newData); | ||
| }} | ||
| title={t('转换为对象')} | ||
| /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| // 渲染区域编辑器(特殊格式) | ||
| const renderRegionEditor = () => { | ||
| const entries = Object.entries(jsonData); | ||
| const defaultEntry = entries.find(([key]) => key === 'default'); | ||
| const modelEntries = entries.filter(([key]) => key !== 'default'); | ||
|
|
||
| return ( | ||
| <div className="space-y-2"> | ||
| {/* 默认区域 */} | ||
| <Form.Slot label={t('默认区域')}> | ||
| <Input | ||
| placeholder={t('默认区域,如: us-central1')} | ||
| value={defaultEntry ? defaultEntry[1] : ''} | ||
| onChange={(value) => updateValue('default', value)} | ||
| /> | ||
| </Form.Slot> | ||
|
|
||
| {/* 模型专用区域 */} | ||
| <Form.Slot label={t('模型专用区域')}> | ||
| <div> | ||
| {modelEntries.map(([modelName, region], index) => ( | ||
| <Row key={index} gutter={8} align="middle" className="mb-2"> | ||
| <Col span={10}> | ||
| <Input | ||
| placeholder={t('模型名称')} | ||
| value={modelName} | ||
| onChange={(newKey) => updateKey(modelName, newKey)} | ||
| /> | ||
| </Col> | ||
| <Col span={12}> | ||
| <Input | ||
| placeholder={t('区域')} | ||
| value={region} | ||
| onChange={(newValue) => updateValue(modelName, newValue)} | ||
| /> | ||
| </Col> | ||
| <Col span={2}> | ||
| <Button | ||
| icon={<IconDelete />} | ||
| type="danger" | ||
| theme="borderless" | ||
| onClick={() => removeKeyValue(modelName)} | ||
| style={{ width: '100%' }} | ||
| /> | ||
| </Col> | ||
| </Row> | ||
| ))} | ||
|
|
||
| <div className="mt-2 flex justify-center"> | ||
| <Button | ||
| icon={<IconPlus />} | ||
| onClick={addKeyValue} | ||
| type="primary" | ||
| theme="outline" | ||
| > | ||
| {t('添加模型区域')} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| </Form.Slot> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| // 渲染可视化编辑器 |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Translate remaining Chinese comments to English
Replace Chinese comments with English:
- Line 282:
// 添加嵌套对象→// Add nested object - Line 298:
// 渲染参数值输入控件(支持嵌套)→// Render parameter value input control (supports nesting) - Line 314:
// 渲染参数值输入控件(支持嵌套)→// Render value input control (supports nesting) - Line 346:
// 渲染嵌套对象→// Render nested object - Line 447:
// 字符串或其他原始类型→// String or other primitive types - Line 467:
// 将当前值转换为对象→// Convert current value to object - Line 478:
// 渲染区域编辑器(特殊格式)→// Render region editor (special format) - Line 486:
/* 默认区域 */→/* Default region */ - Line 495:
/* 模型专用区域 */→/* Model-specific regions */ - Line 542:
// 渲染可视化编辑器→// Render visual editor
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js from lines 282 to 542, replace all
Chinese comments with their English equivalents as specified: line 282 to "Add
nested object", line 298 and 314 to "Render parameter value input control
(supports nesting)" and "Render value input control (supports nesting)"
respectively, line 346 to "Render nested object", line 447 to "String or other
primitive types", line 467 to "Convert current value to object", line 478 to
"Render region editor (special format)", line 486 to "Default region", line 495
to "Model-specific regions", and line 542 to "Render visual editor".
| const existingKeys = Object.keys(newData[parentKey]); | ||
| let counter = 1; | ||
| let newKey = `field_${counter}`; | ||
| while (newData[parentKey].hasOwnProperty(newKey)) { |
There was a problem hiding this comment.
Use Object.prototype.hasOwnProperty for safety
Same issue as before - direct use of hasOwnProperty can fail.
- while (newData[parentKey].hasOwnProperty(newKey)) {
+ while (Object.prototype.hasOwnProperty.call(newData[parentKey], newKey)) {📝 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.
| while (newData[parentKey].hasOwnProperty(newKey)) { | |
| while (Object.prototype.hasOwnProperty.call(newData[parentKey], newKey)) { |
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js at line 306, replace the direct
call to hasOwnProperty on newData[parentKey] with
Object.prototype.hasOwnProperty.call(newData[parentKey], newKey) to ensure safe
property checking and avoid potential issues with objects that may override
hasOwnProperty.
| let convertedValue = newValue; | ||
| if (newValue === 'true') convertedValue = true; | ||
| else if (newValue === 'false') convertedValue = false; | ||
| else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { |
There was a problem hiding this comment.
Fix incorrect numeric conversion logic
The condition newValue !== '0' will prevent the string '0' from being converted to the number 0, which is likely unintended.
- else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
+ else if (!isNaN(newValue) && newValue !== '') {📝 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.
| else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { | |
| else if (!isNaN(newValue) && newValue !== '') { |
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js at line 399, the condition
`newValue !== '0'` incorrectly blocks the string '0' from being converted to the
number 0. Remove the `newValue !== '0'` check from the condition to allow '0' to
be properly converted to a numeric value.
| let convertedValue = newValue; | ||
| if (newValue === 'true') convertedValue = true; | ||
| else if (newValue === 'false') convertedValue = false; | ||
| else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { |
There was a problem hiding this comment.
Fix the same numeric conversion issue
Same problem with preventing '0' from being converted to number.
- else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') {
+ else if (!isNaN(newValue) && newValue !== '') {📝 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.
| else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { | |
| else if (!isNaN(newValue) && newValue !== '') { |
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js at line 457, the condition
incorrectly prevents the string '0' from being converted to a number. Modify the
condition to allow '0' to be converted by removing the check that excludes '0',
ensuring that numeric strings including '0' are properly converted.
| {/* JSON错误提示 */} | ||
| {hasJsonError && ( | ||
| <Banner | ||
| type="danger" | ||
| description={`JSON 格式错误: ${jsonError}`} | ||
| className="mb-3" | ||
| /> | ||
| )} | ||
|
|
||
| {/* 编辑器内容 */} | ||
| {editMode === 'visual' ? ( | ||
| <div> | ||
| {renderVisualEditor()} | ||
| {/* 隐藏的Form字段用于验证和数据绑定 */} | ||
| <Form.Input | ||
| field={field} | ||
| value={value} | ||
| rules={rules} | ||
| style={{ display: 'none' }} | ||
| noLabel={true} | ||
| {...props} | ||
| /> | ||
| </div> | ||
| ) : ( | ||
| <div> | ||
| <TextArea | ||
| placeholder={placeholder} | ||
| value={value} | ||
| onChange={handleManualChange} | ||
| showClear={showClear} | ||
| rows={Math.max(8, value ? value.split('\n').length : 8)} | ||
| /> | ||
| {/* 隐藏的Form字段用于验证和数据绑定 */} | ||
| <Form.Input | ||
| field={field} | ||
| value={value} | ||
| rules={rules} | ||
| style={{ display: 'none' }} | ||
| noLabel={true} | ||
| {...props} | ||
| /> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* 额外文本显示在卡片底部 */} |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Translate final Chinese comments to English
Replace Chinese comments with English:
- Line 591:
{/* JSON错误提示 */}→{/* JSON error message */} - Line 600:
{/* 编辑器内容 */}→{/* Editor content */} - Line 604:
{/* 隐藏的Form字段用于验证和数据绑定 */}→{/* Hidden Form field for validation and data binding */} - Line 623:
{/* 隐藏的Form字段用于验证和数据绑定 */}→{/* Hidden Form field for validation and data binding */} - Line 635:
{/* 额外文本显示在卡片底部 */}→{/* Extra text displayed at the bottom of the card */}
🤖 Prompt for AI Agents
In web/src/components/common/ui/JSONEditor.js around lines 591 to 635, replace
all Chinese comments with their English equivalents as specified: change
"JSON错误提示" to "JSON error message", "编辑器内容" to "Editor content", both instances
of "隐藏的Form字段用于验证和数据绑定" to "Hidden Form field for validation and data binding",
and "额外文本显示在卡片底部" to "Extra text displayed at the bottom of the card".
… fix endpoints rendering, and clean up deps
- Why
- Needed to separate help text from action buttons in JSONEditor for better layout and UX.
- Models table should robustly render both new object-based endpoint mappings and legacy arrays.
- Columns should re-render when vendor map changes.
- Minor import cleanups for consistency.
- What
- JSONEditor.js
- Added optional prop extraFooter to render content below the extraText divider.
- Kept extraText rendered via Divider; extraFooter appears on the next line for clear separation.
- EditModelModal.jsx
- Moved endpoint group buttons from extraText into extraFooter to display under the helper text.
- Kept merge-logic: group items are merged into current endpoints JSON with key override semantics.
- Consolidated lucide-react imports into a single line.
- ModelsColumnDefs.js
- Made endpoint renderer resilient:
- Supports object-based JSON (keys as endpoint types) and legacy array format.
- Displays keys/items as tags and limits the number shown; uses stringToColor for visual consistency.
- Consolidated Semi UI imports into a single line.
- ModelsTable.jsx
- Fixed columns memoization dependency to include vendorMap, ensuring re-render when vendor data changes.
- Notes
- Backward-compatible: extraFooter is additive; existing JSONEditor usage remains unchanged.
- No API changes to backend.
- No linter errors introduced.
- Files touched
- web/src/components/common/ui/JSONEditor.js
- web/src/components/table/models/modals/EditModelModal.jsx
- web/src/components/table/models/ModelsColumnDefs.js
- web/src/components/table/models/ModelsTable.jsx
- Impact
- Clearer UI for endpoint editing (buttons now below helper text).
- Correct endpoints display for object-based mappings in models list.
- More reliable reactivity when vendor data updates.
…” card in EditPrefillGroupModal - Move `items` field (`JSONEditor` for endpoint type, `Form.TagInput` otherwise) into the first “Basic Information” card - Remove the second “Content Configuration” card and its header; consolidate to a single-card layout - Preserve form initialization, validation, and submit logic; API payload structure remains unchanged - Improves clarity and reduces visual clutter without altering behavior - Lint passes Affected file: - `web/src/components/table/models/modals/EditPrefillGroupModal.jsx` No breaking changes.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (14)
web/src/components/table/models/modals/EditModelModal.jsx (4)
79-81: Add error notifications for failed API callsThe error is silently caught without notifying the user when vendor loading fails.
} catch (error) { - // ignore + showError(t('Failed to load vendors')); }
97-99: Add error notifications for failed API callsThe error is silently caught without notifying the user when prefill groups loading fails.
} catch (error) { - // ignore + showError(t('Failed to load prefill groups')); }
143-147: Validate vendor_id exists in vendors listWhen loading a model for editing, the vendor_id might reference a non-existent vendor if the vendor was deleted or hasn't loaded yet.
// 处理status,将数字转为布尔值 data.status = data.status === 1; + // Validate vendor_id exists + if (data.vendor_id && !vendors.find(v => v.id === data.vendor_id)) { + console.warn(`Vendor ID ${data.vendor_id} not found in vendors list`); + data.vendor_id = undefined; + } if (formApiRef.current) { formApiRef.current.setValues({ ...getInitValues(), ...data }); }
218-220: Move form reset to success path onlyThe form reset happens regardless of submission success or failure. This might clear user input even when submission fails, forcing users to re-enter data.
showSuccess(t('模型创建成功!')); props.refresh(); props.handleClose(); + formApiRef.current?.setValues(getInitValues()); } else { showError(t(message)); } } } catch (error) { showError(error.response?.data?.message || t('操作失败')); } setLoading(false); - formApiRef.current?.setValues(getInitValues());web/src/components/common/ui/JSONEditor.js (10)
386-388: Handle or document the ignored parse errorEmpty catch blocks can hide errors and make debugging difficult.
} catch { - // ignore parse error + // Invalid JSON input - keep current value unchanged + // User will see the invalid JSON and can correct it }
46-77: Use English comments for consistency and optimize duplicate JSON parsingThe code contains Chinese comments and parses the same JSON twice during initialization.
- // 初始化JSON数据 + // Initialize JSON data const [jsonData, setJsonData] = useState(() => { - // 初始化时解析JSON数据 + // Parse JSON data on initialization if (typeof value === 'string' && value.trim()) { try { const parsed = JSON.parse(value); return parsed; } catch (error) { return {}; } } if (typeof value === 'object' && value !== null) { return value; } return {}; }); - // 根据键数量决定默认编辑模式 + // Determine default edit mode based on key count const [editMode, setEditMode] = useState(() => { - // 如果初始JSON数据的键数量大于10个,则默认使用手动模式 - if (typeof value === 'string' && value.trim()) { - try { - const parsed = JSON.parse(value); - const keyCount = Object.keys(parsed).length; - return keyCount > 10 ? 'manual' : 'visual'; - } catch (error) { - // JSON无效时默认显示手动编辑模式 - return 'manual'; - } - } - return 'visual'; + // Reuse the parsed jsonData to avoid duplicate parsing + const keyCount = Object.keys(jsonData).length; + return keyCount > 10 ? 'manual' : 'visual'; });Note: You'll need to restructure the initialization to avoid the circular dependency.
80-96: Remove console.log and use English commentsProduction code should not contain console.log statements, and comments should be in English.
- // 数据同步 - 当value变化时总是更新jsonData(如果JSON有效) + // Data sync - always update jsonData when value changes (if JSON is valid) useEffect(() => { try { let parsed = {}; if (typeof value === 'string' && value.trim()) { parsed = JSON.parse(value); } else if (typeof value === 'object' && value !== null) { parsed = value; } setJsonData(parsed); setJsonError(''); } catch (error) { - console.log('JSON解析失败:', error.message); + // JSON parsing failed - handled by setting error state setJsonError(error.message); - // JSON格式错误时不更新jsonData + // Don't update jsonData when JSON format is invalid } }, [value]);
98-218: Translate Chinese comments to EnglishMultiple handler functions contain Chinese comments that should be in English for consistency.
Replace all Chinese comments with English translations throughout these handler functions:
- Line 98:
// 处理可视化编辑的数据变化→// Handle visual editing data changes- Line 112:
// 处理手动编辑的数据变化→// Handle manual editing data changes- Line 115:
// 验证JSON格式→// Validate JSON format- Line 130:
// 切换编辑模式→// Toggle edit mode- Line 155:
// 添加键值对→// Add key-value pair- Line 169:
// 删除键值对→// Remove key-value pair- Line 176:
// 更新键名→// Update key name- Line 190:
// 更新值→// Update value- Line 197:
// 填入模板→// Fill template
161-161: Use Object.prototype.hasOwnProperty for safetyDirect use of
hasOwnPropertycan fail if the object has a property with the same name.- while (newData.hasOwnProperty(newKey)) { + while (Object.prototype.hasOwnProperty.call(newData, newKey)) {
284-544: Translate remaining Chinese comments to EnglishReplace Chinese comments with English:
- Line 284:
// 添加嵌套对象→// Add nested object- Line 316:
// 渲染参数值输入控件(支持嵌套)→// Render parameter value input control (supports nesting)- Line 348:
// 渲染嵌套对象→// Render nested object- Line 449:
// 字符串或其他原始类型→// String or other primitive types- Line 469:
// 将当前值转换为对象→// Convert current value to object- Line 480:
// 渲染区域编辑器(特殊格式)→// Render region editor (special format)- Line 488:
/* 默认区域 */→/* Default region */- Line 497:
/* 模型专用区域 */→/* Model-specific regions */- Line 544:
// 渲染可视化编辑器→// Render visual editor
308-308: Use Object.prototype.hasOwnProperty for safetySame issue as before - direct use of
hasOwnPropertycan fail.- while (newData[parentKey].hasOwnProperty(newKey)) { + while (Object.prototype.hasOwnProperty.call(newData[parentKey], newKey)) {
401-401: Fix incorrect numeric conversion logicThe condition
newValue !== '0'will prevent the string '0' from being converted to the number 0, which is likely unintended.- else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { + else if (!isNaN(newValue) && newValue !== '') {
459-459: Fix the same numeric conversion issueSame problem with preventing '0' from being converted to number.
- else if (!isNaN(newValue) && newValue !== '' && newValue !== '0') { + else if (!isNaN(newValue) && newValue !== '') {
593-647: Translate final Chinese comments to EnglishReplace Chinese comments with English:
- Line 593:
{/* JSON错误提示 */}→{/* JSON error message */}- Line 602:
{/* 编辑器内容 */}→{/* Editor content */}- Line 606:
{/* 隐藏的Form字段用于验证和数据绑定 */}→{/* Hidden Form field for validation and data binding */}- Line 625:
{/* 隐藏的Form字段用于验证和数据绑定 */}→{/* Hidden Form field for validation and data binding */}- Line 637:
{/* 额外文本显示在卡片底部 */}→{/* Extra text displayed at the bottom of the card */}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
web/src/components/common/ui/JSONEditor.js(1 hunks)web/src/components/table/models/ModelsColumnDefs.js(1 hunks)web/src/components/table/models/ModelsTable.jsx(1 hunks)web/src/components/table/models/modals/EditModelModal.jsx(1 hunks)web/src/components/table/models/modals/EditPrefillGroupModal.jsx(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- web/src/components/table/models/ModelsTable.jsx
🚧 Files skipped from review as they are similar to previous changes (2)
- web/src/components/table/models/modals/EditPrefillGroupModal.jsx
- web/src/components/table/models/ModelsColumnDefs.js
| const nameRuleOptions = [ | ||
| { label: '精确名称匹配', value: 0 }, | ||
| { label: '前缀名称匹配', value: 1 }, | ||
| { label: '包含名称匹配', value: 2 }, | ||
| { label: '后缀名称匹配', value: 3 }, | ||
| ]; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Wrap Chinese text with translation function
The label values in nameRuleOptions contain hardcoded Chinese text. These should be translated in the component where they're used, not hardcoded here.
const nameRuleOptions = [
- { label: '精确名称匹配', value: 0 },
- { label: '前缀名称匹配', value: 1 },
- { label: '包含名称匹配', value: 2 },
- { label: '后缀名称匹配', value: 3 },
+ { label: 'exact_match', value: 0 },
+ { label: 'prefix_match', value: 1 },
+ { label: 'contains_match', value: 2 },
+ { label: 'suffix_match', value: 3 },
];Then translate these keys when rendering in line 307:
-optionList={nameRuleOptions.map(o => ({ label: t(o.label), value: o.value }))}
+optionList={nameRuleOptions.map(o => ({ label: t(`name_rule.${o.label}`), value: o.value }))}📝 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.
| const nameRuleOptions = [ | |
| { label: '精确名称匹配', value: 0 }, | |
| { label: '前缀名称匹配', value: 1 }, | |
| { label: '包含名称匹配', value: 2 }, | |
| { label: '后缀名称匹配', value: 3 }, | |
| ]; | |
| const nameRuleOptions = [ | |
| { label: 'exact_match', value: 0 }, | |
| { label: 'prefix_match', value: 1 }, | |
| { label: 'contains_match', value: 2 }, | |
| { label: 'suffix_match', value: 3 }, | |
| ]; |
🤖 Prompt for AI Agents
In web/src/components/table/models/modals/EditModelModal.jsx lines 49 to 54, the
label values in the nameRuleOptions array are hardcoded Chinese text. Remove the
Chinese text from this array and replace them with translation keys or
identifiers instead. Then, in the component rendering at line 307, wrap the
labels with the translation function to display the correct localized text
dynamically.
| onClick={() => { | ||
| try { | ||
| const current = formApiRef.current?.getValue('endpoints') || ''; | ||
| let base = {}; | ||
| if (current && current.trim()) base = JSON.parse(current); | ||
| const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {}); | ||
| const merged = { ...base, ...groupObj }; | ||
| formApiRef.current?.setValue('endpoints', JSON.stringify(merged, null, 2)); | ||
| } catch (e) { | ||
| try { | ||
| const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {}); | ||
| formApiRef.current?.setValue('endpoints', JSON.stringify(groupObj, null, 2)); | ||
| } catch { } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Simplify error handling and provide user feedback
The nested try-catch blocks make error handling complex and difficult to understand. Consider simplifying and providing user feedback when JSON parsing fails.
onClick={() => {
- try {
- const current = formApiRef.current?.getValue('endpoints') || '';
- let base = {};
- if (current && current.trim()) base = JSON.parse(current);
- const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {});
- const merged = { ...base, ...groupObj };
- formApiRef.current?.setValue('endpoints', JSON.stringify(merged, null, 2));
- } catch (e) {
- try {
- const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {});
- formApiRef.current?.setValue('endpoints', JSON.stringify(groupObj, null, 2));
- } catch { }
- }
+ const current = formApiRef.current?.getValue('endpoints') || '';
+ let base = {};
+ let groupObj = {};
+
+ // Parse current value
+ if (current && current.trim()) {
+ try {
+ base = JSON.parse(current);
+ } catch (e) {
+ showError(t('Current endpoints JSON is invalid'));
+ return;
+ }
+ }
+
+ // Parse group items
+ try {
+ groupObj = typeof group.items === 'string'
+ ? JSON.parse(group.items || '{}')
+ : (group.items || {});
+ } catch (e) {
+ showError(t('Prefill group JSON is invalid'));
+ return;
+ }
+
+ const merged = { ...base, ...groupObj };
+ formApiRef.current?.setValue('endpoints', JSON.stringify(merged, null, 2));
}}📝 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.
| onClick={() => { | |
| try { | |
| const current = formApiRef.current?.getValue('endpoints') || ''; | |
| let base = {}; | |
| if (current && current.trim()) base = JSON.parse(current); | |
| const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {}); | |
| const merged = { ...base, ...groupObj }; | |
| formApiRef.current?.setValue('endpoints', JSON.stringify(merged, null, 2)); | |
| } catch (e) { | |
| try { | |
| const groupObj = typeof group.items === 'string' ? JSON.parse(group.items || '{}') : (group.items || {}); | |
| formApiRef.current?.setValue('endpoints', JSON.stringify(groupObj, null, 2)); | |
| } catch { } | |
| } | |
| onClick={() => { | |
| const current = formApiRef.current?.getValue('endpoints') || ''; | |
| let base = {}; | |
| let groupObj = {}; | |
| // Parse current value | |
| if (current && current.trim()) { | |
| try { | |
| base = JSON.parse(current); | |
| } catch (e) { | |
| showError(t('Current endpoints JSON is invalid')); | |
| return; | |
| } | |
| } | |
| // Parse group items | |
| try { | |
| groupObj = typeof group.items === 'string' | |
| ? JSON.parse(group.items || '{}') | |
| : (group.items || {}); | |
| } catch (e) { | |
| showError(t('Prefill group JSON is invalid')); | |
| return; | |
| } | |
| const merged = { ...base, ...groupObj }; | |
| formApiRef.current?.setValue( | |
| 'endpoints', | |
| JSON.stringify(merged, null, 2) | |
| ); | |
| }} |
🤖 Prompt for AI Agents
In web/src/components/table/models/modals/EditModelModal.jsx around lines 401 to
414, the nested try-catch blocks for JSON parsing complicate error handling and
lack user feedback. Refactor by consolidating the parsing logic into a single
try-catch block, and in the catch block, provide clear user feedback (e.g., an
alert or error message) indicating the JSON parsing failure instead of silently
failing. This will simplify the code and improve user experience.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
web/src/components/table/models/modals/EditPrefillGroupModal.jsx (1)
86-87: Fix inconsistent edit condition check.The condition for checking edit mode is inconsistent. Line 58 uses
editingGroup && editingGroup.id !== undefinedforisEdit, but line 86 directly checkseditingGroup.idwhich could cause issues ifeditingGroupis null or undefined.Apply this diff to fix the inconsistency:
- if (editingGroup.id) { + if (isEdit) {
🧹 Nitpick comments (2)
web/src/components/table/models/modals/EditPrefillGroupModal.jsx (2)
227-238: Simplify JSONEditor value prop for better readability.The inline ternary expression for the JSONEditor value prop is complex and could cause unnecessary re-renders. Consider extracting this logic to a useMemo hook or computed value.
+ const jsonEditorValue = useMemo(() => { + if (selectedType !== 'endpoint') return ''; + const currentValue = formRef.current?.getValue('items'); + if (currentValue !== undefined) return currentValue; + return typeof editingGroup?.items === 'string' + ? editingGroup.items + : JSON.stringify(editingGroup?.items || {}, null, 2); + }, [selectedType, editingGroup?.items]); {selectedType === 'endpoint' ? ( <JSONEditor field="items" label={t('端点映射')} - value={formRef.current?.getValue('items') ?? (typeof editingGroup?.items === 'string' ? editingGroup.items : JSON.stringify(editingGroup.items || {}, null, 2))} + value={jsonEditorValue} onChange={(val) => formRef.current?.setValue('items', val)} editorType='object' placeholder={'{\n "openai": {"path": "/v1/chat/completions", "method": "POST"}\n}'} template={ENDPOINT_TEMPLATE} templateLabel={t('填入模板')} extraText={t('键为端点类型,值为路径和方法对象')} /> ) : (
166-180: Extract complex initialization logic to improve maintainability.The IIFE for items initialization is complex and difficult to test. Consider extracting this to a separate function or using useMemo for better readability and testability.
+ const getInitialItems = useCallback(() => { + try { + if (editingGroup?.type === 'endpoint') { + return typeof editingGroup?.items === 'string' + ? editingGroup.items + : JSON.stringify(editingGroup.items || {}, null, 2); + } + return Array.isArray(editingGroup?.items) ? editingGroup.items : []; + } catch { + return editingGroup?.type === 'endpoint' ? '' : []; + } + }, [editingGroup?.type, editingGroup?.items]); initValues={{ name: editingGroup?.name || '', type: editingGroup?.type || 'tag', description: editingGroup?.description || '', - items: (() => { - try { - if (editingGroup?.type === 'endpoint') { - // 保持原始字符串 - return typeof editingGroup?.items === 'string' - ? editingGroup.items - : JSON.stringify(editingGroup.items || {}, null, 2); - } - return Array.isArray(editingGroup?.items) - ? editingGroup.items - : []; - } catch { - return editingGroup?.type === 'endpoint' ? '' : []; - } - })(), + items: getInitialItems(), }}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
web/src/components/table/models/modals/EditPrefillGroupModal.jsx(1 hunks)
🔇 Additional comments (2)
web/src/components/table/models/modals/EditPrefillGroupModal.jsx (2)
20-52: LGTM! Well-structured imports and helpful template.The imports are comprehensive and the ENDPOINT_TEMPLATE provides useful defaults for different API endpoint types, enhancing user experience.
53-71: LGTM! Proper state management and edit condition handling.The component properly manages state with appropriate hooks and the
isEditcondition correctly checks botheditingGroupexistence andidpresence, which is consistent and safe.
…r PrefillGroup.Items - Why: Avoid adding `gorm.io/datatypes` for a single field; the rest of the codebase does not use it, and using the standard library keeps dependencies lean. - What: - Switched `PrefillGroup.Items` from `datatypes.JSON` to `json.RawMessage`. - Updated imports in `model/prefill_group.go` to use `encoding/json` and removed the unused `gorm.io/datatypes`. - Preserved `gorm:"type:json"` so DB column behavior remains the same. - Impact: - API response/request shape for `items` remains unchanged (still JSON). - DB schema behavior is unchanged; GORM migration continues to handle the field as JSON. - No other references to `datatypes` exist; no `go.mod` changes needed. - Lints pass for the modified file. Files changed: - model/prefill_group.go No breaking changes.
…illGroup.Items; fix JSON scan across drivers - Why: - Avoid introducing `gorm.io/datatypes` for a single field. - Align with existing pattern (`ChannelInfo`, `Properties`) using `Scanner`/`Valuer`. - Fix runtime error when drivers return JSON as string. - What: - Introduced `JSONValue` (based on `json.RawMessage`) implementing `sql.Scanner` and `driver.Valuer`, with `MarshalJSON`/`UnmarshalJSON` to preserve raw JSON in API. - Updated `PrefillGroup.Items` to use `JSONValue` with `gorm:"type:json"`. - Localized comments in `model/prefill_group.go` to Chinese. - Impact: - Resolves “unsupported Scan, storing driver.Value type string into type *json.RawMessage”. - Works with MySQL/Postgres/SQLite whether JSON is returned as `[]byte` or `string`. - API and DB schema remain unchanged; no `go.mod` changes; lints pass. Files changed: - model/prefill_group.go
…ize JSONEditor manual mode
- Why:
- Eliminate `gorm.io/datatypes` for a single field and fix scan errors when drivers return JSON as string.
- Prevent JSONEditor manual mode from locking on invalid JSON and from appending stray characters after “Fill Template”.
- What:
- Backend (`model/prefill_group.go`):
- Replaced `datatypes.JSON` with `JSONValue` (based on `json.RawMessage`) for `PrefillGroup.Items`.
- Implemented `sql.Scanner` and `driver.Valuer` to accept both `[]byte` and `string`.
- Implemented `MarshalJSON`/`UnmarshalJSON` to preserve raw JSON in API without base64.
- Converted comments to Chinese.
- Frontend (`web/src/components/common/ui/JSONEditor.js`):
- Added `manualText` buffer for manual mode to avoid input being overridden by external value.
- Only propagate `onChange` when manual text is valid JSON; otherwise show error but do not block typing.
- Safe manual-mode rendering: derive rows from `manualText` and avoid calling `split` on non-strings.
- Improved mode toggle: populate `manualText` from visual data; validate before switching back to visual.
- Fixed “Fill Template” to sync `manualText`, `jsonData`, and `onChange` to avoid stray trailing characters.
- Impact:
- Resolves: “unsupported Scan, storing driver.Value type string into type *json.RawMessage”.
- Resolves: `value.split is not a function` in manual mode.
- Resolves: extra `s` appended after inserting template.
- API shape and DB column type remain the same (`gorm:"type:json"`); no `go.mod` changes.
- Lints pass for modified files.
Files changed:
- model/prefill_group.go
- web/src/components/common/ui/JSONEditor.js
Merge pull request QuantumNous#1452 from QuantumNous/refactor/model-pricing
Summary by CodeRabbit
New Features
Enhancements
Bug Fixes
Chores