abad0d3cc0
- expose a draft commit handle from the model pricing editor panel before saving. - commit the open visual editor into the parent form before page-level save runs. - support both desktop side editor and mobile sheet save paths.
368 lines
12 KiB
TypeScript
Vendored
368 lines
12 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { memo, useCallback, useRef, useState } from 'react'
|
|
import { type UseFormReturn } from 'react-hook-form'
|
|
import { Code2, Eye } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@/components/ui/form'
|
|
import { Switch } from '@/components/ui/switch'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import {
|
|
SettingsForm,
|
|
SettingsSwitchContent,
|
|
SettingsSwitchItem,
|
|
} from '../components/settings-form-layout'
|
|
import { SettingsPageActionsPortal } from '../components/settings-page-context'
|
|
import {
|
|
ModelRatioVisualEditor,
|
|
type ModelRatioVisualEditorHandle,
|
|
} from './model-ratio-visual-editor'
|
|
|
|
type ModelFormValues = {
|
|
ModelPrice: string
|
|
ModelRatio: string
|
|
CacheRatio: string
|
|
CreateCacheRatio: string
|
|
CompletionRatio: string
|
|
ImageRatio: string
|
|
AudioRatio: string
|
|
AudioCompletionRatio: string
|
|
ExposeRatioEnabled: boolean
|
|
BillingMode: string
|
|
BillingExpr: string
|
|
}
|
|
|
|
type ModelRatioFormProps = {
|
|
form: UseFormReturn<ModelFormValues>
|
|
onSave: (values: ModelFormValues) => Promise<void>
|
|
onReset: () => void
|
|
isSaving: boolean
|
|
isResetting: boolean
|
|
}
|
|
|
|
export const ModelRatioForm = memo(function ModelRatioForm({
|
|
form,
|
|
onSave,
|
|
onReset,
|
|
isSaving,
|
|
isResetting,
|
|
}: ModelRatioFormProps) {
|
|
const { t } = useTranslation()
|
|
const [editMode, setEditMode] = useState<'visual' | 'json'>('visual')
|
|
const visualEditorRef = useRef<ModelRatioVisualEditorHandle>(null)
|
|
|
|
const handleFieldChange = useCallback(
|
|
(field: keyof ModelFormValues, value: string) => {
|
|
form.setValue(field, value, {
|
|
shouldValidate: true,
|
|
shouldDirty: true,
|
|
})
|
|
},
|
|
[form]
|
|
)
|
|
|
|
const toggleEditMode = useCallback(() => {
|
|
setEditMode((prev) => (prev === 'visual' ? 'json' : 'visual'))
|
|
}, [])
|
|
|
|
const handleSave = useCallback(async () => {
|
|
if (editMode === 'visual') {
|
|
const committed = await visualEditorRef.current?.commitOpenEditor()
|
|
if (committed === false) return
|
|
}
|
|
|
|
await form.handleSubmit(onSave)()
|
|
}, [editMode, form, onSave])
|
|
|
|
return (
|
|
<div className='space-y-6'>
|
|
<div className='flex justify-end'>
|
|
<Button variant='outline' size='sm' onClick={toggleEditMode}>
|
|
{editMode === 'visual' ? (
|
|
<>
|
|
<Code2 className='mr-2 h-4 w-4' />
|
|
{t('Switch to JSON')}
|
|
</>
|
|
) : (
|
|
<>
|
|
<Eye className='mr-2 h-4 w-4' />
|
|
{t('Switch to Visual')}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
<Form {...form}>
|
|
<SettingsPageActionsPortal>
|
|
<Button
|
|
type='button'
|
|
variant='destructive'
|
|
size='sm'
|
|
onClick={onReset}
|
|
disabled={isResetting}
|
|
>
|
|
{t('Reset prices')}
|
|
</Button>
|
|
<Button
|
|
type='button'
|
|
size='sm'
|
|
onClick={handleSave}
|
|
disabled={isSaving}
|
|
>
|
|
{isSaving ? t('Saving...') : t('Save model prices')}
|
|
</Button>
|
|
</SettingsPageActionsPortal>
|
|
{editMode === 'visual' ? (
|
|
<div className='space-y-6'>
|
|
<ModelRatioVisualEditor
|
|
ref={visualEditorRef}
|
|
modelPrice={form.watch('ModelPrice')}
|
|
modelRatio={form.watch('ModelRatio')}
|
|
cacheRatio={form.watch('CacheRatio')}
|
|
createCacheRatio={form.watch('CreateCacheRatio')}
|
|
completionRatio={form.watch('CompletionRatio')}
|
|
imageRatio={form.watch('ImageRatio')}
|
|
audioRatio={form.watch('AudioRatio')}
|
|
audioCompletionRatio={form.watch('AudioCompletionRatio')}
|
|
billingMode={form.watch('BillingMode')}
|
|
billingExpr={form.watch('BillingExpr')}
|
|
onChange={(field, value) => {
|
|
const fieldMap: Record<string, keyof ModelFormValues> = {
|
|
'billing_setting.billing_mode': 'BillingMode',
|
|
'billing_setting.billing_expr': 'BillingExpr',
|
|
}
|
|
const formField =
|
|
fieldMap[field] || (field as keyof ModelFormValues)
|
|
handleFieldChange(formField, value)
|
|
}}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='ExposeRatioEnabled'
|
|
render={({ field }) => (
|
|
<SettingsSwitchItem>
|
|
<SettingsSwitchContent>
|
|
<FormLabel>{t('Expose ratio API')}</FormLabel>
|
|
<FormDescription>
|
|
{t(
|
|
'Allow clients to query configured ratios via `/api/ratio`.'
|
|
)}
|
|
</FormDescription>
|
|
</SettingsSwitchContent>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</SettingsSwitchItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<SettingsForm onSubmit={form.handleSubmit(onSave)}>
|
|
<FormField
|
|
control={form.control}
|
|
name='ModelPrice'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Model fixed pricing')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={8} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'JSON map of model → USD cost per request. Takes precedence over ratio based billing.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='ModelRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Model ratio')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={8} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'JSON map of model → multiplier applied to quota billing.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='CacheRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Prompt cache ratio')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={8} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t('Optional ratio used when upstream cache hits occur.')}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='CreateCacheRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Create cache ratio')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={8} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Ratio applied when creating cache entries for supported models.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='CompletionRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Completion ratio')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={8} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Applies to custom completion endpoints. JSON map of model → ratio.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='ImageRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Image ratio')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={6} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Configure per-model ratio for image inputs or outputs.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='AudioRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Audio ratio')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={6} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Ratio applied to audio inputs where supported by the upstream model.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='AudioCompletionRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Audio completion ratio')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={6} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Ratio applied to audio completions for streaming models.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='ExposeRatioEnabled'
|
|
render={({ field }) => (
|
|
<SettingsSwitchItem>
|
|
<SettingsSwitchContent>
|
|
<FormLabel>{t('Expose ratio API')}</FormLabel>
|
|
<FormDescription>
|
|
{t(
|
|
'Allow clients to query configured ratios via `/api/ratio`.'
|
|
)}
|
|
</FormDescription>
|
|
</SettingsSwitchContent>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</SettingsSwitchItem>
|
|
)}
|
|
/>
|
|
</SettingsForm>
|
|
)}
|
|
</Form>
|
|
</div>
|
|
)
|
|
})
|