feat:webui支持更加优化的模型配置,优化多处UI体验,支持设置视觉和cache价格,修复多重表达不生效的问题,修复表情包路径错误
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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('表达')
|
||||
|
||||
@@ -11,6 +11,7 @@ export type {
|
||||
UseAutoSaveReturnGeneric,
|
||||
} from './useAutoSave'
|
||||
export {
|
||||
ChatPromptsHook,
|
||||
ChatTalkValueRulesHook,
|
||||
ExpressionGroupsHook,
|
||||
ExpressionLearningListHook,
|
||||
|
||||
Reference in New Issue
Block a user