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:
晴猫
2026-03-15 11:14:56 +09:00
parent da7aafe665
commit a9969ad361
6 changed files with 1335 additions and 477 deletions

View File

@@ -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 V3Qwen
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>
)
}