pref:优化webui界面,增加prompt模板元信息
This commit is contained in:
@@ -175,6 +175,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
value={values[field.name]}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
schema={field}
|
||||
parentValues={values}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -185,6 +186,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
value={values[field.name]}
|
||||
onChange={(v) => onChange(field.name, v)}
|
||||
schema={field}
|
||||
parentValues={values}
|
||||
>
|
||||
<DynamicField
|
||||
schema={field}
|
||||
@@ -300,6 +302,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
onChange={(v) => onChange(key, v)}
|
||||
schema={nestedField ?? nestedSchema}
|
||||
nestedSchema={nestedSchema}
|
||||
parentValues={values}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@@ -313,6 +316,7 @@ export const DynamicConfigForm: React.FC<DynamicConfigFormProps> = ({
|
||||
onChange={(v) => onChange(key, v)}
|
||||
schema={nestedField ?? nestedSchema}
|
||||
nestedSchema={nestedSchema}
|
||||
parentValues={values}
|
||||
>
|
||||
<DynamicConfigForm
|
||||
schema={nestedSchema}
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface FieldHookComponentProps {
|
||||
onChange?: (value: unknown) => void
|
||||
children?: ReactNode
|
||||
schema?: ConfigSchema | FieldSchema
|
||||
parentValues?: Record<string, unknown>
|
||||
/**
|
||||
* 如果当前字段是 `List[ConfigBase]` 或嵌套 ConfigBase,
|
||||
* 这里会传入对应子配置类的 ConfigSchema,便于自定义编辑器
|
||||
|
||||
@@ -8,6 +8,9 @@ export interface PromptFileInfo {
|
||||
name: string
|
||||
size: number
|
||||
modified_at: number
|
||||
display_name: string
|
||||
advanced: boolean
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface PromptCatalog {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 修改此处的版本号后,所有展示版本的地方都会自动更新
|
||||
*/
|
||||
|
||||
export const APP_VERSION = '1.0.3'
|
||||
export const APP_VERSION = '1.0.5'
|
||||
export const APP_NAME = 'MaiBot Dashboard'
|
||||
export const APP_FULL_NAME = `${APP_NAME} v${APP_VERSION}`
|
||||
|
||||
|
||||
@@ -48,11 +48,10 @@ const TOAST_DISPLAY_DELAY = 500
|
||||
/** Tab 标签页的首选排列顺序 (host field name) */
|
||||
const TAB_ORDER = [
|
||||
'bot',
|
||||
'personality',
|
||||
'chat',
|
||||
'expression',
|
||||
'visual',
|
||||
'a_memorix',
|
||||
'visual',
|
||||
'message_receive',
|
||||
'emoji',
|
||||
'voice',
|
||||
@@ -65,10 +64,8 @@ const TAB_ORDER = [
|
||||
/** 默认展示的主配置栏目 */
|
||||
const DEFAULT_VISIBLE_TAB_IDS = new Set([
|
||||
'bot',
|
||||
'personality',
|
||||
'chat',
|
||||
'expression',
|
||||
'visual',
|
||||
'a_memorix',
|
||||
])
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -63,27 +63,29 @@ export const BotInfoSection = React.memo(function BotInfoSection({ config, onCha
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">基本信息</h3>
|
||||
<h3 className="text-lg font-semibold mb-4">基础</h3>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="platform">平台</Label>
|
||||
<Input
|
||||
id="platform"
|
||||
value={config.platform}
|
||||
onChange={(e) => onChange({ ...config, platform: e.target.value })}
|
||||
placeholder="qq"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="platform">平台</Label>
|
||||
<Input
|
||||
id="platform"
|
||||
value={config.platform}
|
||||
onChange={(e) => onChange({ ...config, platform: e.target.value })}
|
||||
placeholder="qq"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="qq_account">QQ账号</Label>
|
||||
<Input
|
||||
id="qq_account"
|
||||
value={config.qq_account}
|
||||
onChange={(e) => onChange({ ...config, qq_account: e.target.value })}
|
||||
placeholder="123456789"
|
||||
/>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="qq_account">QQ账号</Label>
|
||||
<Input
|
||||
id="qq_account"
|
||||
value={config.qq_account}
|
||||
onChange={(e) => onChange({ ...config, qq_account: e.target.value })}
|
||||
placeholder="123456789"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
|
||||
@@ -311,23 +311,6 @@ export const FeaturesSection = React.memo(function FeaturesSection({
|
||||
启用表情包过滤
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{emojiConfig.content_filtration && (
|
||||
<div className="grid gap-2 pl-6 border-l-2 border-primary/20">
|
||||
<Label htmlFor="filtration_prompt">过滤要求</Label>
|
||||
<Input
|
||||
id="filtration_prompt"
|
||||
value={emojiConfig.filtration_prompt}
|
||||
onChange={(e) =>
|
||||
onEmojiChange({ ...emojiConfig, filtration_prompt: e.target.value })
|
||||
}
|
||||
placeholder="符合公序良俗"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
只有符合此要求的表情包才会被保存
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,6 @@ export interface EmojiConfig {
|
||||
check_interval: number
|
||||
steal_emoji: boolean
|
||||
content_filtration: boolean
|
||||
filtration_prompt: string
|
||||
}
|
||||
|
||||
export interface MemoryConfig {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { FileText, Loader2, RefreshCw, Save, Search } from 'lucide-react'
|
||||
import { FileText, Loader2, RefreshCw, Save, Search, SlidersHorizontal } from 'lucide-react'
|
||||
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -36,6 +36,7 @@ export function PromptManagementPage() {
|
||||
const [loadingFile, setLoadingFile] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [showAdvancedPrompts, setShowAdvancedPrompts] = useState(false)
|
||||
|
||||
const hasUnsavedChanges = content !== savedContent
|
||||
|
||||
@@ -44,13 +45,30 @@ export function PromptManagementPage() {
|
||||
return catalog.files[language] ?? []
|
||||
}, [catalog, language])
|
||||
|
||||
const visiblePromptFiles = useMemo<PromptFileInfo[]>(() => {
|
||||
return showAdvancedPrompts ? promptFiles : promptFiles.filter((file) => !file.advanced)
|
||||
}, [promptFiles, showAdvancedPrompts])
|
||||
|
||||
const filteredFiles = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLowerCase()
|
||||
if (!normalizedQuery) return promptFiles
|
||||
return promptFiles.filter((file) => file.name.toLowerCase().includes(normalizedQuery))
|
||||
}, [promptFiles, query])
|
||||
if (!normalizedQuery) return visiblePromptFiles
|
||||
return visiblePromptFiles.filter((file) => {
|
||||
const searchableText = [
|
||||
file.name,
|
||||
file.display_name,
|
||||
file.description,
|
||||
].join(' ').toLowerCase()
|
||||
return searchableText.includes(normalizedQuery)
|
||||
})
|
||||
}, [visiblePromptFiles, query])
|
||||
|
||||
const selectedFile = promptFiles.find((file) => file.name === filename)
|
||||
useEffect(() => {
|
||||
if (!filename || showAdvancedPrompts) return
|
||||
const currentFile = promptFiles.find((file) => file.name === filename)
|
||||
if (!currentFile?.advanced) return
|
||||
setFilename(visiblePromptFiles[0]?.name ?? '')
|
||||
}, [filename, promptFiles, showAdvancedPrompts, visiblePromptFiles])
|
||||
|
||||
const loadCatalog = useCallback(async () => {
|
||||
try {
|
||||
@@ -70,7 +88,10 @@ export function PromptManagementPage() {
|
||||
setLanguage(nextLanguage)
|
||||
|
||||
const nextFiles = nextLanguage ? result.data.files[nextLanguage] ?? [] : []
|
||||
setFilename((current) => nextFiles.some((file) => file.name === current) ? current : nextFiles[0]?.name ?? '')
|
||||
const nextBasicFiles = nextFiles.filter((file) => !file.advanced)
|
||||
setFilename((current) =>
|
||||
nextFiles.some((file) => file.name === current) ? current : nextBasicFiles[0]?.name ?? nextFiles[0]?.name ?? ''
|
||||
)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '加载 Prompt 目录失败',
|
||||
@@ -130,7 +151,8 @@ export function PromptManagementPage() {
|
||||
setLanguage(nextLanguage)
|
||||
setQuery('')
|
||||
const nextFiles = catalog?.files[nextLanguage] ?? []
|
||||
setFilename(nextFiles[0]?.name ?? '')
|
||||
const nextVisibleFiles = showAdvancedPrompts ? nextFiles : nextFiles.filter((file) => !file.advanced)
|
||||
setFilename(nextVisibleFiles[0]?.name ?? '')
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -181,6 +203,14 @@ export function PromptManagementPage() {
|
||||
<RefreshCw className={cn('mr-2 h-4 w-4', loadingCatalog && 'animate-spin')} />
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
variant={showAdvancedPrompts ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setShowAdvancedPrompts((current) => !current)}
|
||||
>
|
||||
<SlidersHorizontal className="mr-2 h-4 w-4" />
|
||||
{showAdvancedPrompts ? '隐藏高级' : '显示高级'}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleSave} disabled={!hasUnsavedChanges || saving || loadingFile || !filename}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{saving ? '保存中' : hasUnsavedChanges ? '保存' : '已保存'}
|
||||
@@ -194,7 +224,7 @@ export function PromptManagementPage() {
|
||||
<CardTitle className="flex items-center gap-2 text-sm">
|
||||
<FileText className="h-4 w-4" />
|
||||
Prompt 文件
|
||||
<Badge variant="secondary" className="ml-auto">{promptFiles.length}</Badge>
|
||||
<Badge variant="secondary" className="ml-auto">{filteredFiles.length}</Badge>
|
||||
</CardTitle>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
@@ -226,8 +256,16 @@ export function PromptManagementPage() {
|
||||
filename === file.name ? 'bg-accent text-accent-foreground' : 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<div className="truncate font-medium" title={file.name}>{file.name}</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">{formatFileSize(file.size)}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="truncate font-medium" title={file.display_name || file.name}>
|
||||
{file.display_name || file.name}
|
||||
</div>
|
||||
{file.advanced && <Badge variant="outline" className="shrink-0 text-[10px]">高级</Badge>}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">{file.name} · {formatFileSize(file.size)}</div>
|
||||
{file.description && (
|
||||
<div className="mt-1 line-clamp-2 text-xs text-muted-foreground">{file.description}</div>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
@@ -240,12 +278,18 @@ export function PromptManagementPage() {
|
||||
<Card className="min-h-0 overflow-hidden">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-3 space-y-0 pb-3">
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="truncate text-sm">{filename || '未选择文件'}</CardTitle>
|
||||
<CardTitle className="flex items-center gap-2 truncate text-sm">
|
||||
<span className="truncate">{selectedFile?.display_name || filename || '未选择文件'}</span>
|
||||
{selectedFile?.advanced && <Badge variant="outline" className="shrink-0">高级</Badge>}
|
||||
</CardTitle>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{language}
|
||||
{selectedFile ? ` · ${formatFileSize(selectedFile.size)}` : ''}
|
||||
{hasUnsavedChanges ? ' · 有未保存修改' : ''}
|
||||
</p>
|
||||
{selectedFile?.description && (
|
||||
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{selectedFile.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="min-h-0 p-0">
|
||||
|
||||
Reference in New Issue
Block a user