Feat/auto group (#6590)

* feat(token): support custom auto group order

* feat(keys): enhance auto group presentation

* fix(keys): rework Auto flow border and compact inherited order

The Auto group highlight previously tinted the whole control surface
with a gradient and animated only a 1px top sweep, which read as a
background color rather than a flowing border. Replace it with a
border-only effect: an aria-hidden, pointer-events-none overlay whose
conic gradient is masked down to a thin ring hugging the rounded
perimeter, so the highlight travels around all four edges and corners
every 3.2s. The interior stays neutral with a restrained static
primary border and glow; prefers-reduced-motion hides the moving
layer while keeping the static emphasis.

The inherited global Auto order also rendered as spacious two-line
rows with circular sequence markers, wasting drawer space. Render it
as a compact wrapping strip of one-line chips (index, name, ratio
badge) with descriptions kept accessible via title and sr-only text,
scrolling only past a much smaller max height.

Custom add/remove/reorder editing, empty-array inheritance semantics,
and the submit payload are unchanged.

* fix(keys): preserve Auto inheritance and unify effects

* refactor(keys): temporarily disable AutoGroupBadge in api-key-group-cell
This commit is contained in:
Calcium-Ion
2026-08-01 23:19:01 +08:00
committed by GitHub
parent bd585d78ef
commit 0ab0202060
57 changed files with 3922 additions and 210 deletions
@@ -56,6 +56,7 @@ const defaultBillingSettings: BillingSettings = {
UserUsableGroups: '',
GroupGroupRatio: '',
AutoGroups: '',
MaxTokenAutoGroups: 5,
DefaultUseAutoGroup: false,
'group_ratio_setting.group_special_usable_group': '{}',
PayAddress: '',
@@ -46,6 +46,7 @@ const getGroupDefaults = (settings: BillingSettings) => ({
UserUsableGroups: settings.UserUsableGroups,
GroupGroupRatio: settings.GroupGroupRatio,
AutoGroups: settings.AutoGroups,
MaxTokenAutoGroups: settings.MaxTokenAutoGroups,
DefaultUseAutoGroup: settings.DefaultUseAutoGroup,
GroupSpecialUsableGroup:
settings['group_ratio_setting.group_special_usable_group'],
@@ -0,0 +1,40 @@
/*
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 assert from 'node:assert/strict'
import { describe, test } from 'node:test'
import { positiveIntegerSchema } from '../../utils/numeric-field'
const t = (key: string) => key
const schema = positiveIntegerSchema(t('Enter a positive integer'))
describe('per-token Auto group limit validation', () => {
test('accepts any positive integer without a product upper bound', () => {
assert.equal(schema.safeParse(1000).success, true)
})
test('rejects zero, negative, and fractional limits', () => {
for (const maxTokenAutoGroups of [0, -1, 1.5]) {
const result = schema.safeParse(maxTokenAutoGroups)
assert.equal(result.success, false)
if (result.success) continue
assert.equal(result.error.issues[0]?.message, 'Enter a positive integer')
}
})
})
@@ -43,6 +43,7 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Sheet,
SheetContent,
@@ -59,6 +60,7 @@ import {
} 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'
@@ -68,6 +70,7 @@ type GroupFormValues = {
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
MaxTokenAutoGroups: number
DefaultUseAutoGroup: boolean
GroupSpecialUsableGroup: string
}
@@ -169,6 +172,34 @@ export const GroupRatioForm = memo(function GroupRatioForm({
userUsableGroups={form.watch('UserUsableGroups')}
groupGroupRatio={form.watch('GroupGroupRatio')}
autoGroups={form.watch('AutoGroups')}
maxTokenAutoGroupsField={
<FormField
control={form.control}
name='MaxTokenAutoGroups'
render={({ field, fieldState }) => (
<FormItem data-invalid={fieldState.invalid}>
<FormLabel>
{t('Maximum custom groups per token')}
</FormLabel>
<FormControl>
<Input
{...safeNumberFieldProps(field)}
type='number'
min={1}
step={1}
aria-invalid={fieldState.invalid}
/>
</FormControl>
<FormDescription>
{t(
'Limits only token-specific Auto snapshots. Global Auto inheritance remains unlimited.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
}
groupSpecialUsableGroup={form.watch('GroupSpecialUsableGroup')}
onChange={(field, value) =>
handleFieldChange(field as keyof GroupFormValues, value)
@@ -339,6 +370,31 @@ export const GroupRatioForm = memo(function GroupRatioForm({
)}
/>
<FormField
control={form.control}
name='MaxTokenAutoGroups'
render={({ field, fieldState }) => (
<FormItem data-invalid={fieldState.invalid}>
<FormLabel>{t('Maximum custom groups per token')}</FormLabel>
<FormControl>
<Input
{...safeNumberFieldProps(field)}
type='number'
min={1}
step={1}
aria-invalid={fieldState.invalid}
/>
</FormControl>
<FormDescription>
{t(
'Limits only token-specific Auto snapshots. Global Auto inheritance remains unlimited.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='GroupSpecialUsableGroup'
@@ -24,7 +24,14 @@ import {
Plus,
Trash2,
} from 'lucide-react'
import { useState, useMemo, useEffect, useCallback, memo } from 'react'
import {
useState,
useMemo,
useEffect,
useCallback,
memo,
type ReactNode,
} from 'react'
import { useTranslation } from 'react-i18next'
import { StaticDataTable } from '@/components/data-table/static/static-data-table'
@@ -76,6 +83,7 @@ type GroupRatioVisualEditorProps = {
userUsableGroups: string
groupGroupRatio: string
autoGroups: string
maxTokenAutoGroupsField: ReactNode
groupSpecialUsableGroup: string
onChange: (field: string, value: string) => void
}
@@ -257,6 +265,7 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({
userUsableGroups,
groupGroupRatio,
autoGroups,
maxTokenAutoGroupsField,
groupSpecialUsableGroup,
onChange,
}: GroupRatioVisualEditorProps) {
@@ -351,6 +360,7 @@ export const GroupRatioVisualEditor = memo(function GroupRatioVisualEditor({
</CardHeader>
<CardContent>
<div className='space-y-4'>
{maxTokenAutoGroupsField}
<GroupNameSelect
options={autoGroupCandidates}
value={null}
@@ -60,6 +60,7 @@ const defaultModelSettings: ModelSettings = {
UserUsableGroups: '',
GroupGroupRatio: '',
AutoGroups: '',
MaxTokenAutoGroups: 5,
DefaultUseAutoGroup: false,
'group_ratio_setting.group_special_usable_group': '{}',
RetryTimes: 0,
@@ -31,6 +31,7 @@ import { resetModelRatios } from '../api'
import { SettingsPageTitleStatusPortal } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
import { positiveIntegerSchema } from '../utils/numeric-field'
import { GroupRatioForm } from './group-ratio-form'
import { ModelRatioForm } from './model-ratio-form'
import { ToolPriceSettings } from './tool-price-settings'
@@ -130,6 +131,7 @@ const createGroupSchema = (t: Translate) =>
parsed.every((item) => typeof item === 'string'),
predicateMessage: 'Expected a JSON array of group identifiers',
}),
MaxTokenAutoGroups: positiveIntegerSchema(t('Enter a positive integer')),
DefaultUseAutoGroup: z.boolean(),
GroupSpecialUsableGroup: createJsonStringField(t),
})
@@ -204,6 +206,7 @@ export function RatioSettingsCard({
UserUsableGroups: normalizeJsonString(groupDefaults.UserUsableGroups),
GroupGroupRatio: normalizeJsonString(groupDefaults.GroupGroupRatio),
AutoGroups: normalizeJsonString(groupDefaults.AutoGroups),
MaxTokenAutoGroups: groupDefaults.MaxTokenAutoGroups,
DefaultUseAutoGroup: groupDefaults.DefaultUseAutoGroup,
GroupSpecialUsableGroup: normalizeJsonString(
groupDefaults.GroupSpecialUsableGroup
@@ -290,6 +293,7 @@ export function RatioSettingsCard({
UserUsableGroups: normalizeJsonString(groupDefaults.UserUsableGroups),
GroupGroupRatio: normalizeJsonString(groupDefaults.GroupGroupRatio),
AutoGroups: normalizeJsonString(groupDefaults.AutoGroups),
MaxTokenAutoGroups: groupDefaults.MaxTokenAutoGroups,
DefaultUseAutoGroup: groupDefaults.DefaultUseAutoGroup,
GroupSpecialUsableGroup: normalizeJsonString(
groupDefaults.GroupSpecialUsableGroup
@@ -360,6 +364,7 @@ export function RatioSettingsCard({
UserUsableGroups: normalizeJsonString(values.UserUsableGroups),
GroupGroupRatio: normalizeJsonString(values.GroupGroupRatio),
AutoGroups: normalizeJsonString(values.AutoGroups),
MaxTokenAutoGroups: values.MaxTokenAutoGroups,
DefaultUseAutoGroup: values.DefaultUseAutoGroup,
GroupSpecialUsableGroup: normalizeJsonString(
values.GroupSpecialUsableGroup
@@ -382,6 +387,8 @@ export function RatioSettingsCard({
const apiKey = apiKeyMap[key] || key
await updateOption.mutateAsync({ key: apiKey, value: normalized[key] })
}
groupNormalizedDefaults.current = normalized
},
[updateOption]
)
@@ -223,6 +223,7 @@ export type ModelSettings = {
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
MaxTokenAutoGroups: number
DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string
RetryTimes: number
@@ -277,6 +278,7 @@ export type BillingSettings = {
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
MaxTokenAutoGroups: number
DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string
PayAddress: string
@@ -22,6 +22,11 @@ import type {
FieldPath,
FieldValues,
} from 'react-hook-form'
import { z } from 'zod'
export function positiveIntegerSchema(message: string) {
return z.number().int(message).positive(message)
}
/**
* Props produced by {@link safeNumberFieldProps} for a native