Merge remote-tracking branch 'origin/main' into refactor/ui

This commit is contained in:
t0ng7u
2026-07-11 23:07:05 +08:00
9 changed files with 63 additions and 18 deletions
@@ -18,9 +18,10 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { Code2, Eye, RotateCcw, Save } from 'lucide-react' import { Code2, Eye, RotateCcw, Save } from 'lucide-react'
import { memo, useCallback, useRef, useState } from 'react' import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { type UseFormReturn } from 'react-hook-form' import type { UseFormReturn } from 'react-hook-form'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { Button } from '@/components/design-system/button' import { Button } from '@/components/design-system/button'
import { JsonCodeEditor } from '@/components/json-code-editor' import { JsonCodeEditor } from '@/components/json-code-editor'
@@ -180,6 +181,18 @@ export const ModelRatioForm = memo(function ModelRatioForm({
enabled: isUnsetVariant, enabled: isUnsetVariant,
}) })
const enabledModelsError = isUnsetVariant
? enabledModelsQuery.isError ||
(enabledModelsQuery.data !== undefined &&
!enabledModelsQuery.data.success)
: false
const enabledModelsErrorMessage = enabledModelsQuery.data?.message
useEffect(() => {
if (!enabledModelsError) return
toast.error(enabledModelsErrorMessage || t('Failed to load enabled models'))
}, [enabledModelsError, enabledModelsErrorMessage, t])
const handleFieldChange = useCallback( const handleFieldChange = useCallback(
(field: keyof ModelFormValues, value: string) => { (field: keyof ModelFormValues, value: string) => {
form.setValue(field, value, { form.setValue(field, value, {
@@ -266,6 +279,9 @@ export const ModelRatioForm = memo(function ModelRatioForm({
candidateModelNames={ candidateModelNames={
isUnsetVariant ? enabledModelsQuery.data?.data : undefined isUnsetVariant ? enabledModelsQuery.data?.data : undefined
} }
candidateModelsLoading={
isUnsetVariant && enabledModelsQuery.isLoading
}
filterMode={isUnsetVariant ? 'unset' : 'all'} filterMode={isUnsetVariant ? 'unset' : 'all'}
onSave={handleSave} onSave={handleSave}
isSaving={isSaving} isSaving={isSaving}
@@ -16,13 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { import type {
type ColumnFiltersState, ColumnFiltersState,
type OnChangeFn, OnChangeFn,
type PaginationState, PaginationState,
type RowSelectionState, RowSelectionState,
type VisibilityState, VisibilityState,
type SortingState, SortingState,
} from '@tanstack/react-table' } from '@tanstack/react-table'
import { Copy, Plus } from 'lucide-react' import { Copy, Plus } from 'lucide-react'
import { import {
@@ -51,6 +51,7 @@ import { combineBillingExpr } from '@/features/pricing/lib/billing-expr'
import { useMediaQuery } from '@/hooks' import { useMediaQuery } from '@/hooks'
import { safeJsonParse } from '../utils/json-parser' import { safeJsonParse } from '../utils/json-parser'
import type { PricingMode } from './model-pricing-core'
import { import {
ModelPricingEditorPanel, ModelPricingEditorPanel,
type ModelPricingEditorPanelHandle, type ModelPricingEditorPanelHandle,
@@ -87,6 +88,7 @@ type ModelRatioVisualEditorProps = {
billingMode: string billingMode: string
billingExpr: string billingExpr: string
candidateModelNames?: string[] candidateModelNames?: string[]
candidateModelsLoading?: boolean
filterMode?: 'all' | 'unset' filterMode?: 'all' | 'unset'
onChange: (field: string, value: string) => void onChange: (field: string, value: string) => void
onSave: () => void | Promise<void> onSave: () => void | Promise<void>
@@ -125,6 +127,7 @@ const ModelRatioVisualEditorComponent = forwardRef<
billingMode, billingMode,
billingExpr, billingExpr,
candidateModelNames, candidateModelNames,
candidateModelsLoading,
filterMode = 'all', filterMode = 'all',
onChange, onChange,
onSave, onSave,
@@ -219,7 +222,7 @@ const ModelRatioVisualEditorComponent = forwardRef<
...draftByName.keys(), ...draftByName.keys(),
]) ])
return Array.from(modelNames) return [...modelNames]
.map((name) => { .map((name) => {
const saved = savedByName.get(name) const saved = savedByName.get(name)
const draft = draftByName.get(name) const draft = draftByName.get(name)
@@ -289,6 +292,12 @@ const ModelRatioVisualEditorComponent = forwardRef<
const handleEdit = useCallback( const handleEdit = useCallback(
(model: ModelRow) => { (model: ModelRow) => {
const editableModel = model.draft ?? model.saved ?? model const editableModel = model.draft ?? model.saved ?? model
let editBillingMode: PricingMode = 'per-token'
if (editableModel.billingMode === 'tiered_expr') {
editBillingMode = 'tiered_expr'
} else if (editableModel.price && editableModel.price !== '') {
editBillingMode = 'per-request'
}
setEditData({ setEditData({
name: editableModel.name, name: editableModel.name,
price: editableModel.price, price: editableModel.price,
@@ -299,12 +308,7 @@ const ModelRatioVisualEditorComponent = forwardRef<
imageRatio: editableModel.imageRatio, imageRatio: editableModel.imageRatio,
audioRatio: editableModel.audioRatio, audioRatio: editableModel.audioRatio,
audioCompletionRatio: editableModel.audioCompletionRatio, audioCompletionRatio: editableModel.audioCompletionRatio,
billingMode: billingMode: editBillingMode,
editableModel.billingMode === 'tiered_expr'
? 'tiered_expr'
: editableModel.price && editableModel.price !== ''
? 'per-request'
: 'per-token',
billingExpr: editableModel.billingExpr, billingExpr: editableModel.billingExpr,
requestRuleExpr: editableModel.requestRuleExpr, requestRuleExpr: editableModel.requestRuleExpr,
}) })
@@ -632,7 +636,11 @@ const ModelRatioVisualEditorComponent = forwardRef<
return return
} }
persistPricingData(sourceData, targetNames) // Persist to the source model too, so targets never carry pricing the
// source itself would lose if the editor draft were abandoned.
persistPricingData(sourceData, [
...new Set([sourceData.name, ...targetNames]),
])
table.resetRowSelection() table.resetRowSelection()
toast.success( toast.success(
t('Applied {{name}} pricing to {{count}} models', { t('Applied {{name}} pricing to {{count}} models', {
@@ -663,7 +671,9 @@ const ModelRatioVisualEditorComponent = forwardRef<
if (table.getState().globalFilter) { if (table.getState().globalFilter) {
emptyStateText = t('No models match your search') emptyStateText = t('No models match your search')
} else if (filterMode === 'unset') { } else if (filterMode === 'unset') {
emptyStateText = t('No models with unset prices') emptyStateText = candidateModelsLoading
? t('Loading...')
: t('No models with unset prices')
} }
return ( return (
@@ -821,6 +831,17 @@ export const ModelRatioVisualEditor = memo(
// Custom equality check - only re-render if JSON props actually changed // Custom equality check - only re-render if JSON props actually changed
(prevProps, nextProps) => { (prevProps, nextProps) => {
return ( return (
prevProps.savedModelPrice === nextProps.savedModelPrice &&
prevProps.savedModelRatio === nextProps.savedModelRatio &&
prevProps.savedCacheRatio === nextProps.savedCacheRatio &&
prevProps.savedCreateCacheRatio === nextProps.savedCreateCacheRatio &&
prevProps.savedCompletionRatio === nextProps.savedCompletionRatio &&
prevProps.savedImageRatio === nextProps.savedImageRatio &&
prevProps.savedAudioRatio === nextProps.savedAudioRatio &&
prevProps.savedAudioCompletionRatio ===
nextProps.savedAudioCompletionRatio &&
prevProps.savedBillingMode === nextProps.savedBillingMode &&
prevProps.savedBillingExpr === nextProps.savedBillingExpr &&
prevProps.modelPrice === nextProps.modelPrice && prevProps.modelPrice === nextProps.modelPrice &&
prevProps.modelRatio === nextProps.modelRatio && prevProps.modelRatio === nextProps.modelRatio &&
prevProps.cacheRatio === nextProps.cacheRatio && prevProps.cacheRatio === nextProps.cacheRatio &&
@@ -832,6 +853,7 @@ export const ModelRatioVisualEditor = memo(
prevProps.billingMode === nextProps.billingMode && prevProps.billingMode === nextProps.billingMode &&
prevProps.billingExpr === nextProps.billingExpr && prevProps.billingExpr === nextProps.billingExpr &&
prevProps.candidateModelNames === nextProps.candidateModelNames && prevProps.candidateModelNames === nextProps.candidateModelNames &&
prevProps.candidateModelsLoading === nextProps.candidateModelsLoading &&
prevProps.filterMode === nextProps.filterMode && prevProps.filterMode === nextProps.filterMode &&
prevProps.onChange === nextProps.onChange && prevProps.onChange === nextProps.onChange &&
prevProps.onSave === nextProps.onSave && prevProps.onSave === nextProps.onSave &&
+1
View File
@@ -1842,6 +1842,7 @@
"Failed to load": "Failed to load", "Failed to load": "Failed to load",
"Failed to load API keys": "Failed to load API keys", "Failed to load API keys": "Failed to load API keys",
"Failed to load billing history": "Failed to load billing history", "Failed to load billing history": "Failed to load billing history",
"Failed to load enabled models": "Failed to load enabled models",
"Failed to load home page content": "Failed to load home page content", "Failed to load home page content": "Failed to load home page content",
"Failed to load image": "Failed to load image", "Failed to load image": "Failed to load image",
"Failed to load key status": "Failed to load key status", "Failed to load key status": "Failed to load key status",
+1
View File
@@ -1842,6 +1842,7 @@
"Failed to load": "Échec du chargement", "Failed to load": "Échec du chargement",
"Failed to load API keys": "Échec du chargement des Clés API", "Failed to load API keys": "Échec du chargement des Clés API",
"Failed to load billing history": "Échec du chargement de l'historique de facturation", "Failed to load billing history": "Échec du chargement de l'historique de facturation",
"Failed to load enabled models": "Échec du chargement des modèles activés",
"Failed to load home page content": "Échec du chargement du contenu de la page d'accueil", "Failed to load home page content": "Échec du chargement du contenu de la page d'accueil",
"Failed to load image": "Échec du chargement de l'image", "Failed to load image": "Échec du chargement de l'image",
"Failed to load key status": "Échec du chargement du statut des clés", "Failed to load key status": "Échec du chargement du statut des clés",
+1
View File
@@ -1842,6 +1842,7 @@
"Failed to load": "読み込みに失敗しました", "Failed to load": "読み込みに失敗しました",
"Failed to load API keys": "APIキーの読み込みに失敗しました", "Failed to load API keys": "APIキーの読み込みに失敗しました",
"Failed to load billing history": "請求履歴の読み込みに失敗しました", "Failed to load billing history": "請求履歴の読み込みに失敗しました",
"Failed to load enabled models": "有効なモデルの取得に失敗しました",
"Failed to load home page content": "ホームページの内容の読み込みに失敗しました", "Failed to load home page content": "ホームページの内容の読み込みに失敗しました",
"Failed to load image": "画像の読み込みに失敗しました", "Failed to load image": "画像の読み込みに失敗しました",
"Failed to load key status": "キー状態の読み込みに失敗しました", "Failed to load key status": "キー状態の読み込みに失敗しました",
+1
View File
@@ -1842,6 +1842,7 @@
"Failed to load": "Не удалось загрузить", "Failed to load": "Не удалось загрузить",
"Failed to load API keys": "Не удалось загрузить API ключи", "Failed to load API keys": "Не удалось загрузить API ключи",
"Failed to load billing history": "Не удалось загрузить историю платежей", "Failed to load billing history": "Не удалось загрузить историю платежей",
"Failed to load enabled models": "Не удалось загрузить включённые модели",
"Failed to load home page content": "Не удалось загрузить содержимое главной страницы", "Failed to load home page content": "Не удалось загрузить содержимое главной страницы",
"Failed to load image": "Не удалось загрузить изображение", "Failed to load image": "Не удалось загрузить изображение",
"Failed to load key status": "Не удалось загрузить статус ключей", "Failed to load key status": "Не удалось загрузить статус ключей",
+1
View File
@@ -1842,6 +1842,7 @@
"Failed to load": "Tải thất bại", "Failed to load": "Tải thất bại",
"Failed to load API keys": "Không thể tải khóa API", "Failed to load API keys": "Không thể tải khóa API",
"Failed to load billing history": "Không thể tải lịch sử thanh toán", "Failed to load billing history": "Không thể tải lịch sử thanh toán",
"Failed to load enabled models": "Không thể tải các mô hình đã bật",
"Failed to load home page content": "Không thể tải nội dung trang chủ", "Failed to load home page content": "Không thể tải nội dung trang chủ",
"Failed to load image": "Không thể tải ảnh", "Failed to load image": "Không thể tải ảnh",
"Failed to load key status": "Không thể tải trạng thái khóa", "Failed to load key status": "Không thể tải trạng thái khóa",
+1
View File
@@ -1842,6 +1842,7 @@
"Failed to load": "載入失敗", "Failed to load": "載入失敗",
"Failed to load API keys": "載入 API 金鑰失敗", "Failed to load API keys": "載入 API 金鑰失敗",
"Failed to load billing history": "載入收費歷史失敗", "Failed to load billing history": "載入收費歷史失敗",
"Failed to load enabled models": "獲取啟用模型失敗",
"Failed to load home page content": "載入首頁內容失敗", "Failed to load home page content": "載入首頁內容失敗",
"Failed to load image": "無法載入圖像", "Failed to load image": "無法載入圖像",
"Failed to load key status": "載入金鑰狀態失敗", "Failed to load key status": "載入金鑰狀態失敗",
+1
View File
@@ -1842,6 +1842,7 @@
"Failed to load": "加载失败", "Failed to load": "加载失败",
"Failed to load API keys": "加载 API 密钥失败", "Failed to load API keys": "加载 API 密钥失败",
"Failed to load billing history": "加载计费历史失败", "Failed to load billing history": "加载计费历史失败",
"Failed to load enabled models": "获取启用模型失败",
"Failed to load home page content": "加载首页内容失败", "Failed to load home page content": "加载首页内容失败",
"Failed to load image": "无法加载图像", "Failed to load image": "无法加载图像",
"Failed to load key status": "加载密钥状态失败", "Failed to load key status": "加载密钥状态失败",