pref:优化webui界面,增加prompt模板元信息

This commit is contained in:
SengokuCola
2026-05-05 17:57:19 +08:00
parent 0d43d3ec05
commit a5e4ac8531
42 changed files with 826 additions and 410 deletions

View File

@@ -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: [],