- AI 自动驳回:法规/品牌安全 HIGH 违规或总分<40 自动打回上传阶段 - 功效词可配置:从硬编码改为品牌方在规则页面自行管理 - 驳回通知:AI 驳回时只通知达人,含具体原因 - 达人端:脚本/视频页面展示 AI 驳回原因 + 重新上传入口 - 规则页面:新增"功效词"分类 - 种子数据:新增 6 条默认功效词 - 其他:代理商管理下拉修复、AI 配置模型列表扩展、视觉模型标签修正、规则编辑放开限制 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
561 lines
22 KiB
TypeScript
561 lines
22 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect, useCallback } from 'react'
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'
|
||
import { Button } from '@/components/ui/Button'
|
||
import { useToast } from '@/components/ui/Toast'
|
||
import {
|
||
Bot,
|
||
Eye,
|
||
Mic,
|
||
Settings,
|
||
CheckCircle,
|
||
XCircle,
|
||
Loader2,
|
||
Info,
|
||
Shield,
|
||
AlertTriangle,
|
||
RefreshCw,
|
||
Clock
|
||
} from 'lucide-react'
|
||
import { api } from '@/lib/api'
|
||
import { USE_MOCK } from '@/contexts/AuthContext'
|
||
import type { AIProvider, AIConfigResponse, ConnectionTestResponse, ModelInfo } from '@/types/ai-config'
|
||
|
||
// AI 提供商选项
|
||
const providerOptions: { value: AIProvider | string; label: string }[] = [
|
||
{ value: 'oneapi', label: 'OneAPI 中转服务' },
|
||
{ value: 'anthropic', label: 'Anthropic Claude' },
|
||
{ value: 'openai', label: 'OpenAI' },
|
||
{ value: 'deepseek', label: 'DeepSeek' },
|
||
{ value: 'qwen', label: '通义千问' },
|
||
{ value: 'doubao', label: '豆包' },
|
||
{ value: 'zhipu', label: '智谱' },
|
||
{ value: 'moonshot', label: 'Moonshot' },
|
||
]
|
||
|
||
// 预设可用模型列表
|
||
const mockModels: Record<string, ModelInfo[]> = {
|
||
text: [
|
||
// Anthropic Claude
|
||
{ id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5' },
|
||
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5' },
|
||
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' },
|
||
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' },
|
||
// OpenAI
|
||
{ id: 'gpt-4o', name: 'GPT-4o' },
|
||
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini' },
|
||
{ id: 'gpt-4-turbo', name: 'GPT-4 Turbo' },
|
||
{ id: 'o1', name: 'o1' },
|
||
{ id: 'o3-mini', name: 'o3-mini' },
|
||
// Google
|
||
{ id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash' },
|
||
{ id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro' },
|
||
{ id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro' },
|
||
// DeepSeek
|
||
{ id: 'deepseek-chat', name: 'DeepSeek V3' },
|
||
{ id: 'deepseek-reasoner', name: 'DeepSeek R1' },
|
||
// 通义千问
|
||
{ id: 'qwen-max', name: '通义千问 Max' },
|
||
{ id: 'qwen-plus', name: '通义千问 Plus' },
|
||
{ id: 'qwen-turbo', name: '通义千问 Turbo' },
|
||
// 豆包
|
||
{ id: 'doubao-pro-256k', name: '豆包 Pro 256K' },
|
||
{ id: 'doubao-pro-32k', name: '豆包 Pro 32K' },
|
||
// 智谱
|
||
{ id: 'glm-4-plus', name: 'GLM-4 Plus' },
|
||
{ id: 'glm-4', name: 'GLM-4' },
|
||
// Moonshot
|
||
{ id: 'moonshot-v1-128k', name: 'Moonshot V1 128K' },
|
||
{ id: 'moonshot-v1-32k', name: 'Moonshot V1 32K' },
|
||
],
|
||
vision: [
|
||
// Anthropic Claude (原生多模态)
|
||
{ id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5' },
|
||
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5' },
|
||
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4' },
|
||
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' },
|
||
// OpenAI
|
||
{ id: 'gpt-4o', name: 'GPT-4o' },
|
||
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini' },
|
||
// Google
|
||
{ id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash' },
|
||
{ id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro' },
|
||
{ id: 'gemini-1.5-pro', name: 'Gemini 1.5 Pro' },
|
||
// 通义千问 VL
|
||
{ id: 'qwen-vl-max', name: '通义千问 VL Max' },
|
||
{ id: 'qwen-vl-plus', name: '通义千问 VL Plus' },
|
||
// 智谱
|
||
{ id: 'glm-4v-plus', name: 'GLM-4V Plus' },
|
||
{ id: 'glm-4v', name: 'GLM-4V' },
|
||
],
|
||
audio: [
|
||
{ id: 'whisper-large-v3', name: 'Whisper Large V3' },
|
||
{ id: 'whisper-large-v3-turbo', name: 'Whisper Large V3 Turbo' },
|
||
{ id: 'whisper-medium', name: 'Whisper Medium' },
|
||
{ id: 'sensevoice-v1', name: 'SenseVoice V1 (阿里)' },
|
||
{ id: 'paraformer-realtime-v2', name: 'Paraformer V2 (阿里)' },
|
||
],
|
||
}
|
||
|
||
type TestStatus = 'idle' | 'testing' | 'success' | 'failed'
|
||
|
||
function ConfigSkeleton() {
|
||
return (
|
||
<div className="space-y-6 animate-pulse">
|
||
<div className="h-32 bg-bg-elevated rounded-lg" />
|
||
<div className="h-48 bg-bg-elevated rounded-lg" />
|
||
<div className="h-32 bg-bg-elevated rounded-lg" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function AIConfigPage() {
|
||
const toast = useToast()
|
||
const [loading, setLoading] = useState(true)
|
||
const [saving, setSaving] = useState(false)
|
||
|
||
const [provider, setProvider] = useState<string>('oneapi')
|
||
const [baseUrl, setBaseUrl] = useState('https://oneapi.intelligrow.cn')
|
||
const [apiKey, setApiKey] = useState('')
|
||
const [showApiKey, setShowApiKey] = useState(false)
|
||
const [isConfigured, setIsConfigured] = useState(false)
|
||
|
||
const [llmModel, setLlmModel] = useState('claude-opus-4-5-20251101')
|
||
const [visionModel, setVisionModel] = useState('claude-opus-4-5-20251101')
|
||
const [asrModel, setAsrModel] = useState('whisper-large-v3')
|
||
|
||
const [temperature, setTemperature] = useState(0.7)
|
||
const [maxTokens, setMaxTokens] = useState(2000)
|
||
|
||
const [availableModels, setAvailableModels] = useState<Record<string, ModelInfo[]>>(mockModels)
|
||
const [customLlmModel, setCustomLlmModel] = useState('')
|
||
const [customVisionModel, setCustomVisionModel] = useState('')
|
||
const [customAsrModel, setCustomAsrModel] = useState('')
|
||
|
||
const [testResults, setTestResults] = useState<Record<string, { status: TestStatus; latency?: number; error?: string }>>({
|
||
text: { status: 'idle' },
|
||
vision: { status: 'idle' },
|
||
audio: { status: 'idle' },
|
||
})
|
||
|
||
const loadConfig = useCallback(async () => {
|
||
if (USE_MOCK) {
|
||
setLoading(false)
|
||
return
|
||
}
|
||
try {
|
||
const config = await api.getAIConfig()
|
||
setProvider(config.provider)
|
||
setBaseUrl(config.base_url)
|
||
setApiKey('') // API key is masked, don't fill it
|
||
setIsConfigured(config.is_configured)
|
||
const models = availableModels
|
||
// 如果后端返回的模型不在预设列表中,设为自定义
|
||
if (config.models.text && !(models.text || []).some(m => m.id === config.models.text)) {
|
||
setLlmModel('__custom__')
|
||
setCustomLlmModel(config.models.text)
|
||
} else {
|
||
setLlmModel(config.models.text)
|
||
}
|
||
if (config.models.vision && !(models.vision || []).some(m => m.id === config.models.vision)) {
|
||
setVisionModel('__custom__')
|
||
setCustomVisionModel(config.models.vision)
|
||
} else {
|
||
setVisionModel(config.models.vision)
|
||
}
|
||
if (config.models.audio && !(models.audio || []).some(m => m.id === config.models.audio)) {
|
||
setAsrModel('__custom__')
|
||
setCustomAsrModel(config.models.audio)
|
||
} else {
|
||
setAsrModel(config.models.audio)
|
||
}
|
||
setTemperature(config.parameters.temperature)
|
||
setMaxTokens(config.parameters.max_tokens)
|
||
if (config.available_models && Object.keys(config.available_models).length > 0) {
|
||
setAvailableModels(config.available_models)
|
||
}
|
||
} catch (err: any) {
|
||
// 后端 404 返回 "AI 服务未配置" → Axios 拦截器转为 Error(message)
|
||
// 这是正常的"尚未配置"状态,不弹错误
|
||
const msg = err?.message || ''
|
||
if (msg.includes('未配置')) {
|
||
setIsConfigured(false)
|
||
} else {
|
||
console.error('Failed to load AI config:', err)
|
||
toast.error('加载 AI 配置失败')
|
||
}
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [toast])
|
||
|
||
useEffect(() => { loadConfig() }, [loadConfig])
|
||
|
||
// 获取实际使用的模型 ID(自定义时用输入值)
|
||
const getActualModel = (selected: string, custom: string) =>
|
||
selected === '__custom__' ? custom : selected
|
||
|
||
const handleTestConnection = async () => {
|
||
setTestResults({
|
||
text: { status: 'testing' },
|
||
vision: { status: 'testing' },
|
||
audio: { status: 'testing' },
|
||
})
|
||
|
||
if (USE_MOCK) {
|
||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||
setTestResults(prev => ({ ...prev, text: { status: 'success', latency: 320 } }))
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
setTestResults(prev => ({ ...prev, vision: { status: 'success', latency: 450 } }))
|
||
await new Promise(resolve => setTimeout(resolve, 800))
|
||
setTestResults(prev => ({ ...prev, audio: { status: 'success', latency: 280 } }))
|
||
return
|
||
}
|
||
|
||
try {
|
||
const result: ConnectionTestResponse = await api.testAIConnection({
|
||
provider: provider as AIProvider,
|
||
base_url: baseUrl,
|
||
api_key: apiKey || '***', // use existing key if not changed
|
||
models: { text: getActualModel(llmModel, customLlmModel), vision: getActualModel(visionModel, customVisionModel), audio: getActualModel(asrModel, customAsrModel) },
|
||
})
|
||
const newResults: Record<string, { status: TestStatus; latency?: number; error?: string }> = {}
|
||
for (const [key, r] of Object.entries(result.results)) {
|
||
newResults[key] = {
|
||
status: r.success ? 'success' : 'failed',
|
||
latency: r.latency_ms ?? undefined,
|
||
error: r.error ?? undefined,
|
||
}
|
||
}
|
||
setTestResults(prev => ({ ...prev, ...newResults }))
|
||
if (result.success) {
|
||
toast.success(result.message)
|
||
} else {
|
||
toast.error(result.message)
|
||
}
|
||
} catch (err) {
|
||
toast.error('连接测试失败')
|
||
setTestResults({
|
||
text: { status: 'failed', error: '请求失败' },
|
||
vision: { status: 'failed', error: '请求失败' },
|
||
audio: { status: 'failed', error: '请求失败' },
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleSave = async () => {
|
||
setSaving(true)
|
||
try {
|
||
if (USE_MOCK) {
|
||
await new Promise(resolve => setTimeout(resolve, 500))
|
||
} else {
|
||
await api.updateAIConfig({
|
||
provider: provider as AIProvider,
|
||
base_url: baseUrl,
|
||
api_key: apiKey || '***',
|
||
models: { text: getActualModel(llmModel, customLlmModel), vision: getActualModel(visionModel, customVisionModel), audio: getActualModel(asrModel, customAsrModel) },
|
||
parameters: { temperature, max_tokens: maxTokens },
|
||
})
|
||
}
|
||
toast.success('配置已保存')
|
||
} catch (err) {
|
||
toast.error('保存失败')
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
const getTestStatusIcon = (key: string) => {
|
||
const result = testResults[key]
|
||
if (!result) return null
|
||
switch (result.status) {
|
||
case 'testing':
|
||
return <Loader2 size={16} className="text-blue-500 animate-spin" />
|
||
case 'success':
|
||
return (
|
||
<span className="flex items-center gap-1">
|
||
<CheckCircle size={16} className="text-green-500" />
|
||
{result.latency && <span className="text-xs text-text-tertiary">{result.latency}ms</span>}
|
||
</span>
|
||
)
|
||
case 'failed':
|
||
return (
|
||
<span className="flex items-center gap-1">
|
||
<XCircle size={16} className="text-red-500" />
|
||
{result.error && <span className="text-xs text-accent-coral">{result.error}</span>}
|
||
</span>
|
||
)
|
||
default:
|
||
return null
|
||
}
|
||
}
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="space-y-6 max-w-4xl">
|
||
<h1 className="text-2xl font-bold text-text-primary">AI 服务配置</h1>
|
||
<ConfigSkeleton />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6 max-w-4xl">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-text-primary">AI 服务配置</h1>
|
||
<p className="text-sm text-text-secondary mt-1">配置 AI 服务提供商和模型参数</p>
|
||
</div>
|
||
{isConfigured && (
|
||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-accent-green/15 border border-accent-green/30">
|
||
<CheckCircle size={16} className="text-accent-green" />
|
||
<span className="text-sm font-medium text-accent-green">已配置</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 配置继承提示 */}
|
||
<div className="p-4 bg-accent-indigo/10 rounded-lg border border-accent-indigo/30">
|
||
<div className="flex items-start gap-3">
|
||
<Info size={20} className="text-accent-indigo flex-shrink-0 mt-0.5" />
|
||
<div>
|
||
<p className="text-sm text-accent-indigo font-medium">配置继承说明</p>
|
||
<p className="text-sm text-accent-indigo/80 mt-1">
|
||
品牌方配置后,所属代理商和达人将自动使用此配置。代理商和达人端不可见此配置项。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* AI 提供商 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Bot size={18} className="text-blue-500" />
|
||
AI 提供商
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-text-primary mb-1">提供商选择</label>
|
||
<select
|
||
className="w-full px-3 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||
value={provider}
|
||
onChange={(e) => setProvider(e.target.value)}
|
||
>
|
||
{providerOptions.map(opt => (
|
||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-xs text-text-tertiary mt-1">
|
||
推荐使用 OneAPI 等中转服务商,方便切换不同 AI 模型
|
||
</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 模型配置 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Settings size={18} className="text-purple-500" />
|
||
模型配置
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
{/* 文字处理模型 */}
|
||
<div className="p-4 bg-bg-elevated rounded-lg">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<Bot size={16} className="text-accent-indigo" />
|
||
<span className="font-medium text-text-primary">文字处理模型 (LLM)</span>
|
||
{getTestStatusIcon('text')}
|
||
</div>
|
||
<select
|
||
className="w-full px-3 py-2 border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-indigo bg-bg-card text-text-primary"
|
||
value={llmModel}
|
||
onChange={(e) => { setLlmModel(e.target.value); if (e.target.value !== '__custom__') setCustomLlmModel('') }}
|
||
>
|
||
{(availableModels.text || []).map(model => (
|
||
<option key={model.id} value={model.id}>{model.name} ({model.id})</option>
|
||
))}
|
||
<option value="__custom__">自定义模型...</option>
|
||
</select>
|
||
{llmModel === '__custom__' && (
|
||
<input
|
||
type="text"
|
||
className="w-full mt-2 px-3 py-2 border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-indigo bg-bg-card text-text-primary"
|
||
value={customLlmModel}
|
||
onChange={(e) => setCustomLlmModel(e.target.value)}
|
||
placeholder="输入模型 ID,如 deepseek-chat"
|
||
/>
|
||
)}
|
||
<p className="text-xs text-text-tertiary mt-2">用于 Brief 解析、脚本语义审核、卖点匹配分析</p>
|
||
</div>
|
||
|
||
{/* 视觉理解模型 */}
|
||
<div className="p-4 bg-bg-elevated rounded-lg">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<Eye size={16} className="text-accent-green" />
|
||
<span className="font-medium text-text-primary">视觉理解模型 (Vision)</span>
|
||
{getTestStatusIcon('vision')}
|
||
</div>
|
||
<select
|
||
className="w-full px-3 py-2 border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-indigo bg-bg-card text-text-primary"
|
||
value={visionModel}
|
||
onChange={(e) => { setVisionModel(e.target.value); if (e.target.value !== '__custom__') setCustomVisionModel('') }}
|
||
>
|
||
{(availableModels.vision || []).map(model => (
|
||
<option key={model.id} value={model.id}>{model.name} ({model.id})</option>
|
||
))}
|
||
<option value="__custom__">自定义模型...</option>
|
||
</select>
|
||
{visionModel === '__custom__' && (
|
||
<input
|
||
type="text"
|
||
className="w-full mt-2 px-3 py-2 border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-indigo bg-bg-card text-text-primary"
|
||
value={customVisionModel}
|
||
onChange={(e) => setCustomVisionModel(e.target.value)}
|
||
placeholder="输入模型 ID,如 gpt-4o"
|
||
/>
|
||
)}
|
||
<p className="text-xs text-text-tertiary mt-2">用于脚本文档中的图片审核(竞品 logo、违规画面识别)及视频帧分析</p>
|
||
</div>
|
||
|
||
{/* 音频解析模型 */}
|
||
<div className="p-4 bg-bg-elevated rounded-lg">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<Mic size={16} className="text-orange-400" />
|
||
<span className="font-medium text-text-primary">音频解析模型 (ASR)</span>
|
||
{getTestStatusIcon('audio')}
|
||
</div>
|
||
<select
|
||
className="w-full px-3 py-2 border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-indigo bg-bg-card text-text-primary"
|
||
value={asrModel}
|
||
onChange={(e) => { setAsrModel(e.target.value); if (e.target.value !== '__custom__') setCustomAsrModel('') }}
|
||
>
|
||
{(availableModels.audio || []).map(model => (
|
||
<option key={model.id} value={model.id}>{model.name} ({model.id})</option>
|
||
))}
|
||
<option value="__custom__">自定义模型...</option>
|
||
</select>
|
||
{asrModel === '__custom__' && (
|
||
<input
|
||
type="text"
|
||
className="w-full mt-2 px-3 py-2 border border-border-subtle rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-indigo bg-bg-card text-text-primary"
|
||
value={customAsrModel}
|
||
onChange={(e) => setCustomAsrModel(e.target.value)}
|
||
placeholder="输入模型 ID,如 whisper-large-v3"
|
||
/>
|
||
)}
|
||
<p className="text-xs text-text-tertiary mt-2">用于语音转文字、口播内容提取</p>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 连接配置 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>连接配置</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-text-primary mb-1">Base URL</label>
|
||
<input
|
||
type="text"
|
||
className="w-full px-3 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||
value={baseUrl}
|
||
onChange={(e) => setBaseUrl(e.target.value)}
|
||
placeholder="https://api.openai.com/v1"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-text-primary mb-1">API Key</label>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type={showApiKey ? 'text' : 'password'}
|
||
className="flex-1 px-3 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||
value={apiKey}
|
||
onChange={(e) => setApiKey(e.target.value)}
|
||
placeholder={isConfigured ? '留空使用已保存的密钥' : 'sk-...'}
|
||
/>
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={() => setShowApiKey(!showApiKey)}
|
||
>
|
||
{showApiKey ? '隐藏' : '显示'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 生成参数 */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>生成参数</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
<div>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<label className="text-sm font-medium text-text-primary">Temperature</label>
|
||
<span className="text-sm text-text-secondary">{temperature}</span>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="1"
|
||
step="0.1"
|
||
value={temperature}
|
||
onChange={(e) => setTemperature(parseFloat(e.target.value))}
|
||
className="w-full h-2 bg-bg-elevated rounded-lg appearance-none cursor-pointer accent-accent-indigo"
|
||
/>
|
||
<div className="flex justify-between text-xs text-text-tertiary mt-1">
|
||
<span>精确 (0)</span>
|
||
<span>创意 (1)</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-text-primary mb-2">Max Tokens</label>
|
||
<input
|
||
type="number"
|
||
className="w-32 px-3 py-2 border border-border-subtle rounded-lg bg-bg-elevated text-text-primary focus:outline-none focus:ring-2 focus:ring-accent-indigo"
|
||
value={maxTokens}
|
||
onChange={(e) => setMaxTokens(parseInt(e.target.value))}
|
||
min="100"
|
||
max="8000"
|
||
/>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 安全说明 */}
|
||
<div className="p-4 bg-bg-elevated rounded-lg border border-border-subtle">
|
||
<div className="flex items-start gap-3">
|
||
<Shield size={20} className="text-text-tertiary flex-shrink-0 mt-0.5" />
|
||
<div className="text-sm text-text-secondary">
|
||
<p className="font-medium text-text-primary mb-1">安全说明</p>
|
||
<ul className="space-y-1 text-xs">
|
||
<li>• API Key 使用 AES-256-GCM 加密存储</li>
|
||
<li>• 所有 API 请求强制使用 HTTPS</li>
|
||
<li>• 仅品牌方管理员可查看/修改此配置</li>
|
||
<li>• 配置变更将被记录</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex items-center justify-between pt-4 border-t border-border-subtle">
|
||
<Button variant="secondary" onClick={handleTestConnection}>
|
||
测试连接
|
||
</Button>
|
||
<Button onClick={handleSave} disabled={saving}>
|
||
{saving ? <><Loader2 size={16} className="animate-spin" /> 保存中...</> : '保存配置'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|