pref:优化webui界面,增加prompt模板元信息
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, type CSSProperties } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react'
|
||||
import * as LucideIcons from 'lucide-react'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
|
||||
@@ -37,6 +37,11 @@ export interface ListItemEditorOptions {
|
||||
fieldSchemaOverrides?: Record<string, Partial<FieldSchema>>
|
||||
/** 添加按钮位置 */
|
||||
addButtonPlacement?: 'top' | 'bottom'
|
||||
/** 根据同级配置决定是否默认折叠 */
|
||||
collapseWhen?: (context: { parentValues?: Record<string, unknown> }) => boolean
|
||||
collapsedText?: string
|
||||
expandLabel?: string
|
||||
collapseLabel?: string
|
||||
}
|
||||
|
||||
function resolveLabel(schema?: ConfigSchema | FieldSchema, fieldPath?: string): string {
|
||||
@@ -159,6 +164,7 @@ export function createListItemEditorHook(
|
||||
onChange,
|
||||
schema,
|
||||
nestedSchema,
|
||||
parentValues,
|
||||
value,
|
||||
}) => {
|
||||
const items = useMemo<Record<string, unknown>[]>(() => {
|
||||
@@ -283,6 +289,16 @@ export function createListItemEditorHook(
|
||||
const description = resolveDescription(schema)
|
||||
const iconName = resolveIconName(options.iconName, schema, nestedSchema)
|
||||
const addButtonPlacement = options.addButtonPlacement ?? 'bottom'
|
||||
const shouldCollapse = options.collapseWhen?.({ parentValues }) ?? false
|
||||
const [manuallyExpanded, setManuallyExpanded] = useState(false)
|
||||
const collapsed = shouldCollapse && !manuallyExpanded
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldCollapse) {
|
||||
setManuallyExpanded(false)
|
||||
}
|
||||
}, [shouldCollapse])
|
||||
|
||||
const addButton = (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -310,9 +326,23 @@ export function createListItemEditorHook(
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="space-y-2 pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{renderLucideIcon(iconName, 'h-5 w-5 text-muted-foreground')}
|
||||
<CardTitle className="text-base">{label}</CardTitle>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{renderLucideIcon(iconName, 'h-5 w-5 flex-shrink-0 text-muted-foreground')}
|
||||
<CardTitle className="truncate text-base">{label}</CardTitle>
|
||||
</div>
|
||||
{shouldCollapse && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setManuallyExpanded((current) => !current)}
|
||||
>
|
||||
{collapsed
|
||||
? (options.expandLabel ?? '展开')
|
||||
: (options.collapseLabel ?? '折叠')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<CardDescription className="whitespace-pre-line">{description}</CardDescription>
|
||||
@@ -322,6 +352,12 @@ export function createListItemEditorHook(
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{collapsed ? (
|
||||
<div className="rounded-md border border-dashed border-muted-foreground/25 bg-muted/10 p-4 text-sm text-muted-foreground">
|
||||
{options.collapsedText ?? '当前配置已折叠,可手动展开查看或编辑。'}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{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">
|
||||
@@ -360,6 +396,8 @@ export function createListItemEditorHook(
|
||||
})
|
||||
)}
|
||||
{addButtonPlacement === 'bottom' && addButton}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import type { FieldHookComponent } from '@/lib/field-hooks'
|
||||
|
||||
import { createJsonFieldHook } from './JsonFieldHookFactory'
|
||||
import { createListItemEditorHook } from './ListItemEditorHookFactory'
|
||||
|
||||
type ExpressionRuleType = 'group' | 'private'
|
||||
|
||||
interface ExpressionGroupTarget {
|
||||
platform: string
|
||||
item_id: string
|
||||
rule_type: ExpressionRuleType
|
||||
}
|
||||
|
||||
interface ExpressionGroupValue {
|
||||
expression_groups: ExpressionGroupTarget[]
|
||||
}
|
||||
|
||||
const ruleTypeLabel = (rule: unknown) => {
|
||||
if (rule === 'private') return '私聊'
|
||||
if (rule === 'group') return '群聊'
|
||||
@@ -28,9 +55,60 @@ const collectStringList = (value: unknown): string[] => {
|
||||
.filter((item) => item.length > 0)
|
||||
}
|
||||
|
||||
const normalizeExpressionRuleType = (value: unknown): ExpressionRuleType => {
|
||||
return value === 'private' ? 'private' : 'group'
|
||||
}
|
||||
|
||||
const normalizeExpressionTarget = (value: unknown): ExpressionGroupTarget => {
|
||||
const source =
|
||||
value && typeof value === 'object'
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
return {
|
||||
platform:
|
||||
typeof source.platform === 'string' ? source.platform.trim() : 'qq',
|
||||
item_id:
|
||||
typeof source.item_id === 'string' ? source.item_id.trim() : '',
|
||||
rule_type: normalizeExpressionRuleType(source.rule_type),
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeExpressionGroups = (value: unknown): ExpressionGroupValue[] => {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.map((item) => {
|
||||
const source =
|
||||
item && typeof item === 'object'
|
||||
? (item as Record<string, unknown>)
|
||||
: {}
|
||||
const members = Array.isArray(source.expression_groups)
|
||||
? source.expression_groups.map(normalizeExpressionTarget)
|
||||
: []
|
||||
return { expression_groups: members }
|
||||
})
|
||||
}
|
||||
|
||||
const createExpressionTarget = (): ExpressionGroupTarget => ({
|
||||
platform: 'qq',
|
||||
item_id: '',
|
||||
rule_type: 'group',
|
||||
})
|
||||
|
||||
const formatExpressionTarget = (target: ExpressionGroupTarget): string => {
|
||||
const platform = target.platform.trim()
|
||||
const itemId = target.item_id.trim()
|
||||
const rule = ruleTypeLabel(target.rule_type)
|
||||
if (!platform && !itemId) return `全局 · ${rule}`
|
||||
if (!itemId) return `${platform} · ${rule}`
|
||||
return `${platform}:${itemId} · ${rule}`
|
||||
}
|
||||
|
||||
export const ChatTalkValueRulesHook = createListItemEditorHook({
|
||||
addLabel: '添加发言频率规则',
|
||||
addButtonPlacement: 'top',
|
||||
collapseWhen: ({ parentValues }) => parentValues?.enable_talk_value_rules === false,
|
||||
collapsedText: '动态发言频率规则未启用,规则列表已折叠。展开后仍可查看或编辑已有规则。',
|
||||
expandLabel: '展开规则',
|
||||
collapseLabel: '折叠规则',
|
||||
helperText: '可按平台/聊天流/时段分别配置发言频率,留空表示全局。',
|
||||
emptyText: '尚未配置任何规则,将使用全局默认频率。',
|
||||
fieldRows: [
|
||||
@@ -135,11 +213,219 @@ export const RegexRulesHook = createListItemEditorHook({
|
||||
},
|
||||
})
|
||||
|
||||
export const ExpressionGroupsHook = createJsonFieldHook({
|
||||
emptyValue: [],
|
||||
helperText: '表达互通组使用 JSON 编辑。每一项包含一个 expression_groups 数组。',
|
||||
placeholder: '[\n {\n "expression_groups": [\n {\n "platform": "qq",\n "item_id": "123456",\n "rule_type": "group"\n }\n ]\n }\n]',
|
||||
})
|
||||
export const ExpressionGroupsHook: FieldHookComponent = ({ onChange, value }) => {
|
||||
const groups = normalizeExpressionGroups(value)
|
||||
|
||||
const updateGroups = (nextGroups: ExpressionGroupValue[]) => {
|
||||
onChange?.(nextGroups)
|
||||
}
|
||||
|
||||
const addGroup = () => {
|
||||
updateGroups([...groups, { expression_groups: [] }])
|
||||
}
|
||||
|
||||
const removeGroup = (groupIndex: number) => {
|
||||
updateGroups(groups.filter((_, index) => index !== groupIndex))
|
||||
}
|
||||
|
||||
const addMember = (groupIndex: number) => {
|
||||
updateGroups(
|
||||
groups.map((group, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
expression_groups: [
|
||||
...group.expression_groups,
|
||||
createExpressionTarget(),
|
||||
],
|
||||
}
|
||||
: group
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const removeMember = (groupIndex: number, memberIndex: number) => {
|
||||
updateGroups(
|
||||
groups.map((group, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
expression_groups: group.expression_groups.filter(
|
||||
(_, currentMemberIndex) => currentMemberIndex !== memberIndex
|
||||
),
|
||||
}
|
||||
: group
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const updateMember = (
|
||||
groupIndex: number,
|
||||
memberIndex: number,
|
||||
patch: Partial<ExpressionGroupTarget>
|
||||
) => {
|
||||
updateGroups(
|
||||
groups.map((group, index) =>
|
||||
index === groupIndex
|
||||
? {
|
||||
expression_groups: group.expression_groups.map(
|
||||
(member, currentMemberIndex) =>
|
||||
currentMemberIndex === memberIndex
|
||||
? { ...member, ...patch }
|
||||
: member
|
||||
),
|
||||
}
|
||||
: group
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 rounded-lg border bg-card p-4 sm:p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-base font-semibold">表达互通组</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
每个互通组内的聊天流会共享已学习的表达方式。成员会保存为
|
||||
expression_groups 数组结构。
|
||||
</p>
|
||||
</div>
|
||||
<Button type="button" size="sm" variant="outline" onClick={addGroup}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加互通组
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{groups.length === 0 ? (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
暂无互通组,点击“添加互通组”开始配置。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{groups.map((group, groupIndex) => (
|
||||
<div
|
||||
key={groupIndex}
|
||||
className="space-y-3 rounded-md border bg-muted/20 p-3 sm:p-4"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
互通组 {groupIndex + 1}
|
||||
</span>
|
||||
<Badge variant="secondary">
|
||||
{group.expression_groups.length} 个成员
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => addMember(groupIndex)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加成员
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label={`删除互通组 ${groupIndex + 1}`}
|
||||
onClick={() => removeGroup(groupIndex)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{group.expression_groups.length === 0 ? (
|
||||
<div className="rounded-md bg-background/70 px-3 py-4 text-sm text-muted-foreground">
|
||||
这个互通组还没有成员。
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{group.expression_groups.map((member, memberIndex) => (
|
||||
<div
|
||||
key={`${groupIndex}-${memberIndex}`}
|
||||
className="grid gap-3 rounded-md bg-background/80 p-3 md:grid-cols-[minmax(7rem,0.75fr)_minmax(10rem,1fr)_minmax(8rem,0.8fr)_auto]"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">平台</Label>
|
||||
<Input
|
||||
value={member.platform}
|
||||
placeholder="qq"
|
||||
onChange={(event) =>
|
||||
updateMember(groupIndex, memberIndex, {
|
||||
platform: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">账号 / 群号</Label>
|
||||
<Input
|
||||
className="font-mono"
|
||||
value={member.item_id}
|
||||
placeholder="123456"
|
||||
onChange={(event) =>
|
||||
updateMember(groupIndex, memberIndex, {
|
||||
item_id: event.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">类型</Label>
|
||||
<Select
|
||||
value={member.rule_type}
|
||||
onValueChange={(nextRuleType) =>
|
||||
updateMember(groupIndex, memberIndex, {
|
||||
rule_type: normalizeExpressionRuleType(nextRuleType),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="group">群聊</SelectItem>
|
||||
<SelectItem value="private">私聊</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-end justify-between gap-2 md:justify-end">
|
||||
<span className="min-w-0 truncate text-xs text-muted-foreground md:hidden">
|
||||
{formatExpressionTarget(member)}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
aria-label={`删除互通组 ${groupIndex + 1} 的成员 ${memberIndex + 1}`}
|
||||
onClick={() => removeMember(groupIndex, memberIndex)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{group.expression_groups.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.expression_groups.map((member, memberIndex) => (
|
||||
<Badge key={memberIndex} variant="outline">
|
||||
{formatExpressionTarget(member)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const MCPRootItemsHook = createJsonFieldHook({
|
||||
emptyValue: [],
|
||||
|
||||
Reference in New Issue
Block a user