上传完整的WebUI前端仓库

This commit is contained in:
墨梓柒
2026-01-13 06:24:35 +08:00
parent a9187dc312
commit 812296590e
184 changed files with 47854 additions and 1 deletions

View File

@@ -0,0 +1,6 @@
/**
* Bot 配置页面相关 hooks
*/
export { useAutoSave, useConfigAutoSave } from './useAutoSave'
export type { UseAutoSaveOptions, UseAutoSaveReturn, AutoSaveState } from './useAutoSave'

View File

@@ -0,0 +1,166 @@
import { useEffect, useRef, useCallback } from 'react'
import { updateBotConfigSection } from '@/lib/config-api'
import type { ConfigSectionName } from '../types'
export interface UseAutoSaveOptions {
/** 防抖延迟时间(毫秒),默认 2000ms */
debounceMs?: number
/** 保存成功回调 */
onSaveSuccess?: () => void
/** 保存失败回调 */
onSaveError?: (error: Error) => void
}
export interface UseAutoSaveReturn {
/** 触发自动保存 */
triggerAutoSave: (sectionName: ConfigSectionName, sectionData: unknown) => void
/** 立即保存(不防抖) */
saveNow: (sectionName: ConfigSectionName, sectionData: unknown) => Promise<void>
/** 取消待处理的自动保存 */
cancelPendingAutoSave: () => void
}
export interface AutoSaveState {
/** 是否正在保存中 */
isAutoSaving: boolean
/** 是否有未保存的更改 */
hasUnsavedChanges: boolean
}
/**
* 自动保存 hook
*
* 用于监听配置变化并自动防抖保存到后端
*
* @example
* ```tsx
* const { triggerAutoSave } = useAutoSave({
* isInitialLoad,
* setAutoSaving,
* setHasUnsavedChanges,
* })
*
* // 配置变化时触发
* useEffect(() => {
* if (config) triggerAutoSave('bot', config)
* }, [config])
* ```
*/
export function useAutoSave(
isInitialLoad: boolean,
setAutoSaving: (saving: boolean) => void,
setHasUnsavedChanges: (hasChanges: boolean) => void,
options: UseAutoSaveOptions = {}
): UseAutoSaveReturn {
const { debounceMs = 2000, onSaveSuccess, onSaveError } = options
const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// 执行保存操作
const saveSection = useCallback(
async (sectionName: ConfigSectionName, sectionData: unknown) => {
try {
setAutoSaving(true)
await updateBotConfigSection(sectionName, sectionData)
setHasUnsavedChanges(false)
onSaveSuccess?.()
} catch (error) {
console.error(`自动保存 ${sectionName} 失败:`, error)
setHasUnsavedChanges(true)
onSaveError?.(error instanceof Error ? error : new Error(String(error)))
} finally {
setAutoSaving(false)
}
},
[setAutoSaving, setHasUnsavedChanges, onSaveSuccess, onSaveError]
)
// 触发自动保存(带防抖)
const triggerAutoSave = useCallback(
(sectionName: ConfigSectionName, sectionData: unknown) => {
if (isInitialLoad) return
setHasUnsavedChanges(true)
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current)
}
autoSaveTimerRef.current = setTimeout(() => {
saveSection(sectionName, sectionData)
}, debounceMs)
},
[isInitialLoad, setHasUnsavedChanges, saveSection, debounceMs]
)
// 立即保存(不防抖)
const saveNow = useCallback(
async (sectionName: ConfigSectionName, sectionData: unknown) => {
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current)
autoSaveTimerRef.current = null
}
await saveSection(sectionName, sectionData)
},
[saveSection]
)
// 取消待处理的自动保存
const cancelPendingAutoSave = useCallback(() => {
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current)
autoSaveTimerRef.current = null
}
}, [])
// 组件卸载时清理定时器
useEffect(() => {
return () => {
if (autoSaveTimerRef.current) {
clearTimeout(autoSaveTimerRef.current)
}
}
}, [])
return {
triggerAutoSave,
saveNow,
cancelPendingAutoSave,
}
}
/**
* 创建配置自动保存 effect
*
* 这是一个工厂函数,用于创建监听特定配置变化并触发自动保存的 effect
* 简化重复的 useEffect 代码
*
* @example
* ```tsx
* // 使用方式 1: 直接在组件中调用
* useConfigAutoSave(botConfig, 'bot', isInitialLoad, triggerAutoSave)
* useConfigAutoSave(chatConfig, 'chat', isInitialLoad, triggerAutoSave)
*
* // 使用方式 2: 批量配置
* const configs = [
* { config: botConfig, section: 'bot' },
* { config: chatConfig, section: 'chat' },
* ] as const
*
* configs.forEach(({ config, section }) => {
* useConfigAutoSave(config, section, isInitialLoad, triggerAutoSave)
* })
* ```
*/
export function useConfigAutoSave<T>(
config: T | null,
sectionName: ConfigSectionName,
isInitialLoad: boolean,
triggerAutoSave: (sectionName: ConfigSectionName, data: unknown) => void
): void {
useEffect(() => {
if (config && !isInitialLoad) {
triggerAutoSave(sectionName, config)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config])
}

View File

@@ -0,0 +1,24 @@
/**
* Bot 配置模块
*
* 这个模块包含麦麦主程序配置页面的所有组件和类型
*
* 目录结构:
* - types.ts: 类型定义
* - hooks/: 自定义 hooks
* - useAutoSave.ts: 自动保存 hook
* - sections/: 各个配置区块组件
* - BotInfoSection.tsx
* - PersonalitySection.tsx
* - ChatSection.tsx
* - ...等
*/
// 类型导出
export * from './types'
// Hooks 导出
export * from './hooks'
// Section 组件导出
export * from './sections'

View File

@@ -0,0 +1,192 @@
import React from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Plus, Trash2 } from 'lucide-react'
import type { BotConfig } from '../types'
interface BotInfoSectionProps {
config: BotConfig
onChange: (config: BotConfig) => void
}
export const BotInfoSection = React.memo(function BotInfoSection({ config, onChange }: BotInfoSectionProps) {
// 确保 platforms 和 alias_names 始终是数组
const platforms = config.platforms || []
const aliasNames = config.alias_names || []
const addPlatform = () => {
onChange({ ...config, platforms: [...platforms, ''] })
}
const removePlatform = (index: number) => {
onChange({
...config,
platforms: platforms.filter((_, i) => i !== index),
})
}
const updatePlatform = (index: number, value: string) => {
const newPlatforms = [...platforms]
newPlatforms[index] = value
onChange({ ...config, platforms: newPlatforms })
}
const addAlias = () => {
onChange({ ...config, alias_names: [...aliasNames, ''] })
}
const removeAlias = (index: number) => {
onChange({
...config,
alias_names: aliasNames.filter((_, i) => i !== index),
})
}
const updateAlias = (index: number, value: string) => {
const newAliases = [...aliasNames]
newAliases[index] = value
onChange({ ...config, alias_names: newAliases })
}
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>
<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-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 className="grid gap-2">
<Label htmlFor="nickname"></Label>
<Input
id="nickname"
value={config.nickname}
onChange={(e) => onChange({ ...config, nickname: e.target.value })}
placeholder="麦麦"
/>
</div>
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label></Label>
<Button onClick={addAlias} size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="space-y-2">
{aliasNames.map((alias, index) => (
<div key={index} className="flex gap-2">
<Input
value={alias}
onChange={(e) => updateAlias(index, e.target.value)}
placeholder="小麦"
/>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="icon" variant="outline">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{alias || '(空)'}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => removeAlias(index)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))}
{aliasNames.length === 0 && (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
</div>
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label></Label>
<Button onClick={addPlatform} size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="space-y-2">
{platforms.map((platform, index) => (
<div key={index} className="flex gap-2">
<Input
value={platform}
onChange={(e) => updatePlatform(index, e.target.value)}
placeholder="wx:114514"
/>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="icon" variant="outline">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{platform || '(空)'}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => removePlatform(index)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))}
{platforms.length === 0 && (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
</div>
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,610 @@
import React, { useState, useEffect, useMemo } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Slider } from '@/components/ui/slider'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Plus, Trash2, Eye, Clock } from 'lucide-react'
import type { ChatConfig } from '../types'
interface ChatSectionProps {
config: ChatConfig
onChange: (config: ChatConfig) => void
}
// 时间选择组件
const TimeRangePicker = React.memo(function TimeRangePicker({
value,
onChange,
}: {
value: string
onChange: (value: string) => void
}) {
// 解析初始值
const parsedValue = useMemo(() => {
const parts = value.split('-')
if (parts.length === 2) {
const [start, end] = parts
const [sh, sm] = start.split(':')
const [eh, em] = end.split(':')
return {
startHour: sh ? sh.padStart(2, '0') : '00',
startMinute: sm ? sm.padStart(2, '0') : '00',
endHour: eh ? eh.padStart(2, '0') : '23',
endMinute: em ? em.padStart(2, '0') : '59',
}
}
return {
startHour: '00',
startMinute: '00',
endHour: '23',
endMinute: '59',
}
}, [value])
const [startHour, setStartHour] = useState(parsedValue.startHour)
const [startMinute, setStartMinute] = useState(parsedValue.startMinute)
const [endHour, setEndHour] = useState(parsedValue.endHour)
const [endMinute, setEndMinute] = useState(parsedValue.endMinute)
// 当value变化时同步状态
useEffect(() => {
setStartHour(parsedValue.startHour)
setStartMinute(parsedValue.startMinute)
setEndHour(parsedValue.endHour)
setEndMinute(parsedValue.endMinute)
}, [parsedValue])
const updateTime = (
newStartHour: string,
newStartMinute: string,
newEndHour: string,
newEndMinute: string
) => {
const newValue = `${newStartHour}:${newStartMinute}-${newEndHour}:${newEndMinute}`
onChange(newValue)
}
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start font-mono text-sm">
<Clock className="h-4 w-4 mr-2" />
{value || '选择时间段'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-72 sm:w-80">
<div className="space-y-4">
<div>
<h4 className="font-medium text-sm mb-3"></h4>
<div className="grid grid-cols-2 gap-2 sm:gap-3">
<div>
<Label className="text-xs"></Label>
<Select
value={startHour}
onValueChange={(v) => {
setStartHour(v)
updateTime(v, startMinute, endHour, endMinute)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 24 }, (_, i) => i).map((h) => (
<SelectItem key={h} value={h.toString().padStart(2, '0')}>
{h.toString().padStart(2, '0')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"></Label>
<Select
value={startMinute}
onValueChange={(v) => {
setStartMinute(v)
updateTime(startHour, v, endHour, endMinute)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 60 }, (_, i) => i).map((m) => (
<SelectItem key={m} value={m.toString().padStart(2, '0')}>
{m.toString().padStart(2, '0')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div>
<h4 className="font-medium text-sm mb-3"></h4>
<div className="grid grid-cols-2 gap-2 sm:gap-3">
<div>
<Label className="text-xs"></Label>
<Select
value={endHour}
onValueChange={(v) => {
setEndHour(v)
updateTime(startHour, startMinute, v, endMinute)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 24 }, (_, i) => i).map((h) => (
<SelectItem key={h} value={h.toString().padStart(2, '0')}>
{h.toString().padStart(2, '0')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs"></Label>
<Select
value={endMinute}
onValueChange={(v) => {
setEndMinute(v)
updateTime(startHour, startMinute, endHour, v)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Array.from({ length: 60 }, (_, i) => i).map((m) => (
<SelectItem key={m} value={m.toString().padStart(2, '0')}>
{m.toString().padStart(2, '0')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
</PopoverContent>
</Popover>
)
})
// 预览窗口组件
const RulePreview = React.memo(function RulePreview({ rule }: { rule: { target: string; time: string; value: number } }) {
const previewText = `{ target = "${rule.target}", time = "${rule.time}", value = ${rule.value.toFixed(1)} }`
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 sm:w-96">
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<div className="rounded-md bg-muted p-3 font-mono text-xs break-all">
{previewText}
</div>
<p className="text-xs text-muted-foreground">
bot_config.toml
</p>
</div>
</PopoverContent>
</Popover>
)
})
export const ChatSection = React.memo(function ChatSection({ config, onChange }: ChatSectionProps) {
// 添加发言频率规则
const addTalkValueRule = () => {
onChange({
...config,
talk_value_rules: [
...config.talk_value_rules,
{ target: '', time: '00:00-23:59', value: 1.0 },
],
})
}
// 删除发言频率规则
const removeTalkValueRule = (index: number) => {
onChange({
...config,
talk_value_rules: config.talk_value_rules.filter((_, i) => i !== index),
})
}
// 更新发言频率规则
const updateTalkValueRule = (
index: number,
field: 'target' | 'time' | 'value',
value: string | number
) => {
const newRules = [...config.talk_value_rules]
newRules[index] = {
...newRules[index],
[field]: value,
}
onChange({
...config,
talk_value_rules: newRules,
})
}
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>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="talk_value"></Label>
<Input
id="talk_value"
type="number"
step="0.1"
min="0"
max="1"
value={config.talk_value}
onChange={(e) => onChange({ ...config, talk_value: parseFloat(e.target.value) })}
/>
<p className="text-xs text-muted-foreground"> 0-1</p>
</div>
<div className="grid gap-2">
<Label htmlFor="think_mode"></Label>
<Select
value={config.think_mode || 'classic'}
onValueChange={(value) => onChange({ ...config, think_mode: value as 'classic' | 'deep' | 'dynamic' })}
>
<SelectTrigger id="think_mode">
<SelectValue placeholder="选择思考模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="classic"> - </SelectItem>
<SelectItem value="deep"> - </SelectItem>
<SelectItem value="dynamic"> - </SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="flex items-center space-x-2">
<Switch
id="mentioned_bot_reply"
checked={config.mentioned_bot_reply}
onCheckedChange={(checked) =>
onChange({ ...config, mentioned_bot_reply: checked })
}
/>
<Label htmlFor="mentioned_bot_reply" className="cursor-pointer">
</Label>
</div>
<div className="grid gap-2">
<Label htmlFor="max_context_size"></Label>
<Input
id="max_context_size"
type="number"
min="1"
value={config.max_context_size}
onChange={(e) =>
onChange({ ...config, max_context_size: parseInt(e.target.value) })
}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="planner_smooth"></Label>
<Input
id="planner_smooth"
type="number"
step="1"
min="0"
value={config.planner_smooth}
onChange={(e) =>
onChange({ ...config, planner_smooth: parseFloat(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
planner 1-50
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="plan_reply_log_max_per_chat"></Label>
<Input
id="plan_reply_log_max_per_chat"
type="number"
step="1"
min="100"
value={config.plan_reply_log_max_per_chat ?? 1024}
onChange={(e) =>
onChange({ ...config, plan_reply_log_max_per_chat: parseInt(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
Plan/Reply
</p>
</div>
<div className="flex items-center space-x-2">
<Switch
id="llm_quote"
checked={config.llm_quote ?? false}
onCheckedChange={(checked) =>
onChange({ ...config, llm_quote: checked })
}
/>
<Label htmlFor="llm_quote" className="cursor-pointer">
LLM
</Label>
</div>
<p className="text-xs text-muted-foreground -mt-2 ml-10">
LLM
</p>
<div className="flex items-center space-x-2">
<Switch
id="enable_talk_value_rules"
checked={config.enable_talk_value_rules}
onCheckedChange={(checked) =>
onChange({ ...config, enable_talk_value_rules: checked })
}
/>
<Label htmlFor="enable_talk_value_rules" className="cursor-pointer">
</Label>
</div>
</div>
</div>
{/* 动态发言频率规则配置 */}
{config.enable_talk_value_rules && (
<div className="border-t pt-6">
<div className="flex items-center justify-between mb-4">
<div>
<h4 className="text-base font-semibold"></h4>
<p className="text-xs text-muted-foreground mt-1">
ID调整发言频率
</p>
</div>
<Button onClick={addTalkValueRule} size="sm">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
{config.talk_value_rules && config.talk_value_rules.length > 0 ? (
<div className="space-y-4">
{config.talk_value_rules.map((rule, index) => (
<div key={index} className="rounded-lg border p-4 bg-muted/50 space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">
#{index + 1}
</span>
<div className="flex items-center gap-2">
<RulePreview rule={rule} />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
#{index + 1}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => removeTalkValueRule(index)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="space-y-4">
{/* 配置类型选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={rule.target === '' ? 'global' : 'specific'}
onValueChange={(value) => {
if (value === 'global') {
updateTalkValueRule(index, 'target', '')
} else {
updateTalkValueRule(index, 'target', 'qq::group')
}
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="global"></SelectItem>
<SelectItem value="specific"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 详细配置选项 - 只在非全局时显示 */}
{rule.target !== '' && (() => {
const parts = rule.target.split(':')
const platform = parts[0] || 'qq'
const chatId = parts[1] || ''
const chatType = parts[2] || 'group'
return (
<div className="grid gap-4 p-3 sm:p-4 rounded-lg bg-muted/50">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={platform}
onValueChange={(value) => {
updateTalkValueRule(index, 'target', `${value}:${chatId}:${chatType}`)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="qq">QQ</SelectItem>
<SelectItem value="wx"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label className="text-xs font-medium"> ID</Label>
<Input
value={chatId}
onChange={(e) => {
updateTalkValueRule(index, 'target', `${platform}:${e.target.value}:${chatType}`)
}}
placeholder="输入群 ID"
className="font-mono text-sm"
/>
</div>
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={chatType}
onValueChange={(value) => {
updateTalkValueRule(index, 'target', `${platform}:${chatId}:${value}`)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="group">group</SelectItem>
<SelectItem value="private">private</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<p className="text-xs text-muted-foreground">
ID{rule.target || '(未设置)'}
</p>
</div>
)
})()}
{/* 时间段选择器 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"> (Time)</Label>
<TimeRangePicker
value={rule.time}
onChange={(v) => updateTalkValueRule(index, 'time', v)}
/>
<p className="text-xs text-muted-foreground">
23:00-02:00
</p>
</div>
{/* 发言频率滑块 */}
<div className="grid gap-3">
<div className="flex items-center justify-between">
<Label htmlFor={`rule-value-${index}`} className="text-xs font-medium">
(Value)
</Label>
<Input
id={`rule-value-${index}`}
type="number"
step="0.01"
min="0.01"
max="1"
value={rule.value}
onChange={(e) => {
const val = parseFloat(e.target.value)
if (!isNaN(val)) {
updateTalkValueRule(index, 'value', Math.max(0.01, Math.min(1, val)))
}
}}
className="w-20 h-8 text-xs"
/>
</div>
<Slider
value={[rule.value]}
onValueChange={(values) =>
updateTalkValueRule(index, 'value', values[0])
}
min={0.01}
max={1}
step={0.01}
className="w-full"
/>
<div className="flex justify-between text-xs text-muted-foreground">
<span>0.01 ()</span>
<span>0.5</span>
<span>1.0 ()</span>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-8 text-muted-foreground">
<p className="text-sm">"添加规则"</p>
</div>
)}
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<h5 className="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-2">
📝
</h5>
<ul className="text-xs text-blue-800 dark:text-blue-200 space-y-1">
<li> <strong>Target </strong></li>
<li> <strong>Target </strong>platform:id:type</li>
<li> <strong></strong></li>
<li> <strong></strong> 23:00-02:00 112</li>
<li> <strong></strong> 0-10 1 </li>
</ul>
</div>
</div>
)}
</div>
)
})

View File

@@ -0,0 +1,97 @@
import React from 'react'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import type { DebugConfig } from '../types'
interface DebugSectionProps {
config: DebugConfig
onChange: (config: DebugConfig) => void
}
export const DebugSection = React.memo(function DebugSection({ config, onChange }: DebugSectionProps) {
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<h3 className="text-lg font-semibold"></h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> Prompt</Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={config.show_prompt}
onCheckedChange={(checked) => onChange({ ...config, show_prompt: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> Prompt</Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={config.show_replyer_prompt}
onCheckedChange={(checked) => onChange({ ...config, show_replyer_prompt: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label></Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={config.show_replyer_reasoning}
onCheckedChange={(checked) =>
onChange({ ...config, show_replyer_reasoning: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> Jargon Prompt</Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={config.show_jargon_prompt}
onCheckedChange={(checked) => onChange({ ...config, show_jargon_prompt: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> Prompt</Label>
<p className="text-sm text-muted-foreground"></p>
</div>
<Switch
checked={config.show_memory_prompt}
onCheckedChange={(checked) => onChange({ ...config, show_memory_prompt: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> Planner Prompt</Label>
<p className="text-sm text-muted-foreground"> Planner </p>
</div>
<Switch
checked={config.show_planner_prompt}
onCheckedChange={(checked) => onChange({ ...config, show_planner_prompt: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> LPMM </Label>
<p className="text-sm text-muted-foreground"> LPMM </p>
</div>
<Switch
checked={config.show_lpmm_paragraph}
onCheckedChange={(checked) => onChange({ ...config, show_lpmm_paragraph: checked })}
/>
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,215 @@
import React, { useState } from 'react'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { X } from 'lucide-react'
import type { DreamConfig } from '../types'
interface DreamSectionProps {
config: DreamConfig
onChange: (config: DreamConfig) => void
}
interface TimeRange {
startTime: string
endTime: string
}
export const DreamSection = React.memo(function DreamSection({ config, onChange }: DreamSectionProps) {
// 解析 dream_send 为 platform 和 userId
const parseDreamSend = (dreamSend: string): { platform: string; userId: string } => {
if (!dreamSend || !dreamSend.includes(':')) {
return { platform: 'qq', userId: '' }
}
const [platform, userId] = dreamSend.split(':')
return { platform, userId }
}
const { platform: initialPlatform, userId: initialUserId } = parseDreamSend(config.dream_send)
const [platform, setPlatform] = useState(initialPlatform)
const [userId, setUserId] = useState(initialUserId)
// 解析时间段字符串为开始和结束时间
const parseTimeRange = (range: string): TimeRange => {
const [start, end] = range.split('-')
return { startTime: start || '09:00', endTime: end || '22:00' }
}
// 更新 dream_send
const updateDreamSend = (newPlatform: string, newUserId: string) => {
const dreamSend = newUserId ? `${newPlatform}:${newUserId}` : ''
onChange({ ...config, dream_send: dreamSend })
}
const handlePlatformChange = (value: string) => {
setPlatform(value)
updateDreamSend(value, userId)
}
const handleUserIdChange = (value: string) => {
setUserId(value)
updateDreamSend(platform, value)
}
const handleAddTimeRange = () => {
onChange({
...config,
dream_time_ranges: [...config.dream_time_ranges, '09:00-22:00']
})
}
const handleRemoveTimeRange = (index: number) => {
onChange({
...config,
dream_time_ranges: config.dream_time_ranges.filter((_, i) => i !== index)
})
}
const handleTimeRangeChange = (index: number, field: 'startTime' | 'endTime', value: string) => {
const newRanges = [...config.dream_time_ranges]
const currentRange = parseTimeRange(newRanges[index])
if (field === 'startTime') {
currentRange.startTime = value
} else {
currentRange.endTime = value
}
newRanges[index] = `${currentRange.startTime}-${currentRange.endTime}`
onChange({
...config,
dream_time_ranges: newRanges
})
}
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
<h3 className="text-lg font-semibold"></h3>
<div className="space-y-2">
<Label htmlFor="interval_minutes"></Label>
<Input
id="interval_minutes"
type="number"
min="1"
value={config.interval_minutes}
onChange={(e) => onChange({ ...config, interval_minutes: Number(e.target.value) })}
/>
<p className="text-xs text-muted-foreground">30</p>
</div>
<div className="space-y-2">
<Label htmlFor="max_iterations"></Label>
<Input
id="max_iterations"
type="number"
min="1"
value={config.max_iterations}
onChange={(e) => onChange({ ...config, max_iterations: Number(e.target.value) })}
/>
<p className="text-xs text-muted-foreground">20</p>
</div>
<div className="space-y-2">
<Label htmlFor="first_delay_seconds"></Label>
<Input
id="first_delay_seconds"
type="number"
min="0"
value={config.first_delay_seconds}
onChange={(e) => onChange({ ...config, first_delay_seconds: Number(e.target.value) })}
/>
<p className="text-xs text-muted-foreground">60</p>
</div>
<div className="space-y-2">
<Label></Label>
<div className="flex gap-2">
<Select value={platform} onValueChange={handlePlatformChange}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="选择平台" />
</SelectTrigger>
<SelectContent>
<SelectItem value="qq">QQ</SelectItem>
<SelectItem value="wx"></SelectItem>
<SelectItem value="webui">WebUI</SelectItem>
</SelectContent>
</Select>
<Input
type="text"
placeholder="输入用户ID (例如: 123456)"
value={userId}
onChange={(e) => handleUserIdChange(e.target.value)}
className="flex-1"
/>
</div>
<p className="text-xs text-muted-foreground">
IDID为空则不推送
</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label></Label>
<Button type="button" size="sm" onClick={handleAddTimeRange}>
</Button>
</div>
<p className="text-xs text-muted-foreground">
23:00 02:00
</p>
<div className="space-y-2">
{config.dream_time_ranges.map((range, index) => {
const { startTime, endTime } = parseTimeRange(range)
return (
<div key={index} className="flex items-center gap-2">
<Input
type="time"
value={startTime}
onChange={(e) => handleTimeRangeChange(index, 'startTime', e.target.value)}
className="w-[140px]"
/>
<span className="text-muted-foreground"></span>
<Input
type="time"
value={endTime}
onChange={(e) => handleTimeRangeChange(index, 'endTime', e.target.value)}
className="w-[140px]"
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemoveTimeRange(index)}
>
<X className="h-4 w-4" />
</Button>
</div>
)
})}
{config.dream_time_ranges.length === 0 && (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Switch
id="dream_visible"
checked={config.dream_visible}
onCheckedChange={(checked) => onChange({ ...config, dream_visible: checked })}
/>
<Label htmlFor="dream_visible" className="cursor-pointer">
</Label>
</div>
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
)
})

View File

@@ -0,0 +1,311 @@
import React from 'react'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Switch } from '@/components/ui/switch'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Plus, Trash2, AlertTriangle, Eye, Code2 } from 'lucide-react'
import type { ExperimentalConfig } from '../types'
interface ChatPromptData {
platform: string
id: string
type: 'group' | 'private'
prompt: string
}
interface ExperimentalSectionProps {
config: ExperimentalConfig
onChange: (config: ExperimentalConfig) => void
}
export const ExperimentalSection = React.memo(function ExperimentalSection({ config, onChange }: ExperimentalSectionProps) {
// 解析 chat_prompt 字符串为结构化数据
const parseChatPrompt = (promptStr: string): ChatPromptData => {
const parts = promptStr.split(':')
if (parts.length >= 4) {
const platform = parts[0]
const id = parts[1]
const type = parts[2] as 'group' | 'private'
const prompt = parts.slice(3).join(':') // 处理 prompt 中可能包含的冒号
return { platform, id, type, prompt }
}
return { platform: 'qq', id: '', type: 'group', prompt: '' }
}
// 将结构化数据转换为字符串
const stringifyChatPrompt = (data: ChatPromptData): string => {
return `${data.platform}:${data.id}:${data.type}:${data.prompt}`
}
const addChatPrompt = () => {
onChange({ ...config, chat_prompts: [...config.chat_prompts, 'qq::group:'] })
}
const removeChatPrompt = (index: number) => {
onChange({
...config,
chat_prompts: config.chat_prompts.filter((_, i) => i !== index),
})
}
const updateChatPrompt = (index: number, data: Partial<ChatPromptData>) => {
const currentData = parseChatPrompt(config.chat_prompts[index])
const newData = { ...currentData, ...data }
const newPrompts = [...config.chat_prompts]
newPrompts[index] = stringifyChatPrompt(newData)
onChange({ ...config, chat_prompts: newPrompts })
}
// 预览组件
const ChatPromptPreview = ({ promptStr }: { promptStr: string }) => {
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 sm:w-96">
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<div className="rounded-md bg-muted p-3 font-mono text-xs break-all">
"{promptStr}"
</div>
<p className="text-xs text-muted-foreground">
bot_config.toml
</p>
</div>
</PopoverContent>
</Popover>
)
}
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
<div className="flex items-start gap-3 p-3 rounded-lg bg-orange-500/10 border border-orange-500/20">
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0 mt-0.5" />
<div className="space-y-1">
<h4 className="font-medium text-orange-500"></h4>
<p className="text-sm text-muted-foreground">
使
</p>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid gap-6">
<div className="flex items-center space-x-2">
<Switch
id="lpmm_memory"
checked={config.lpmm_memory ?? false}
onCheckedChange={(checked) =>
onChange({ ...config, lpmm_memory: checked })
}
/>
<Label htmlFor="lpmm_memory" className="cursor-pointer">
LPMM
</Label>
</div>
<p className="text-xs text-muted-foreground -mt-4">
chat_history_summarizer
</p>
<div className="grid gap-2">
<Label htmlFor="private_plan_style"></Label>
<Textarea
id="private_plan_style"
value={config.private_plan_style}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, private_plan_style: e.target.value })}
placeholder="私聊的说话规则和行为风格(不推荐修改)"
rows={4}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="grid gap-4">
<div className="flex items-center justify-between">
<div>
<Label> Prompt </Label>
<p className="text-xs text-muted-foreground mt-1">
prompt
</p>
</div>
<Button onClick={addChatPrompt} size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="space-y-4">
{config.chat_prompts.map((promptStr, index) => {
const data = parseChatPrompt(promptStr)
return (
<div key={index} className="rounded-lg border p-4 space-y-4 bg-card">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">
Prompt {index + 1}
</span>
<div className="flex items-center gap-2">
<ChatPromptPreview promptStr={promptStr} />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm" variant="ghost">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
prompt
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => removeChatPrompt(index)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="grid gap-4">
{/* 平台选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={data.platform}
onValueChange={(value) => updateChatPrompt(index, { platform: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择平台" />
</SelectTrigger>
<SelectContent>
<SelectItem value="qq">QQ</SelectItem>
<SelectItem value="wx"></SelectItem>
<SelectItem value="webui">WebUI</SelectItem>
</SelectContent>
</Select>
</div>
{/* ID 输入 */}
<div className="grid gap-2">
<Label className="text-xs font-medium">
{data.type === 'group' ? '群号' : '用户ID'}
</Label>
<Input
value={data.id}
onChange={(e) => updateChatPrompt(index, { id: e.target.value })}
placeholder={data.type === 'group' ? '输入群号' : '输入用户ID'}
className="font-mono"
/>
</div>
{/* 类型选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={data.type}
onValueChange={(value: 'group' | 'private') => updateChatPrompt(index, { type: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="group"> (group)</SelectItem>
<SelectItem value="private"> (private)</SelectItem>
</SelectContent>
</Select>
</div>
{/* Prompt 内容 */}
<div className="grid gap-2">
<Label className="text-xs font-medium">Prompt </Label>
<Textarea
value={data.prompt}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => updateChatPrompt(index, { prompt: e.target.value })}
placeholder="输入额外的 prompt 内容,例如:这是一个摄影群,你精通摄影知识"
rows={3}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 原始格式显示 */}
<div className="rounded-md bg-muted/50 p-3">
<div className="flex items-center gap-2 mb-2">
<Code2 className="h-3 w-3 text-muted-foreground" />
<span className="text-xs font-medium text-muted-foreground"></span>
</div>
<code className="text-xs font-mono text-muted-foreground break-all">
{promptStr || '(未配置)'}
</code>
</div>
</div>
</div>
)
})}
{config.chat_prompts.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<p className="text-sm"> prompt </p>
<p className="text-xs mt-1">"添加配置"</p>
</div>
)}
</div>
{/* 使用说明 */}
<div className="text-xs text-muted-foreground space-y-2 p-4 rounded-lg bg-muted/30 border">
<p className="font-medium text-foreground">💡 使</p>
<ul className="list-disc list-inside space-y-1 pl-2">
<li></li>
<li>QQWebUI</li>
<li></li>
<li>Prompt </li>
</ul>
<p className="font-medium text-foreground mt-3">📝 </p>
<ul className="list-disc list-inside space-y-1 pl-2">
<li><code className="text-xs bg-muted px-1 py-0.5 rounded"></code></li>
<li><code className="text-xs bg-muted px-1 py-0.5 rounded"></code></li>
<li><code className="text-xs bg-muted px-1 py-0.5 rounded"></code></li>
</ul>
</div>
</div>
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,996 @@
import React, { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import { Plus, Trash2, Eye } from 'lucide-react'
import type { ExpressionConfig } from '../types'
interface ExpressionGroupMemberInputProps {
member: string
groupIndex: number
memberIndex: number
availableChatIds: string[]
onUpdate: (groupIndex: number, memberIndex: number, value: string) => void
onRemove: (groupIndex: number, memberIndex: number) => void
}
const ExpressionGroupMemberInput = React.memo(function ExpressionGroupMemberInput({
member,
groupIndex,
memberIndex,
availableChatIds,
onUpdate,
onRemove,
}: ExpressionGroupMemberInputProps) {
// 判断当前成员是否在可选列表中
const isFromList = availableChatIds.includes(member) || member === '*'
const [inputMode, setInputMode] = useState(!isFromList)
return (
<div className="flex gap-2">
{/* 输入模式切换 */}
<div className="flex-1 flex gap-2">
{inputMode ? (
// 手动输入模式
<>
<Input
value={member}
onChange={(e) => onUpdate(groupIndex, memberIndex, e.target.value)}
placeholder='输入 "*" 或 "qq:123456:group"'
className="flex-1"
/>
{availableChatIds.length > 0 && (
<Button
size="sm"
variant="outline"
onClick={() => setInputMode(false)}
title="切换到下拉选择"
>
</Button>
)}
</>
) : (
// 下拉选择模式
<>
<Select
value={member}
onValueChange={(value) => onUpdate(groupIndex, memberIndex, value)}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="选择聊天流" />
</SelectTrigger>
<SelectContent>
<SelectItem value="*">* ()</SelectItem>
{availableChatIds.map((chatId, idx) => (
<SelectItem key={idx} value={chatId}>
{chatId}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
size="sm"
variant="outline"
onClick={() => setInputMode(true)}
title="切换到手动输入"
>
</Button>
</>
)}
</div>
{/* 删除按钮 */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="icon" variant="outline">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"{member || '(空)'}"
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => onRemove(groupIndex, memberIndex)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
})
interface ExpressionSectionProps {
config: ExpressionConfig
onChange: (config: ExpressionConfig) => void
}
export const ExpressionSection = React.memo(function ExpressionSection({
config,
onChange,
}: ExpressionSectionProps) {
// 添加学习规则
const addLearningRule = () => {
onChange({
...config,
learning_list: [...config.learning_list, ['', 'enable', 'enable', '1.0']],
})
}
// 删除学习规则
const removeLearningRule = (index: number) => {
onChange({
...config,
learning_list: config.learning_list.filter((_, i) => i !== index),
})
}
// 更新学习规则
const updateLearningRule = (
index: number,
field: 0 | 1 | 2 | 3,
value: string
) => {
const newList = [...config.learning_list]
newList[index][field] = value
onChange({
...config,
learning_list: newList,
})
}
// 预览组件
const LearningRulePreview = ({ rule }: { rule: [string, string, string, string] }) => {
const previewText = `["${rule[0]}", "${rule[1]}", "${rule[2]}", "${rule[3]}"]`
return (
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-1" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 sm:w-96">
<div className="space-y-2">
<h4 className="font-medium text-sm"></h4>
<div className="rounded-md bg-muted p-3 font-mono text-xs break-all">
{previewText}
</div>
<p className="text-xs text-muted-foreground">
bot_config.toml
</p>
</div>
</PopoverContent>
</Popover>
)
}
// 添加表达组
const addExpressionGroup = () => {
onChange({
...config,
expression_groups: [...config.expression_groups, []],
})
}
// 删除表达组
const removeExpressionGroup = (index: number) => {
onChange({
...config,
expression_groups: config.expression_groups.filter((_, i) => i !== index),
})
}
// 添加组成员
const addGroupMember = (groupIndex: number) => {
const newGroups = [...config.expression_groups]
newGroups[groupIndex] = [...newGroups[groupIndex], '']
onChange({
...config,
expression_groups: newGroups,
})
}
// 删除组成员
const removeGroupMember = (groupIndex: number, memberIndex: number) => {
const newGroups = [...config.expression_groups]
newGroups[groupIndex] = newGroups[groupIndex].filter((_, i) => i !== memberIndex)
onChange({
...config,
expression_groups: newGroups,
})
}
// 更新组成员
const updateGroupMember = (groupIndex: number, memberIndex: number, value: string) => {
const newGroups = [...config.expression_groups]
newGroups[groupIndex][memberIndex] = value
onChange({
...config,
expression_groups: newGroups,
})
}
return (
<div className="space-y-6">
{/* 黑话设置 - 移到顶部 */}
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<h3 className="text-lg font-semibold mb-4"></h3>
<div>
<div className="flex items-center space-x-2">
<Switch
id="all_global_jargon"
checked={config.all_global_jargon ?? false}
onCheckedChange={(checked) =>
onChange({ ...config, all_global_jargon: checked })
}
/>
<Label htmlFor="all_global_jargon" className="cursor-pointer">
</Label>
</div>
<p className="text-xs text-muted-foreground mt-2">
</p>
</div>
<div>
<div className="flex items-center space-x-2">
<Switch
id="enable_jargon_explanation"
checked={config.enable_jargon_explanation ?? true}
onCheckedChange={(checked) =>
onChange({ ...config, enable_jargon_explanation: checked })
}
/>
<Label htmlFor="enable_jargon_explanation" className="cursor-pointer">
</Label>
</div>
<p className="text-xs text-muted-foreground mt-2">
LLM调用
</p>
</div>
<div>
<Label htmlFor="jargon_mode"></Label>
<Select
value={config.jargon_mode ?? 'context'}
onValueChange={(value) => onChange({ ...config, jargon_mode: value })}
>
<SelectTrigger id="jargon_mode" className="mt-2">
<SelectValue placeholder="选择黑话解释来源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="context"></SelectItem>
<SelectItem value="planner">Planner模式使unknown_words列表</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground mt-2">
使<br />
Planner模式使Planner在reply动作中给出的unknown_words列表进行黑话检索
</p>
</div>
</div>
{/* 表达学习配置 */}
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
<div>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold"></h3>
<p className="text-sm text-muted-foreground mt-1">
使
</p>
</div>
<Button onClick={addLearningRule} size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="space-y-4">
{config.learning_list.map((rule, index) => {
// 检查是否已有全局配置rule[0] === ''
const hasGlobalConfig = config.learning_list.some((r, i) => i !== index && r[0] === '')
const isGlobal = rule[0] === ''
// 解析聊天流 ID格式platform:id:type
const parts = rule[0].split(':')
const platform = parts[0] || 'qq'
const chatId = parts[1] || ''
const chatType = parts[2] || 'group'
return (
<div key={index} className="rounded-lg border p-4 space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">
{index + 1} {isGlobal && '(全局配置)'}
</span>
<div className="flex items-center gap-2">
<LearningRulePreview rule={rule} />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm" variant="ghost">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{index + 1}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => removeLearningRule(index)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="space-y-4">
{/* 配置类型选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={isGlobal ? 'global' : 'specific'}
onValueChange={(value) => {
if (value === 'global') {
updateLearningRule(index, 0, '')
} else {
// 切换到详细配置时,设置默认值
updateLearningRule(index, 0, 'qq::group')
}
}}
disabled={hasGlobalConfig && !isGlobal}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="global"></SelectItem>
<SelectItem value="specific" disabled={hasGlobalConfig && !isGlobal}>
</SelectItem>
</SelectContent>
</Select>
{hasGlobalConfig && !isGlobal && (
<p className="text-xs text-amber-600">
</p>
)}
</div>
{/* 详细配置选项 - 只在非全局时显示 */}
{!isGlobal && (
<div className="grid gap-4 p-3 sm:p-4 rounded-lg bg-muted/50">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{/* 平台选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={platform}
onValueChange={(value) => {
updateLearningRule(index, 0, `${value}:${chatId}:${chatType}`)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="qq">QQ</SelectItem>
<SelectItem value="wx"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 群 ID 输入 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"> ID</Label>
<Input
value={chatId}
onChange={(e) => {
updateLearningRule(index, 0, `${platform}:${e.target.value}:${chatType}`)
}}
placeholder="输入群 ID"
className="font-mono text-sm"
/>
</div>
{/* 类型选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={chatType}
onValueChange={(value) => {
updateLearningRule(index, 0, `${platform}:${chatId}:${value}`)
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="group">group</SelectItem>
<SelectItem value="private">private</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<p className="text-xs text-muted-foreground">
ID{rule[0] || '(未设置)'}
</p>
</div>
)}
{/* 使用学到的表达 - 改为开关 */}
<div className="grid gap-2">
<div className="flex items-center justify-between">
<div>
<Label className="text-xs font-medium">使</Label>
<p className="text-xs text-muted-foreground mt-1">
使
</p>
</div>
<Switch
checked={rule[1] === 'enable'}
onCheckedChange={(checked) =>
updateLearningRule(index, 1, checked ? 'enable' : 'disable')
}
/>
</div>
</div>
{/* 学习表达 - 改为开关 */}
<div className="grid gap-2">
<div className="flex items-center justify-between">
<div>
<Label className="text-xs font-medium"></Label>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
<Switch
checked={rule[2] === 'enable'}
onCheckedChange={(checked) =>
updateLearningRule(index, 2, checked ? 'enable' : 'disable')
}
/>
</div>
</div>
{/* 启用黑话学习 - 改为开关 */}
<div className="grid gap-2">
<div className="flex items-center justify-between">
<div>
<Label className="text-xs font-medium"></Label>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
<Switch
checked={rule[3] === 'true' || rule[3] === 'enable'}
onCheckedChange={(checked) =>
updateLearningRule(index, 3, checked ? 'true' : 'false')
}
/>
</div>
</div>
</div>
</div>
)
})}
{config.learning_list.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
"添加规则"
</div>
)}
</div>
</div>
</div>
{/* 表达反思配置 */}
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold"></h3>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
{/* 自动表达优化 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="expression_self_reflect" className="cursor-pointer font-medium">
</Label>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Switch
id="expression_self_reflect"
checked={config.expression_self_reflect ?? false}
onCheckedChange={(checked) =>
onChange({ ...config, expression_self_reflect: checked })
}
/>
</div>
{config.expression_self_reflect && (
<div className="space-y-4 pl-4 border-l-2 border-primary/20">
{/* 自动检查间隔 */}
<div className="space-y-2">
<Label htmlFor="expression_auto_check_interval">
</Label>
<Input
id="expression_auto_check_interval"
type="number"
min="60"
value={config.expression_auto_check_interval ?? 3600}
onChange={(e) =>
onChange({
...config,
expression_auto_check_interval: parseInt(e.target.value) || 3600,
})
}
/>
<p className="text-xs text-muted-foreground">
36001
</p>
</div>
{/* 每次检查数量 */}
<div className="space-y-2">
<Label htmlFor="expression_auto_check_count">
</Label>
<Input
id="expression_auto_check_count"
type="number"
min="1"
max="100"
value={config.expression_auto_check_count ?? 10}
onChange={(e) =>
onChange({
...config,
expression_auto_check_count: parseInt(e.target.value) || 10,
})
}
/>
<p className="text-xs text-muted-foreground">
10
</p>
</div>
{/* 自定义评估标准 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label></Label>
<Button
onClick={() => {
onChange({
...config,
expression_auto_check_custom_criteria: [
...(config.expression_auto_check_custom_criteria || []),
'',
],
})
}}
size="sm"
variant="outline"
>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="space-y-2">
{(config.expression_auto_check_custom_criteria || []).map((criterion, index) => (
<div key={index} className="flex gap-2">
<Input
value={criterion}
onChange={(e) => {
const newCriteria = [...(config.expression_auto_check_custom_criteria || [])]
newCriteria[index] = e.target.value
onChange({ ...config, expression_auto_check_custom_criteria: newCriteria })
}}
placeholder="输入评估标准,例如:是否符合角色人设"
className="flex-1"
/>
<Button
onClick={() => {
onChange({
...config,
expression_auto_check_custom_criteria: (config.expression_auto_check_custom_criteria || []).filter((_, i) => i !== index),
})
}}
size="icon"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
{(!config.expression_auto_check_custom_criteria || config.expression_auto_check_custom_criteria.length === 0) && (
<div className="text-center py-4 text-muted-foreground text-sm">
"添加标准"
</div>
)}
</div>
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
)}
</div>
{/* 仅使用已检查的表达方式 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="expression_checked_only" className="cursor-pointer font-medium">
使
</Label>
<p className="text-xs text-muted-foreground">
使使使
</p>
</div>
<Switch
id="expression_checked_only"
checked={config.expression_checked_only ?? false}
onCheckedChange={(checked) =>
onChange({ ...config, expression_checked_only: checked })
}
/>
</div>
</div>
{/* 手动表达优化 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label htmlFor="expression_manual_reflect" className="cursor-pointer font-medium">
</Label>
<p className="text-xs text-muted-foreground">
</p>
</div>
<Switch
id="expression_manual_reflect"
checked={config.expression_manual_reflect ?? false}
onCheckedChange={(checked) =>
onChange({ ...config, expression_manual_reflect: checked })
}
/>
</div>
{config.expression_manual_reflect && (
<div className="space-y-4 pl-4 border-l-2 border-primary/20">
{/* 表达反思操作员 ID */}
<div className="rounded-lg border p-4 space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
</div>
<div className="space-y-4">
{(() => {
const operatorId = config.manual_reflect_operator_id || ''
const parts = operatorId.split(':')
const platform = parts[0] || 'qq'
const chatId = parts[1] || ''
const chatType = parts[2] || 'private'
return (
<div className="grid gap-4 p-3 sm:p-4 rounded-lg bg-muted/50">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{/* 平台选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={platform}
onValueChange={(value) => {
onChange({ ...config, manual_reflect_operator_id: `${value}:${chatId}:${chatType}` })
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="qq">QQ</SelectItem>
<SelectItem value="wx"></SelectItem>
</SelectContent>
</Select>
</div>
{/* ID 输入 */}
<div className="grid gap-2">
<Label className="text-xs font-medium">/ ID</Label>
<Input
value={chatId}
onChange={(e) => {
onChange({ ...config, manual_reflect_operator_id: `${platform}:${e.target.value}:${chatType}` })
}}
placeholder="输入 ID"
className="font-mono text-sm"
/>
</div>
{/* 类型选择 */}
<div className="grid gap-2">
<Label className="text-xs font-medium"></Label>
<Select
value={chatType}
onValueChange={(value) => {
onChange({ ...config, manual_reflect_operator_id: `${platform}:${chatId}:${value}` })
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="private">private</SelectItem>
<SelectItem value="group">group</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<p className="text-xs text-muted-foreground">
ID{config.manual_reflect_operator_id || '(未设置)'}
</p>
<p className="text-xs text-muted-foreground">
IDplatform:id:type ( "qq:123456:private" "qq:654321:group")
</p>
</div>
)
})()}
</div>
</div>
{/* 允许反思的聊天流列表 */}
<div className="rounded-lg border p-4 space-y-4">
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-medium"></span>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
<Button
onClick={() => {
onChange({
...config,
allow_reflect: [...(config.allow_reflect || []), 'qq::group'],
})
}}
size="sm"
variant="outline"
>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="space-y-2">
{(config.allow_reflect || []).map((chatId, index) => {
const parts = chatId.split(':')
const platform = parts[0] || 'qq'
const id = parts[1] || ''
const chatType = parts[2] || 'group'
return (
<div key={index} className="flex items-center gap-2 p-3 rounded-lg bg-muted/50">
<Select
value={platform}
onValueChange={(value) => {
const newList = [...config.allow_reflect]
newList[index] = `${value}:${id}:${chatType}`
onChange({ ...config, allow_reflect: newList })
}}
>
<SelectTrigger className="w-24">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="qq">QQ</SelectItem>
<SelectItem value="wx"></SelectItem>
</SelectContent>
</Select>
<Input
value={id}
onChange={(e) => {
const newList = [...config.allow_reflect]
newList[index] = `${platform}:${e.target.value}:${chatType}`
onChange({ ...config, allow_reflect: newList })
}}
placeholder="ID"
className="flex-1 font-mono text-sm"
/>
<Select
value={chatType}
onValueChange={(value) => {
const newList = [...config.allow_reflect]
newList[index] = `${platform}:${id}:${value}`
onChange({ ...config, allow_reflect: newList })
}}
>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="group"></SelectItem>
<SelectItem value="private"></SelectItem>
</SelectContent>
</Select>
<Button
onClick={() => {
onChange({
...config,
allow_reflect: config.allow_reflect.filter((_, i) => i !== index),
})
}}
size="sm"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)
})}
{(!config.allow_reflect || config.allow_reflect.length === 0) && (
<div className="text-center py-4 text-muted-foreground text-sm">
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
</div>
{/* 表达共享组配置 */}
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
<div>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold"></h3>
<p className="text-sm text-muted-foreground mt-1">
</p>
</div>
<Button onClick={addExpressionGroup} size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="space-y-4">
{config.expression_groups.map((group, groupIndex) => {
// 获取所有已配置的聊天流 ID用于下拉框选项
const availableChatIds = config.learning_list
.map(rule => rule[0])
.filter(id => id !== '') // 过滤掉全局配置
return (
<div key={groupIndex} className="rounded-lg border p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">
{groupIndex + 1}
{group.length === 1 && group[0] === '*' && '(全局共享)'}
</span>
<div className="flex gap-2">
<Button
onClick={() => addGroupMember(groupIndex)}
size="sm"
variant="outline"
>
<Plus className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm" variant="ghost">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{groupIndex + 1}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => removeExpressionGroup(groupIndex)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
<div className="space-y-2">
{group.map((member, memberIndex) => (
<ExpressionGroupMemberInput
key={`${groupIndex}-${memberIndex}`}
member={member}
groupIndex={groupIndex}
memberIndex={memberIndex}
availableChatIds={availableChatIds}
onUpdate={updateGroupMember}
onRemove={removeGroupMember}
/>
))}
</div>
<p className="text-xs text-muted-foreground">
"*"
</p>
</div>
)
})}
{config.expression_groups.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
"添加共享组"
</div>
)}
</div>
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,336 @@
import React from 'react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import type { EmojiConfig, MemoryConfig, ToolConfig, VoiceConfig } from '../types'
interface FeaturesSectionProps {
emojiConfig: EmojiConfig
memoryConfig: MemoryConfig
toolConfig: ToolConfig
voiceConfig: VoiceConfig
onEmojiChange: (config: EmojiConfig) => void
onMemoryChange: (config: MemoryConfig) => void
onToolChange: (config: ToolConfig) => void
onVoiceChange: (config: VoiceConfig) => void
}
export const FeaturesSection = React.memo(function FeaturesSection({
emojiConfig,
memoryConfig,
toolConfig,
voiceConfig,
onEmojiChange,
onMemoryChange,
onToolChange,
onVoiceChange,
}: FeaturesSectionProps) {
return (
<div className="space-y-6">
{/* 工具设置 */}
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<div>
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Switch
id="enable_tool"
checked={toolConfig.enable_tool}
onCheckedChange={(checked) => onToolChange({ ...toolConfig, enable_tool: checked })}
/>
<Label htmlFor="enable_tool" className="cursor-pointer">
</Label>
</div>
<p className="text-xs text-muted-foreground -mt-2">
使
</p>
<div className="flex items-center space-x-2 pt-2">
<Switch
id="enable_asr"
checked={voiceConfig.enable_asr}
onCheckedChange={(checked) => onVoiceChange({ ...voiceConfig, enable_asr: checked })}
/>
<Label htmlFor="enable_asr" className="cursor-pointer">
</Label>
</div>
<p className="text-xs text-muted-foreground -mt-2">
</p>
</div>
</div>
</div>
{/* 记忆设置 */}
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<div>
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="max_agent_iterations"></Label>
<Input
id="max_agent_iterations"
type="number"
min="1"
value={memoryConfig.max_agent_iterations}
onChange={(e) =>
onMemoryChange({ ...memoryConfig, max_agent_iterations: parseInt(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground"> 1</p>
</div>
<div className="grid gap-2">
<Label htmlFor="agent_timeout_seconds"></Label>
<Input
id="agent_timeout_seconds"
type="number"
min="1"
step="0.1"
value={memoryConfig.agent_timeout_seconds ?? 120}
onChange={(e) =>
onMemoryChange({ ...memoryConfig, agent_timeout_seconds: parseFloat(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground"></p>
</div>
<div className="flex items-center space-x-2">
<Switch
id="enable_jargon_detection"
checked={memoryConfig.enable_jargon_detection ?? true}
onCheckedChange={(checked) =>
onMemoryChange({ ...memoryConfig, enable_jargon_detection: checked })
}
/>
<Label htmlFor="enable_jargon_detection" className="cursor-pointer">
</Label>
</div>
<p className="text-xs text-muted-foreground -mt-2">
</p>
<div className="flex items-center space-x-2">
<Switch
id="global_memory"
checked={memoryConfig.global_memory ?? false}
onCheckedChange={(checked) =>
onMemoryChange({ ...memoryConfig, global_memory: checked })
}
/>
<Label htmlFor="global_memory" className="cursor-pointer">
</Label>
</div>
<p className="text-xs text-muted-foreground -mt-2">
</p>
{/* 聊天历史总结配置 */}
<div className="border-t pt-4 mt-4">
<h4 className="text-sm font-semibold mb-3"></h4>
<div className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="chat_history_topic_check_message_threshold"></Label>
<Input
id="chat_history_topic_check_message_threshold"
type="number"
min="1"
value={memoryConfig.chat_history_topic_check_message_threshold ?? 80}
onChange={(e) =>
onMemoryChange({ ...memoryConfig, chat_history_topic_check_message_threshold: parseInt(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="chat_history_topic_check_time_hours"></Label>
<Input
id="chat_history_topic_check_time_hours"
type="number"
min="0.1"
step="0.1"
value={memoryConfig.chat_history_topic_check_time_hours ?? 8.0}
onChange={(e) =>
onMemoryChange({ ...memoryConfig, chat_history_topic_check_time_hours: parseFloat(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="chat_history_topic_check_min_messages"></Label>
<Input
id="chat_history_topic_check_min_messages"
type="number"
min="1"
value={memoryConfig.chat_history_topic_check_min_messages ?? 20}
onChange={(e) =>
onMemoryChange({ ...memoryConfig, chat_history_topic_check_min_messages: parseInt(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="chat_history_finalize_no_update_checks"></Label>
<Input
id="chat_history_finalize_no_update_checks"
type="number"
min="1"
value={memoryConfig.chat_history_finalize_no_update_checks ?? 3}
onChange={(e) =>
onMemoryChange({ ...memoryConfig, chat_history_finalize_no_update_checks: parseInt(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
N次检查无新增内容时触发打包存储
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="chat_history_finalize_message_count"></Label>
<Input
id="chat_history_finalize_message_count"
type="number"
min="1"
value={memoryConfig.chat_history_finalize_message_count ?? 5}
onChange={(e) =>
onMemoryChange({ ...memoryConfig, chat_history_finalize_message_count: parseInt(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 表情包设置 */}
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<div>
<h3 className="text-lg font-semibold mb-4"></h3>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="emoji_chance"></Label>
<Input
id="emoji_chance"
type="number"
step="0.1"
min="0"
max="1"
value={emojiConfig.emoji_chance}
onChange={(e) =>
onEmojiChange({ ...emojiConfig, emoji_chance: parseFloat(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground"> 0-1</p>
</div>
<div className="grid gap-2">
<Label htmlFor="max_reg_num"></Label>
<Input
id="max_reg_num"
type="number"
min="1"
value={emojiConfig.max_reg_num}
onChange={(e) =>
onEmojiChange({ ...emojiConfig, max_reg_num: parseInt(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground"></p>
</div>
<div className="grid gap-2">
<Label htmlFor="check_interval"></Label>
<Input
id="check_interval"
type="number"
min="1"
value={emojiConfig.check_interval}
onChange={(e) =>
onEmojiChange({ ...emojiConfig, check_interval: parseInt(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
</p>
</div>
<div className="flex items-center space-x-2">
<Switch
id="do_replace"
checked={emojiConfig.do_replace}
onCheckedChange={(checked) =>
onEmojiChange({ ...emojiConfig, do_replace: checked })
}
/>
<Label htmlFor="do_replace" className="cursor-pointer">
</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="steal_emoji"
checked={emojiConfig.steal_emoji}
onCheckedChange={(checked) =>
onEmojiChange({ ...emojiConfig, steal_emoji: checked })
}
/>
<Label htmlFor="steal_emoji" className="cursor-pointer">
</Label>
</div>
<p className="text-xs text-muted-foreground -mt-2">
</p>
<div className="flex items-center space-x-2">
<Switch
id="content_filtration"
checked={emojiConfig.content_filtration}
onCheckedChange={(checked) =>
onEmojiChange({ ...emojiConfig, content_filtration: checked })
}
/>
<Label htmlFor="content_filtration" className="cursor-pointer">
</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>
</div>
)
})

View File

@@ -0,0 +1,150 @@
import React from 'react'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import type { LPMMKnowledgeConfig } from '../types'
interface LPMMSectionProps {
config: LPMMKnowledgeConfig
onChange: (config: LPMMKnowledgeConfig) => void
}
export const LPMMSection = React.memo(function LPMMSection({ config, onChange }: LPMMSectionProps) {
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<h3 className="text-lg font-semibold">LPMM </h3>
<div className="grid gap-4">
<div className="flex items-center space-x-2">
<Switch
checked={config.enable}
onCheckedChange={(checked) => onChange({ ...config, enable: checked })}
/>
<Label className="cursor-pointer"> LPMM </Label>
</div>
{config.enable && (
<>
<div className="grid gap-2">
<Label>LPMM </Label>
<Select
value={config.lpmm_mode}
onValueChange={(value) => onChange({ ...config, lpmm_mode: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择 LPMM 模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="classic"></SelectItem>
<SelectItem value="agent">Agent </SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid gap-2">
<Label> TopK</Label>
<Input
type="number"
min="1"
value={config.rag_synonym_search_top_k}
onChange={(e) =>
onChange({ ...config, rag_synonym_search_top_k: parseInt(e.target.value) })
}
/>
</div>
<div className="grid gap-2">
<Label></Label>
<Input
type="number"
step="0.1"
min="0"
max="1"
value={config.rag_synonym_threshold}
onChange={(e) =>
onChange({ ...config, rag_synonym_threshold: parseFloat(e.target.value) })
}
/>
</div>
<div className="grid gap-2">
<Label>线</Label>
<Input
type="number"
min="1"
value={config.info_extraction_workers}
onChange={(e) =>
onChange({ ...config, info_extraction_workers: parseInt(e.target.value) })
}
/>
</div>
<div className="grid gap-2">
<Label></Label>
<Input
type="number"
min="1"
value={config.embedding_dimension}
onChange={(e) =>
onChange({ ...config, embedding_dimension: parseInt(e.target.value) })
}
/>
</div>
<div className="grid gap-2">
<Label>线</Label>
<Input
type="number"
min="1"
value={config.max_embedding_workers}
onChange={(e) =>
onChange({ ...config, max_embedding_workers: parseInt(e.target.value) })
}
/>
</div>
<div className="grid gap-2">
<Label></Label>
<Input
type="number"
min="1"
value={config.embedding_chunk_size}
onChange={(e) =>
onChange({ ...config, embedding_chunk_size: parseInt(e.target.value) })
}
/>
</div>
<div className="grid gap-2">
<Label></Label>
<Input
type="number"
min="1"
value={config.max_synonym_entities}
onChange={(e) =>
onChange({ ...config, max_synonym_entities: parseInt(e.target.value) })
}
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={config.enable_ppr}
onCheckedChange={(checked) => onChange({ ...config, enable_ppr: checked })}
/>
<Label className="cursor-pointer"> PPR ()</Label>
</div>
</>
)}
</div>
</div>
)
})

View File

@@ -0,0 +1,264 @@
import React, { useState } from 'react'
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 { Plus, Trash2 } from 'lucide-react'
import type { LogConfig } from '../types'
interface LogSectionProps {
config: LogConfig
onChange: (config: LogConfig) => void
}
export const LogSection = React.memo(function LogSection({ config, onChange }: LogSectionProps) {
const [newLibrary, setNewLibrary] = useState('')
const [newLogLevel, setNewLogLevel] = useState('WARNING')
const addSuppressedLibrary = () => {
if (newLibrary && !config.suppress_libraries.includes(newLibrary)) {
onChange({
...config,
suppress_libraries: [...config.suppress_libraries, newLibrary],
})
setNewLibrary('')
}
}
const removeSuppressedLibrary = (library: string) => {
onChange({
...config,
suppress_libraries: config.suppress_libraries.filter((l) => l !== library),
})
}
const addLibraryLogLevel = () => {
if (newLibrary && !config.library_log_levels[newLibrary]) {
onChange({
...config,
library_log_levels: { ...config.library_log_levels, [newLibrary]: newLogLevel },
})
setNewLibrary('')
setNewLogLevel('WARNING')
}
}
const removeLibraryLogLevel = (library: string) => {
const newLevels = { ...config.library_log_levels }
delete newLevels[library]
onChange({ ...config, library_log_levels: newLevels })
}
const logLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
const logLevelStyles = ['FULL', 'compact', 'lite']
const colorTextOptions = ['none', 'title', 'full']
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>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid gap-2">
<Label></Label>
<Input
value={config.date_style}
onChange={(e) => onChange({ ...config, date_style: e.target.value })}
placeholder="例如: m-d H:i:s"
/>
<p className="text-xs text-muted-foreground">m=, d=, H=, i=, s=</p>
</div>
<div className="grid gap-2">
<Label></Label>
<Select
value={config.log_level_style}
onValueChange={(value) => onChange({ ...config, log_level_style: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{logLevelStyles.map((style) => (
<SelectItem key={style} value={style}>
{style}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label></Label>
<Select
value={config.color_text}
onValueChange={(value) => onChange({ ...config, color_text: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{colorTextOptions.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label></Label>
<Select
value={config.log_level}
onValueChange={(value) => onChange({ ...config, log_level: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{logLevels.map((level) => (
<SelectItem key={level} value={level}>
{level}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label></Label>
<Select
value={config.console_log_level}
onValueChange={(value) => onChange({ ...config, console_log_level: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{logLevels.map((level) => (
<SelectItem key={level} value={level}>
{level}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label></Label>
<Select
value={config.file_log_level}
onValueChange={(value) => onChange({ ...config, file_log_level: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{logLevels.map((level) => (
<SelectItem key={level} value={level}>
{level}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 屏蔽的库 */}
<div>
<Label className="mb-2 block"></Label>
<div className="flex gap-2 mb-2">
<Input
value={newLibrary}
onChange={(e) => setNewLibrary(e.target.value)}
placeholder="输入库名"
className="flex-1"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
addSuppressedLibrary()
}
}}
/>
<Button onClick={addSuppressedLibrary} size="sm" className="flex-shrink-0">
<Plus className="h-4 w-4" strokeWidth={2} fill="none" />
</Button>
</div>
<div className="flex flex-wrap gap-2">
{config.suppress_libraries.map((library) => (
<div
key={library}
className="flex items-center gap-1 bg-secondary px-3 py-1 rounded-md"
>
<span className="text-sm">{library}</span>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={() => removeSuppressedLibrary(library)}
>
<Trash2 className="h-3 w-3" strokeWidth={2} fill="none" />
</Button>
</div>
))}
</div>
</div>
{/* 特定库日志级别 */}
<div>
<Label className="mb-2 block"></Label>
<div className="flex gap-2 mb-2">
<Input
value={newLibrary}
onChange={(e) => setNewLibrary(e.target.value)}
placeholder="输入库名"
className="flex-1"
/>
<Select value={newLogLevel} onValueChange={setNewLogLevel}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
{logLevels.map((level) => (
<SelectItem key={level} value={level}>
{level}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={addLibraryLogLevel} size="sm">
<Plus className="h-4 w-4" strokeWidth={2} fill="none" />
</Button>
</div>
<div className="space-y-2">
{Object.entries(config.library_log_levels).map(([library, level]) => (
<div
key={library}
className="flex items-center justify-between bg-secondary px-3 py-2 rounded-md"
>
<span className="text-sm font-medium">{library}</span>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">{level}</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => removeLibraryLogLevel(library)}
>
<Trash2 className="h-3 w-3" strokeWidth={2} fill="none" />
</Button>
</div>
</div>
))}
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,203 @@
import React, { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Plus, Trash2 } from 'lucide-react'
import type { MaimMessageConfig } from '../types'
interface MaimMessageSectionProps {
config: MaimMessageConfig
onChange: (config: MaimMessageConfig) => void
}
export const MaimMessageSection = React.memo(function MaimMessageSection({ config, onChange }: MaimMessageSectionProps) {
const [newToken, setNewToken] = useState('')
const [newApiKey, setNewApiKey] = useState('')
const addToken = () => {
if (newToken && !config.auth_token.includes(newToken)) {
onChange({ ...config, auth_token: [...config.auth_token, newToken] })
setNewToken('')
}
}
const removeToken = (index: number) => {
onChange({
...config,
auth_token: config.auth_token.filter((_, i) => i !== index),
})
}
const addApiKey = () => {
if (newApiKey && !config.api_server_allowed_api_keys.includes(newApiKey)) {
onChange({ ...config, api_server_allowed_api_keys: [...config.api_server_allowed_api_keys, newApiKey] })
setNewApiKey('')
}
}
const removeApiKey = (index: number) => {
onChange({
...config,
api_server_allowed_api_keys: config.api_server_allowed_api_keys.filter((_, i) => i !== index),
})
}
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-6">
{/* 认证令牌 */}
<div>
<h3 className="text-lg font-semibold mb-2"> API </h3>
<p className="text-sm text-muted-foreground mb-3"> API </p>
<div className="flex gap-2 mb-2">
<Input
value={newToken}
onChange={(e) => setNewToken(e.target.value)}
placeholder="输入认证令牌"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
addToken()
}
}}
/>
<Button onClick={addToken} size="sm">
<Plus className="h-4 w-4" strokeWidth={2} fill="none" />
</Button>
</div>
<div className="space-y-2">
{config.auth_token.map((token, index) => (
<div
key={index}
className="flex items-center justify-between bg-secondary px-3 py-2 rounded-md"
>
<span className="text-sm font-mono">{token}</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => removeToken(index)}
>
<Trash2 className="h-3 w-3" strokeWidth={2} fill="none" />
</Button>
</div>
))}
</div>
</div>
{/* 新版 API Server */}
<div>
<h3 className="text-lg font-semibold mb-4"> API Server </h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label> API Server</Label>
<p className="text-sm text-muted-foreground">
API Server
</p>
</div>
<Switch
checked={config.enable_api_server}
onCheckedChange={(checked) => onChange({ ...config, enable_api_server: checked })}
/>
</div>
{config.enable_api_server && (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid gap-2">
<Label></Label>
<Input
value={config.api_server_host}
onChange={(e) => onChange({ ...config, api_server_host: e.target.value })}
placeholder="0.0.0.0"
/>
</div>
<div className="grid gap-2">
<Label></Label>
<Input
type="number"
value={config.api_server_port}
onChange={(e) => onChange({ ...config, api_server_port: parseInt(e.target.value) })}
placeholder="8090"
/>
</div>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={config.api_server_use_wss}
onCheckedChange={(checked) => onChange({ ...config, api_server_use_wss: checked })}
/>
<Label> WSS </Label>
</div>
{config.api_server_use_wss && (
<div className="grid gap-4">
<div className="grid gap-2">
<Label>SSL </Label>
<Input
value={config.api_server_cert_file}
onChange={(e) => onChange({ ...config, api_server_cert_file: e.target.value })}
placeholder="cert.pem"
/>
</div>
<div className="grid gap-2">
<Label>SSL </Label>
<Input
value={config.api_server_key_file}
onChange={(e) => onChange({ ...config, api_server_key_file: e.target.value })}
placeholder="key.pem"
/>
</div>
</div>
)}
{/* API Keys */}
<div>
<Label className="mb-2 block"> API Key </Label>
<p className="text-sm text-muted-foreground mb-2"></p>
<div className="flex gap-2 mb-2">
<Input
value={newApiKey}
onChange={(e) => setNewApiKey(e.target.value)}
placeholder="输入 API Key"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
addApiKey()
}
}}
/>
<Button onClick={addApiKey} size="sm">
<Plus className="h-4 w-4" strokeWidth={2} fill="none" />
</Button>
</div>
<div className="space-y-2">
{config.api_server_allowed_api_keys.map((apiKey, index) => (
<div
key={index}
className="flex items-center justify-between bg-secondary px-3 py-2 rounded-md"
>
<span className="text-sm font-mono">{apiKey}</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
onClick={() => removeApiKey(index)}
>
<Trash2 className="h-3 w-3" strokeWidth={2} fill="none" />
</Button>
</div>
))}
</div>
</div>
</>
)}
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,259 @@
import React, { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Plus, Trash2, AlertTriangle } from 'lucide-react'
import type { MessageReceiveConfig } from '../types'
interface MessageReceiveSectionProps {
config: MessageReceiveConfig
onChange: (config: MessageReceiveConfig) => void
}
/**
* 消息过滤配置模块
* 管理 ban_words、ban_msgs_regex 和 mute_group_list
*/
export default function MessageReceiveSection({
config,
onChange,
}: MessageReceiveSectionProps) {
const [newBanWord, setNewBanWord] = useState('')
const [newBanRegex, setNewBanRegex] = useState('')
// === 禁用词管理 ===
const handleAddBanWord = () => {
const trimmed = newBanWord.trim()
if (trimmed && !config.ban_words.includes(trimmed)) {
onChange({
...config,
ban_words: [...config.ban_words, trimmed],
})
setNewBanWord('')
}
}
const handleRemoveBanWord = (index: number) => {
onChange({
...config,
ban_words: config.ban_words.filter((_, i) => i !== index),
})
}
const handleBanWordKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddBanWord()
}
}
// === 禁用正则表达式管理 ===
const handleAddBanRegex = () => {
const trimmed = newBanRegex.trim()
if (trimmed && !config.ban_msgs_regex.includes(trimmed)) {
// 验证正则表达式语法
try {
new RegExp(trimmed)
onChange({
...config,
ban_msgs_regex: [...config.ban_msgs_regex, trimmed],
})
setNewBanRegex('')
} catch (err) {
alert(`正则表达式语法错误:${(err as Error).message}`)
}
}
}
const handleRemoveBanRegex = (index: number) => {
onChange({
...config,
ban_msgs_regex: config.ban_msgs_regex.filter((_, i) => i !== index),
})
}
const handleBanRegexKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddBanRegex()
}
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="ban_words" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="ban_words"></TabsTrigger>
<TabsTrigger value="ban_regex"></TabsTrigger>
</TabsList>
{/* 禁用关键词 Tab */}
<TabsContent value="ban_words" className="space-y-4">
<div className="space-y-2">
<div className="flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-1 flex-shrink-0" />
<p className="text-sm text-muted-foreground">
Bot
</p>
</div>
<div className="flex gap-2">
<Input
placeholder="输入要禁用的关键词(按回车添加)"
value={newBanWord}
onChange={(e) => setNewBanWord(e.target.value)}
onKeyDown={handleBanWordKeyDown}
/>
<Button onClick={handleAddBanWord} size="icon">
<Plus className="h-4 w-4" />
</Button>
</div>
{config.ban_words.length === 0 ? (
<div className="rounded-md border border-dashed p-8 text-center">
<p className="text-sm text-muted-foreground">
</p>
</div>
) : (
<div className="space-y-2">
{config.ban_words.map((word, index) => (
<div
key={index}
className="flex items-center justify-between rounded-md border p-3"
>
<code className="text-sm">{word}</code>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
<code>"{word}"</code>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => handleRemoveBanWord(index)}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))}
</div>
)}
</div>
</TabsContent>
{/* 禁用正则表达式 Tab */}
<TabsContent value="ban_regex" className="space-y-4">
<div className="space-y-2">
<div className="flex items-start gap-2">
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-1 flex-shrink-0" />
<div className="text-sm text-muted-foreground space-y-1">
<p></p>
<p className="text-xs">
</p>
</div>
</div>
<div className="flex gap-2">
<Textarea
placeholder="输入正则表达式(按回车添加)&#10;示例https?://[^\s]+ 匹配链接"
value={newBanRegex}
onChange={(e) => setNewBanRegex(e.target.value)}
onKeyDown={handleBanRegexKeyDown}
className="min-h-[60px] font-mono text-sm"
/>
<Button onClick={handleAddBanRegex} size="icon">
<Plus className="h-4 w-4" />
</Button>
</div>
{config.ban_msgs_regex.length === 0 ? (
<div className="rounded-md border border-dashed p-8 text-center">
<p className="text-sm text-muted-foreground">
</p>
</div>
) : (
<div className="space-y-2">
{config.ban_msgs_regex.map((regex, index) => (
<div
key={index}
className="flex items-center justify-between rounded-md border p-3"
>
<code className="text-sm font-mono flex-1 break-all">
{regex}
</code>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon" className="ml-2 flex-shrink-0">
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
<code>"{regex}"</code>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => handleRemoveBanRegex(index)}
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))}
</div>
)}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,164 @@
import React from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { Plus, Trash2 } from 'lucide-react'
import type { PersonalityConfig } from '../types'
interface PersonalitySectionProps {
config: PersonalityConfig
onChange: (config: PersonalityConfig) => void
}
export const PersonalitySection = React.memo(function PersonalitySection({ config, onChange }: PersonalitySectionProps) {
const addState = () => {
onChange({ ...config, states: [...config.states, ''] })
}
const removeState = (index: number) => {
onChange({
...config,
states: config.states.filter((_, i) => i !== index),
})
}
const updateState = (index: number, value: string) => {
const newStates = [...config.states]
newStates[index] = value
onChange({ ...config, states: newStates })
}
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>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="personality"></Label>
<Textarea
id="personality"
value={config.personality}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, personality: e.target.value })}
placeholder="描述人格特质和身份特征建议120字以内"
rows={3}
/>
<p className="text-xs text-muted-foreground">
120
</p>
</div>
{/* 多重人格配置 - 移到人格特质下方 */}
<div className="grid gap-2">
<div className="flex items-center justify-between">
<Label></Label>
<Button onClick={addState} size="sm" variant="outline">
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<p className="text-xs text-muted-foreground">
</p>
<div className="space-y-2">
{config.states.map((state, index) => (
<div key={index} className="flex gap-2">
<Textarea
value={state}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => updateState(index, e.target.value)}
placeholder="描述一个人格状态"
rows={2}
/>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="icon" variant="outline">
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={() => removeState(index)}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))}
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="state_probability"></Label>
<Input
id="state_probability"
type="number"
step="0.1"
min="0"
max="1"
value={config.state_probability}
onChange={(e) =>
onChange({ ...config, state_probability: parseFloat(e.target.value) })
}
/>
<p className="text-xs text-muted-foreground">
0.0-1.0
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="reply_style"></Label>
<Textarea
id="reply_style"
value={config.reply_style}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, reply_style: e.target.value })}
placeholder="描述说话的表达风格和习惯"
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="plan_style"></Label>
<Textarea
id="plan_style"
value={config.plan_style}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, plan_style: e.target.value })}
placeholder="麦麦的说话规则和行为风格"
rows={5}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="visual_style"></Label>
<Textarea
id="visual_style"
value={config.visual_style}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange({ ...config, visual_style: e.target.value })}
placeholder="识图时的处理规则"
rows={3}
/>
</div>
</div>
</div>
</div>
)
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
import React from 'react'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import type { TelemetryConfig } from '../types'
interface TelemetrySectionProps {
config: TelemetryConfig
onChange: (config: TelemetryConfig) => void
}
export const TelemetrySection = React.memo(function TelemetrySection({ config, onChange }: TelemetrySectionProps) {
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<h3 className="text-lg font-semibold"></h3>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label></Label>
<p className="text-sm text-muted-foreground">
</p>
</div>
<Switch
checked={config.enable}
onCheckedChange={(checked) => onChange({ ...config, enable: checked })}
/>
</div>
</div>
)
})

View File

@@ -0,0 +1,27 @@
import React from 'react'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import type { VoiceConfig } from '../types'
interface VoiceSectionProps {
config: VoiceConfig
onChange: (config: VoiceConfig) => void
}
export const VoiceSection = React.memo(function VoiceSection({ config, onChange }: VoiceSectionProps) {
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<h3 className="text-lg font-semibold"></h3>
<div className="flex items-center space-x-2">
<Switch
checked={config.enable_asr}
onCheckedChange={(checked) => onChange({ ...config, enable_asr: checked })}
/>
<Label className="cursor-pointer"></Label>
</div>
<p className="text-xs text-muted-foreground">
</p>
</div>
)
})

View File

@@ -0,0 +1,287 @@
import React, { useState } from 'react'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { X, Plus } from 'lucide-react'
import type { WebUIConfig } from '../types'
interface WebUISectionProps {
config: WebUIConfig
onChange: (config: WebUIConfig) => void
}
export const WebUISection = React.memo(function WebUISection({ config, onChange }: WebUISectionProps) {
const [newAllowedIp, setNewAllowedIp] = useState('')
const [newTrustedProxy, setNewTrustedProxy] = useState('')
const [showDisableWarning, setShowDisableWarning] = useState(false)
// 将逗号分隔的字符串转换为数组
const allowedIpsList = config.allowed_ips
? config.allowed_ips.split(',').map(ip => ip.trim()).filter(ip => ip)
: []
const trustedProxiesList = config.trusted_proxies
? config.trusted_proxies.split(',').map(ip => ip.trim()).filter(ip => ip)
: []
// 处理添加IP白名单
const handleAddAllowedIp = () => {
if (!newAllowedIp.trim()) return
const updatedList = [...allowedIpsList, newAllowedIp.trim()]
onChange({ ...config, allowed_ips: updatedList.join(',') })
setNewAllowedIp('')
}
// 处理删除IP白名单
const handleRemoveAllowedIp = (index: number) => {
const updatedList = allowedIpsList.filter((_, i) => i !== index)
onChange({ ...config, allowed_ips: updatedList.join(',') })
}
// 处理添加信任代理
const handleAddTrustedProxy = () => {
if (!newTrustedProxy.trim()) return
const updatedList = [...trustedProxiesList, newTrustedProxy.trim()]
onChange({ ...config, trusted_proxies: updatedList.join(',') })
setNewTrustedProxy('')
}
// 处理删除信任代理
const handleRemoveTrustedProxy = (index: number) => {
const updatedList = trustedProxiesList.filter((_, i) => i !== index)
onChange({ ...config, trusted_proxies: updatedList.join(',') })
}
// 处理WebUI开关变更
const handleEnabledChange = (checked: boolean) => {
if (!checked && config.enabled) {
// 用户尝试关闭WebUI显示警告
setShowDisableWarning(true)
} else {
// 用户开启WebUI直接更新
onChange({ ...config, enabled: checked })
}
}
// 确认关闭WebUI
const confirmDisableWebUI = () => {
onChange({ ...config, enabled: false })
setShowDisableWarning(false)
}
return (
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
<h3 className="text-lg font-semibold">WebUI </h3>
<div className="grid gap-4">
<div className="flex items-center space-x-2">
<Switch
checked={config.enabled}
onCheckedChange={handleEnabledChange}
/>
<Label className="cursor-pointer"> WebUI</Label>
</div>
{config.enabled && (
<>
<div className="grid gap-2">
<Label></Label>
<Select
value={config.mode}
onValueChange={(value) => onChange({ ...config, mode: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择运行模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="development"></SelectItem>
<SelectItem value="production"></SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
注意: WebUI .env WEBUI_HOST WEBUI_PORT
</p>
</div>
<div className="grid gap-2">
<Label></Label>
<Select
value={config.anti_crawler_mode}
onValueChange={(value) => onChange({ ...config, anti_crawler_mode: value })}
>
<SelectTrigger>
<SelectValue placeholder="选择防爬虫模式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="false"></SelectItem>
<SelectItem value="basic"></SelectItem>
<SelectItem value="loose"></SelectItem>
<SelectItem value="strict"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2 sm:col-span-2">
<Label>IP </Label>
<div className="flex gap-2">
<Input
value={newAllowedIp}
onChange={(e) => setNewAllowedIp(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddAllowedIp()
}
}}
placeholder="输入IP地址后按回车或点击添加"
/>
<Button
type="button"
size="sm"
onClick={handleAddAllowedIp}
disabled={!newAllowedIp.trim()}
>
<Plus className="h-4 w-4" />
</Button>
</div>
{allowedIpsList.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{allowedIpsList.map((ip, index) => (
<Badge key={index} variant="secondary" className="flex items-center gap-1">
{ip}
<button
type="button"
onClick={() => handleRemoveAllowedIp(index)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
<p className="text-xs text-muted-foreground">
IPCIDR格式和通配符127.0.0.1192.168.1.0/24
</p>
</div>
<div className="grid gap-2 sm:col-span-2">
<Label> IP</Label>
<div className="flex gap-2">
<Input
value={newTrustedProxy}
onChange={(e) => setNewTrustedProxy(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleAddTrustedProxy()
}
}}
placeholder="输入代理IP后按回车或点击添加"
/>
<Button
type="button"
size="sm"
onClick={handleAddTrustedProxy}
disabled={!newTrustedProxy.trim()}
>
<Plus className="h-4 w-4" />
</Button>
</div>
{trustedProxiesList.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{trustedProxiesList.map((ip, index) => (
<Badge key={index} variant="secondary" className="flex items-center gap-1">
{ip}
<button
type="button"
onClick={() => handleRemoveTrustedProxy(index)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
)}
<p className="text-xs text-muted-foreground">
IP的X-Forwarded-For头才被信任
</p>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={config.trust_xff}
onCheckedChange={(checked) => onChange({ ...config, trust_xff: checked })}
/>
<Label className="cursor-pointer"> X-Forwarded-For </Label>
</div>
<div className="flex items-center space-x-2">
<Switch
checked={config.secure_cookie}
onCheckedChange={(checked) => onChange({ ...config, secure_cookie: checked })}
/>
<Label className="cursor-pointer"> Cookie HTTPS</Label>
</div>
<div className="grid gap-2">
<div className="flex items-center space-x-2">
<Switch
checked={config.enable_paragraph_content}
onCheckedChange={(checked) => onChange({ ...config, enable_paragraph_content: checked })}
/>
<Label className="cursor-pointer"></Label>
</div>
<p className="text-xs text-muted-foreground">
embedding storeMB
</p>
</div>
</>
)}
</div>
{/* 关闭WebUI警告对话框 */}
<AlertDialog open={showDisableWarning} onOpenChange={setShowDisableWarning}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> WebUI</AlertDialogTitle>
<AlertDialogDescription>
WebUI WebUI 访
<br />
<br />
WebUI 访
<br />
<br />
WebUI
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={confirmDisableWebUI}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
})

View File

@@ -0,0 +1,19 @@
/**
* Bot 配置页面各个 Section 组件
*/
export { BotInfoSection } from './BotInfoSection'
export { PersonalitySection } from './PersonalitySection'
export { ChatSection } from './ChatSection'
export { DreamSection } from './DreamSection'
export { LPMMSection } from './LPMMSection'
export { LogSection } from './LogSection'
export { DebugSection } from './DebugSection'
export { ExperimentalSection } from './ExperimentalSection'
export { MaimMessageSection } from './MaimMessageSection'
export { TelemetrySection } from './TelemetrySection'
export { FeaturesSection } from './FeaturesSection'
export { ExpressionSection } from './ExpressionSection'
export { ProcessingSection } from './ProcessingSection'
export { default as MessageReceiveSection } from './MessageReceiveSection'
export { WebUISection } from './WebUISection'

View File

@@ -0,0 +1,259 @@
/**
* Bot 配置页面相关类型定义
*/
export interface BotConfig {
platform: string
qq_account: string | number
nickname: string
platforms: string[]
alias_names: string[]
}
export interface PersonalityConfig {
personality: string
reply_style: string
interest: string
plan_style: string
visual_style: string
states: string[]
state_probability: number
}
export interface ChatConfig {
talk_value: number
mentioned_bot_reply: boolean
max_context_size: number
planner_smooth: number
think_mode: 'classic' | 'deep' | 'dynamic'
plan_reply_log_max_per_chat: number
llm_quote: boolean
enable_talk_value_rules: boolean
talk_value_rules: Array<{
target: string
time: string
value: number
}>
}
export interface ExpressionConfig {
learning_list: Array<[string, string, string, string]>
expression_groups: Array<string[]>
expression_manual_reflect: boolean
manual_reflect_operator_id: string
allow_reflect: string[]
expression_self_reflect: boolean
expression_auto_check_interval: number
expression_auto_check_count: number
expression_auto_check_custom_criteria: string[]
expression_checked_only: boolean
all_global_jargon: boolean
enable_jargon_explanation: boolean
jargon_mode: string
}
export interface EmojiConfig {
emoji_chance: number
max_reg_num: number
do_replace: boolean
check_interval: number
steal_emoji: boolean
content_filtration: boolean
filtration_prompt: string
}
export interface MemoryConfig {
max_agent_iterations: number
agent_timeout_seconds: number
enable_jargon_detection: boolean
global_memory: boolean
chat_history_topic_check_message_threshold: number
chat_history_topic_check_time_hours: number
chat_history_topic_check_min_messages: number
chat_history_finalize_no_update_checks: number
chat_history_finalize_message_count: number
}
export interface ToolConfig {
enable_tool: boolean
}
// MoodConfig 已在后端移除
export interface VoiceConfig {
enable_asr: boolean
}
export interface MessageReceiveConfig {
ban_words: string[]
ban_msgs_regex: string[]
}
export interface DreamConfig {
interval_minutes: number
max_iterations: number
first_delay_seconds: number
dream_send: string
dream_time_ranges: string[]
dream_visible: boolean
}
export interface LPMMKnowledgeConfig {
enable: boolean
lpmm_mode: string
rag_synonym_search_top_k: number
rag_synonym_threshold: number
info_extraction_workers: number
qa_relation_search_top_k: number
qa_relation_threshold: number
qa_paragraph_search_top_k: number
qa_paragraph_node_weight: number
qa_ent_filter_top_k: number
qa_ppr_damping: number
qa_res_top_k: number
embedding_dimension: number
max_embedding_workers: number
embedding_chunk_size: number
max_synonym_entities: number
enable_ppr: boolean
}
export interface KeywordRule {
keywords?: string[]
regex?: string[]
reaction: string
}
export interface KeywordReactionConfig {
keyword_rules: KeywordRule[]
regex_rules: KeywordRule[]
}
export interface ResponsePostProcessConfig {
enable_response_post_process: boolean
}
export interface ChineseTypoConfig {
enable: boolean
error_rate: number
min_freq: number
tone_error_rate: number
word_replace_rate: number
}
export interface ResponseSplitterConfig {
enable: boolean
max_length: number
max_sentence_num: number
enable_kaomoji_protection: boolean
enable_overflow_return_all: boolean
}
export interface LogConfig {
date_style: string
log_level_style: string
color_text: string
log_level: string
console_log_level: string
file_log_level: string
suppress_libraries: string[]
library_log_levels: Record<string, string>
}
export interface DebugConfig {
show_prompt: boolean
show_replyer_prompt: boolean
show_replyer_reasoning: boolean
show_jargon_prompt: boolean
show_memory_prompt: boolean
show_planner_prompt: boolean
show_lpmm_paragraph: boolean
}
export interface ExperimentalConfig {
private_plan_style: string
chat_prompts: string[]
lpmm_memory: boolean
}
export interface MaimMessageConfig {
auth_token: string[]
enable_api_server: boolean
api_server_host: string
api_server_port: number
api_server_use_wss: boolean
api_server_cert_file: string
api_server_key_file: string
api_server_allowed_api_keys: string[]
}
export interface TelemetryConfig {
enable: boolean
}
/**
* WebUI 配置
* 注意: host 和 port 配置已移至环境变量 WEBUI_HOST 和 WEBUI_PORT
*/
export interface WebUIConfig {
enabled: boolean
mode: string
anti_crawler_mode: string
allowed_ips: string
trusted_proxies: string
trust_xff: boolean
secure_cookie: boolean
enable_paragraph_content: boolean
}
/**
* 所有配置的聚合类型
*/
export interface AllBotConfigs {
botConfig: BotConfig | null
personalityConfig: PersonalityConfig | null
chatConfig: ChatConfig | null
expressionConfig: ExpressionConfig | null
emojiConfig: EmojiConfig | null
memoryConfig: MemoryConfig | null
toolConfig: ToolConfig | null
voiceConfig: VoiceConfig | null
messageReceiveConfig: MessageReceiveConfig | null
dreamConfig: DreamConfig | null
lpmmConfig: LPMMKnowledgeConfig | null
keywordReactionConfig: KeywordReactionConfig | null
responsePostProcessConfig: ResponsePostProcessConfig | null
chineseTypoConfig: ChineseTypoConfig | null
responseSplitterConfig: ResponseSplitterConfig | null
logConfig: LogConfig | null
debugConfig: DebugConfig | null
experimentalConfig: ExperimentalConfig | null
maimMessageConfig: MaimMessageConfig | null
telemetryConfig: TelemetryConfig | null
}
/**
* 配置节名称到类型的映射
*/
export type ConfigSectionName =
| 'bot'
| 'personality'
| 'chat'
| 'expression'
| 'emoji'
| 'memory'
| 'tool'
| 'voice'
| 'message_receive'
| 'dream'
| 'lpmm_knowledge'
| 'keyword_reaction'
| 'response_post_process'
| 'chinese_typo'
| 'response_splitter'
| 'log'
| 'debug'
| 'experimental'
| 'maim_message'
| 'telemetry'
| 'webui'