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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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