From dede1e2968f882d98b36bb4526769f6fd90c73d9 Mon Sep 17 00:00:00 2001 From: CaIon Date: Wed, 6 May 2026 20:14:35 +0800 Subject: [PATCH] fix(default): improve billing settings forms --- .../components/profile-settings-card.tsx | 6 +- .../billing/section-registry.tsx | 10 +- .../general/quota-settings-section.tsx | 34 +- .../models/group-ratio-form.tsx | 190 ++++- .../models/group-ratio-visual-editor.tsx | 666 ++++++++++-------- .../models/group-special-usable-editor.tsx | 7 +- .../models/model-ratio-form.tsx | 8 +- .../models/ratio-settings-card.tsx | 38 +- .../locales/_reports/ja.untranslated.json | 2 +- .../locales/_reports/ru.untranslated.json | 26 +- .../locales/_reports/vi.untranslated.json | 2 +- web/default/src/i18n/locales/en.json | 189 +++-- web/default/src/i18n/locales/fr.json | 189 +++-- web/default/src/i18n/locales/ja.json | 189 +++-- web/default/src/i18n/locales/ru.json | 189 +++-- web/default/src/i18n/locales/vi.json | 189 +++-- web/default/src/i18n/locales/zh.json | 189 +++-- 17 files changed, 1291 insertions(+), 832 deletions(-) diff --git a/web/default/src/features/profile/components/profile-settings-card.tsx b/web/default/src/features/profile/components/profile-settings-card.tsx index ed760a15..8764d95d 100644 --- a/web/default/src/features/profile/components/profile-settings-card.tsx +++ b/web/default/src/features/profile/components/profile-settings-card.tsx @@ -51,10 +51,10 @@ export function ProfileSettingsCard({ icon={} > - + {t('Account Bindings')} @@ -62,7 +62,7 @@ export function ProfileSettingsCard({ diff --git a/web/default/src/features/system-settings/billing/section-registry.tsx b/web/default/src/features/system-settings/billing/section-registry.tsx index 54c7ea61..e795ab68 100644 --- a/web/default/src/features/system-settings/billing/section-registry.tsx +++ b/web/default/src/features/system-settings/billing/section-registry.tsx @@ -45,9 +45,13 @@ const BILLING_SECTIONS = [ QuotaForInviter: settings.QuotaForInviter, QuotaForInvitee: settings.QuotaForInvitee, TopUpLink: settings.TopUpLink, - 'general_setting.docs_link': settings['general_setting.docs_link'], - 'quota_setting.enable_free_model_pre_consume': - settings['quota_setting.enable_free_model_pre_consume'], + general_setting: { + docs_link: settings['general_setting.docs_link'], + }, + quota_setting: { + enable_free_model_pre_consume: + settings['quota_setting.enable_free_model_pre_consume'], + }, }} /> ), diff --git a/web/default/src/features/system-settings/general/quota-settings-section.tsx b/web/default/src/features/system-settings/general/quota-settings-section.tsx index 466eca4b..057f931c 100644 --- a/web/default/src/features/system-settings/general/quota-settings-section.tsx +++ b/web/default/src/features/system-settings/general/quota-settings-section.tsx @@ -1,4 +1,5 @@ import * as z from 'zod' +import type { ChangeEvent } from 'react' import type { Resolver } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' @@ -25,9 +26,13 @@ const quotaSchema = z.object({ PreConsumedQuota: z.coerce.number().min(0), QuotaForInviter: z.coerce.number().min(0), QuotaForInvitee: z.coerce.number().min(0), - TopUpLink: z.string().url().optional().or(z.literal('')), - 'general_setting.docs_link': z.string().url().optional().or(z.literal('')), - 'quota_setting.enable_free_model_pre_consume': z.boolean(), + TopUpLink: z.string(), + general_setting: z.object({ + docs_link: z.string(), + }), + quota_setting: z.object({ + enable_free_model_pre_consume: z.boolean(), + }), }) type QuotaFormValues = z.infer @@ -41,6 +46,13 @@ export function QuotaSettingsSection({ }: QuotaSettingsSectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() + const handleNumberChange = + (onChange: (value: number | string) => void) => + (event: ChangeEvent) => { + onChange( + event.target.value === '' ? '' : event.currentTarget.valueAsNumber + ) + } const { form, handleSubmit, isDirty, isSubmitting } = useSettingsForm({ @@ -79,8 +91,8 @@ export function QuotaSettingsSection({ field.onChange(e.target.valueAsNumber)} + value={field.value ?? ''} + onChange={handleNumberChange(field.onChange)} name={field.name} onBlur={field.onBlur} ref={field.ref} @@ -103,8 +115,8 @@ export function QuotaSettingsSection({ field.onChange(e.target.valueAsNumber)} + value={field.value ?? ''} + onChange={handleNumberChange(field.onChange)} name={field.name} onBlur={field.onBlur} ref={field.ref} @@ -127,8 +139,8 @@ export function QuotaSettingsSection({ field.onChange(e.target.valueAsNumber)} + value={field.value ?? ''} + onChange={handleNumberChange(field.onChange)} name={field.name} onBlur={field.onBlur} ref={field.ref} @@ -151,8 +163,8 @@ export function QuotaSettingsSection({ field.onChange(e.target.valueAsNumber)} + value={field.value ?? ''} + onChange={handleNumberChange(field.onChange)} name={field.name} onBlur={field.onBlur} ref={field.ref} diff --git a/web/default/src/features/system-settings/models/group-ratio-form.tsx b/web/default/src/features/system-settings/models/group-ratio-form.tsx index 1e92d73a..cfc23dea 100644 --- a/web/default/src/features/system-settings/models/group-ratio-form.tsx +++ b/web/default/src/features/system-settings/models/group-ratio-form.tsx @@ -1,8 +1,14 @@ import { memo, useCallback, useState } from 'react' import { type UseFormReturn } from 'react-hook-form' -import { Code2, Eye } from 'lucide-react' +import { Code2, Eye, HelpCircle } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion' import { Form, FormControl, @@ -14,6 +20,13 @@ import { } from '@/components/ui/form' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet' import { GroupRatioVisualEditor } from './group-ratio-visual-editor' import { GroupSpecialUsableRulesEditor } from './group-special-usable-editor' @@ -40,6 +53,7 @@ export const GroupRatioForm = memo(function GroupRatioForm({ }: GroupRatioFormProps) { const { t } = useTranslation() const [editMode, setEditMode] = useState<'visual' | 'json'>('visual') + const [guideOpen, setGuideOpen] = useState(false) const handleFieldChange = useCallback( (field: keyof GroupFormValues, value: string) => { @@ -57,7 +71,15 @@ export const GroupRatioForm = memo(function GroupRatioForm({ return (
-
+
+
+ +
{editMode === 'visual' ? (
@@ -276,3 +300,165 @@ export const GroupRatioForm = memo(function GroupRatioForm({
) }) + +type GroupPricingGuideProps = { + open: boolean + onOpenChange: (open: boolean) => void +} + +function GuideCodeBlock({ children }: { children: string }) { + return ( +
+      {children}
+    
+ ) +} + +function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) { + const { t } = useTranslation() + + return ( + + + + {t('Group pricing usage guide')} + + {t( + 'Understand how user groups, token groups, ratios, and special rules work together.' + )} + + + +
+
+

{t('Core concepts')}

+
+

+ + {t('User group')} + + {': '} + {t( + 'Assigned by administrators and used to represent a user level, such as default or vip.' + )} +

+

+ + {t('Token group')} + + {': '} + {t( + 'Selected when creating a token and used as the default billing group for API calls.' + )} +

+

+ + {t('Ratio')} + + {': '} + {t( + 'A billing multiplier. Lower ratios mean lower API call costs.' + )} +

+

+ + {t('User selectable')} + + {': '} + {t( + 'When enabled, users can pick this group when creating tokens.' + )} +

+
+
+ + + + {t('Pricing group example')} + +

+ {t( + 'Use the pricing group table to manage the ratio and whether the group appears in the token creation dropdown.' + )} +

+ + {`${t('Group name')} ${t('Ratio')} ${t('User selectable')} ${t('Description')} +standard 1.0 ${t('Yes')} ${t('Standard price')} +premium 0.5 ${t('Yes')} ${t('Premium plan, half price')} +vip 0.5 ${t('No')} ${t('Assigned by administrator only')}`} + +

+ {t( + 'Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.' + )} +

+
+
+ + + {t('Auto group behavior')} + +

+ {t( + 'When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.' + )} +

+ {`["default", "vip"]`} +

+ {t( + 'If default auto group is enabled, newly created tokens start with auto instead of an empty group.' + )} +

+
+
+ + + {t('Special ratio rules')} + +

+ {t( + 'Special ratios override the token group ratio for specific user group and token group combinations.' + )} +

+ {`{ + "vip": { + "standard": 0.8, + "premium": 0.3 + } +}`} +

+ {t( + 'Only configured combinations are overridden. All other calls keep the token group base ratio.' + )} +

+
+
+ + + {t('Special usable group rules')} + +

+ {t( + 'Special usable group rules can add, remove, or append selectable token groups for a specific user group.' + )} +

+ {`{ + "vip": { + "+:premium": "${t('Premium plan, half price')}", + "-:default": "remove", + "special": "${t('Special group')}" + } +}`} +

+ {t( + 'Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.' + )} +

+
+
+
+
+
+
+ ) +} diff --git a/web/default/src/features/system-settings/models/group-ratio-visual-editor.tsx b/web/default/src/features/system-settings/models/group-ratio-visual-editor.tsx index cf427329..8441ba5f 100644 --- a/web/default/src/features/system-settings/models/group-ratio-visual-editor.tsx +++ b/web/default/src/features/system-settings/models/group-ratio-visual-editor.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, memo } from 'react' +import { useState, useMemo, useEffect, useCallback, memo } from 'react' import { Pencil, Plus, Trash2, GripVertical, ChevronDown } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' @@ -14,6 +14,7 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible' +import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, @@ -48,8 +49,11 @@ type SimpleGroup = { value: string } -type UsableGroup = { +type GroupPricingRow = { + _id: string name: string + ratio: number + selectable: boolean description: string } @@ -58,6 +62,90 @@ type GroupOverride = { ratio: number } +const sectionCardClassName = + 'relative shadow-sm ring-0 before:pointer-events-none before:absolute before:inset-0 before:rounded-xl before:border before:border-border/90' +const sectionHeaderClassName = 'border-b bg-muted/20' + +let groupPricingIdCounter = 0 +function createGroupPricingId() { + groupPricingIdCounter += 1 + return `gpr_${groupPricingIdCounter}` +} + +function normalizeRatio(value: unknown): number { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : 1 +} + +function buildGroupPricingRows( + groupRatio: string, + userUsableGroups: string +): GroupPricingRow[] { + const ratioMap = safeJsonParse>(groupRatio, { + fallback: {}, + context: 'group ratios', + }) + const usableMap = safeJsonParse>(userUsableGroups, { + fallback: {}, + context: 'user usable groups', + }) + const names = new Set([...Object.keys(ratioMap), ...Object.keys(usableMap)]) + + return Array.from(names).map((name) => ({ + _id: createGroupPricingId(), + name, + ratio: normalizeRatio(ratioMap[name]), + selectable: Object.prototype.hasOwnProperty.call(usableMap, name), + description: String(usableMap[name] ?? ''), + })) +} + +function serializeGroupPricingRows(rows: GroupPricingRow[]) { + const groupRatio: Record = {} + const userUsableGroups: Record = {} + + for (const row of rows) { + const name = row.name.trim() + if (!name) continue + groupRatio[name] = normalizeRatio(row.ratio) + if (row.selectable) { + userUsableGroups[name] = row.description + } + } + + return { + GroupRatio: JSON.stringify(groupRatio, null, 2), + UserUsableGroups: JSON.stringify(userUsableGroups, null, 2), + } +} + +function groupPricingSignature(rows: GroupPricingRow[]): string { + const serialized = serializeGroupPricingRows(rows) + return JSON.stringify({ + groupRatio: safeJsonParse(serialized.GroupRatio, { + fallback: {}, + silent: true, + }), + userUsableGroups: safeJsonParse(serialized.UserUsableGroups, { + fallback: {}, + silent: true, + }), + }) +} + +function sourceGroupPricingSignature( + groupRatio: string, + userUsableGroups: string +): string { + return JSON.stringify({ + groupRatio: safeJsonParse(groupRatio, { fallback: {}, silent: true }), + userUsableGroups: safeJsonParse(userUsableGroups, { + fallback: {}, + silent: true, + }), + }) +} + export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ groupRatio, topupGroupRatio, @@ -73,9 +161,6 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ >(null) const [simpleEditData, setSimpleEditData] = useState(null) - const [usableDialogOpen, setUsableDialogOpen] = useState(false) - const [usableEditData, setUsableEditData] = useState(null) - const [autoGroupDialogOpen, setAutoGroupDialogOpen] = useState(false) const [autoGroupInput, setAutoGroupInput] = useState('') @@ -89,18 +174,6 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ const [userGroupDialogOpen, setUserGroupDialogOpen] = useState(false) const [userGroupInput, setUserGroupInput] = useState('') - // Parse group ratios - const groupRatioList = useMemo(() => { - const map = safeJsonParse>(groupRatio, { - fallback: {}, - context: 'group ratios', - }) - return Object.entries(map).map(([name, value]) => ({ - name, - value: String(value), - })) - }, [groupRatio]) - // Parse topup group ratios const topupRatioList = useMemo(() => { const map = safeJsonParse>(topupGroupRatio, { @@ -113,18 +186,6 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ })) }, [topupGroupRatio]) - // Parse usable groups - const usableGroupsList = useMemo(() => { - const map = safeJsonParse>(userUsableGroups, { - fallback: {}, - context: 'user usable groups', - }) - return Object.entries(map).map(([name, description]) => ({ - name, - description: String(description), - })) - }, [userUsableGroups]) - // Parse auto groups const autoGroupsList = useMemo(() => { return safeJsonParse(autoGroups, { @@ -204,42 +265,6 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ onChange(field, JSON.stringify(map, null, 2)) } - // Usable groups handlers - const handleUsableAdd = () => { - setUsableEditData(null) - setUsableDialogOpen(true) - } - - const handleUsableEdit = (group: UsableGroup) => { - setUsableEditData(group) - setUsableDialogOpen(true) - } - - const handleUsableSave = (name: string, description: string) => { - const map = safeJsonParse>(userUsableGroups, { - fallback: {}, - silent: true, - }) - - if (usableEditData && usableEditData.name !== name) { - delete map[usableEditData.name] - } - - map[name] = description - - onChange('UserUsableGroups', JSON.stringify(map, null, 2)) - setUsableDialogOpen(false) - } - - const handleUsableDelete = (name: string) => { - const map = safeJsonParse>(userUsableGroups, { - fallback: {}, - silent: true, - }) - delete map[name] - onChange('UserUsableGroups', JSON.stringify(map, null, 2)) - } - // Auto groups handlers const handleAutoGroupAdd = () => { setAutoGroupInput('') @@ -366,75 +391,16 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ } return ( -
- {/* Group Ratios */} - - - {t('Group ratios')} - - {t('Base multipliers applied when users select specific groups.')} - - - -
- - {groupRatioList.length > 0 && ( -
- - - - {t('Group name')} - {t('Ratio')} - - {t('Actions')} - - - - - {groupRatioList.map((group) => ( - - - {group.name} - - {group.value} - -
- - -
-
-
- ))} -
-
-
- )} -
-
-
+
+ {/* Topup Group Ratios */} - - + + {t('Top-up group ratios')} {t('Multipliers for recharge pricing based on user groups.')} @@ -504,8 +470,8 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ {/* Inter-group ratio overrides */} - - + + {t('Inter-group ratio overrides')} {t( @@ -625,70 +591,9 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ - {/* Usable Groups */} - - - {t('Selectable groups')} - - {t('Groups that users can select when creating API keys.')} - - - -
- - {usableGroupsList.length > 0 && ( -
- - - - {t('Group name')} - {t('Description')} - - {t('Actions')} - - - - - {usableGroupsList.map((group) => ( - - - {group.name} - - {group.description} - -
- - -
-
-
- ))} -
-
-
- )} -
-
-
- {/* Auto Groups */} - - + + {t('Auto assignment order')} {t( @@ -753,14 +658,6 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ type={simpleDialogType} /> - {/* Usable Group Dialog */} - - {/* Auto Group Dialog */} @@ -835,6 +732,233 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({ ) }) +type GroupPricingTableProps = { + groupRatio: string + userUsableGroups: string + onChange: (field: string, value: string) => void +} + +function GroupPricingTable({ + groupRatio, + userUsableGroups, + onChange, +}: GroupPricingTableProps) { + const { t } = useTranslation() + const [rows, setRows] = useState(() => + buildGroupPricingRows(groupRatio, userUsableGroups) + ) + + useEffect(() => { + const incomingSignature = sourceGroupPricingSignature( + groupRatio, + userUsableGroups + ) + setRows((currentRows) => { + if (groupPricingSignature(currentRows) === incomingSignature) { + return currentRows + } + return buildGroupPricingRows(groupRatio, userUsableGroups) + }) + }, [groupRatio, userUsableGroups]) + + const emitRows = useCallback( + (nextRows: GroupPricingRow[]) => { + setRows(nextRows) + const serialized = serializeGroupPricingRows(nextRows) + onChange('GroupRatio', serialized.GroupRatio) + onChange('UserUsableGroups', serialized.UserUsableGroups) + }, + [onChange] + ) + + const updateRow = useCallback( + ( + id: string, + field: Exclude, + value: string | number | boolean + ) => { + emitRows( + rows.map((row) => (row._id === id ? { ...row, [field]: value } : row)) + ) + }, + [emitRows, rows] + ) + + const addRow = useCallback(() => { + const existingNames = new Set(rows.map((row) => row.name)) + let index = 1 + let name = `group_${index}` + while (existingNames.has(name)) { + index += 1 + name = `group_${index}` + } + emitRows([ + ...rows, + { + _id: createGroupPricingId(), + name, + ratio: 1, + selectable: true, + description: '', + }, + ]) + }, [emitRows, rows]) + + const removeRow = useCallback( + (id: string) => { + emitRows(rows.filter((row) => row._id !== id)) + }, + [emitRows, rows] + ) + + const duplicateNames = useMemo(() => { + const counts = new Map() + for (const row of rows) { + const name = row.name.trim() + if (!name) continue + counts.set(name, (counts.get(name) ?? 0) + 1) + } + return Array.from(counts.entries()) + .filter(([, count]) => count > 1) + .map(([name]) => name) + }, [rows]) + + return ( + + +
+
+ {t('Pricing groups')} + + {t('Edit billing ratios and user-selectable groups in one table.')} + +
+ +
+
+ +
+
+ + + + {t('Group name')} + {t('Ratio')} + + {t('User selectable')} + + {t('Description')} + + {t('Actions')} + + + + + {rows.length === 0 ? ( + + + {t('No groups yet. Add a group to get started.')} + + + ) : ( + rows.map((row) => ( + + + + updateRow(row._id, 'name', event.target.value) + } + aria-invalid={duplicateNames.includes( + row.name.trim() + )} + /> + + + + updateRow( + row._id, + 'ratio', + normalizeRatio(event.target.value) + ) + } + /> + + +
+ + updateRow( + row._id, + 'selectable', + checked === true + ) + } + aria-label={t('User selectable')} + /> +
+
+ + {row.selectable ? ( + + updateRow( + row._id, + 'description', + event.target.value + ) + } + /> + ) : ( + + - + + )} + + + + +
+ )) + )} +
+
+
+ + {duplicateNames.length > 0 && ( +

+ {t('Duplicate group names: {{names}}', { + names: duplicateNames.join(', '), + })} +

+ )} +
+
+
+ ) +} + // Simple Group Dialog Component type SimpleGroupDialogProps = { open: boolean @@ -857,6 +981,17 @@ function SimpleGroupDialog({ const title = type === 'groupRatio' ? t('group ratio') : t('top-up ratio') + useEffect(() => { + if (!open) { + setName('') + setValue('') + return + } + + setName(editData?.name ?? '') + setValue(editData?.value ?? '') + }, [editData, open]) + const handleSave = () => { if (!name.trim() || !value.trim()) return onSave(name.trim(), value.trim()) @@ -865,19 +1000,7 @@ function SimpleGroupDialog({ } return ( - { - onOpenChange(open) - if (open && editData) { - setName(editData.name) - setValue(editData.value) - } else { - setName('') - setValue('') - } - }} - > + @@ -926,88 +1049,6 @@ function SimpleGroupDialog({ ) } -// Usable Group Dialog Component -type UsableGroupDialogProps = { - open: boolean - onOpenChange: (open: boolean) => void - onSave: (name: string, description: string) => void - editData: UsableGroup | null -} - -function UsableGroupDialog({ - open, - onOpenChange, - onSave, - editData, -}: UsableGroupDialogProps) { - const { t } = useTranslation() - const [name, setName] = useState('') - const [description, setDescription] = useState('') - - const handleSave = () => { - if (!name.trim() || !description.trim()) return - onSave(name.trim(), description.trim()) - setName('') - setDescription('') - } - - return ( - { - onOpenChange(open) - if (open && editData) { - setName(editData.name) - setDescription(editData.description) - } else { - setName('') - setDescription('') - } - }} - > - - - - {editData ? t('Edit selectable group') : t('Add selectable group')} - - - {t( - 'Configure a group that users can select when creating API keys.' - )} - - -
-
- - setName(e.target.value)} - placeholder={t('vip')} - disabled={!!editData} - /> -
-
- - setDescription(e.target.value)} - placeholder={t('VIP users with premium access')} - /> -
-
- - - - -
-
- ) -} - // Group Override Dialog Component type GroupOverrideDialogProps = { open: boolean @@ -1028,6 +1069,17 @@ function GroupOverrideDialog({ const [targetGroup, setTargetGroup] = useState('') const [ratio, setRatio] = useState('') + useEffect(() => { + if (!open) { + setTargetGroup('') + setRatio('') + return + } + + setTargetGroup(editData?.targetGroup ?? '') + setRatio(editData ? String(editData.ratio) : '') + }, [editData, open]) + const handleSave = () => { if (!targetGroup.trim() || !ratio.trim()) return const parsedRatio = parseFloat(ratio) @@ -1039,19 +1091,7 @@ function GroupOverrideDialog({ } return ( - { - onOpenChange(open) - if (open && editData) { - setTargetGroup(editData.targetGroup) - setRatio(String(editData.ratio)) - } else { - setTargetGroup('') - setRatio('') - } - }} - > + diff --git a/web/default/src/features/system-settings/models/group-special-usable-editor.tsx b/web/default/src/features/system-settings/models/group-special-usable-editor.tsx index 5d5ac2c2..4be5f4a6 100644 --- a/web/default/src/features/system-settings/models/group-special-usable-editor.tsx +++ b/web/default/src/features/system-settings/models/group-special-usable-editor.tsx @@ -27,6 +27,9 @@ import { StatusBadge } from '@/components/status-badge' const OP_ADD = 'add' as const const OP_REMOVE = 'remove' as const const OP_APPEND = 'append' as const +const sectionCardClassName = + 'relative shadow-sm ring-0 before:pointer-events-none before:absolute before:inset-0 before:rounded-xl before:border before:border-border/90' +const sectionHeaderClassName = 'border-b bg-muted/20' type OpType = typeof OP_ADD | typeof OP_REMOVE | typeof OP_APPEND @@ -344,8 +347,8 @@ export function GroupSpecialUsableRulesEditor( }, [rules]) return ( - - + + {t('Special usable group rules')} {t( diff --git a/web/default/src/features/system-settings/models/model-ratio-form.tsx b/web/default/src/features/system-settings/models/model-ratio-form.tsx index 4c222cf2..f8a2552f 100644 --- a/web/default/src/features/system-settings/models/model-ratio-form.tsx +++ b/web/default/src/features/system-settings/models/model-ratio-form.tsx @@ -132,7 +132,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
@@ -323,7 +323,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
diff --git a/web/default/src/features/system-settings/models/ratio-settings-card.tsx b/web/default/src/features/system-settings/models/ratio-settings-card.tsx index 691585ec..c1d3dbbe 100644 --- a/web/default/src/features/system-settings/models/ratio-settings-card.tsx +++ b/web/default/src/features/system-settings/models/ratio-settings-card.tsx @@ -207,7 +207,7 @@ export function RatioSettingsCard({ mutationFn: resetModelRatios, onSuccess: (data) => { if (data.success) { - toast.success(t('Model ratios reset successfully')) + toast.success(t('Model prices reset successfully')) queryClient.invalidateQueries({ queryKey: ['system-options'] }) setConfirmOpen(false) } else { @@ -422,7 +422,7 @@ export function RatioSettingsCard({ }, [resetMutate]) const tabLabels: Record = { - models: 'Model ratios', + models: 'Model prices', groups: 'Group ratios', 'tool-prices': 'Tool prices', 'upstream-sync': 'Upstream price sync', @@ -480,26 +480,30 @@ export function RatioSettingsCard({ return ( - - - {visibleTabs.map((tab) => ( - - {t(tabLabels[tab])} - - ))} - + {visibleTabs.length === 1 ? ( + renderTabContent(defaultTab) + ) : ( + + + {visibleTabs.map((tab) => ( + + {t(tabLabels[tab])} + + ))} + - {visibleTabs.map((tab) => ( - - {renderTabContent(tab)} - - ))} - + {visibleTabs.map((tab) => ( + + {renderTabContent(tab)} + + ))} + + )}