MaiBot/dashboard/src/routes/config/bot.tsx

764 lines
30 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
BotInfoSection,
PersonalitySection,
ChatSection,
DreamSection,
LPMMSection,
LogSection,
DebugSection,
ExperimentalSection,
MaimMessageSection,
TelemetrySection,
FeaturesSection,
ExpressionSection,
ProcessingSection,
MessageReceiveSection,
WebUISection,
} from './bot/sections'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Save, Power, Code2, Layout } from 'lucide-react'
import { getBotConfig, updateBotConfig, getBotConfigRaw, updateBotConfigRaw } from '@/lib/config-api'
import { useToast } from '@/hooks/use-toast'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Info } from 'lucide-react'
import { RestartOverlay } from '@/components/restart-overlay'
import { RestartProvider, useRestart } from '@/lib/restart-context'
import { CodeEditor } from '@/components'
import { parse as parseToml } from 'smol-toml'
// 导入模块化的类型定义
import type {
BotConfig,
PersonalityConfig,
ChatConfig,
ExpressionConfig,
EmojiConfig,
MemoryConfig,
ToolConfig,
VoiceConfig,
MessageReceiveConfig,
DreamConfig,
LPMMKnowledgeConfig,
KeywordReactionConfig,
ResponsePostProcessConfig,
ChineseTypoConfig,
ResponseSplitterConfig,
LogConfig,
DebugConfig,
ExperimentalConfig,
MaimMessageConfig,
TelemetryConfig,
WebUIConfig,
} from './bot/types'
// 导入 useAutoSave hook
import { useAutoSave, useConfigAutoSave } from './bot/hooks'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
// 导入动态表单和 Hook 系统
import { DynamicConfigForm } from '@/components/dynamic-form'
import { fieldHooks } from '@/lib/field-hooks'
import { ChatSectionHook } from '@/routes/config/bot/hooks'
// ==================== 常量定义 ====================
/** Toast 显示前的延迟时间 (毫秒) */
const TOAST_DISPLAY_DELAY = 500
// 主导出组件:包装 RestartProvider
export function BotConfigPage() {
return (
<RestartProvider>
<BotConfigPageContent />
</RestartProvider>
)
}
// 内部实现组件
function BotConfigPageContent() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [autoSaving, setAutoSaving] = useState(false)
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false)
const [editMode, setEditMode] = useState<'visual' | 'source'>('visual')
const [sourceCode, setSourceCode] = useState<string>('')
const [hasTomlError, setHasTomlError] = useState(false)
const [tomlErrorMessage, setTomlErrorMessage] = useState<string>('')
const { toast } = useToast()
const { triggerRestart, isRestarting } = useRestart()
// 配置状态
const [botConfig, setBotConfig] = useState<BotConfig | null>(null)
const [personalityConfig, setPersonalityConfig] = useState<PersonalityConfig | null>(null)
const [chatConfig, setChatConfig] = useState<ChatConfig | null>(null)
const [expressionConfig, setExpressionConfig] = useState<ExpressionConfig | null>(null)
const [emojiConfig, setEmojiConfig] = useState<EmojiConfig | null>(null)
const [memoryConfig, setMemoryConfig] = useState<MemoryConfig | null>(null)
const [toolConfig, setToolConfig] = useState<ToolConfig | null>(null)
const [voiceConfig, setVoiceConfig] = useState<VoiceConfig | null>(null)
const [messageReceiveConfig, setMessageReceiveConfig] = useState<MessageReceiveConfig | null>(null)
const [dreamConfig, setDreamConfig] = useState<DreamConfig | null>(null)
const [lpmmConfig, setLpmmConfig] = useState<LPMMKnowledgeConfig | null>(null)
const [keywordReactionConfig, setKeywordReactionConfig] = useState<KeywordReactionConfig | null>(null)
const [responsePostProcessConfig, setResponsePostProcessConfig] = useState<ResponsePostProcessConfig | null>(null)
const [chineseTypoConfig, setChineseTypoConfig] = useState<ChineseTypoConfig | null>(null)
const [responseSplitterConfig, setResponseSplitterConfig] = useState<ResponseSplitterConfig | null>(null)
const [logConfig, setLogConfig] = useState<LogConfig | null>(null)
const [debugConfig, setDebugConfig] = useState<DebugConfig | null>(null)
const [experimentalConfig, setExperimentalConfig] = useState<ExperimentalConfig | null>(null)
const [maimMessageConfig, setMaimMessageConfig] = useState<MaimMessageConfig | null>(null)
const [telemetryConfig, setTelemetryConfig] = useState<TelemetryConfig | null>(null)
const [webuiConfig, setWebuiConfig] = useState<WebUIConfig | null>(null)
// 用于标记初始加载和配置缓存
const initialLoadRef = useRef(true)
const configRef = useRef<Record<string, unknown>>({})
// ==================== 辅助函数 ====================
/**
* 翻译 TOML 错误信息为中文
*/
const translateTomlError = (errorMessage: string): string => {
// 分行处理,保留多行格式
const lines = errorMessage.split('\n')
// 翻译第一行(主要错误信息)
let firstLine = lines[0]
// 移除 "Error: " 前缀(如果有)
firstLine = firstLine.replace(/^Error:\s*/, '')
// 常见 TOML 错误模式匹配和翻译
const translations: Array<[RegExp, string | ((match: RegExpMatchArray) => string)]> = [
// Invalid TOML document 系列
[/Invalid TOML document: unrecognized escape sequence/, 'TOML 文档错误:无法识别的转义序列(提示:在双引号字符串中使用 \\\\ 转义反斜杠,或使用单引号字符串)'],
[/Invalid TOML document: only letter, numbers, dashes and underscores are allowed in keys/, 'TOML 文档错误:键名只能包含字母、数字、短横线和下划线'],
[/Invalid TOML document: (.+)/, 'TOML 文档错误:$1'],
// 位置错误系列
[/Unexpected character.*at line (\d+), column (\d+)/, '第 $1 行第 $2 列:意外的字符'],
[/Expected.*at line (\d+), column (\d+)/, '第 $1 行第 $2 列:缺少必要的字符'],
[/Invalid.*at line (\d+), column (\d+)/, '第 $1 行第 $2 列:无效的语法'],
[/Unterminated string at line (\d+)/, '第 $1 行:字符串未正常结束(缺少引号)'],
[/Duplicate key.*at line (\d+)/, '第 $1 行:重复的键名'],
[/Invalid escape sequence at line (\d+)/, '第 $1 行:无效的转义序列(提示:在双引号字符串中使用 \\\\ 转义反斜杠)'],
[/Expected.*but got.*at line (\d+)/, '第 $1 行:类型不匹配'],
[/line (\d+), column (\d+)/, '第 $1 行第 $2 列'],
// 通用错误系列
[/Unexpected end of input/, '意外的文件结束(可能缺少闭合符号)'],
[/Unexpected token/, '意外的标记'],
[/Invalid number/, '无效的数字'],
[/Invalid date/, '无效的日期格式'],
[/Invalid boolean/, '无效的布尔值(应为 true 或 false'],
[/Unexpected character/, '意外的字符'],
[/unrecognized escape sequence/, '无法识别的转义序列'],
]
// 尝试翻译第一行
for (const [pattern, replacement] of translations) {
if (pattern.test(firstLine)) {
firstLine = firstLine.replace(pattern, replacement as string)
break
}
}
// 重组多行错误信息
if (lines.length > 1) {
lines[0] = firstLine
return lines.join('\n')
}
return firstLine
}
/**
* 解析并设置所有配置状态
* 抽取自 loadConfig 和 handleModeChange 中的重复逻辑
*/
const parseAndSetConfig = useCallback((config: Record<string, unknown>) => {
configRef.current = config
setBotConfig(config.bot as BotConfig)
setPersonalityConfig(config.personality as PersonalityConfig)
// 确保 talk_value_rules 有默认值
const chatConfigData = config.chat as ChatConfig
if (!chatConfigData.talk_value_rules) {
chatConfigData.talk_value_rules = []
}
setChatConfig(chatConfigData)
setExpressionConfig(config.expression as ExpressionConfig)
setEmojiConfig(config.emoji as EmojiConfig)
setMemoryConfig(config.memory as MemoryConfig)
setToolConfig(config.tool as ToolConfig)
setVoiceConfig(config.voice as VoiceConfig)
setMessageReceiveConfig(config.message_receive as MessageReceiveConfig)
setDreamConfig(config.dream as DreamConfig)
setLpmmConfig(config.lpmm_knowledge as LPMMKnowledgeConfig)
setKeywordReactionConfig(config.keyword_reaction as KeywordReactionConfig)
setResponsePostProcessConfig(config.response_post_process as ResponsePostProcessConfig)
setChineseTypoConfig(config.chinese_typo as ChineseTypoConfig)
setResponseSplitterConfig(config.response_splitter as ResponseSplitterConfig)
setLogConfig(config.log as LogConfig)
setDebugConfig(config.debug as DebugConfig)
setExperimentalConfig(config.experimental as ExperimentalConfig)
setMaimMessageConfig(config.maim_message as MaimMessageConfig)
setTelemetryConfig(config.telemetry as TelemetryConfig)
setWebuiConfig(config.webui as WebUIConfig)
}, [])
/**
* 构建完整的配置对象用于保存
* 抽取自 saveConfig 和 handleSaveAndRestart 中的重复逻辑
*/
const buildFullConfig = useCallback(() => {
return {
...configRef.current,
bot: botConfig,
personality: personalityConfig,
chat: chatConfig,
expression: expressionConfig,
emoji: emojiConfig,
memory: memoryConfig,
tool: toolConfig,
voice: voiceConfig,
message_receive: messageReceiveConfig,
dream: dreamConfig,
lpmm_knowledge: lpmmConfig,
keyword_reaction: keywordReactionConfig,
response_post_process: responsePostProcessConfig,
chinese_typo: chineseTypoConfig,
response_splitter: responseSplitterConfig,
log: logConfig,
debug: debugConfig,
experimental: experimentalConfig,
maim_message: maimMessageConfig,
telemetry: telemetryConfig,
webui: webuiConfig,
}
}, [
botConfig, personalityConfig, chatConfig, expressionConfig,
emojiConfig, memoryConfig, toolConfig,
voiceConfig, messageReceiveConfig, dreamConfig, lpmmConfig, keywordReactionConfig, responsePostProcessConfig,
chineseTypoConfig, responseSplitterConfig, logConfig, debugConfig, experimentalConfig,
maimMessageConfig, telemetryConfig, webuiConfig
])
// 加载源代码
const loadSourceCode = useCallback(async () => {
try {
const raw = await getBotConfigRaw()
// 将 TOML 基本字符串中的转义序列转换为实际字符以便在编辑器中正确显示
// 使用正则表达式只处理双引号字符串内的转义序列,不影响单引号字符串
const unescaped = raw.replace(/"([^"]*)"/g, (_match, content) => {
const decoded = content
.replace(/\\n/g, '\n') // 换行符
.replace(/\\t/g, '\t') // 制表符
.replace(/\\r/g, '\r') // 回车符
.replace(/\\"/g, '"') // 双引号
.replace(/\\\\/g, '\\') // 反斜杠(必须放在最后)
return `"${decoded}"`
})
setSourceCode(unescaped)
setHasTomlError(false)
} catch (error) {
toast({
variant: 'destructive',
title: '加载失败',
description: error instanceof Error ? error.message : '加载源代码失败',
})
}
}, [toast])
// 加载配置
const loadConfig = useCallback(async () => {
try {
setLoading(true)
const config = await getBotConfig()
parseAndSetConfig(config)
setHasUnsavedChanges(false)
initialLoadRef.current = false
// 同时加载源代码
await loadSourceCode()
} catch (error) {
console.error('加载配置失败:', error)
toast({
title: '加载失败',
description: '无法加载配置文件',
variant: 'destructive',
})
} finally {
setLoading(false)
}
}, [toast, loadSourceCode, parseAndSetConfig])
useEffect(() => {
loadConfig()
}, [loadConfig])
useEffect(() => {
fieldHooks.register('chat', ChatSectionHook, 'replace')
return () => {
fieldHooks.unregister('chat')
}
}, [])
// 使用模块化的 useAutoSave hook
const { triggerAutoSave, cancelPendingAutoSave } = useAutoSave(
initialLoadRef.current,
setAutoSaving,
setHasUnsavedChanges
)
// 使用 useConfigAutoSave hook 简化配置变化监听
// 注意: useConfigAutoSave 是一个 hook不能在条件语句或循环中调用
// 因此我们仍然需要逐个调用,但代码更简洁
useConfigAutoSave(botConfig, 'bot', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(personalityConfig, 'personality', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(chatConfig, 'chat', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(expressionConfig, 'expression', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(emojiConfig, 'emoji', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(memoryConfig, 'memory', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(toolConfig, 'tool', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(voiceConfig, 'voice', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(dreamConfig, 'dream', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(lpmmConfig, 'lpmm_knowledge', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(keywordReactionConfig, 'keyword_reaction', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(responsePostProcessConfig, 'response_post_process', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(chineseTypoConfig, 'chinese_typo', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(responseSplitterConfig, 'response_splitter', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(logConfig, 'log', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(debugConfig, 'debug', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(maimMessageConfig, 'maim_message', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(telemetryConfig, 'telemetry', initialLoadRef.current, triggerAutoSave)
useConfigAutoSave(webuiConfig, 'webui', initialLoadRef.current, triggerAutoSave)
// 保存源代码
const saveSourceCode = async () => {
try {
setSaving(true)
// 前端验证 TOML 格式
try {
parseToml(sourceCode)
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'TOML 格式错误'
const translatedMsg = translateTomlError(errorMsg)
setHasTomlError(true)
setTomlErrorMessage(translatedMsg)
toast({
variant: 'destructive',
title: 'TOML 格式错误',
description: translatedMsg,
})
setSaving(false)
return
}
// 将双引号字符串中的实际字符转换回 TOML 转义序列
// 使用正则表达式只处理双引号字符串内的内容,不影响单引号字符串
const escaped = sourceCode.replace(/"([^"]*)"/g, (_match, content) => {
const encoded = content
.replace(/\\/g, '\\\\') // 反斜杠(必须放在最前)
.replace(/"/g, '\\"') // 双引号
.replace(/\n/g, '\\n') // 换行符
.replace(/\t/g, '\\t') // 制表符
.replace(/\r/g, '\\r') // 回车符
return `"${encoded}"`
})
await updateBotConfigRaw(escaped)
setHasUnsavedChanges(false)
setHasTomlError(false)
setTomlErrorMessage('')
toast({
title: '保存成功',
description: '配置已保存',
})
// 重新加载可视化配置
await loadConfig()
} catch (error) {
setHasTomlError(true)
const errorMsg = error instanceof Error ? error.message : '保存配置失败'
setTomlErrorMessage(errorMsg)
toast({
variant: 'destructive',
title: '保存失败',
description: errorMsg,
})
} finally {
setSaving(false)
}
}
// 处理模式切换
const handleModeChange = async (mode: 'visual' | 'source') => {
if (hasUnsavedChanges) {
toast({
variant: 'destructive',
title: '切换失败',
description: '请先保存当前更改',
})
return
}
setEditMode(mode)
if (mode === 'source') {
await loadSourceCode()
} else {
// 切换回可视化时,直接重新加载配置但不显示全局 loading
try {
const config = await getBotConfig()
parseAndSetConfig(config)
setHasUnsavedChanges(false)
} catch (error) {
console.error('加载配置失败:', error)
toast({
title: '加载失败',
description: '无法加载配置文件',
variant: 'destructive',
})
}
}
}
// 手动保存
const saveConfig = async () => {
try {
setSaving(true)
// 取消待处理的自动保存
cancelPendingAutoSave()
await updateBotConfig(buildFullConfig())
setHasUnsavedChanges(false)
toast({
title: '保存成功',
description: '麦麦主程序配置已保存',
})
} catch (error) {
console.error('保存配置失败:', error)
toast({
title: '保存失败',
description: (error as Error).message,
variant: 'destructive',
})
} finally {
setSaving(false)
}
}
// 重启麦麦
const handleRestart = async () => {
await triggerRestart()
}
// 保存并重启
const handleSaveAndRestart = async () => {
try {
setSaving(true)
// 取消待处理的自动保存
cancelPendingAutoSave()
await updateBotConfig(buildFullConfig())
setHasUnsavedChanges(false)
toast({
title: '保存成功',
description: '配置已保存,即将重启麦麦...',
})
// 等待一下让用户看到保存成功的提示
await new Promise(resolve => setTimeout(resolve, TOAST_DISPLAY_DELAY))
await handleRestart()
} catch (error) {
console.error('保存失败:', error)
toast({
title: '保存失败',
description: (error as Error).message,
variant: 'destructive',
})
} finally {
setSaving(false)
}
}
if (loading) {
return (
<ScrollArea className="h-full">
<div className="space-y-4 sm:space-y-6 p-4 sm:p-6">
<div className="flex items-center justify-center h-64">
<p className="text-muted-foreground">...</p>
</div>
</div>
</ScrollArea>
)
}
return (
<ScrollArea className="h-full">
<div className="space-y-4 sm:space-y-6 p-4 sm:p-6">
{/* 页面标题 */}
<div className="flex flex-col gap-3 sm:gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-1 text-xs sm:text-sm"></p>
</div>
{/* 按钮组 - 桌面端靠右 */}
<div className="flex gap-2 flex-shrink-0">
<Button
onClick={editMode === 'visual' ? saveConfig : saveSourceCode}
disabled={saving || autoSaving || !hasUnsavedChanges || isRestarting}
size="sm"
variant="outline"
className="w-20 sm:w-24"
>
<Save className="h-4 w-4 flex-shrink-0" strokeWidth={2} fill="none" />
<span className="ml-1 truncate text-xs sm:text-sm">
{saving ? '保存中' : autoSaving ? '自动' : hasUnsavedChanges ? '保存' : '已保存'}
</span>
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
disabled={saving || autoSaving || isRestarting}
size="sm"
className="w-20 sm:w-28"
>
<Power className="h-4 w-4 flex-shrink-0" />
<span className="ml-1 truncate text-xs sm:text-sm">
{isRestarting ? '重启中' : hasUnsavedChanges ? '保存重启' : '重启'}
</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription asChild>
<div>
<p>
{hasUnsavedChanges
? '当前有未保存的配置更改。点击确认将先保存配置,然后重启麦麦使新配置生效。重启过程中麦麦将暂时离线。'
: '即将重启麦麦主程序。重启过程中麦麦将暂时离线,配置将在重启后生效。'
}
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={hasUnsavedChanges ? handleSaveAndRestart : handleRestart}>
{hasUnsavedChanges ? '保存并重启' : '确认重启'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
{/* 模式切换 - 单独一行 */}
<div className="flex">
<Tabs value={editMode} onValueChange={(v) => handleModeChange(v as 'visual' | 'source')} className="w-full">
<TabsList className="h-8 sm:h-9 w-full grid grid-cols-2">
<TabsTrigger value="visual" className="text-xs sm:text-sm">
<Layout className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
</TabsTrigger>
<TabsTrigger value="source" className="text-xs sm:text-sm">
<Code2 className="h-3 w-3 sm:h-4 sm:w-4 mr-1" />
</TabsTrigger>
</TabsList>
</Tabs>
</div>
</div>
{/* 重启提示 */}
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
<strong></strong>"保存并重启"
</AlertDescription>
</Alert>
{/* 源代码模式 */}
{editMode === 'source' && (
<div className="space-y-4">
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
<strong></strong> TOML TOML
{hasTomlError && tomlErrorMessage && (
<div className="text-destructive font-semibold mt-3 p-3 bg-destructive/10 rounded-md">
<div className="font-bold mb-2"> TOML </div>
<pre className="text-sm font-mono whitespace-pre-wrap break-words">
{tomlErrorMessage}
</pre>
</div>
)}
</AlertDescription>
</Alert>
<CodeEditor
value={sourceCode}
onChange={(value) => {
setSourceCode(value)
setHasUnsavedChanges(true)
// 清除之前的错误状态
if (hasTomlError) {
setHasTomlError(false)
setTomlErrorMessage('')
}
}}
language="toml"
theme="dark"
height="calc(100vh - 280px)"
minHeight="500px"
placeholder="TOML 配置内容"
/>
</div>
)}
{/* 可视化模式 */}
{editMode === 'visual' && (
<>
{/* 标签页 */}
<Tabs defaultValue="bot" className="w-full">
<TabsList className="flex flex-wrap h-auto gap-1 p-1 sm:grid sm:grid-cols-5 lg:grid-cols-10">
<TabsTrigger value="bot" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
<TabsTrigger value="personality" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
<TabsTrigger value="chat" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
<TabsTrigger value="expression" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
<TabsTrigger value="features" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
<TabsTrigger value="processing" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
<TabsTrigger value="dream" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
<TabsTrigger value="lpmm" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
<TabsTrigger value="webui" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm">WebUI</TabsTrigger>
<TabsTrigger value="other" className="text-xs px-2 py-1.5 sm:px-3 sm:py-2 data-[state=active]:shadow-sm"></TabsTrigger>
</TabsList>
{/* 基本信息 */}
<TabsContent value="bot" className="space-y-4">
{botConfig && <BotInfoSection config={botConfig} onChange={setBotConfig} />}
</TabsContent>
{/* 人格配置 */}
<TabsContent value="personality" className="space-y-4">
{personalityConfig && (
<PersonalitySection config={personalityConfig} onChange={setPersonalityConfig} />
)}
</TabsContent>
{/* 聊天配置 */}
<TabsContent value="chat" className="space-y-4">
{chatConfig && (
<DynamicConfigForm
schema={{
className: 'ChatConfig',
classDoc: '聊天配置',
fields: [],
nested: {},
}}
values={{ chat: chatConfig }}
onChange={(field, value) => {
if (field === 'chat') {
setChatConfig(value as ChatConfig)
setHasUnsavedChanges(true)
}
}}
hooks={fieldHooks}
/>
)}
</TabsContent>
{/* 表达配置 */}
<TabsContent value="expression" className="space-y-4">
{expressionConfig && (
<ExpressionSection config={expressionConfig} onChange={setExpressionConfig} />
)}
</TabsContent>
{/* 功能配置(合并表情、记忆、工具) */}
<TabsContent value="features" className="space-y-4">
{emojiConfig && memoryConfig && toolConfig && voiceConfig && (
<FeaturesSection
emojiConfig={emojiConfig}
memoryConfig={memoryConfig}
toolConfig={toolConfig}
voiceConfig={voiceConfig}
onEmojiChange={setEmojiConfig}
onMemoryChange={setMemoryConfig}
onToolChange={setToolConfig}
onVoiceChange={setVoiceConfig}
/>
)}
</TabsContent>
{/* 处理配置(关键词反应和回复后处理) */}
<TabsContent value="processing" className="space-y-4">
{keywordReactionConfig && responsePostProcessConfig && chineseTypoConfig && responseSplitterConfig && (
<ProcessingSection
keywordReactionConfig={keywordReactionConfig}
responsePostProcessConfig={responsePostProcessConfig}
chineseTypoConfig={chineseTypoConfig}
responseSplitterConfig={responseSplitterConfig}
onKeywordReactionChange={setKeywordReactionConfig}
onResponsePostProcessChange={setResponsePostProcessConfig}
onChineseTypoChange={setChineseTypoConfig}
onResponseSplitterChange={setResponseSplitterConfig}
/>
)}
{messageReceiveConfig && (
<MessageReceiveSection
config={messageReceiveConfig}
onChange={setMessageReceiveConfig}
/>
)}
</TabsContent>
{/* 做梦配置 */}
<TabsContent value="dream" className="space-y-4">
{dreamConfig && <DreamSection config={dreamConfig} onChange={setDreamConfig} />}
</TabsContent>
{/* 知识库配置 */}
<TabsContent value="lpmm" className="space-y-4">
{lpmmConfig && <LPMMSection config={lpmmConfig} onChange={setLpmmConfig} />}
</TabsContent>
{/* WebUI 配置 */}
<TabsContent value="webui" className="space-y-4">
{webuiConfig && <WebUISection config={webuiConfig} onChange={setWebuiConfig} />}
</TabsContent>
{/* 其他配置 */}
<TabsContent value="other" className="space-y-4">
{logConfig && <LogSection config={logConfig} onChange={setLogConfig} />}
{debugConfig && <DebugSection config={debugConfig} onChange={setDebugConfig} />}
{experimentalConfig && <ExperimentalSection config={experimentalConfig} onChange={setExperimentalConfig} />}
{maimMessageConfig && <MaimMessageSection config={maimMessageConfig} onChange={setMaimMessageConfig} />}
{telemetryConfig && <TelemetrySection config={telemetryConfig} onChange={setTelemetryConfig} />}
</TabsContent>
</Tabs>
</>
)}
{/* 重启遮罩层 */}
<RestartOverlay />
</div>
</ScrollArea>
)
}