refactor: enhance setup page with translation support and default configurations
- Added translation support for various text elements using `useTranslation`. - Created default personality and emoji configurations to streamline setup. - Updated step titles and descriptions to use translated strings. - Improved validation messages to be translatable. - Refactored loading and success/error messages for better user feedback. - Enhanced UI structure for better readability and maintainability.
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
// 设置向导各步骤表单组件
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ExternalLink, Eye, EyeOff, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
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 { Switch } from '@/components/ui/switch'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -15,12 +15,15 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { X, ExternalLink, Eye, EyeOff } from 'lucide-react'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
|
||||
import type {
|
||||
BotBasicConfig,
|
||||
PersonalityConfig,
|
||||
EmojiConfig,
|
||||
OtherBasicConfig,
|
||||
PersonalityConfig,
|
||||
SiliconFlowConfig,
|
||||
} from './types'
|
||||
|
||||
@@ -34,13 +37,7 @@ const KNOWN_PLATFORMS: Record<string, string> = {
|
||||
kook: 'kook',
|
||||
}
|
||||
|
||||
const PLATFORM_OPTIONS = [
|
||||
{ value: 'qq', label: 'QQ' },
|
||||
{ value: 'telegram', label: 'Telegram' },
|
||||
{ value: 'discord', label: 'Discord' },
|
||||
{ value: 'kook', label: 'Kook' },
|
||||
{ value: 'custom', label: '其他平台' },
|
||||
]
|
||||
const PLATFORM_OPTIONS = ['qq', 'telegram', 'discord', 'kook', 'custom'] as const
|
||||
|
||||
function normalizePlatform(raw: string): string {
|
||||
const key = raw.trim().toLowerCase()
|
||||
@@ -56,17 +53,21 @@ function deriveSelectedPlatform(config: BotBasicConfig): { selected: string; cus
|
||||
if (!platform) {
|
||||
return { selected: '', customName: '' }
|
||||
}
|
||||
const known = PLATFORM_OPTIONS.find((opt) => opt.value === platform && opt.value !== 'custom')
|
||||
const known = PLATFORM_OPTIONS.find((value) => value === platform && value !== 'custom')
|
||||
if (known) {
|
||||
return { selected: platform, customName: '' }
|
||||
}
|
||||
return { selected: 'custom', customName: platform }
|
||||
}
|
||||
|
||||
function upsertPlatformAccount(platforms: string[], platformName: string, accountId: string): string[] {
|
||||
function upsertPlatformAccount(
|
||||
platforms: string[],
|
||||
platformName: string,
|
||||
accountId: string
|
||||
): string[] {
|
||||
const normalized = normalizePlatform(platformName)
|
||||
const filtered = platforms.filter((p) => {
|
||||
const prefix = p.split(':')[0]
|
||||
const filtered = platforms.filter((platform) => {
|
||||
const prefix = platform.split(':')[0]
|
||||
return normalizePlatform(prefix) !== normalized
|
||||
})
|
||||
if (accountId.trim()) {
|
||||
@@ -77,8 +78,8 @@ function upsertPlatformAccount(platforms: string[], platformName: string, accoun
|
||||
|
||||
function getPrimaryAccount(platforms: string[], platformName: string): string {
|
||||
const normalized = normalizePlatform(platformName)
|
||||
const entry = platforms.find((p) => {
|
||||
const prefix = p.split(':')[0]
|
||||
const entry = platforms.find((platform) => {
|
||||
const prefix = platform.split(':')[0]
|
||||
return normalizePlatform(prefix) === normalized
|
||||
})
|
||||
return entry ? entry.split(':').slice(1).join(':') : ''
|
||||
@@ -90,58 +91,53 @@ interface BotBasicFormProps {
|
||||
}
|
||||
|
||||
export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const derived = deriveSelectedPlatform(config)
|
||||
const [selectedPlatform, setSelectedPlatform] = useState(derived.selected)
|
||||
const [customPlatformName, setCustomPlatformName] = useState(derived.customName)
|
||||
const [primaryAccount, setPrimaryAccount] = useState(() => {
|
||||
if (derived.selected === 'qq') {
|
||||
return config.qq_account > 0 ? String(config.qq_account) : ''
|
||||
}
|
||||
if (config.platform) {
|
||||
return getPrimaryAccount(config.platforms, config.platform)
|
||||
}
|
||||
return ''
|
||||
})
|
||||
const [selectedPlatformOverride, setSelectedPlatformOverride] = useState<string | null>(null)
|
||||
const [customPlatformNameOverride, setCustomPlatformNameOverride] = useState<string | null>(null)
|
||||
const selectedPlatform = selectedPlatformOverride ?? derived.selected
|
||||
const customPlatformName = customPlatformNameOverride ?? derived.customName
|
||||
const primaryAccount =
|
||||
selectedPlatform === 'qq'
|
||||
? config.qq_account > 0
|
||||
? String(config.qq_account)
|
||||
: ''
|
||||
: config.platform
|
||||
? getPrimaryAccount(config.platforms, config.platform)
|
||||
: ''
|
||||
|
||||
// Re-derive when config loads from API (e.g. after initial fetch)
|
||||
useEffect(() => {
|
||||
const d = deriveSelectedPlatform(config)
|
||||
setSelectedPlatform(d.selected)
|
||||
setCustomPlatformName(d.customName)
|
||||
if (d.selected === 'qq') {
|
||||
setPrimaryAccount(config.qq_account > 0 ? String(config.qq_account) : '')
|
||||
} else if (config.platform) {
|
||||
setPrimaryAccount(getPrimaryAccount(config.platforms, config.platform))
|
||||
}
|
||||
}, [config.platform, config.qq_account, config.platforms])
|
||||
const platformOptions = [
|
||||
{ value: 'qq', label: 'QQ' },
|
||||
{ value: 'telegram', label: 'Telegram' },
|
||||
{ value: 'discord', label: 'Discord' },
|
||||
{ value: 'kook', label: 'Kook' },
|
||||
{ value: 'custom', label: t('setupPage.forms.botBasic.platform.options.custom') },
|
||||
]
|
||||
|
||||
const handlePlatformChange = (value: string) => {
|
||||
setSelectedPlatform(value)
|
||||
setSelectedPlatformOverride(value)
|
||||
const realPlatform = value === 'custom' ? customPlatformName : value
|
||||
setPrimaryAccount('')
|
||||
onChange({
|
||||
...config,
|
||||
platform: normalizePlatform(realPlatform),
|
||||
qq_account: value === 'qq' ? config.qq_account : config.qq_account, // preserve
|
||||
qq_account: value === 'qq' ? config.qq_account : config.qq_account,
|
||||
})
|
||||
}
|
||||
|
||||
const handleCustomNameChange = (name: string) => {
|
||||
setCustomPlatformName(name)
|
||||
setCustomPlatformNameOverride(name)
|
||||
const normalized = normalizePlatform(name)
|
||||
// Move account to new platform name if we had one
|
||||
const newPlatforms = primaryAccount
|
||||
const nextPlatforms = primaryAccount
|
||||
? upsertPlatformAccount(config.platforms, normalized, primaryAccount)
|
||||
: config.platforms
|
||||
onChange({
|
||||
...config,
|
||||
platform: normalized,
|
||||
platforms: newPlatforms,
|
||||
platforms: nextPlatforms,
|
||||
})
|
||||
}
|
||||
|
||||
const handleAccountChange = (accountId: string) => {
|
||||
setPrimaryAccount(accountId)
|
||||
const realPlatform = selectedPlatform === 'custom' ? customPlatformName : selectedPlatform
|
||||
const normalized = normalizePlatform(realPlatform)
|
||||
|
||||
@@ -172,37 +168,39 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
const handleRemoveAlias = (index: number) => {
|
||||
onChange({
|
||||
...config,
|
||||
alias_names: config.alias_names.filter((_, i) => i !== index),
|
||||
alias_names: config.alias_names.filter((_, aliasIndex) => aliasIndex !== index),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="platform">平台 *</Label>
|
||||
<Label htmlFor="platform">{t('setupPage.forms.botBasic.platform.label')}</Label>
|
||||
<Select value={selectedPlatform} onValueChange={handlePlatformChange}>
|
||||
<SelectTrigger id="platform">
|
||||
<SelectValue placeholder="请选择平台" />
|
||||
<SelectValue placeholder={t('setupPage.forms.botBasic.platform.placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PLATFORM_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
{platformOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选择机器人运行的平台
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.platform.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{selectedPlatform === 'custom' && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="custom_platform_name">平台名称 *</Label>
|
||||
<Label htmlFor="custom_platform_name">
|
||||
{t('setupPage.forms.botBasic.customPlatform.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="custom_platform_name"
|
||||
placeholder="请输入平台名称,如 matrix"
|
||||
placeholder={t('setupPage.forms.botBasic.customPlatform.placeholder')}
|
||||
value={customPlatformName}
|
||||
onChange={(e) => handleCustomNameChange(e.target.value)}
|
||||
/>
|
||||
@@ -211,58 +209,63 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
|
||||
{selectedPlatform === 'qq' && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="qq_account">QQ账号 *</Label>
|
||||
<Label htmlFor="qq_account">{t('setupPage.forms.botBasic.qqAccount.label')}</Label>
|
||||
<Input
|
||||
id="qq_account"
|
||||
type="number"
|
||||
placeholder="请输入机器人的QQ账号"
|
||||
placeholder={t('setupPage.forms.botBasic.qqAccount.placeholder')}
|
||||
value={primaryAccount}
|
||||
onChange={(e) => handleAccountChange(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人登录使用的QQ账号
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.qqAccount.description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPlatform && selectedPlatform !== 'qq' && (selectedPlatform !== 'custom' || customPlatformName) && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="primary_account">账号ID *</Label>
|
||||
<Input
|
||||
id="primary_account"
|
||||
placeholder="请输入机器人的账号ID"
|
||||
value={primaryAccount}
|
||||
onChange={(e) => handleAccountChange(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人在该平台上的账号标识
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{selectedPlatform &&
|
||||
selectedPlatform !== 'qq' &&
|
||||
(selectedPlatform !== 'custom' || customPlatformName) && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="primary_account">
|
||||
{t('setupPage.forms.botBasic.primaryAccount.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="primary_account"
|
||||
placeholder={t('setupPage.forms.botBasic.primaryAccount.placeholder')}
|
||||
value={primaryAccount}
|
||||
onChange={(e) => handleAccountChange(e.target.value)}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.primaryAccount.description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="nickname">昵称 *</Label>
|
||||
<Label htmlFor="nickname">{t('setupPage.forms.botBasic.nickname.label')}</Label>
|
||||
<Input
|
||||
id="nickname"
|
||||
placeholder="请输入机器人的昵称"
|
||||
placeholder={t('setupPage.forms.botBasic.nickname.placeholder')}
|
||||
value={config.nickname}
|
||||
onChange={(e) => onChange({ ...config, nickname: e.target.value })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人的主要称呼名称
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.nickname.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>别名</Label>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
<Label>{t('setupPage.forms.botBasic.alias.label')}</Label>
|
||||
<div className="mb-2 flex flex-wrap gap-2">
|
||||
{config.alias_names.map((alias, index) => (
|
||||
<Badge key={index} variant="secondary" className="gap-1">
|
||||
{alias}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveAlias(index)}
|
||||
className="ml-1 hover:text-destructive"
|
||||
className="hover:text-destructive ml-1"
|
||||
aria-label={t('setupPage.forms.botBasic.alias.remove', { alias })}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
@@ -272,7 +275,7 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="alias_input"
|
||||
placeholder="输入别名后按回车添加"
|
||||
placeholder={t('setupPage.forms.botBasic.alias.placeholder')}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAddAlias((e.target as HTMLInputElement).value)
|
||||
@@ -284,20 +287,18 @@ export function BotBasicForm({ config, onChange }: BotBasicFormProps) {
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const input = document.getElementById(
|
||||
'alias_input'
|
||||
) as HTMLInputElement
|
||||
const input = document.getElementById('alias_input') as HTMLInputElement
|
||||
if (input) {
|
||||
handleAddAlias(input.value)
|
||||
input.value = ''
|
||||
}
|
||||
}}
|
||||
>
|
||||
添加
|
||||
{t('setupPage.forms.botBasic.alias.add')}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人的其他称呼,可以添加多个
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.botBasic.alias.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -311,79 +312,81 @@ interface PersonalityFormProps {
|
||||
}
|
||||
|
||||
export function PersonalityForm({ config, onChange }: PersonalityFormProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="personality">人格特征 *</Label>
|
||||
<Label htmlFor="personality">{t('setupPage.forms.personality.personality.label')}</Label>
|
||||
<Textarea
|
||||
id="personality"
|
||||
placeholder="描述机器人的人格特质和身份特征(建议120字以内)"
|
||||
placeholder={t('setupPage.forms.personality.personality.placeholder')}
|
||||
value={config.personality}
|
||||
onChange={(e) => onChange({ ...config, personality: e.target.value })}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
例如:是一个女大学生,现在在读大二,会刷贴吧
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.personality.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="reply_style">表达风格 *</Label>
|
||||
<Label htmlFor="reply_style">{t('setupPage.forms.personality.replyStyle.label')}</Label>
|
||||
<Textarea
|
||||
id="reply_style"
|
||||
placeholder="描述机器人说话的表达风格、表达习惯"
|
||||
placeholder={t('setupPage.forms.personality.replyStyle.placeholder')}
|
||||
value={config.reply_style}
|
||||
onChange={(e) => onChange({ ...config, reply_style: e.target.value })}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
例如:回复平淡一些,简短一些,说中文,参考贴吧、知乎和微博的回复风格
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.replyStyle.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="interest">兴趣 *</Label>
|
||||
<Label htmlFor="interest">{t('setupPage.forms.personality.interest.label')}</Label>
|
||||
<Textarea
|
||||
id="interest"
|
||||
placeholder="描述机器人感兴趣的话题"
|
||||
placeholder={t('setupPage.forms.personality.interest.placeholder')}
|
||||
value={config.interest}
|
||||
onChange={(e) => onChange({ ...config, interest: e.target.value })}
|
||||
rows={2}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
会影响机器人对什么话题进行回复
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.interest.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="plan_style">群聊说话规则 *</Label>
|
||||
<Label htmlFor="plan_style">{t('setupPage.forms.personality.planStyle.label')}</Label>
|
||||
<Textarea
|
||||
id="plan_style"
|
||||
placeholder="机器人在群聊中的行为风格和规则"
|
||||
placeholder={t('setupPage.forms.personality.planStyle.placeholder')}
|
||||
value={config.plan_style}
|
||||
onChange={(e) => onChange({ ...config, plan_style: e.target.value })}
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
定义机器人在群聊中如何行动,例如回复频率、条件等
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.planStyle.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="private_plan_style">私聊说话规则 *</Label>
|
||||
<Label htmlFor="private_plan_style">
|
||||
{t('setupPage.forms.personality.privatePlanStyle.label')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="private_plan_style"
|
||||
placeholder="机器人在私聊中的行为风格和规则"
|
||||
placeholder={t('setupPage.forms.personality.privatePlanStyle.placeholder')}
|
||||
value={config.private_plan_style}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, private_plan_style: e.target.value })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, private_plan_style: e.target.value })}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
定义机器人在私聊中的行为方式
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.personality.privatePlanStyle.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -397,12 +400,14 @@ interface EmojiFormProps {
|
||||
}
|
||||
|
||||
export function EmojiForm({ config, onChange }: EmojiFormProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="emoji_chance">表情包激活概率</Label>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<Label htmlFor="emoji_chance">{t('setupPage.forms.emoji.emojiChance.label')}</Label>
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{(config.emoji_chance * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -413,62 +418,54 @@ export function EmojiForm({ config, onChange }: EmojiFormProps) {
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={config.emoji_chance}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, emoji_chance: Number(e.target.value) })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, emoji_chance: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人发送表情包的概率
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.emojiChance.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="max_reg_num">最大表情包数量</Label>
|
||||
<Label htmlFor="max_reg_num">{t('setupPage.forms.emoji.maxRegNum.label')}</Label>
|
||||
<Input
|
||||
id="max_reg_num"
|
||||
type="number"
|
||||
min="1"
|
||||
max="200"
|
||||
value={config.max_reg_num}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, max_reg_num: Number(e.target.value) })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, max_reg_num: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
机器人最多保存的表情包数量
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.maxRegNum.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="do_replace">达到最大数量时替换</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
开启后会删除旧表情包,关闭则不再收集新表情包
|
||||
<Label htmlFor="do_replace">{t('setupPage.forms.emoji.doReplace.label')}</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.doReplace.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="do_replace"
|
||||
checked={config.do_replace}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, do_replace: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, do_replace: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="check_interval">检查间隔(分钟)</Label>
|
||||
<Label htmlFor="check_interval">{t('setupPage.forms.emoji.checkInterval.label')}</Label>
|
||||
<Input
|
||||
id="check_interval"
|
||||
type="number"
|
||||
min="1"
|
||||
max="120"
|
||||
value={config.check_interval}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, check_interval: Number(e.target.value) })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, check_interval: Number(e.target.value) })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
检查表情包注册、破损、删除的时间间隔
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.checkInterval.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -476,49 +473,47 @@ export function EmojiForm({ config, onChange }: EmojiFormProps) {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="steal_emoji">偷取表情包</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
允许机器人将一些表情包据为己有
|
||||
<Label htmlFor="steal_emoji">{t('setupPage.forms.emoji.stealEmoji.label')}</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.stealEmoji.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="steal_emoji"
|
||||
checked={config.steal_emoji}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, steal_emoji: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, steal_emoji: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="content_filtration">启用表情包过滤</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
只保存符合要求的表情包
|
||||
<Label htmlFor="content_filtration">
|
||||
{t('setupPage.forms.emoji.contentFiltration.label')}
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.contentFiltration.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="content_filtration"
|
||||
checked={config.content_filtration}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, content_filtration: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, content_filtration: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{config.content_filtration && (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="filtration_prompt">过滤要求</Label>
|
||||
<Label htmlFor="filtration_prompt">
|
||||
{t('setupPage.forms.emoji.filtrationPrompt.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="filtration_prompt"
|
||||
placeholder="例如:符合公序良俗"
|
||||
placeholder={t('setupPage.forms.emoji.filtrationPrompt.placeholder')}
|
||||
value={config.filtration_prompt}
|
||||
onChange={(e) =>
|
||||
onChange({ ...config, filtration_prompt: e.target.value })
|
||||
}
|
||||
onChange={(e) => onChange({ ...config, filtration_prompt: e.target.value })}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
描述表情包应该符合的要求
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.emoji.filtrationPrompt.description')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -533,21 +528,21 @@ interface OtherBasicFormProps {
|
||||
}
|
||||
|
||||
export function OtherBasicForm({ config, onChange }: OtherBasicFormProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="enable_tool">启用工具系统</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
允许机器人使用各种工具增强功能
|
||||
<Label htmlFor="enable_tool">{t('setupPage.forms.other.enableTool.label')}</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.other.enableTool.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="enable_tool"
|
||||
checked={config.enable_tool}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, enable_tool: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, enable_tool: checked })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -555,17 +550,15 @@ export function OtherBasicForm({ config, onChange }: OtherBasicFormProps) {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="all_global">启用全局黑话模式</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
允许机器人学习和使用群组黑话
|
||||
<Label htmlFor="all_global">{t('setupPage.forms.other.allGlobal.label')}</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.other.allGlobal.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="all_global"
|
||||
checked={config.all_global}
|
||||
onCheckedChange={(checked) =>
|
||||
onChange({ ...config, all_global: checked })
|
||||
}
|
||||
onCheckedChange={(checked) => onChange({ ...config, all_global: checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -579,32 +572,53 @@ interface SiliconFlowFormProps {
|
||||
}
|
||||
|
||||
export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
|
||||
const { t } = useTranslation()
|
||||
const [showApiKey, setShowApiKey] = useState(false)
|
||||
const apiKeyToggleLabel = showApiKey
|
||||
? t('setupPage.forms.siliconFlow.apiKey.hide')
|
||||
: t('setupPage.forms.siliconFlow.apiKey.show')
|
||||
const autoConfigItems = [
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.deepseek'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.qwen3'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.qwen3Vl'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.senseVoice'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.bgeM3'),
|
||||
t('setupPage.forms.siliconFlow.autoConfig.items.lpmm'),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/30 border border-blue-200 dark:border-blue-800 p-4">
|
||||
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5">
|
||||
<svg className="h-5 w-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<svg
|
||||
className="h-5 w-5 text-blue-600 dark:text-blue-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 text-sm">
|
||||
<p className="font-medium text-blue-900 dark:text-blue-100 mb-1">
|
||||
关于硅基流动 (SiliconFlow)
|
||||
<p className="mb-1 font-medium text-blue-900 dark:text-blue-100">
|
||||
{t('setupPage.forms.siliconFlow.about.title')}
|
||||
</p>
|
||||
<p className="text-blue-700 dark:text-blue-300 mb-2">
|
||||
硅基流动提供了完整的模型覆盖,包括 DeepSeek V3、Qwen、视觉模型、语音识别和嵌入模型。
|
||||
只需一个 API Key 即可使用麦麦的所有功能!
|
||||
<p className="mb-2 text-blue-700 dark:text-blue-300">
|
||||
{t('setupPage.forms.siliconFlow.about.description')}
|
||||
</p>
|
||||
<a
|
||||
href="https://cloud.siliconflow.cn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:underline font-medium"
|
||||
className="inline-flex items-center gap-1 font-medium text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
前往硅基流动获取 API Key
|
||||
{t('setupPage.forms.siliconFlow.about.link')}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -612,7 +626,7 @@ export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="siliconflow_api_key">SiliconFlow API Key *</Label>
|
||||
<Label htmlFor="siliconflow_api_key">{t('setupPage.forms.siliconFlow.apiKey.label')}</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="siliconflow_api_key"
|
||||
@@ -620,46 +634,44 @@ export function SiliconFlowForm({ config, onChange }: SiliconFlowFormProps) {
|
||||
placeholder="sk-..."
|
||||
value={config.api_key}
|
||||
onChange={(e) => onChange({ api_key: e.target.value })}
|
||||
className="font-mono pr-10"
|
||||
className="pr-10 font-mono"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
||||
className="absolute top-0 right-0 h-full px-3 hover:bg-transparent"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
aria-label={apiKeyToggleLabel}
|
||||
title={apiKeyToggleLabel}
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
||||
<EyeOff className="text-muted-foreground h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||
<Eye className="text-muted-foreground h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
请输入您的硅基流动 API 密钥。获取后,麦麦将自动配置所有必需的模型。
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t('setupPage.forms.siliconFlow.apiKey.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-muted/50 p-4 text-sm space-y-2">
|
||||
<p className="font-medium">将自动配置以下模型:</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground ml-2">
|
||||
<li>DeepSeek V3 - 主要对话和工具模型</li>
|
||||
<li>Qwen3 30B - 高频小任务和工具调用</li>
|
||||
<li>Qwen3 VL 30B - 图像识别</li>
|
||||
<li>SenseVoice - 语音识别</li>
|
||||
<li>BGE-M3 - 文本嵌入</li>
|
||||
<li>知识库相关模型 (LPMM)</li>
|
||||
<div className="bg-muted/50 space-y-2 rounded-lg p-4 text-sm">
|
||||
<p className="font-medium">{t('setupPage.forms.siliconFlow.autoConfig.title')}</p>
|
||||
<ul className="text-muted-foreground ml-2 list-inside list-disc space-y-1">
|
||||
{autoConfigItems.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-950/30 p-4">
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950/30">
|
||||
<p className="text-sm text-amber-900 dark:text-amber-100">
|
||||
<span className="font-medium">💡 提示:</span>
|
||||
完成向导后,您可以在"系统设置 → 模型配置"中添加更多 API 提供商和模型。
|
||||
<span className="font-medium">{t('setupPage.forms.siliconFlow.hint.title')}</span>
|
||||
{t('setupPage.forms.siliconFlow.hint.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Sparkles,
|
||||
ArrowRight,
|
||||
CheckCircle2,
|
||||
SkipForward,
|
||||
Bot,
|
||||
User,
|
||||
Smile,
|
||||
Settings,
|
||||
Key,
|
||||
CheckCircle2,
|
||||
Globe,
|
||||
Key,
|
||||
Settings,
|
||||
SkipForward,
|
||||
Smile,
|
||||
Sparkles,
|
||||
User,
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
@@ -69,7 +69,7 @@ import { RestartProvider, useRestart } from '@/lib/restart-context'
|
||||
import { RestartOverlay } from '@/components/restart-overlay'
|
||||
|
||||
const LANGUAGE_CODES = ['zh', 'en', 'ja', 'ko'] as const
|
||||
const LANGUAGE_NAMES: Record<typeof LANGUAGE_CODES[number], string> = {
|
||||
const LANGUAGE_NAMES: Record<(typeof LANGUAGE_CODES)[number], string> = {
|
||||
zh: '中文',
|
||||
en: 'English',
|
||||
ja: '日本語',
|
||||
@@ -88,10 +88,26 @@ export function SetupPage() {
|
||||
// 内部实现组件
|
||||
function SetupPageContent() {
|
||||
const navigate = useNavigate()
|
||||
const { t, i18n: i18nInstance } = useTranslation()
|
||||
const { toast } = useToast()
|
||||
const { triggerRestart } = useRestart()
|
||||
const { i18n: i18nInstance } = useTranslation()
|
||||
const currentLang = i18nInstance.language || 'zh'
|
||||
const currentLang = i18nInstance.resolvedLanguage || i18nInstance.language || 'zh'
|
||||
const createDefaultPersonalityConfig = (): PersonalityConfig => ({
|
||||
personality: t('setupPage.defaults.personality.personality'),
|
||||
reply_style: t('setupPage.defaults.personality.replyStyle'),
|
||||
interest: t('setupPage.defaults.personality.interest'),
|
||||
plan_style: t('setupPage.defaults.personality.planStyle'),
|
||||
private_plan_style: t('setupPage.defaults.personality.privatePlanStyle'),
|
||||
})
|
||||
const createDefaultEmojiConfig = (): EmojiConfig => ({
|
||||
emoji_chance: 0.4,
|
||||
max_reg_num: 40,
|
||||
do_replace: true,
|
||||
check_interval: 10,
|
||||
steal_emoji: true,
|
||||
content_filtration: false,
|
||||
filtration_prompt: t('setupPage.defaults.emoji.filtrationPrompt'),
|
||||
})
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [isCompleting, setIsCompleting] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
@@ -107,28 +123,12 @@ function SetupPageContent() {
|
||||
})
|
||||
|
||||
// 步骤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.某句话如果已经被回复过,不要重复回复',
|
||||
})
|
||||
const [personality, setPersonality] = useState<PersonalityConfig>(() =>
|
||||
createDefaultPersonalityConfig()
|
||||
)
|
||||
|
||||
// 步骤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: '符合公序良俗',
|
||||
})
|
||||
const [emoji, setEmoji] = useState<EmojiConfig>(() => createDefaultEmojiConfig())
|
||||
|
||||
// 步骤4:其他基础配置
|
||||
const [otherBasic, setOtherBasic] = useState<OtherBasicConfig>({
|
||||
@@ -144,32 +144,32 @@ function SetupPageContent() {
|
||||
const steps: SetupStep[] = [
|
||||
{
|
||||
id: 'bot-basic',
|
||||
title: 'Bot基础',
|
||||
description: '配置机器人的基本信息',
|
||||
title: t('setupPage.steps.botBasic.title'),
|
||||
description: t('setupPage.steps.botBasic.description'),
|
||||
icon: Bot,
|
||||
},
|
||||
{
|
||||
id: 'personality',
|
||||
title: '人格配置',
|
||||
description: '定义机器人的性格和说话风格',
|
||||
title: t('setupPage.steps.personality.title'),
|
||||
description: t('setupPage.steps.personality.description'),
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
id: 'emoji',
|
||||
title: '表情包',
|
||||
description: '配置表情包相关设置',
|
||||
title: t('setupPage.steps.emoji.title'),
|
||||
description: t('setupPage.steps.emoji.description'),
|
||||
icon: Smile,
|
||||
},
|
||||
{
|
||||
id: 'other',
|
||||
title: '其他设置',
|
||||
description: '工具、情绪系统等配置',
|
||||
title: t('setupPage.steps.other.title'),
|
||||
description: t('setupPage.steps.other.description'),
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
id: 'siliconflow',
|
||||
title: 'API配置',
|
||||
description: '配置硅基流动API密钥',
|
||||
title: t('setupPage.steps.siliconFlow.title'),
|
||||
description: t('setupPage.steps.siliconFlow.description'),
|
||||
icon: Key,
|
||||
},
|
||||
]
|
||||
@@ -198,11 +198,9 @@ function SetupPageContent() {
|
||||
setSiliconFlow(silicon)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '加载配置失败',
|
||||
title: t('setupPage.toast.loadFailedTitle'),
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: '无法加载现有配置,将使用默认值',
|
||||
error instanceof Error ? error.message : t('setupPage.toast.loadFailedDescription'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -211,7 +209,7 @@ function SetupPageContent() {
|
||||
}
|
||||
|
||||
loadConfigs()
|
||||
}, [toast])
|
||||
}, [t, toast])
|
||||
|
||||
// 保存当前步骤配置
|
||||
const saveCurrentStep = async () => {
|
||||
@@ -236,14 +234,16 @@ function SetupPageContent() {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: '保存成功',
|
||||
description: `${steps[currentStep].title}配置已保存`,
|
||||
title: t('setupPage.toast.saveSuccessTitle'),
|
||||
description: t('setupPage.toast.saveSuccessDescription', {
|
||||
step: steps[currentStep].title,
|
||||
}),
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '保存失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
title: t('setupPage.toast.saveFailedTitle'),
|
||||
description: error instanceof Error ? error.message : t('setupPage.toast.unknownError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
return false
|
||||
@@ -254,15 +254,17 @@ function SetupPageContent() {
|
||||
|
||||
// Step 1 验证
|
||||
function validateBotBasic(config: BotBasicConfig): string | null {
|
||||
if (!config.platform) return '请选择平台'
|
||||
if (!config.nickname.trim()) return '请输入昵称'
|
||||
if (!config.platform) return t('setupPage.validation.selectPlatform')
|
||||
if (!config.nickname.trim()) return t('setupPage.validation.enterNickname')
|
||||
if (config.platform === 'qq') {
|
||||
if (!config.qq_account || config.qq_account <= 0) return '请输入QQ账号'
|
||||
if (!config.qq_account || config.qq_account <= 0) {
|
||||
return t('setupPage.validation.enterQqAccount')
|
||||
}
|
||||
} else {
|
||||
const hasAccount = config.platforms.some(
|
||||
(p) => p.startsWith(config.platform + ':') && p.split(':')[1]?.trim()
|
||||
)
|
||||
if (!hasAccount) return '请输入账号ID'
|
||||
if (!hasAccount) return t('setupPage.validation.enterAccountId')
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -272,7 +274,11 @@ function SetupPageContent() {
|
||||
if (currentStep === 0) {
|
||||
const error = validateBotBasic(botBasic)
|
||||
if (error) {
|
||||
toast({ title: '验证失败', description: error, variant: 'destructive' })
|
||||
toast({
|
||||
title: t('setupPage.toast.validationFailedTitle'),
|
||||
description: error,
|
||||
variant: 'destructive',
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -308,16 +314,18 @@ function SetupPageContent() {
|
||||
await completeSetup()
|
||||
|
||||
toast({
|
||||
title: '配置完成',
|
||||
description: '麦麦正在重启以应用新配置...',
|
||||
title: t('setupPage.toast.completeSuccessTitle'),
|
||||
description: t('setupPage.toast.completeSuccessDescription', {
|
||||
appName: APP_NAME,
|
||||
}),
|
||||
})
|
||||
|
||||
// 3. 触发麦麦重启(使用新的重启组件)
|
||||
await triggerRestart()
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '配置失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
title: t('setupPage.toast.completeFailedTitle'),
|
||||
description: error instanceof Error ? error.message : t('setupPage.toast.unknownError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
} finally {
|
||||
@@ -331,8 +339,8 @@ function SetupPageContent() {
|
||||
navigate({ to: '/' })
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '跳过失败',
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
title: t('setupPage.toast.skipFailedTitle'),
|
||||
description: error instanceof Error ? error.message : t('setupPage.toast.unknownError'),
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
@@ -344,9 +352,7 @@ function SetupPageContent() {
|
||||
case 0:
|
||||
return <BotBasicForm config={botBasic} onChange={setBotBasic} />
|
||||
case 1:
|
||||
return (
|
||||
<PersonalityForm config={personality} onChange={setPersonality} />
|
||||
)
|
||||
return <PersonalityForm config={personality} onChange={setPersonality} />
|
||||
case 2:
|
||||
return <EmojiForm config={emoji} onChange={setEmoji} />
|
||||
case 3:
|
||||
@@ -359,18 +365,19 @@ function SetupPageContent() {
|
||||
}
|
||||
|
||||
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">
|
||||
<div className="from-primary/5 via-background to-secondary/5 relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-br p-4 md:p-6">
|
||||
{/* 重启遮罩层 */}
|
||||
<RestartOverlay />
|
||||
|
||||
{/* 语言切换 */}
|
||||
<div className="absolute right-4 top-4 z-20">
|
||||
<div className="absolute top-4 right-4 z-20">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
<span className="hidden sm:inline text-xs">
|
||||
{LANGUAGE_NAMES[currentLang.split('-')[0] as typeof LANGUAGE_CODES[number]] ?? currentLang}
|
||||
<span className="hidden text-xs sm:inline">
|
||||
{LANGUAGE_NAMES[currentLang.split('-')[0] as (typeof LANGUAGE_CODES)[number]] ??
|
||||
currentLang}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -381,12 +388,10 @@ function SetupPageContent() {
|
||||
onClick={() => i18nInstance.changeLanguage(code)}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
currentLang.split('-')[0] === code && 'font-semibold text-primary'
|
||||
currentLang.split('-')[0] === code && 'text-primary font-semibold'
|
||||
)}
|
||||
>
|
||||
{currentLang.split('-')[0] === code && (
|
||||
<span className="mr-2">✓</span>
|
||||
)}
|
||||
{currentLang.split('-')[0] === code && <span className="mr-2">✓</span>}
|
||||
{LANGUAGE_NAMES[code]}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
@@ -395,232 +400,217 @@ function SetupPageContent() {
|
||||
</div>
|
||||
|
||||
{/* 背景装饰 */}
|
||||
<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 className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div className="bg-primary/5 absolute top-1/4 left-1/4 h-64 w-64 rounded-full blur-3xl md:h-96 md:w-96" />
|
||||
<div className="bg-secondary/5 absolute right-1/4 bottom-1/4 h-64 w-64 rounded-full blur-3xl md:h-96 md:w-96" />
|
||||
</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 className="border-primary h-12 w-12 animate-spin rounded-full border-4 border-t-transparent" />
|
||||
</div>
|
||||
<p className="text-lg font-medium">加载配置中...</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
正在读取现有配置
|
||||
</p>
|
||||
<p className="text-lg font-medium">{t('setupPage.loading.title')}</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm">{t('setupPage.loading.description')}</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 className="mb-6 text-center md:mb-8">
|
||||
<div className="bg-primary/10 mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-2xl md:h-16 md:w-16">
|
||||
<Sparkles
|
||||
className="text-primary h-6 w-6 md:h-8 md:w-8"
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</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>
|
||||
<h1 className="mb-2 text-2xl font-bold md:text-3xl">{t('setupPage.header.title')}</h1>
|
||||
<p className="text-muted-foreground text-sm md:text-base">
|
||||
{t('setupPage.header.description', { appName: APP_NAME })}
|
||||
</p>
|
||||
</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="mb-6 md:mb-8">
|
||||
<div className="mb-2 flex items-center justify-between text-xs md:text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{t('setupPage.progress.stepCounter', {
|
||||
current: currentStep + 1,
|
||||
total: steps.length,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-primary font-medium">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{/* 步骤指示器 */}
|
||||
<div className="mb-6 flex justify-between md:mb-8">
|
||||
{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 top-3 left-1/2 h-0.5 w-full md:top-4',
|
||||
index < currentStep ? 'bg-primary' : 'bg-border'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentStep === steps.length - 1 ? (
|
||||
{/* 步骤圆圈 */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 flex h-6 w-6 items-center justify-center rounded-full border-2 transition-all md:h-8 md:w-8',
|
||||
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(
|
||||
'max-w-[60px] truncate text-center text-[10px] md:max-w-none md:text-xs md:whitespace-normal',
|
||||
index === currentStep
|
||||
? 'text-foreground font-medium'
|
||||
: 'text-muted-foreground'
|
||||
)}
|
||||
title={step.title}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 步骤内容卡片 */}
|
||||
<Card className="mb-6 shadow-lg md:mb-8">
|
||||
<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 font-semibold md:text-2xl">
|
||||
{steps[currentStep].title}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm md:text-base">
|
||||
{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 items-center justify-between gap-3 sm:flex-row sm:gap-0">
|
||||
<Button
|
||||
onClick={handleComplete}
|
||||
disabled={isCompleting || isSaving}
|
||||
className="flex-1 sm:flex-none"
|
||||
variant="outline"
|
||||
onClick={handlePrevious}
|
||||
disabled={currentStep === 0 || isSaving}
|
||||
className="order-2 w-full sm:order-1 sm:w-auto"
|
||||
>
|
||||
{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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{t('setupPage.actions.previous')}
|
||||
</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" />
|
||||
保存中...
|
||||
</>
|
||||
|
||||
<div className="order-1 flex w-full gap-2 sm:order-2 sm:w-auto">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex-1 gap-2 sm:flex-none"
|
||||
disabled={isSaving || isCompleting}
|
||||
>
|
||||
<SkipForward className="h-4 w-4" strokeWidth={2} fill="none" />
|
||||
{t('setupPage.actions.skip')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('setupPage.skipDialog.title')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('setupPage.skipDialog.description')}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleSkip}>
|
||||
{t('setupPage.skipDialog.confirm')}
|
||||
</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
|
||||
? t('setupPage.actions.saving')
|
||||
: t('setupPage.actions.completing')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('setupPage.actions.complete')}
|
||||
<CheckCircle2 className="ml-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
下一步
|
||||
<ArrowRight
|
||||
className="ml-2 h-4 w-4"
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</>
|
||||
<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" />
|
||||
{t('setupPage.actions.saving')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('setupPage.actions.next')}
|
||||
<ArrowRight className="ml-2 h-4 w-4" strokeWidth={2} fill="none" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 页脚提示 */}
|
||||
<div className="relative z-10 mt-6 md:mt-8 text-center text-xs text-muted-foreground">
|
||||
<p>您可以随时在设置中修改这些配置</p>
|
||||
</div>
|
||||
{/* 页脚提示 */}
|
||||
<div className="text-muted-foreground relative z-10 mt-6 text-center text-xs md:mt-8">
|
||||
<p>{t('setupPage.footer')}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user