Files
mai-bot/dashboard/src/routes/setup.tsx
2026-01-13 06:24:35 +08:00

554 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react'
import { useNavigate } from '@tanstack/react-router'
import {
Sparkles,
ArrowRight,
CheckCircle2,
SkipForward,
Bot,
User,
Smile,
Settings,
Key,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { cn } from '@/lib/utils'
import { APP_NAME } from '@/lib/version'
import { useToast } from '@/hooks/use-toast'
import type {
SetupStep,
BotBasicConfig,
PersonalityConfig,
EmojiConfig,
OtherBasicConfig,
SiliconFlowConfig,
} from './setup/types'
import {
BotBasicForm,
PersonalityForm,
EmojiForm,
OtherBasicForm,
SiliconFlowForm,
} from './setup/StepForms'
import {
loadBotBasicConfig,
loadPersonalityConfig,
loadEmojiConfig,
loadOtherBasicConfig,
loadSiliconFlowConfig,
saveBotBasicConfig,
savePersonalityConfig,
saveEmojiConfig,
saveOtherBasicConfig,
saveSiliconFlowConfig,
completeSetup,
} from './setup/api'
import { RestartProvider, useRestart } from '@/lib/restart-context'
import { RestartOverlay } from '@/components/restart-overlay'
// 主导出组件:包装 RestartProvider
export function SetupPage() {
return (
<RestartProvider>
<SetupPageContent />
</RestartProvider>
)
}
// 内部实现组件
function SetupPageContent() {
const navigate = useNavigate()
const { toast } = useToast()
const { triggerRestart } = useRestart()
const [currentStep, setCurrentStep] = useState(0)
const [isCompleting, setIsCompleting] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [isLoading, setIsLoading] = useState(true)
// 步骤1Bot基础信息
const [botBasic, setBotBasic] = useState<BotBasicConfig>({
qq_account: 0,
nickname: '',
alias_names: [],
})
// 步骤2人格配置
const [personality, setPersonality] = useState<PersonalityConfig>({
personality: '是一个女大学生,现在在读大二,会刷贴吧。',
reply_style:
'请回复的平淡一些,简短一些,说中文,不要刻意突出自身学科背景。可以参考贴吧,知乎和微博的回复风格。',
interest:
'对技术相关话题,游戏和动漫相关话题感兴趣,也对日常话题感兴趣,不喜欢太过沉重严肃的话题',
plan_style:
'1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用\n2.如果相同的内容已经被执行,请不要重复执行\n3.请控制你的发言频率,不要太过频繁的发言\n4.如果有人对你感到厌烦,请减少回复\n5.如果有人对你进行攻击,或者情绪激动,请你以合适的方法应对',
private_plan_style:
'1.思考**所有**的可用的action中的**每个动作**是否符合当下条件,如果动作使用条件符合聊天内容就使用\n2.如果相同的内容已经被执行,请不要重复执行\n3.某句话如果已经被回复过,不要重复回复',
})
// 步骤3表情包配置
const [emoji, setEmoji] = useState<EmojiConfig>({
emoji_chance: 0.4,
max_reg_num: 40,
do_replace: true,
check_interval: 10,
steal_emoji: true,
content_filtration: false,
filtration_prompt: '符合公序良俗',
})
// 步骤4其他基础配置
const [otherBasic, setOtherBasic] = useState<OtherBasicConfig>({
enable_tool: true,
all_global: true,
})
// 步骤5硅基流动API配置
const [siliconFlow, setSiliconFlow] = useState<SiliconFlowConfig>({
api_key: '',
})
const steps: SetupStep[] = [
{
id: 'bot-basic',
title: 'Bot基础',
description: '配置机器人的基本信息',
icon: Bot,
},
{
id: 'personality',
title: '人格配置',
description: '定义机器人的性格和说话风格',
icon: User,
},
{
id: 'emoji',
title: '表情包',
description: '配置表情包相关设置',
icon: Smile,
},
{
id: 'other',
title: '其他设置',
description: '工具、情绪系统等配置',
icon: Settings,
},
{
id: 'siliconflow',
title: 'API配置',
description: '配置硅基流动API密钥',
icon: Key,
},
]
const progress = ((currentStep + 1) / steps.length) * 100
// 加载现有配置
useEffect(() => {
const loadConfigs = async () => {
try {
setIsLoading(true)
// 并行加载所有配置
const [bot, personality, emoji, other, silicon] = await Promise.all([
loadBotBasicConfig(),
loadPersonalityConfig(),
loadEmojiConfig(),
loadOtherBasicConfig(),
loadSiliconFlowConfig(),
])
setBotBasic(bot)
setPersonality(personality)
setEmoji(emoji)
setOtherBasic(other)
setSiliconFlow(silicon)
} catch (error) {
toast({
title: '加载配置失败',
description:
error instanceof Error
? error.message
: '无法加载现有配置,将使用默认值',
variant: 'destructive',
})
} finally {
setIsLoading(false)
}
}
loadConfigs()
}, [toast])
// 保存当前步骤配置
const saveCurrentStep = async () => {
setIsSaving(true)
try {
switch (currentStep) {
case 0: // Bot基础
await saveBotBasicConfig(botBasic)
break
case 1: // 人格配置
await savePersonalityConfig(personality)
break
case 2: // 表情包
await saveEmojiConfig(emoji)
break
case 3: // 其他设置
await saveOtherBasicConfig(otherBasic)
break
case 4: // 硅基流动API
await saveSiliconFlowConfig(siliconFlow)
break
}
toast({
title: '保存成功',
description: `${steps[currentStep].title}配置已保存`,
})
return true
} catch (error) {
toast({
title: '保存失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive',
})
return false
} finally {
setIsSaving(false)
}
}
const handleNext = async () => {
// 保存当前步骤
const saved = await saveCurrentStep()
if (!saved) return
// 进入下一步
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1)
}
}
const handlePrevious = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1)
}
}
const handleComplete = async () => {
setIsCompleting(true)
try {
// 1. 保存最后一步的配置(硅基流动API Key)
const saved = await saveCurrentStep()
if (!saved) {
setIsCompleting(false)
return
}
// 2. 标记设置完成
await completeSetup()
toast({
title: '配置完成',
description: '麦麦正在重启以应用新配置...',
})
// 3. 触发麦麦重启(使用新的重启组件)
await triggerRestart()
} catch (error) {
toast({
title: '配置失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive',
})
} finally {
setIsCompleting(false)
}
}
const handleSkip = async () => {
try {
await completeSetup()
navigate({ to: '/' })
} catch (error) {
toast({
title: '跳过失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive',
})
}
}
// 渲染当前步骤的表单
const renderStepForm = () => {
switch (currentStep) {
case 0:
return <BotBasicForm config={botBasic} onChange={setBotBasic} />
case 1:
return (
<PersonalityForm config={personality} onChange={setPersonality} />
)
case 2:
return <EmojiForm config={emoji} onChange={setEmoji} />
case 3:
return <OtherBasicForm config={otherBasic} onChange={setOtherBasic} />
case 4:
return <SiliconFlowForm config={siliconFlow} onChange={setSiliconFlow} />
default:
return null
}
}
return (
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-br from-primary/5 via-background to-secondary/5 p-4 md:p-6">
{/* 重启遮罩层 */}
<RestartOverlay />
{/* 背景装饰 */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute left-1/4 top-1/4 h-64 w-64 md:h-96 md:w-96 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute right-1/4 bottom-1/4 h-64 w-64 md:h-96 md:w-96 rounded-full bg-secondary/5 blur-3xl" />
</div>
{/* 加载状态 */}
{isLoading ? (
<div className="relative z-10 text-center">
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center">
<div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent" />
</div>
<p className="text-lg font-medium">...</p>
<p className="text-sm text-muted-foreground mt-2">
</p>
</div>
) : (
<>
{/* 主要内容 */}
<div className="relative z-10 w-full max-w-4xl">
{/* 头部 */}
<div className="mb-6 md:mb-8 text-center">
<div className="mx-auto mb-4 flex h-12 w-12 md:h-16 md:w-16 items-center justify-center rounded-2xl bg-primary/10">
<Sparkles
className="h-6 w-6 md:h-8 md:w-8 text-primary"
strokeWidth={2}
fill="none"
/>
</div>
<h1 className="mb-2 text-2xl md:text-3xl font-bold">
</h1>
<p className="text-sm md:text-base text-muted-foreground">
{APP_NAME}
</p>
</div>
{/* 进度条 */}
<div className="mb-6 md:mb-8">
<div className="mb-2 flex items-center justify-between text-xs md:text-sm">
<span className="text-muted-foreground">
{currentStep + 1} / {steps.length}
</span>
<span className="font-medium text-primary">
{Math.round(progress)}%
</span>
</div>
<Progress value={progress} className="h-2" />
</div>
{/* 步骤指示器 */}
<div className="mb-6 md:mb-8 flex justify-between">
{steps.map((step, index) => {
const Icon = step.icon
return (
<div
key={step.id}
className={cn(
'flex flex-1 flex-col items-center gap-1 md:gap-2',
index < steps.length - 1 && 'relative'
)}
>
{/* 连接线 */}
{index < steps.length - 1 && (
<div
className={cn(
'absolute left-1/2 top-3 md:top-4 h-0.5 w-full',
index < currentStep ? 'bg-primary' : 'bg-border'
)}
/>
)}
{/* 步骤圆圈 */}
<div
className={cn(
'relative z-10 flex h-6 w-6 md:h-8 md:w-8 items-center justify-center rounded-full border-2 transition-all',
index === currentStep
? 'border-primary bg-primary text-primary-foreground'
: index < currentStep
? 'border-primary bg-primary text-primary-foreground'
: 'border-border bg-background text-muted-foreground'
)}
>
{index < currentStep ? (
<CheckCircle2
className="h-3 w-3 md:h-4 md:w-4"
strokeWidth={2.5}
fill="none"
/>
) : (
<Icon className="h-3 w-3 md:h-4 md:w-4" />
)}
</div>
{/* 步骤标题 */}
<span
className={cn(
'text-[10px] md:text-xs text-center max-w-[60px] md:max-w-none truncate md:whitespace-normal',
index === currentStep
? 'font-medium text-foreground'
: 'text-muted-foreground'
)}
title={step.title}
>
{step.title}
</span>
</div>
)
})}
</div>
{/* 步骤内容卡片 */}
<Card className="mb-6 md:mb-8 shadow-lg">
<CardContent className="p-4 md:p-8">
<div className="min-h-[300px] md:min-h-[400px]">
<div className="mb-4 md:mb-6">
<h2 className="mb-2 text-xl md:text-2xl font-semibold">
{steps[currentStep].title}
</h2>
<p className="text-sm md:text-base text-muted-foreground">
{steps[currentStep].description}
</p>
</div>
{/* 表单内容 */}
<ScrollArea className="h-[400px] md:h-[500px]">
<div className="pr-2">
{renderStepForm()}
</div>
</ScrollArea>
</div>
</CardContent>
</Card>
{/* 操作按钮 */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 sm:gap-0">
<Button
variant="outline"
onClick={handlePrevious}
disabled={currentStep === 0 || isSaving}
className="w-full sm:w-auto order-2 sm:order-1"
>
</Button>
<div className="flex gap-2 w-full sm:w-auto order-1 sm:order-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
className="flex-1 sm:flex-none gap-2"
disabled={isSaving || isCompleting}
>
<SkipForward className="h-4 w-4" strokeWidth={2} fill="none" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleSkip}>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{currentStep === steps.length - 1 ? (
<Button
onClick={handleComplete}
disabled={isCompleting || isSaving}
className="flex-1 sm:flex-none"
>
{isCompleting || isSaving ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{isSaving ? '保存中...' : '完成中...'}
</>
) : (
<>
<CheckCircle2
className="ml-2 h-4 w-4"
strokeWidth={2}
fill="none"
/>
</>
)}
</Button>
) : (
<Button
onClick={handleNext}
disabled={isSaving}
className="flex-1 sm:flex-none"
>
{isSaving ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
...
</>
) : (
<>
<ArrowRight
className="ml-2 h-4 w-4"
strokeWidth={2}
fill="none"
/>
</>
)}
</Button>
)}
</div>
</div>
</div>
{/* 页脚提示 */}
<div className="relative z-10 mt-6 md:mt-8 text-center text-xs text-muted-foreground">
<p></p>
</div>
</>
)}
</div>
)
}