a7d019e3a9
Refresh the overview page with an actionable Get Started guide, live API request details, real usage sparklines, and OpenAI-inspired dashboard panels. Add collapsible setup state, role-aware quick actions, and localized copy so returning users can focus on platform health.
463 lines
16 KiB
TypeScript
Vendored
463 lines
16 KiB
TypeScript
Vendored
import { memo, useCallback, useState } from 'react'
|
|
import { type UseFormReturn } from 'react-hook-form'
|
|
import { Code2, Eye, HelpCircle } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from '@/components/ui/accordion'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@/components/ui/form'
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '@/components/ui/sheet'
|
|
import { Switch } from '@/components/ui/switch'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { GroupRatioVisualEditor } from './group-ratio-visual-editor'
|
|
import { GroupSpecialUsableRulesEditor } from './group-special-usable-editor'
|
|
|
|
type GroupFormValues = {
|
|
GroupRatio: string
|
|
TopupGroupRatio: string
|
|
UserUsableGroups: string
|
|
GroupGroupRatio: string
|
|
AutoGroups: string
|
|
DefaultUseAutoGroup: boolean
|
|
GroupSpecialUsableGroup: string
|
|
}
|
|
|
|
type GroupRatioFormProps = {
|
|
form: UseFormReturn<GroupFormValues>
|
|
onSave: (values: GroupFormValues) => Promise<void>
|
|
isSaving: boolean
|
|
}
|
|
|
|
export const GroupRatioForm = memo(function GroupRatioForm({
|
|
form,
|
|
onSave,
|
|
isSaving,
|
|
}: GroupRatioFormProps) {
|
|
const { t } = useTranslation()
|
|
const [editMode, setEditMode] = useState<'visual' | 'json'>('visual')
|
|
const [guideOpen, setGuideOpen] = useState(false)
|
|
|
|
const handleFieldChange = useCallback(
|
|
(field: keyof GroupFormValues, value: string) => {
|
|
form.setValue(field, value, {
|
|
shouldValidate: true,
|
|
shouldDirty: true,
|
|
})
|
|
},
|
|
[form]
|
|
)
|
|
|
|
const toggleEditMode = useCallback(() => {
|
|
setEditMode((prev) => (prev === 'visual' ? 'json' : 'visual'))
|
|
}, [])
|
|
|
|
return (
|
|
<div className='space-y-6'>
|
|
<div className='flex flex-wrap justify-end gap-2'>
|
|
<Button variant='outline' size='sm' onClick={() => setGuideOpen(true)}>
|
|
<HelpCircle className='mr-2 h-4 w-4' />
|
|
{t('Usage guide')}
|
|
</Button>
|
|
<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>
|
|
|
|
<GroupPricingGuide open={guideOpen} onOpenChange={setGuideOpen} />
|
|
|
|
<Form {...form}>
|
|
{editMode === 'visual' ? (
|
|
<div className='space-y-6'>
|
|
<GroupRatioVisualEditor
|
|
groupRatio={form.watch('GroupRatio')}
|
|
topupGroupRatio={form.watch('TopupGroupRatio')}
|
|
userUsableGroups={form.watch('UserUsableGroups')}
|
|
groupGroupRatio={form.watch('GroupGroupRatio')}
|
|
autoGroups={form.watch('AutoGroups')}
|
|
onChange={(field, value) =>
|
|
handleFieldChange(field as keyof GroupFormValues, value)
|
|
}
|
|
/>
|
|
|
|
<GroupSpecialUsableRulesEditor
|
|
value={form.watch('GroupSpecialUsableGroup')}
|
|
onChange={(value) =>
|
|
handleFieldChange('GroupSpecialUsableGroup', value)
|
|
}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='DefaultUseAutoGroup'
|
|
render={({ field }) => (
|
|
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
|
<div className='space-y-0.5'>
|
|
<FormLabel className='text-base'>
|
|
{t('Default to auto groups')}
|
|
</FormLabel>
|
|
<FormDescription>
|
|
{t(
|
|
'When enabled, newly created tokens start in the first auto group.'
|
|
)}
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<Button onClick={form.handleSubmit(onSave)} disabled={isSaving}>
|
|
{isSaving ? t('Saving...') : t('Save group ratios')}
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<form onSubmit={form.handleSubmit(onSave)} className='space-y-6'>
|
|
<FormField
|
|
control={form.control}
|
|
name='GroupRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Group ratios')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={8} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'JSON map of group → ratio applied when the user selects the group explicitly.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='TopupGroupRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Top-up group ratios')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={6} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Optional multiplier per user group used when calculating recharge pricing. Provide a JSON object such as'
|
|
)}
|
|
{` { "default": 1, "vip": 1.2 }`}.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='UserUsableGroups'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Selectable groups')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={6} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'JSON map of group → description exposed when users create API keys.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='GroupGroupRatio'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Inter-group overrides')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={8} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t('Nested JSON: source group →')}{' '}
|
|
{`{ targetGroup: ratio }`}{' '}
|
|
{t(
|
|
'to override billing when a user in one group uses a token of another group.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='AutoGroups'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Auto assignment order')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={6} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'JSON array of group identifiers. When enabled below, new tokens rotate through this list.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='GroupSpecialUsableGroup'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Special usable group rules')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea rows={8} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='DefaultUseAutoGroup'
|
|
render={({ field }) => (
|
|
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
|
<div className='space-y-0.5'>
|
|
<FormLabel className='text-base'>
|
|
{t('Default to auto groups')}
|
|
</FormLabel>
|
|
<FormDescription>
|
|
{t(
|
|
'When enabled, newly created tokens start in the first auto group.'
|
|
)}
|
|
</FormDescription>
|
|
</div>
|
|
<FormControl>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<Button type='submit' disabled={isSaving}>
|
|
{isSaving ? t('Saving...') : t('Save group ratios')}
|
|
</Button>
|
|
</form>
|
|
)}
|
|
</Form>
|
|
</div>
|
|
)
|
|
})
|
|
|
|
type GroupPricingGuideProps = {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
}
|
|
|
|
function GuideCodeBlock({ children }: { children: string }) {
|
|
return (
|
|
<pre className='bg-muted/60 overflow-x-auto rounded-lg border px-3 py-2 text-xs leading-6 whitespace-pre-wrap'>
|
|
{children}
|
|
</pre>
|
|
)
|
|
}
|
|
|
|
function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
|
|
const { t } = useTranslation()
|
|
|
|
return (
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent side='right' className='w-full gap-0 p-0 sm:max-w-2xl'>
|
|
<SheetHeader className='border-b p-4'>
|
|
<SheetTitle>{t('Group pricing usage guide')}</SheetTitle>
|
|
<SheetDescription>
|
|
{t(
|
|
'Understand how user groups, token groups, ratios, and special rules work together.'
|
|
)}
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<div className='space-y-5 overflow-y-auto p-4'>
|
|
<section className='space-y-2'>
|
|
<h3 className='text-sm font-semibold'>{t('Core concepts')}</h3>
|
|
<div className='text-muted-foreground space-y-2 text-sm leading-6'>
|
|
<p>
|
|
<span className='text-foreground font-medium'>
|
|
{t('User group')}
|
|
</span>
|
|
{': '}
|
|
{t(
|
|
'Assigned by administrators and used to represent a user level, such as default or vip.'
|
|
)}
|
|
</p>
|
|
<p>
|
|
<span className='text-foreground font-medium'>
|
|
{t('Token group')}
|
|
</span>
|
|
{': '}
|
|
{t(
|
|
'Selected when creating a token and used as the default billing group for API calls.'
|
|
)}
|
|
</p>
|
|
<p>
|
|
<span className='text-foreground font-medium'>
|
|
{t('Ratio')}
|
|
</span>
|
|
{': '}
|
|
{t(
|
|
'A billing multiplier. Lower ratios mean lower API call costs.'
|
|
)}
|
|
</p>
|
|
<p>
|
|
<span className='text-foreground font-medium'>
|
|
{t('User selectable')}
|
|
</span>
|
|
{': '}
|
|
{t(
|
|
'When enabled, users can pick this group when creating tokens.'
|
|
)}
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<Accordion className='rounded-lg border px-3'>
|
|
<AccordionItem value='groups'>
|
|
<AccordionTrigger>{t('Pricing group example')}</AccordionTrigger>
|
|
<AccordionContent className='space-y-3'>
|
|
<p className='text-muted-foreground text-sm leading-6'>
|
|
{t(
|
|
'Use the pricing group table to manage the ratio and whether the group appears in the token creation dropdown.'
|
|
)}
|
|
</p>
|
|
<GuideCodeBlock>
|
|
{`${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')}`}
|
|
</GuideCodeBlock>
|
|
<p className='text-muted-foreground text-sm leading-6'>
|
|
{t(
|
|
'Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.'
|
|
)}
|
|
</p>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
<AccordionItem value='auto'>
|
|
<AccordionTrigger>{t('Auto group behavior')}</AccordionTrigger>
|
|
<AccordionContent className='space-y-3'>
|
|
<p className='text-muted-foreground text-sm leading-6'>
|
|
{t(
|
|
'When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.'
|
|
)}
|
|
</p>
|
|
<GuideCodeBlock>{`["default", "vip"]`}</GuideCodeBlock>
|
|
<p className='text-muted-foreground text-sm leading-6'>
|
|
{t(
|
|
'If default auto group is enabled, newly created tokens start with auto instead of an empty group.'
|
|
)}
|
|
</p>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
<AccordionItem value='special-ratio'>
|
|
<AccordionTrigger>{t('Special ratio rules')}</AccordionTrigger>
|
|
<AccordionContent className='space-y-3'>
|
|
<p className='text-muted-foreground text-sm leading-6'>
|
|
{t(
|
|
'Special ratios override the token group ratio for specific user group and token group combinations.'
|
|
)}
|
|
</p>
|
|
<GuideCodeBlock>{`{
|
|
"vip": {
|
|
"standard": 0.8,
|
|
"premium": 0.3
|
|
}
|
|
}`}</GuideCodeBlock>
|
|
<p className='text-muted-foreground text-sm leading-6'>
|
|
{t(
|
|
'Only configured combinations are overridden. All other calls keep the token group base ratio.'
|
|
)}
|
|
</p>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
<AccordionItem value='usable'>
|
|
<AccordionTrigger>
|
|
{t('Special usable group rules')}
|
|
</AccordionTrigger>
|
|
<AccordionContent className='space-y-3'>
|
|
<p className='text-muted-foreground text-sm leading-6'>
|
|
{t(
|
|
'Special usable group rules can add, remove, or append selectable token groups for a specific user group.'
|
|
)}
|
|
</p>
|
|
<GuideCodeBlock>{`{
|
|
"vip": {
|
|
"+:premium": "${t('Premium plan, half price')}",
|
|
"-:default": "remove",
|
|
"special": "${t('Special group')}"
|
|
}
|
|
}`}</GuideCodeBlock>
|
|
<p className='text-muted-foreground text-sm leading-6'>
|
|
{t(
|
|
'Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.'
|
|
)}
|
|
</p>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
}
|