You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

480 lines
16 KiB

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>
);
}