perf(model-pricing): refine visual editor actions

- keep the global reset action in the top toolbar while moving visual-mode saves into the model editor footer.
- pin the actions header with the rest of the model table headers so horizontal scrolling keeps context visible.
- add action icons to make save and reset controls easier to scan.
This commit is contained in:
QuentinHsu
2026-06-05 01:04:47 +08:00
parent 6e5a359110
commit 5681c92b3f
4 changed files with 63 additions and 17 deletions
@@ -26,10 +26,11 @@ import {
} from 'react' } from 'react'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { AlertTriangle } from 'lucide-react' import { AlertTriangle, Save } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Alert, AlertDescription } from '@/components/ui/alert' import { Alert, AlertDescription } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { import {
Field, Field,
FieldDescription, FieldDescription,
@@ -86,6 +87,8 @@ type ModelPricingSheetProps = {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
editData?: ModelRatioData | null editData?: ModelRatioData | null
onSave?: () => void | Promise<void>
isSaving?: boolean
} }
type ModelPricingEditorPanelProps = Omit< type ModelPricingEditorPanelProps = Omit<
@@ -102,7 +105,10 @@ export type ModelPricingEditorPanelHandle = {
export const ModelPricingSheet = forwardRef< export const ModelPricingSheet = forwardRef<
ModelPricingEditorPanelHandle, ModelPricingEditorPanelHandle,
ModelPricingSheetProps ModelPricingSheetProps
>(function ModelPricingSheet({ open, onOpenChange, editData }, ref) { >(function ModelPricingSheet(
{ open, onOpenChange, editData, onSave, isSaving },
ref
) {
const { t } = useTranslation() const { t } = useTranslation()
const title = editData ? t('Edit model pricing') : t('Add model pricing') const title = editData ? t('Edit model pricing') : t('Add model pricing')
const description = editData?.name || t('New model') const description = editData?.name || t('New model')
@@ -120,6 +126,8 @@ export const ModelPricingSheet = forwardRef<
<ModelPricingEditorPanel <ModelPricingEditorPanel
ref={ref} ref={ref}
editData={editData} editData={editData}
onSave={onSave}
isSaving={isSaving}
className='h-full rounded-none border-0' className='h-full rounded-none border-0'
/> />
</SheetContent> </SheetContent>
@@ -130,7 +138,10 @@ export const ModelPricingSheet = forwardRef<
export const ModelPricingEditorPanel = forwardRef< export const ModelPricingEditorPanel = forwardRef<
ModelPricingEditorPanelHandle, ModelPricingEditorPanelHandle,
ModelPricingEditorPanelProps ModelPricingEditorPanelProps
>(function ModelPricingEditorPanel({ editData, className }, ref) { >(function ModelPricingEditorPanel(
{ editData, className, onSave, isSaving },
ref
) {
const { t } = useTranslation() const { t } = useTranslation()
const [pricingMode, setPricingMode] = useState<PricingMode>('per-token') const [pricingMode, setPricingMode] = useState<PricingMode>('per-token')
const [promptPrice, setPromptPrice] = useState('') const [promptPrice, setPromptPrice] = useState('')
@@ -461,6 +472,7 @@ export const ModelPricingEditorPanel = forwardRef<
) )
const activeName = watchedValues.name || editData?.name || t('New model') const activeName = watchedValues.name || editData?.name || t('New model')
const showActions = Boolean(onSave)
return ( return (
<div <div
@@ -488,7 +500,7 @@ export const ModelPricingEditorPanel = forwardRef<
className='flex min-h-0 flex-1 flex-col' className='flex min-h-0 flex-1 flex-col'
autoComplete='off' autoComplete='off'
> >
<div className='min-h-0 flex-1 overflow-y-auto p-4'> <div className='min-h-0 flex-1 overflow-y-auto p-4 pb-6'>
<div className='grid items-start gap-4 xl:grid-cols-[minmax(0,1fr)_minmax(220px,260px)]'> <div className='grid items-start gap-4 xl:grid-cols-[minmax(0,1fr)_minmax(220px,260px)]'>
<FieldGroup> <FieldGroup>
{warnings.length > 0 && ( {warnings.length > 0 && (
@@ -670,6 +682,23 @@ export const ModelPricingEditorPanel = forwardRef<
</aside> </aside>
</div> </div>
</div> </div>
{showActions && (
<div className='bg-background/95 supports-[backdrop-filter]:bg-background/80 shrink-0 border-t p-3 backdrop-blur'>
<div className='flex flex-col-reverse gap-2 sm:flex-row sm:justify-end'>
{onSave && (
<Button
type='button'
onClick={onSave}
disabled={isSaving}
className='w-full sm:w-auto'
>
<Save data-icon='inline-start' />
{isSaving ? t('Saving...') : t('Save model prices')}
</Button>
)}
</div>
</div>
)}
</form> </form>
</Form> </Form>
</div> </div>
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
import { memo, useCallback, useRef, useState } from 'react' import { memo, useCallback, useRef, useState } from 'react'
import { type UseFormReturn } from 'react-hook-form' import { type UseFormReturn } from 'react-hook-form'
import { Code2, Eye } from 'lucide-react' import { Code2, Eye, RotateCcw, Save } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
@@ -110,16 +110,20 @@ export const ModelRatioForm = memo(function ModelRatioForm({
onClick={onReset} onClick={onReset}
disabled={isResetting} disabled={isResetting}
> >
<RotateCcw data-icon='inline-start' />
{t('Reset prices')} {t('Reset prices')}
</Button> </Button>
{editMode === 'json' && (
<Button <Button
type='button' type='button'
size='sm' size='sm'
onClick={handleSave} onClick={handleSave}
disabled={isSaving} disabled={isSaving}
> >
<Save data-icon='inline-start' />
{isSaving ? t('Saving...') : t('Save model prices')} {isSaving ? t('Saving...') : t('Save model prices')}
</Button> </Button>
)}
<Button variant='outline' size='sm' onClick={toggleEditMode}> <Button variant='outline' size='sm' onClick={toggleEditMode}>
{editMode === 'visual' ? ( {editMode === 'visual' ? (
<> <>
@@ -160,6 +164,8 @@ export const ModelRatioForm = memo(function ModelRatioForm({
audioCompletionRatio={form.watch('AudioCompletionRatio')} audioCompletionRatio={form.watch('AudioCompletionRatio')}
billingMode={form.watch('BillingMode')} billingMode={form.watch('BillingMode')}
billingExpr={form.watch('BillingExpr')} billingExpr={form.watch('BillingExpr')}
onSave={handleSave}
isSaving={isSaving}
onChange={(field, value) => { onChange={(field, value) => {
const fieldMap: Record<string, keyof ModelFormValues> = { const fieldMap: Record<string, keyof ModelFormValues> = {
'billing_setting.billing_mode': 'BillingMode', 'billing_setting.billing_mode': 'BillingMode',
@@ -136,6 +136,7 @@ export function buildModelRatioColumns({
}, },
{ {
id: 'actions', id: 'actions',
header: () => <div className='text-right'>{t('Actions')}</div>,
cell: ({ row }) => ( cell: ({ row }) => (
<div className='flex justify-end gap-2'> <div className='flex justify-end gap-2'>
<Button <Button
@@ -90,6 +90,8 @@ type ModelRatioVisualEditorProps = {
billingMode: string billingMode: string
billingExpr: string billingExpr: string
onChange: (field: string, value: string) => void onChange: (field: string, value: string) => void
onSave: () => void | Promise<void>
isSaving: boolean
} }
export type ModelRatioVisualEditorHandle = { export type ModelRatioVisualEditorHandle = {
@@ -124,6 +126,8 @@ const ModelRatioVisualEditorComponent = forwardRef<
billingMode, billingMode,
billingExpr, billingExpr,
onChange, onChange,
onSave,
isSaving,
}, },
ref ref
) { ) {
@@ -672,7 +676,7 @@ const ModelRatioVisualEditorComponent = forwardRef<
) : ( ) : (
<div className='min-h-0 flex-1 overflow-auto rounded-md border'> <div className='min-h-0 flex-1 overflow-auto rounded-md border'>
<table className='w-full caption-bottom text-sm tabular-nums'> <table className='w-full caption-bottom text-sm tabular-nums'>
<thead className='bg-background sticky top-0 z-10'> <thead>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className='border-b'> <tr key={headerGroup.id} className='border-b'>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header) => (
@@ -680,9 +684,9 @@ const ModelRatioVisualEditorComponent = forwardRef<
key={header.id} key={header.id}
colSpan={header.colSpan} colSpan={header.colSpan}
className={cn( className={cn(
'text-foreground h-10 px-2 text-left align-middle text-sm font-medium whitespace-nowrap', 'bg-background text-foreground sticky top-0 z-10 h-10 px-2 text-left align-middle text-sm font-medium whitespace-nowrap',
header.column.id === 'actions' && header.column.id === 'actions' &&
'bg-background sticky right-0 z-20 w-24 min-w-24 shadow-[-10px_0_14px_-14px_hsl(var(--foreground))]' 'right-0 z-30 w-24 min-w-24 shadow-[-10px_0_14px_-14px_hsl(var(--foreground))]'
)} )}
> >
{header.isPlaceholder {header.isPlaceholder
@@ -746,6 +750,8 @@ const ModelRatioVisualEditorComponent = forwardRef<
<ModelPricingEditorPanel <ModelPricingEditorPanel
ref={editorPanelRef} ref={editorPanelRef}
editData={editData} editData={editData}
onSave={onSave}
isSaving={isSaving}
className='h-full min-h-0' className='h-full min-h-0'
/> />
) : ( ) : (
@@ -782,6 +788,8 @@ const ModelRatioVisualEditorComponent = forwardRef<
open={sheetOpen} open={sheetOpen}
onOpenChange={setSheetOpen} onOpenChange={setSheetOpen}
editData={editData} editData={editData}
onSave={onSave}
isSaving={isSaving}
/> />
)} )}
</div> </div>
@@ -803,7 +811,9 @@ export const ModelRatioVisualEditor = memo(
prevProps.audioCompletionRatio === nextProps.audioCompletionRatio && prevProps.audioCompletionRatio === nextProps.audioCompletionRatio &&
prevProps.billingMode === nextProps.billingMode && prevProps.billingMode === nextProps.billingMode &&
prevProps.billingExpr === nextProps.billingExpr && prevProps.billingExpr === nextProps.billingExpr &&
prevProps.onChange === nextProps.onChange prevProps.onChange === nextProps.onChange &&
prevProps.onSave === nextProps.onSave &&
prevProps.isSaving === nextProps.isSaving
) )
} }
) )