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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user