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

@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react'
import { useCallback, useMemo, type CSSProperties } from 'react'
import * as LucideIcons from 'lucide-react'
import { Plus, Trash2 } from 'lucide-react'
@@ -31,6 +31,12 @@ export interface ListItemEditorOptions {
emptyText?: string
/** 顶部图标(覆盖 schema 自带的 x-icon */
iconName?: string
/** 紧凑布局:把指定字段放在同一行展示 */
fieldRows?: string[][]
/** Hook-local field UI metadata overrides */
fieldSchemaOverrides?: Record<string, Partial<FieldSchema>>
/** 添加按钮位置 */
addButtonPlacement?: 'top' | 'bottom'
}
function resolveLabel(schema?: ConfigSchema | FieldSchema, fieldPath?: string): string {
@@ -190,9 +196,105 @@ export function createListItemEditorHook(
[items, onChange],
)
const renderItemEditor = (item: Record<string, unknown>, index: number) => {
if (!nestedSchema) {
return null
}
if (!options.fieldRows?.length) {
return (
<DynamicConfigForm
schema={nestedSchema}
values={item}
onChange={(field, fieldValue) =>
handleItemFieldChange(index, field, fieldValue)
}
basePath=""
level={1}
/>
)
}
const applyFieldOverride = (field: FieldSchema): FieldSchema => ({
...field,
...(options.fieldSchemaOverrides?.[field.name] ?? {}),
})
const fieldMap = new Map(
nestedSchema.fields.map((field) => [field.name, applyFieldOverride(field)]),
)
const rowFieldNames = new Set(options.fieldRows.flat())
const remainingFields = nestedSchema.fields
.filter((field) => !rowFieldNames.has(field.name))
.map(applyFieldOverride)
const buildRowSchema = (fields: FieldSchema[]): ConfigSchema => ({
...nestedSchema,
fields,
nested: undefined,
})
return (
<div className="space-y-3">
{options.fieldRows.map((row, rowIndex) => {
const fields = row
.map((fieldName) => fieldMap.get(fieldName))
.filter((field): field is FieldSchema => Boolean(field))
if (fields.length === 0) {
return null
}
return (
<div
key={rowIndex}
className="grid gap-3 md:grid-cols-[repeat(var(--field-count),minmax(0,1fr))]"
style={{ '--field-count': fields.length } as CSSProperties}
>
{fields.map((field) => (
<DynamicConfigForm
key={field.name}
schema={buildRowSchema([field])}
values={item}
onChange={(fieldName, fieldValue) =>
handleItemFieldChange(index, fieldName, fieldValue)
}
basePath=""
level={1}
/>
))}
</div>
)
})}
{remainingFields.length > 0 && (
<DynamicConfigForm
schema={buildRowSchema(remainingFields)}
values={item}
onChange={(field, fieldValue) =>
handleItemFieldChange(index, field, fieldValue)
}
basePath=""
level={1}
/>
)}
</div>
)
}
const label = resolveLabel(schema, fieldPath)
const description = resolveDescription(schema)
const iconName = resolveIconName(options.iconName, schema, nestedSchema)
const addButtonPlacement = options.addButtonPlacement ?? 'bottom'
const addButton = (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAdd}
className="w-full"
>
<Plus className="mr-1 h-4 w-4" />
{options.addLabel ?? '添加一项'}
</Button>
)
if (!nestedSchema) {
return (
@@ -220,6 +322,7 @@ export function createListItemEditorHook(
)}
</CardHeader>
<CardContent className="space-y-3">
{addButtonPlacement === 'top' && addButton}
{items.length === 0 ? (
<div className="rounded-md border border-dashed border-muted-foreground/25 bg-muted/10 p-6 text-center text-sm text-muted-foreground">
{options.emptyText ?? '尚未添加任何条目,点击下方按钮新增。'}
@@ -251,29 +354,12 @@ export function createListItemEditorHook(
</Button>
</div>
<DynamicConfigForm
schema={nestedSchema}
values={item}
onChange={(field, fieldValue) =>
handleItemFieldChange(index, field, fieldValue)
}
basePath=""
level={1}
/>
{renderItemEditor(item, index)}
</div>
)
})
)}
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAdd}
className="w-full"
>
<Plus className="mr-1 h-4 w-4" />
{options.addLabel ?? '添加一项'}
</Button>
{addButtonPlacement === 'bottom' && addButton}
</CardContent>
</Card>
)

View File

@@ -30,8 +30,13 @@ const collectStringList = (value: unknown): string[] => {
export const ChatTalkValueRulesHook = createListItemEditorHook({
addLabel: '添加发言频率规则',
addButtonPlacement: 'top',
helperText: '可按平台/聊天流/时段分别配置发言频率,留空表示全局。',
emptyText: '尚未配置任何规则,将使用全局默认频率。',
fieldRows: [
['platform', 'item_id', 'rule_type'],
['time', 'value'],
],
itemTitle: (item) => {
const time =
typeof item.time === 'string' && item.time.trim()
@@ -43,10 +48,45 @@ export const ChatTalkValueRulesHook = createListItemEditorHook({
},
})
export const ChatPromptsHook = createListItemEditorHook({
addLabel: '添加额外 Prompt',
helperText: '为指定平台和聊天流添加额外提示。platform、item_id 和 prompt 同时留空时表示空条目;填写任意一项后这三项都需要填写。',
emptyText: '尚未配置任何聊天额外 Prompt。',
addButtonPlacement: 'top',
fieldRows: [['platform', 'item_id', 'rule_type']],
fieldSchemaOverrides: {
item_id: {
'x-input-width': '8rem',
'x-layout': 'inline-right',
},
platform: {
'x-input-width': '8rem',
'x-layout': 'inline-right',
},
prompt: {
'x-textarea-min-height': 38,
'x-textarea-rows': 1,
},
rule_type: {
'x-input-width': '8rem',
'x-layout': 'inline-right',
},
},
iconName: 'file-text',
itemTitle: (item) => {
const prompt = typeof item.prompt === 'string' ? item.prompt.trim() : ''
return `${platformLabel(item)} · ${ruleTypeLabel(item.rule_type)} · ${prompt ? truncate(prompt) : '未填写 Prompt'}`
},
})
export const ExpressionLearningListHook = createListItemEditorHook({
addLabel: '添加表达学习规则',
helperText: '为不同聊天流单独配置是否启用表达/jargon 学习。',
emptyText: '尚未配置任何学习规则。',
fieldRows: [
['platform', 'item_id', 'rule_type'],
['use_expression', 'enable_learning', 'enable_jargon_learning'],
],
itemTitle: (item) => {
const flags: string[] = []
if (item.use_expression) flags.push('表达')

View File

@@ -11,6 +11,7 @@ export type {
UseAutoSaveReturnGeneric,
} from './useAutoSave'
export {
ChatPromptsHook,
ChatTalkValueRulesHook,
ExpressionGroupsHook,
ExpressionLearningListHook,