diff --git a/dashboard/src/routes/config/bot/sections/ChatSection.tsx b/dashboard/src/routes/config/bot/sections/ChatSection.tsx index b29024df..6546b4d3 100644 --- a/dashboard/src/routes/config/bot/sections/ChatSection.tsx +++ b/dashboard/src/routes/config/bot/sections/ChatSection.tsx @@ -1,231 +1,18 @@ -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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' 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' +import { RuleList } from './RuleList' + 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 ( - - - - - -
-
-

开始时间

-
-
- - -
-
- - -
-
-
-
-

结束时间

-
-
- - -
-
- - -
-
-
-
-
-
- ) -}) - -// 预览窗口组件 -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 ( - - - - - -
-

配置预览

-
- {previewText} -
-

- 这是保存到 bot_config.toml 文件中的格式 -

-
-
-
- ) -}) - -export const ChatSection = React.memo(function ChatSection({ config, onChange }: ChatSectionProps) { +export function ChatSection({ config, onChange }: ChatSectionProps) { // 添加发言频率规则 const addTalkValueRule = () => { onChange({ @@ -394,217 +181,13 @@ export const ChatSection = React.memo(function ChatSection({ config, onChange }: {/* 动态发言频率规则配置 */} {config.enable_talk_value_rules && ( -
-
-
-

动态发言频率规则

-

- 按时段或聊天流ID调整发言频率,优先匹配具体聊天,再匹配全局规则 -

-
- -
- - {config.talk_value_rules && config.talk_value_rules.length > 0 ? ( -
- {config.talk_value_rules.map((rule, index) => ( -
-
- - 规则 #{index + 1} - -
- - - - - - - - 确认删除 - - 确定要删除规则 #{index + 1} 吗?此操作无法撤销。 - - - - 取消 - removeTalkValueRule(index)}> - 删除 - - - - -
-
- -
- {/* 配置类型选择 */} -
- - -
- - {/* 详细配置选项 - 只在非全局时显示 */} - {rule.target !== '' && (() => { - const parts = rule.target.split(':') - const platform = parts[0] || 'qq' - const chatId = parts[1] || '' - const chatType = parts[2] || 'group' - - return ( -
-
-
- - -
- -
- - { - updateTalkValueRule(index, 'target', `${platform}:${e.target.value}:${chatType}`) - }} - placeholder="输入群 ID" - className="font-mono text-sm" - /> -
- -
- - -
-
-

- 当前聊天流 ID:{rule.target || '(未设置)'} -

-
- ) - })()} - - {/* 时间段选择器 */} -
- - updateTalkValueRule(index, 'time', v)} - /> -

- 支持跨夜区间,例如 23:00-02:00 -

-
- - {/* 发言频率滑块 */} -
-
- - { - 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" - /> -
- - updateTalkValueRule(index, 'value', values[0]) - } - min={0.01} - max={1} - step={0.01} - className="w-full" - /> -
- 0.01 (极少发言) - 0.5 - 1.0 (正常) -
-
-
-
- ))} -
- ) : ( -
-

暂无规则,点击"添加规则"按钮创建

-
- )} - -
-
- 📝 规则说明 -
- -
-
+ )} ) -}) +} diff --git a/dashboard/src/routes/config/bot/sections/RuleEditor.tsx b/dashboard/src/routes/config/bot/sections/RuleEditor.tsx new file mode 100644 index 00000000..81a4246a --- /dev/null +++ b/dashboard/src/routes/config/bot/sections/RuleEditor.tsx @@ -0,0 +1,213 @@ + +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 { Slider } from '@/components/ui/slider' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' + +import { Trash2 } from 'lucide-react' + +import { RulePreview } from './RulePreview' +import { TimeRangePicker } from './TimeRangePicker' + +interface TalkValueRule { + target: string + time: string + value: number +} + +interface RuleEditorProps { + rule: TalkValueRule + index: number + onUpdate: (index: number, field: 'target' | 'time' | 'value', value: string | number) => void + onRemove: (index: number) => void +} + +// 规则编辑器组件 +export function RuleEditor({ rule, index, onUpdate, onRemove }: RuleEditorProps) { + return ( +
+
+ + 规则 #{index + 1} + +
+ + + + + + + + 确认删除 + + 确定要删除规则 #{index + 1} 吗?此操作无法撤销。 + + + + 取消 + onRemove(index)}> + 删除 + + + + +
+
+ +
+ {/* 配置类型选择 */} +
+ + +
+ + {/* 详细配置选项 - 只在非全局时显示 */} + {rule.target !== '' && (() => { + const parts = rule.target.split(':') + const platform = parts[0] || 'qq' + const chatId = parts[1] || '' + const chatType = parts[2] || 'group' + + return ( +
+
+
+ + +
+ +
+ + { + onUpdate(index, 'target', `${platform}:${e.target.value}:${chatType}`) + }} + placeholder="输入群 ID" + className="font-mono text-sm" + /> +
+ +
+ + +
+
+

+ 当前聊天流 ID:{rule.target || '(未设置)'} +

+
+ ) + })()} + + {/* 时间段选择器 */} +
+ + onUpdate(index, 'time', v)} + /> +

+ 支持跨夜区间,例如 23:00-02:00 +

+
+ + {/* 发言频率滑块 */} +
+
+ + { + const val = parseFloat(e.target.value) + if (!isNaN(val)) { + onUpdate(index, 'value', Math.max(0.01, Math.min(1, val))) + } + }} + className="w-20 h-8 text-xs" + /> +
+ + onUpdate(index, 'value', values[0]) + } + min={0.01} + max={1} + step={0.01} + className="w-full" + /> +
+ 0.01 (极少发言) + 0.5 + 1.0 (正常) +
+
+
+
+ ) +} diff --git a/dashboard/src/routes/config/bot/sections/RuleList.tsx b/dashboard/src/routes/config/bot/sections/RuleList.tsx new file mode 100644 index 00000000..b7373f6e --- /dev/null +++ b/dashboard/src/routes/config/bot/sections/RuleList.tsx @@ -0,0 +1,70 @@ + +import { Button } from '@/components/ui/button' + +import { Plus } from 'lucide-react' + +import { RuleEditor } from './RuleEditor' + +interface TalkValueRule { + target: string + time: string + value: number +} + +interface RuleListProps { + rules: TalkValueRule[] + onAdd: () => void + onUpdate: (index: number, field: 'target' | 'time' | 'value', value: string | number) => void + onRemove: (index: number) => void +} + +// 规则列表组件 +export function RuleList({ rules, onAdd, onUpdate, onRemove }: RuleListProps) { + return ( +
+
+
+

动态发言频率规则

+

+ 按时段或聊天流ID调整发言频率,优先匹配具体聊天,再匹配全局规则 +

+
+ +
+ + {rules && rules.length > 0 ? ( +
+ {rules.map((rule, index) => ( + + ))} +
+ ) : ( +
+

暂无规则,点击"添加规则"按钮创建

+
+ )} + +
+
+ 📝 规则说明 +
+
    +
  • Target 为空:全局规则,对所有聊天生效
  • +
  • Target 指定:仅对特定聊天流生效(格式:platform:id:type)
  • +
  • 优先级:先匹配具体聊天流规则,再匹配全局规则
  • +
  • 时间支持跨夜:例如 23:00-02:00 表示晚上11点到次日凌晨2点
  • +
  • 数值范围:建议 0-1,0 表示完全沉默,1 表示正常发言
  • +
+
+
+ ) +} diff --git a/dashboard/src/routes/config/bot/sections/RulePreview.tsx b/dashboard/src/routes/config/bot/sections/RulePreview.tsx new file mode 100644 index 00000000..2b26c1f6 --- /dev/null +++ b/dashboard/src/routes/config/bot/sections/RulePreview.tsx @@ -0,0 +1,40 @@ + +import { Button } from '@/components/ui/button' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' + +import { Eye } from 'lucide-react' + +interface RulePreviewProps { + rule: { + target: string + time: string + value: number + } +} + +// 预览窗口组件 +export function RulePreview({ rule }: RulePreviewProps) { + const previewText = `{ target = "${rule.target}", time = "${rule.time}", value = ${rule.value.toFixed(1)} }` + + return ( + + + + + +
+

配置预览

+
+ {previewText} +
+

+ 这是保存到 bot_config.toml 文件中的格式 +

+
+
+
+ ) +} diff --git a/dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx b/dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx new file mode 100644 index 00000000..c6c7df12 --- /dev/null +++ b/dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx @@ -0,0 +1,170 @@ +import { useEffect, useMemo, useState } from 'react' + +import { Button } from '@/components/ui/button' +import { Label } from '@/components/ui/label' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' + +import { Clock } from 'lucide-react' + +interface TimeRangePickerProps { + value: string + onChange: (value: string) => void +} + +// 时间选择组件 +export function TimeRangePicker({ value, onChange }: TimeRangePickerProps) { + // 解析初始值 + 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 ( + + + + + +
+
+

开始时间

+
+
+ + +
+
+ + +
+
+
+
+

结束时间

+
+
+ + +
+
+ + +
+
+
+
+
+
+ ) +}