|
|
import { useState, useEffect } from 'react';
|
|
|
import { motion } from 'framer-motion';
|
|
|
import {
|
|
|
Brain,
|
|
|
Save,
|
|
|
RefreshCw,
|
|
|
CheckCircle,
|
|
|
AlertCircle,
|
|
|
SlidersHorizontal
|
|
|
} from 'lucide-react';
|
|
|
import { adminApi } from '@/services/adminApi';
|
|
|
|
|
|
interface AIModelConfig {
|
|
|
provider: 'openai' | 'anthropic' | 'local' | 'custom';
|
|
|
model: string;
|
|
|
apiKey: string;
|
|
|
apiUrl: string;
|
|
|
temperature: number;
|
|
|
maxTokens: number;
|
|
|
enabled: boolean;
|
|
|
}
|
|
|
|
|
|
interface MomentumConfig {
|
|
|
calculationPeriod: number;
|
|
|
weightPriceChange: number;
|
|
|
weightVolume: number;
|
|
|
weightTechnical: number;
|
|
|
thresholdStrong: number;
|
|
|
thresholdWeak: number;
|
|
|
}
|
|
|
|
|
|
export default function AIConfig() {
|
|
|
const [aiConfig, setAiConfig] = useState<AIModelConfig>({
|
|
|
provider: 'openai',
|
|
|
model: 'gpt-4',
|
|
|
apiKey: '',
|
|
|
apiUrl: 'https://api.openai.com/v1',
|
|
|
temperature: 0.7,
|
|
|
maxTokens: 2000,
|
|
|
enabled: true,
|
|
|
});
|
|
|
|
|
|
const [momentumConfig, setMomentumConfig] = useState<MomentumConfig>({
|
|
|
calculationPeriod: 20,
|
|
|
weightPriceChange: 0.4,
|
|
|
weightVolume: 0.3,
|
|
|
weightTechnical: 0.3,
|
|
|
thresholdStrong: 80,
|
|
|
thresholdWeak: 40,
|
|
|
});
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
|
const [testing, setTesting] = useState(false);
|
|
|
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
|
|
|
|
useEffect(() => {
|
|
|
fetchConfig();
|
|
|
}, []);
|
|
|
|
|
|
const fetchConfig = async () => {
|
|
|
setLoading(true);
|
|
|
try {
|
|
|
const [aiData, momentumData] = await Promise.allSettled([
|
|
|
adminApi.getAIConfig().catch(() => null),
|
|
|
adminApi.getMomentumConfig().catch(() => null),
|
|
|
]);
|
|
|
|
|
|
if (aiData.status === 'fulfilled' && aiData.value) {
|
|
|
setAiConfig(prev => ({ ...prev, ...aiData.value }));
|
|
|
}
|
|
|
if (momentumData.status === 'fulfilled' && momentumData.value) {
|
|
|
setMomentumConfig(momentumData.value);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Failed to fetch config:', error);
|
|
|
} finally {
|
|
|
setLoading(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
setSaving(true);
|
|
|
setSaveStatus('idle');
|
|
|
|
|
|
try {
|
|
|
await Promise.all([
|
|
|
adminApi.updateAIConfig({
|
|
|
provider: aiConfig.provider,
|
|
|
model: aiConfig.model,
|
|
|
apiKey: aiConfig.apiKey || undefined,
|
|
|
apiUrl: aiConfig.apiUrl,
|
|
|
temperature: aiConfig.temperature,
|
|
|
maxTokens: aiConfig.maxTokens,
|
|
|
enabled: aiConfig.enabled,
|
|
|
}),
|
|
|
adminApi.updateMomentumConfig(momentumConfig),
|
|
|
]);
|
|
|
|
|
|
setSaveStatus('success');
|
|
|
setTimeout(() => setSaveStatus('idle'), 3000);
|
|
|
} catch (error: any) {
|
|
|
setSaveStatus('error');
|
|
|
} finally {
|
|
|
setSaving(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleTestConnection = async () => {
|
|
|
setTesting(true);
|
|
|
setTestResult(null);
|
|
|
|
|
|
try {
|
|
|
const result = await adminApi.testAIConnection();
|
|
|
setTestResult(result);
|
|
|
} catch (error: any) {
|
|
|
setTestResult({
|
|
|
success: false,
|
|
|
message: error.message || '连接测试失败'
|
|
|
});
|
|
|
} finally {
|
|
|
setTesting(false);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const modelOptions = {
|
|
|
openai: ['gpt-4', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
|
anthropic: ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku'],
|
|
|
local: ['llama2-7b', 'llama2-13b', 'chatglm3-6b'],
|
|
|
custom: ['custom-model'],
|
|
|
};
|
|
|
|
|
|
if (loading) {
|
|
|
return (
|
|
|
<div className="flex items-center justify-center h-64">
|
|
|
<RefreshCw className="w-8 h-8 text-[#ff6b35] animate-spin" />
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
<div className="space-y-6">
|
|
|
{/* Header */}
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
|
<div>
|
|
|
<h1 className="text-2xl font-bold text-white">AI模型配置</h1>
|
|
|
<p className="text-[#b0b0b0] mt-1">配置AI分析模型和动量计算参数</p>
|
|
|
</div>
|
|
|
<div className="flex gap-3">
|
|
|
<button
|
|
|
onClick={handleTestConnection}
|
|
|
disabled={testing}
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#2a2a2a] text-white rounded-lg hover:bg-[#333] transition-colors disabled:opacity-50"
|
|
|
>
|
|
|
{testing ? (
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
) : (
|
|
|
<CheckCircle className="w-4 h-4" />
|
|
|
)}
|
|
|
测试连接
|
|
|
</button>
|
|
|
<button
|
|
|
onClick={handleSave}
|
|
|
disabled={saving}
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#ff6b35] text-white rounded-lg hover:bg-[#ff6b35]/90 transition-colors disabled:opacity-50"
|
|
|
>
|
|
|
{saving ? (
|
|
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
|
|
) : (
|
|
|
<Save className="w-4 h-4" />
|
|
|
)}
|
|
|
保存配置
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* Test Result */}
|
|
|
{testResult && (
|
|
|
<motion.div
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
className={`p-4 rounded-lg flex items-center gap-3 ${
|
|
|
testResult.success
|
|
|
? 'bg-green-500/10 border border-green-500/30'
|
|
|
: 'bg-red-500/10 border border-red-500/30'
|
|
|
}`}
|
|
|
>
|
|
|
{testResult.success ? (
|
|
|
<CheckCircle className="w-5 h-5 text-green-400" />
|
|
|
) : (
|
|
|
<AlertCircle className="w-5 h-5 text-red-400" />
|
|
|
)}
|
|
|
<span className={testResult.success ? 'text-green-400' : 'text-red-400'}>
|
|
|
{testResult.message}
|
|
|
</span>
|
|
|
</motion.div>
|
|
|
)}
|
|
|
|
|
|
{/* Save Status */}
|
|
|
{saveStatus !== 'idle' && (
|
|
|
<motion.div
|
|
|
initial={{ opacity: 0, y: -10 }}
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
className={`p-4 rounded-lg flex items-center gap-3 ${
|
|
|
saveStatus === 'success'
|
|
|
? 'bg-green-500/10 border border-green-500/30'
|
|
|
: 'bg-red-500/10 border border-red-500/30'
|
|
|
}`}
|
|
|
>
|
|
|
{saveStatus === 'success' ? (
|
|
|
<>
|
|
|
<CheckCircle className="w-5 h-5 text-green-400" />
|
|
|
<span className="text-green-400">配置保存成功!</span>
|
|
|
</>
|
|
|
) : (
|
|
|
<>
|
|
|
<AlertCircle className="w-5 h-5 text-red-400" />
|
|
|
<span className="text-red-400">保存失败,请重试。</span>
|
|
|
</>
|
|
|
)}
|
|
|
</motion.div>
|
|
|
)}
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
|
{/* AI Provider Config */}
|
|
|
<motion.div
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
|
|
>
|
|
|
<div className="flex items-center gap-3 mb-6">
|
|
|
<Brain className="w-5 h-5 text-[#ff6b35]" />
|
|
|
<h2 className="text-white font-semibold">AI 服务提供商</h2>
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
{/* Enable/Disable */}
|
|
|
<div className="flex items-center justify-between p-3 bg-[#0a0a0a] rounded-lg">
|
|
|
<span className="text-white">启用 AI 分析</span>
|
|
|
<button
|
|
|
onClick={() => setAiConfig(prev => ({ ...prev, enabled: !prev.enabled }))}
|
|
|
className={`w-12 h-6 rounded-full transition-colors relative ${
|
|
|
aiConfig.enabled ? 'bg-[#ff6b35]' : 'bg-[#2a2a2a]'
|
|
|
}`}
|
|
|
>
|
|
|
<span className={`absolute top-1 w-4 h-4 rounded-full bg-white transition-all ${
|
|
|
aiConfig.enabled ? 'left-7' : 'left-1'
|
|
|
}`} />
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
{/* Provider */}
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">服务提供商</label>
|
|
|
<select
|
|
|
value={aiConfig.provider}
|
|
|
onChange={(e) => setAiConfig(prev => ({
|
|
|
...prev,
|
|
|
provider: e.target.value as AIModelConfig['provider'],
|
|
|
model: modelOptions[e.target.value as keyof typeof modelOptions][0]
|
|
|
}))}
|
|
|
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2.5 text-white outline-none focus:border-[#ff6b35]"
|
|
|
>
|
|
|
<option value="openai">OpenAI</option>
|
|
|
<option value="anthropic">Anthropic Claude</option>
|
|
|
<option value="local">本地模型</option>
|
|
|
<option value="custom">自定义</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
|
|
|
{/* Model */}
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">模型</label>
|
|
|
<select
|
|
|
value={aiConfig.model}
|
|
|
onChange={(e) => setAiConfig(prev => ({ ...prev, model: e.target.value }))}
|
|
|
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2.5 text-white outline-none focus:border-[#ff6b35]"
|
|
|
>
|
|
|
{modelOptions[aiConfig.provider].map(model => (
|
|
|
<option key={model} value={model}>{model}</option>
|
|
|
))}
|
|
|
</select>
|
|
|
</div>
|
|
|
|
|
|
{/* API Key */}
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">API Key</label>
|
|
|
<input
|
|
|
type="password"
|
|
|
value={aiConfig.apiKey}
|
|
|
onChange={(e) => setAiConfig(prev => ({ ...prev, apiKey: e.target.value }))}
|
|
|
placeholder="输入 API Key"
|
|
|
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2.5 text-white placeholder-[#666] outline-none focus:border-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
{/* API URL */}
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">API URL</label>
|
|
|
<input
|
|
|
type="text"
|
|
|
value={aiConfig.apiUrl}
|
|
|
onChange={(e) => setAiConfig(prev => ({ ...prev, apiUrl: e.target.value }))}
|
|
|
placeholder="https://api.example.com/v1"
|
|
|
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2.5 text-white placeholder-[#666] outline-none focus:border-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
{/* Temperature */}
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">
|
|
|
Temperature: {aiConfig.temperature}
|
|
|
</label>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="0"
|
|
|
max="2"
|
|
|
step="0.1"
|
|
|
value={aiConfig.temperature}
|
|
|
onChange={(e) => setAiConfig(prev => ({ ...prev, temperature: parseFloat(e.target.value) }))}
|
|
|
className="w-full accent-[#ff6b35]"
|
|
|
/>
|
|
|
<div className="flex justify-between text-xs text-[#666] mt-1">
|
|
|
<span>精确</span>
|
|
|
<span>平衡</span>
|
|
|
<span>创意</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* Max Tokens */}
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">Max Tokens</label>
|
|
|
<input
|
|
|
type="number"
|
|
|
value={aiConfig.maxTokens}
|
|
|
onChange={(e) => setAiConfig(prev => ({ ...prev, maxTokens: parseInt(e.target.value) }))}
|
|
|
min="100"
|
|
|
max="8000"
|
|
|
className="w-full bg-[#0a0a0a] border border-[#2a2a2a] rounded-lg px-4 py-2.5 text-white outline-none focus:border-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
|
|
|
{/* Momentum Calculation Config */}
|
|
|
<motion.div
|
|
|
initial={{ opacity: 0, y: 20 }}
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
transition={{ delay: 0.1 }}
|
|
|
className="bg-[#111111] border border-[#2a2a2a] rounded-xl p-5"
|
|
|
>
|
|
|
<div className="flex items-center gap-3 mb-6">
|
|
|
<SlidersHorizontal className="w-5 h-5 text-[#ff6b35]" />
|
|
|
<h2 className="text-white font-semibold">动量计算参数</h2>
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
{/* Calculation Period */}
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">
|
|
|
计算周期: {momentumConfig.calculationPeriod} 天
|
|
|
</label>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="5"
|
|
|
max="60"
|
|
|
value={momentumConfig.calculationPeriod}
|
|
|
onChange={(e) => setMomentumConfig(prev => ({
|
|
|
...prev,
|
|
|
calculationPeriod: parseInt(e.target.value)
|
|
|
}))}
|
|
|
className="w-full accent-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
{/* Weight Settings */}
|
|
|
<div className="space-y-4 pt-4 border-t border-[#2a2a2a]">
|
|
|
<h3 className="text-white font-medium">权重配置</h3>
|
|
|
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">
|
|
|
价格变化权重: {(momentumConfig.weightPriceChange * 100).toFixed(0)}%
|
|
|
</label>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="0"
|
|
|
max="1"
|
|
|
step="0.05"
|
|
|
value={momentumConfig.weightPriceChange}
|
|
|
onChange={(e) => setMomentumConfig(prev => ({
|
|
|
...prev,
|
|
|
weightPriceChange: parseFloat(e.target.value)
|
|
|
}))}
|
|
|
className="w-full accent-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">
|
|
|
成交量权重: {(momentumConfig.weightVolume * 100).toFixed(0)}%
|
|
|
</label>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="0"
|
|
|
max="1"
|
|
|
step="0.05"
|
|
|
value={momentumConfig.weightVolume}
|
|
|
onChange={(e) => setMomentumConfig(prev => ({
|
|
|
...prev,
|
|
|
weightVolume: parseFloat(e.target.value)
|
|
|
}))}
|
|
|
className="w-full accent-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">
|
|
|
技术指标权重: {(momentumConfig.weightTechnical * 100).toFixed(0)}%
|
|
|
</label>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="0"
|
|
|
max="1"
|
|
|
step="0.05"
|
|
|
value={momentumConfig.weightTechnical}
|
|
|
onChange={(e) => setMomentumConfig(prev => ({
|
|
|
...prev,
|
|
|
weightTechnical: parseFloat(e.target.value)
|
|
|
}))}
|
|
|
className="w-full accent-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* Threshold Settings */}
|
|
|
<div className="space-y-4 pt-4 border-t border-[#2a2a2a]">
|
|
|
<h3 className="text-white font-medium">阈值配置</h3>
|
|
|
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">
|
|
|
强势阈值: {momentumConfig.thresholdStrong}
|
|
|
</label>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="60"
|
|
|
max="95"
|
|
|
value={momentumConfig.thresholdStrong}
|
|
|
onChange={(e) => setMomentumConfig(prev => ({
|
|
|
...prev,
|
|
|
thresholdStrong: parseInt(e.target.value)
|
|
|
}))}
|
|
|
className="w-full accent-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
<label className="block text-sm text-[#b0b0b0] mb-2">
|
|
|
弱势阈值: {momentumConfig.thresholdWeak}
|
|
|
</label>
|
|
|
<input
|
|
|
type="range"
|
|
|
min="20"
|
|
|
max="50"
|
|
|
value={momentumConfig.thresholdWeak}
|
|
|
onChange={(e) => setMomentumConfig(prev => ({
|
|
|
...prev,
|
|
|
thresholdWeak: parseInt(e.target.value)
|
|
|
}))}
|
|
|
className="w-full accent-[#ff6b35]"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</motion.div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|