refactor(config): split ChatSection.tsx into sub-components
- 将 ChatSection.tsx (610行) 拆分为 4 个子组件 - TimeRangePicker.tsx: 时间范围选择器组件 - RulePreview.tsx: 规则预览组件 - RuleEditor.tsx: 规则编辑器组件 - RuleList.tsx: 规则列表组件 - ChatSection.tsx 重构为主容器组件 (197行) - 功能完全不变,构建零错误 - 遵循具名导出规范 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,231 +1,18 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Switch } from '@/components/ui/switch'
|
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 type { ChatConfig } from '../types'
|
||||||
|
|
||||||
|
import { RuleList } from './RuleList'
|
||||||
|
|
||||||
interface ChatSectionProps {
|
interface ChatSectionProps {
|
||||||
config: ChatConfig
|
config: ChatConfig
|
||||||
onChange: (config: ChatConfig) => void
|
onChange: (config: ChatConfig) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// 时间选择组件
|
export function ChatSection({ config, onChange }: ChatSectionProps) {
|
||||||
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 = () => {
|
const addTalkValueRule = () => {
|
||||||
onChange({
|
onChange({
|
||||||
@@ -394,217 +181,13 @@ export const ChatSection = React.memo(function ChatSection({ config, onChange }:
|
|||||||
|
|
||||||
{/* 动态发言频率规则配置 */}
|
{/* 动态发言频率规则配置 */}
|
||||||
{config.enable_talk_value_rules && (
|
{config.enable_talk_value_rules && (
|
||||||
<div className="border-t pt-6">
|
<RuleList
|
||||||
<div className="flex items-center justify-between mb-4">
|
rules={config.talk_value_rules}
|
||||||
<div>
|
onAdd={addTalkValueRule}
|
||||||
<h4 className="text-base font-semibold">动态发言频率规则</h4>
|
onUpdate={updateTalkValueRule}
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
onRemove={removeTalkValueRule}
|
||||||
按时段或聊天流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 表示晚上11点到次日凌晨2点</li>
|
|
||||||
<li>• <strong>数值范围</strong>:建议 0-1,0 表示完全沉默,1 表示正常发言</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
213
dashboard/src/routes/config/bot/sections/RuleEditor.tsx
Normal file
213
dashboard/src/routes/config/bot/sections/RuleEditor.tsx
Normal file
@@ -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 (
|
||||||
|
<div 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={() => onRemove(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') {
|
||||||
|
onUpdate(index, 'target', '')
|
||||||
|
} else {
|
||||||
|
onUpdate(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) => {
|
||||||
|
onUpdate(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) => {
|
||||||
|
onUpdate(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) => {
|
||||||
|
onUpdate(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) => onUpdate(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)) {
|
||||||
|
onUpdate(index, 'value', Math.max(0.01, Math.min(1, val)))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-20 h-8 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
value={[rule.value]}
|
||||||
|
onValueChange={(values) =>
|
||||||
|
onUpdate(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>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
dashboard/src/routes/config/bot/sections/RuleList.tsx
Normal file
70
dashboard/src/routes/config/bot/sections/RuleList.tsx
Normal file
@@ -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 (
|
||||||
|
<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={onAdd} size="sm">
|
||||||
|
<Plus className="h-4 w-4 mr-1" />
|
||||||
|
添加规则
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rules && rules.length > 0 ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{rules.map((rule, index) => (
|
||||||
|
<RuleEditor
|
||||||
|
key={index}
|
||||||
|
rule={rule}
|
||||||
|
index={index}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onRemove={onRemove}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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 表示晚上11点到次日凌晨2点</li>
|
||||||
|
<li>• <strong>数值范围</strong>:建议 0-1,0 表示完全沉默,1 表示正常发言</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
dashboard/src/routes/config/bot/sections/RulePreview.tsx
Normal file
40
dashboard/src/routes/config/bot/sections/RulePreview.tsx
Normal file
@@ -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 (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
170
dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx
Normal file
170
dashboard/src/routes/config/bot/sections/TimeRangePicker.tsx
Normal file
@@ -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 (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user