/* 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 . For commercial licensing, please contact support@quantumnous.com */ import { Code2, Eye, HelpCircle } from 'lucide-react' import { memo, useCallback, useMemo, useState, type ReactNode } from 'react' import type { UseFormReturn } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { sideDrawerContentClassName, sideDrawerFormClassName, sideDrawerHeaderClassName, } from '@/components/drawer-layout' import { JsonCodeEditor } from '@/components/json-code-editor' 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 { Input } from '@/components/ui/input' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@/components/ui/sheet' import { Switch } from '@/components/ui/switch' import { SettingsForm, SettingsSwitchContent, SettingsSwitchItem, } from '../components/settings-form-layout' import { SettingsPageActionsPortal } from '../components/settings-page-context' import { safeJsonParse } from '../utils/json-parser' import { safeNumberFieldProps } from '../utils/numeric-field' 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 MaxTokenAutoGroups: number DefaultUseAutoGroup: boolean GroupSpecialUsableGroup: string } type GroupRatioFormProps = { form: UseFormReturn onSave: (values: GroupFormValues) => Promise 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')) }, []) const watchedGroupRatio = form.watch('GroupRatio') const watchedUserUsableGroups = form.watch('UserUsableGroups') const watchedTopupGroupRatio = form.watch('TopupGroupRatio') const groupNames = useMemo(() => { const ratioMap = safeJsonParse>(watchedGroupRatio, { fallback: {}, silent: true, }) const usableMap = safeJsonParse>( watchedUserUsableGroups, { fallback: {}, silent: true } ) const topupMap = safeJsonParse>( watchedTopupGroupRatio, { fallback: {}, silent: true } ) return [ ...new Set([ ...Object.keys(ratioMap), ...Object.keys(usableMap), ...Object.keys(topupMap), ]), ] }, [watchedGroupRatio, watchedUserUsableGroups, watchedTopupGroupRatio]) return (
{editMode === 'visual' ? (
( {t('Maximum custom groups per token')} {t( 'Limits only token-specific Auto snapshots. Global Auto inheritance remains unlimited.' )} )} /> } groupSpecialUsableGroup={form.watch('GroupSpecialUsableGroup')} onChange={(field, value) => handleFieldChange(field as keyof GroupFormValues, value) } /> handleFieldChange('GroupSpecialUsableGroup', value) } /> ( {t('Default to auto groups')} {t( 'When enabled, newly created tokens start in the first auto group.' )} )} />
) : ( ( {t('Group ratios')} {t( 'JSON map of group → ratio applied when the user selects the group explicitly.' )} )} /> ( {t('Top-up group ratios')} {t( 'Optional multiplier per user group used when calculating recharge pricing. Provide a JSON object such as' )} {` { "default": 1, "vip": 1.2 }`}. )} /> ( {t('Selectable groups')} {t( 'JSON map of group → description exposed when users create API keys.' )} )} /> ( {t('Inter-group overrides')} {t('Nested JSON: source group →')}{' '} {`{ targetGroup: ratio }`}{' '} {t( 'to override billing when a user in one group uses a token of another group.' )} )} /> ( {t('Auto assignment order')} {t( 'JSON array of group identifiers. When enabled below, new tokens rotate through this list.' )} )} /> ( {t('Maximum custom groups per token')} {t( 'Limits only token-specific Auto snapshots. Global Auto inheritance remains unlimited.' )} )} /> ( {t('Special usable group rules')} {t( 'Nested JSON defining per-group rules for adding (+:), removing (-:), or appending usable groups.' )} )} /> ( {t('Default to auto groups')} {t( 'When enabled, newly created tokens start in the first auto group.' )} )} /> )}
) }) type GroupPricingGuideProps = { open: boolean onOpenChange: (open: boolean) => void } function GuideCodeBlock({ children }: { children: string }) { return (
      {children}
    
) } function GuideStepRow({ chip, children, }: { chip: string children: ReactNode }) { return (
{chip} {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('The two roles of a group')}

{t( 'Every group name in the pricing table can be used in two places: on a user (the user group, assigned by admins) and on a token (the token group, chosen when creating the token). Same name pool, two different jobs.' )}

{t('Token group')} {': '} {t( 'decides which channels are used and which base ratio applies.' )}

{t('User group')} {': '} {t( 'decides the top-up ratio, which groups the user can pick for tokens, and whether an override ratio applies.' )}

{t('How a call is priced')}

  1. {t('Find the billing group.')} {' '} {t( 'Use the group set on the token. If the token has no group, use the user group. The auto group tries the auto assignment order from top to bottom.' )}
  2. {t('Find the ratio.')} {' '} {t( 'Look for a special ratio rule matching this user group and this billing group. If one exists, use its ratio. Otherwise use the billing group base ratio from the pricing table.' )}
  3. {t('Charge.')} {' '} {t( 'Cost = model price × that one ratio. Nothing else from the group settings enters the formula.' )}

{t( 'Common pitfall: the user group base ratio is NOT a personal discount. It only applies when the user group itself is the billing group.' )}

{t('Worked example')}

{t( 'The admin configured three groups and one special ratio rule:' )}

{t('Pricing groups')}
{t('Group name')} {t('Ratio')}
default 1.0
premium 0.5
vip 0.8
{t('Special ratio rules')}
{t('Users of vip, when billed as premium, pay ratio')}{' '} 0.3 {' '} {t('(instead of {{ratio}})', { ratio: 0.5 })}

{t( 'Three calls made by the same vip user. Assume the base price of one call is 10.' )}

{t('Call 1: the token group is premium')}
{t( 'Billing group = premium (the token has a group, so use it)' )} {t( 'There is a rule for vip billed as premium → use its ratio 0.3' )} {t('Cost = 10 × 0.3 = 3')}
{t('Call 2: the token group is default')}
{t( 'Billing group = default (the token has a group, so use it)' )} {t( 'No rule for vip billed as default → use the base ratio of default, 1.0 (the 0.8 of vip is not used)' )} {t('Cost = 10 × 1.0 = 10')}
{t('Call 3: the token has no group')}
{t( 'Billing group = vip (the token has no group, so use the user group)' )} {t( 'No rule for vip billed as vip → use the base ratio of vip, 0.8' )} {t('Cost = 10 × 0.8 = 8')}
{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( 'In JSON, the user group is the outer key and the billing group is the inner key. The example below means: vip users pay 0.8 when billed as standard, and 0.3 when billed as premium.' )}

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

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

{t('Special usable group rules')}

{t( 'Special usable group rules make extra token groups visible to, or hide default ones from, users of a specific user group.' )}

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

{t( 'In the visual editor these appear as Extra visible and Hidden. In JSON, +: (or no prefix) adds a group and -: removes one.' )}

) }