feat:webui支持更加优化的模型配置,优化多处UI体验,支持设置视觉和cache价格,修复多重表达不生效的问题,修复表情包路径错误

This commit is contained in:
SengokuCola
2026-05-04 22:52:41 +08:00
parent 14b7bc78a2
commit eea95c1961
38 changed files with 1188 additions and 454 deletions

View File

@@ -76,7 +76,6 @@ function DynamicConfigSection({
basePath,
hooks,
level,
mergedChildren = [],
nestedSchema,
onChange,
sectionDescription,
@@ -87,11 +86,6 @@ function DynamicConfigSection({
basePath: string
hooks: FieldHookRegistry
level: number
mergedChildren?: Array<{
key: string
schema: ConfigSchema
values: Record<string, unknown>
}>
nestedSchema: ConfigSchema
onChange: (field: string, value: unknown) => void
sectionDescription?: string
@@ -100,9 +94,7 @@ function DynamicConfigSection({
values: Record<string, unknown>
}) {
const [advancedVisible, setAdvancedVisible] = React.useState(false)
const hasAdvanced =
hasTopLevelAdvancedFields(nestedSchema) ||
mergedChildren.some((child) => hasTopLevelAdvancedFields(child.schema))
const hasAdvanced = hasTopLevelAdvancedFields(nestedSchema)
return (
<Card>
@@ -135,37 +127,6 @@ function DynamicConfigSection({
level={level}
advancedVisible={hasAdvanced ? advancedVisible : undefined}
/>
{mergedChildren.map((child) => {
const childTitle = resolveSectionTitle(child.schema)
const childDescription = resolveSectionDescription(child.schema, childTitle)
const parentPath = basePath.includes('.')
? basePath.replace(/\.[^.]+$/, '')
: ''
const childPath = buildFieldPath(parentPath, child.key)
return (
<div key={child.key} className="mt-5 border-t border-border/50 pt-4">
<div className="mb-3 space-y-1">
<div className="flex items-center gap-2">
<SectionIcon iconName={child.schema.uiIcon} />
<h3 className="text-sm font-medium">{childTitle}</h3>
</div>
{childDescription && (
<p className="text-xs text-muted-foreground">{childDescription}</p>
)}
</div>
<DynamicConfigForm
schema={child.schema}
values={child.values}
onChange={(field, value) => onChange(`${child.key}.${field}`, value)}
basePath={childPath}
hooks={hooks}
level={level}
advancedVisible={hasAdvanced ? advancedVisible : undefined}
/>
</div>
)
})}
</CardContent>
</Card>
)
@@ -197,17 +158,6 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
() => new Map(schema.fields.map((field) => [field.name, field])),
[schema.fields],
)
const mergedChildKeys = React.useMemo(() => {
const keys = new Set<string>()
for (const nestedSchema of Object.values(schema.nested ?? {})) {
for (const childKey of nestedSchema.uiMergeChildren ?? []) {
if (schema.nested?.[childKey]) {
keys.add(childKey)
}
}
}
return keys
}, [schema.nested])
const renderField = (field: FieldSchema) => {
const fieldPath = buildFieldPath(basePath, field.name)
@@ -294,7 +244,6 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
{schema.nested &&
Object.entries(schema.nested)
.filter(([key]) => !mergedChildKeys.has(key))
.map(([key, nestedSchema]) => {
const nestedField = fieldMap.get(key)
const nestedFieldPath = buildFieldPath(basePath, key)
@@ -342,34 +291,11 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
const sectionTitle = resolveSectionTitle(nestedSchema)
const sectionDescription = resolveSectionDescription(nestedSchema, sectionTitle)
const mergedChildren = (nestedSchema.uiMergeChildren ?? [])
.map((childKey) => {
const childSchema = schema.nested?.[childKey]
if (!childSchema) {
return null
}
return {
key: childKey,
schema: childSchema,
values: (values[childKey] as Record<string, unknown>) || {},
}
})
.filter(
(
child,
): child is {
key: string
schema: ConfigSchema
values: Record<string, unknown>
} => Boolean(child),
)
if (level === 0) {
return (
<DynamicConfigSection
key={key}
mergedChildren={mergedChildren}
nestedSchema={nestedSchema}
values={(values[key] as Record<string, unknown>) || {}}
onChange={onChange}

View File

@@ -31,6 +31,23 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
value,
onChange,
}) => {
const parseNumericValue = (rawValue: unknown, fallback: number) => {
if (typeof rawValue === 'number' && Number.isFinite(rawValue)) {
return rawValue
}
if (typeof rawValue === 'string') {
const parsedValue = schema.type === 'integer'
? parseInt(rawValue, 10)
: parseFloat(rawValue)
if (Number.isFinite(parsedValue)) {
return parsedValue
}
}
return fallback
}
const renderPrimitiveArrayEditor = () => {
const itemType = schema.items?.type ?? 'string'
const arrayValue = Array.isArray(value)
@@ -94,6 +111,12 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
return <IconComponent className="h-4 w-4" />
}
const isRuleTypeSelect =
schema.name === 'rule_type' &&
(schema.type === 'select' || schema['x-widget'] === 'select')
const inlineDescription = isRuleTypeSelect ? '' : schema.description
const selectHoverDescription = isRuleTypeSelect ? schema.description : undefined
const renderFieldHeader = () => (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<Label
@@ -108,9 +131,9 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
<span className="break-all">{schema.label}</span>
{schema.required && <span className="text-destructive">*</span>}
</Label>
{schema.description && (
{inlineDescription && (
<span className="text-[13px] leading-6 text-muted-foreground whitespace-pre-line">
{schema.description}
{inlineDescription}
</span>
)}
</div>
@@ -129,6 +152,9 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
case 'slider':
return renderSlider()
case 'input':
if (type === 'integer' || type === 'number') {
return renderNumberInput()
}
return renderTextInput()
case 'number':
return renderNumberInput()
@@ -214,7 +240,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
* 渲染 Slider 组件(用于 number 类型 + x-widget: slider
*/
const renderSlider = () => {
const numValue = typeof value === 'number' ? value : (schema.default as number ?? 0)
const numValue = parseNumericValue(value, schema.default as number ?? 0)
const min = schema.minValue ?? 0
const max = schema.maxValue ?? 100
const step = schema.step ?? 1
@@ -241,7 +267,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
* 渲染 Input[type="number"] 组件(用于 number/integer 类型)
*/
const renderNumberInput = () => {
const numValue = typeof value === 'number' ? value : (schema.default as number ?? 0)
const numValue = parseNumericValue(value, schema.default as number ?? 0)
const min = schema.minValue
const max = schema.maxValue
const step = schema.step ?? (schema.type === 'integer' ? 1 : 0.1)
@@ -250,7 +276,12 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
<Input
type="number"
value={numValue}
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
onChange={(e) => {
const nextValue = schema.type === 'integer'
? parseInt(e.target.value, 10)
: parseFloat(e.target.value)
onChange(Number.isFinite(nextValue) ? nextValue : 0)
}}
min={min}
max={max}
step={step}
@@ -262,7 +293,12 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
* 渲染 Input[type="text"] 组件(用于 string 类型)
*/
const renderTextInput = (type: 'password' | 'text' = 'text') => {
const strValue = typeof value === 'string' ? value : (schema.default as string ?? '')
const strValue =
typeof value === 'string'
? value
: value === null || value === undefined
? String(schema.default ?? '')
: String(value)
return (
<Input
type={type}
@@ -277,11 +313,19 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
*/
const renderTextarea = () => {
const strValue = typeof value === 'string' ? value : (schema.default as string ?? '')
const minHeight = typeof schema['x-textarea-min-height'] === 'number'
? schema['x-textarea-min-height']
: undefined
const rows = typeof schema['x-textarea-rows'] === 'number'
? schema['x-textarea-rows']
: 4
return (
<Textarea
value={strValue}
onChange={(e) => onChange(e.target.value)}
rows={4}
rows={rows}
minHeight={minHeight}
/>
)
}
@@ -303,7 +347,7 @@ export const DynamicField: React.FC<DynamicFieldProps> = ({
return (
<Select value={strValue} onValueChange={(val) => onChange(val)}>
<SelectTrigger>
<SelectTrigger title={selectHoverDescription}>
<SelectValue placeholder={`Select ${schema.label}`} />
</SelectTrigger>
<SelectContent>

View File

@@ -100,6 +100,42 @@ describe('DynamicField', () => {
expect(screen.getByText('Custom field requires Hook')).toBeInTheDocument()
})
it('renders number Input when x-widget is input but type is integer', () => {
const schema: FieldSchema = {
name: 'test_integer_input_widget',
type: 'integer',
label: 'Test Integer Input Widget',
description: 'A numeric field rendered as input',
required: false,
'x-widget': 'input',
default: 0,
}
const onChange = vi.fn()
render(<DynamicField schema={schema} value={2} onChange={onChange} />)
const input = screen.getByRole('spinbutton')
expect(input).toBeInTheDocument()
expect(input).toHaveValue(2)
})
it('parses string values for numeric input widgets', () => {
const schema: FieldSchema = {
name: 'test_string_number_input_widget',
type: 'integer',
label: 'Test String Number Input Widget',
description: 'A numeric field with legacy string value',
required: false,
'x-widget': 'input',
default: 0,
}
const onChange = vi.fn()
render(<DynamicField schema={schema} value="2" onChange={onChange} />)
expect(screen.getByRole('spinbutton')).toHaveValue(2)
})
})
describe('type fallback', () => {
@@ -305,6 +341,27 @@ describe('DynamicField', () => {
await user.type(input, '123')
expect(onChange).toHaveBeenCalled()
})
it('triggers numeric onChange for input widget with integer type', async () => {
const schema: FieldSchema = {
name: 'test_integer_input_widget_change',
type: 'integer',
label: 'Test Integer Input Widget Change',
description: 'A numeric field rendered as input',
required: false,
'x-widget': 'input',
default: 0,
}
const onChange = vi.fn()
const user = userEvent.setup()
render(<DynamicField schema={schema} value={0} onChange={onChange} />)
const input = screen.getByRole('spinbutton')
await user.clear(input)
await user.type(input, '5')
expect(onChange).toHaveBeenLastCalledWith(5)
})
})
describe('visual features', () => {
@@ -377,6 +434,25 @@ describe('DynamicField', () => {
expect(screen.getByText('50')).toBeInTheDocument()
expect(screen.getByText('25')).toBeInTheDocument()
})
it('parses string values for slider widgets', () => {
const schema: FieldSchema = {
name: 'test_slider_string_value',
type: 'number',
label: 'Test Slider String Value',
description: 'A slider with legacy string value',
required: false,
'x-widget': 'slider',
minValue: 0,
maxValue: 10,
default: 0,
}
const onChange = vi.fn()
render(<DynamicField schema={schema} value="2.5" onChange={onChange} />)
expect(screen.getByText('2.5')).toBeInTheDocument()
})
})
describe('select features', () => {