import { useState, useEffect, useCallback } from 'react' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../contexts/LanguageContext' import { Plus, Copy, Trash2, Check, ChevronDown, ChevronRight, Settings, BarChart3, Target, Shield, Zap, Activity, Save, Sparkles, Eye, Play, FileText, Loader2, RefreshCw, Clock, Bot, Terminal, Code, Send, } from 'lucide-react' import type { Strategy, StrategyConfig, AIModel } from '../types' import { confirmToast, notify } from '../lib/notify' import { CoinSourceEditor } from '../components/strategy/CoinSourceEditor' import { IndicatorEditor } from '../components/strategy/IndicatorEditor' import { RiskControlEditor } from '../components/strategy/RiskControlEditor' import { PromptSectionsEditor } from '../components/strategy/PromptSectionsEditor' const API_BASE = import.meta.env.VITE_API_BASE || '' export function StrategyStudioPage() { const { token } = useAuth() const { language } = useLanguage() const [strategies, setStrategies] = useState([]) const [selectedStrategy, setSelectedStrategy] = useState(null) const [editingConfig, setEditingConfig] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isSaving, setIsSaving] = useState(false) const [error, setError] = useState(null) const [hasChanges, setHasChanges] = useState(false) // AI Models for test run const [aiModels, setAiModels] = useState([]) const [selectedModelId, setSelectedModelId] = useState('') // Accordion states for left panel const [expandedSections, setExpandedSections] = useState({ coinSource: true, indicators: false, riskControl: false, promptSections: false, customPrompt: false, }) // Right panel states const [activeRightTab, setActiveRightTab] = useState<'prompt' | 'test'>('prompt') const [promptPreview, setPromptPreview] = useState<{ system_prompt: string user_prompt?: string prompt_variant: string config_summary: Record } | null>(null) const [isLoadingPrompt, setIsLoadingPrompt] = useState(false) const [selectedVariant, setSelectedVariant] = useState('balanced') // AI Test Run states const [aiTestResult, setAiTestResult] = useState<{ system_prompt?: string user_prompt?: string ai_response?: string reasoning?: string decisions?: unknown[] error?: string duration_ms?: number } | null>(null) const [isRunningAiTest, setIsRunningAiTest] = useState(false) const toggleSection = (section: keyof typeof expandedSections) => { setExpandedSections((prev) => ({ ...prev, [section]: !prev[section], })) } // Fetch AI Models const fetchAiModels = useCallback(async () => { if (!token) return try { const response = await fetch(`${API_BASE}/api/models`, { headers: { Authorization: `Bearer ${token}` }, }) if (response.ok) { const data = await response.json() // 后端返回的是数组,不是 { models: [] } const allModels = Array.isArray(data) ? data : (data.models || []) const enabledModels = allModels.filter((m: AIModel) => m.enabled) setAiModels(enabledModels) if (enabledModels.length > 0 && !selectedModelId) { setSelectedModelId(enabledModels[0].id) } } } catch (err) { console.error('Failed to fetch AI models:', err) } }, [token, selectedModelId]) // Fetch strategies const fetchStrategies = useCallback(async () => { if (!token) return try { const response = await fetch(`${API_BASE}/api/strategies`, { headers: { Authorization: `Bearer ${token}` }, }) if (!response.ok) throw new Error('Failed to fetch strategies') const data = await response.json() setStrategies(data.strategies || []) // Select active or first strategy const active = data.strategies?.find((s: Strategy) => s.is_active) if (active) { setSelectedStrategy(active) setEditingConfig(active.config) } else if (data.strategies?.length > 0) { setSelectedStrategy(data.strategies[0]) setEditingConfig(data.strategies[0].config) } } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } finally { setIsLoading(false) } }, [token]) useEffect(() => { fetchStrategies() fetchAiModels() }, [fetchStrategies, fetchAiModels]) // Create new strategy const handleCreateStrategy = async () => { if (!token) return try { const configResponse = await fetch( `${API_BASE}/api/strategies/default-config?lang=${language}`, { headers: { Authorization: `Bearer ${token}` } } ) const defaultConfig = await configResponse.json() const response = await fetch(`${API_BASE}/api/strategies`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ name: language === 'zh' ? '新策略' : 'New Strategy', description: '', config: defaultConfig, }), }) if (!response.ok) throw new Error('Failed to create strategy') const result = await response.json() await fetchStrategies() // Auto-select the newly created strategy if (result.id) { const now = new Date().toISOString() const newStrategy = { id: result.id, name: language === 'zh' ? '新策略' : 'New Strategy', description: '', is_active: false, is_default: false, config: defaultConfig, created_at: now, updated_at: now, } setSelectedStrategy(newStrategy) setEditingConfig(defaultConfig) setHasChanges(false) } } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } } // Delete strategy const handleDeleteStrategy = async (id: string) => { if (!token) return const confirmed = await confirmToast( language === 'zh' ? '确定删除此策略?' : 'Delete this strategy?', { title: language === 'zh' ? '确认删除' : 'Confirm Delete', okText: language === 'zh' ? '删除' : 'Delete', cancelText: language === 'zh' ? '取消' : 'Cancel', } ) if (!confirmed) return try { const response = await fetch(`${API_BASE}/api/strategies/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}` }, }) if (!response.ok) throw new Error('Failed to delete strategy') notify.success(language === 'zh' ? '策略已删除' : 'Strategy deleted') // Clear selection if deleted strategy was selected if (selectedStrategy?.id === id) { setSelectedStrategy(null) setEditingConfig(null) setHasChanges(false) } await fetchStrategies() } catch (err) { const errorMsg = err instanceof Error ? err.message : 'Unknown error' setError(errorMsg) notify.error(errorMsg) } } // Duplicate strategy const handleDuplicateStrategy = async (id: string) => { if (!token) return try { const response = await fetch(`${API_BASE}/api/strategies/${id}/duplicate`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ name: language === 'zh' ? '策略副本' : 'Strategy Copy', }), }) if (!response.ok) throw new Error('Failed to duplicate strategy') await fetchStrategies() } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } } // Activate strategy const handleActivateStrategy = async (id: string) => { if (!token) return try { const response = await fetch(`${API_BASE}/api/strategies/${id}/activate`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }) if (!response.ok) throw new Error('Failed to activate strategy') await fetchStrategies() } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } } // Save strategy const handleSaveStrategy = async () => { if (!token || !selectedStrategy || !editingConfig) return setIsSaving(true) try { const response = await fetch( `${API_BASE}/api/strategies/${selectedStrategy.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ name: selectedStrategy.name, description: selectedStrategy.description, config: editingConfig, }), } ) if (!response.ok) throw new Error('Failed to save strategy') setHasChanges(false) await fetchStrategies() } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } finally { setIsSaving(false) } } // Update config section const updateConfig = ( section: K, value: StrategyConfig[K] ) => { if (!editingConfig) return setEditingConfig({ ...editingConfig, [section]: value, }) setHasChanges(true) } // Fetch prompt preview const fetchPromptPreview = async () => { if (!token || !editingConfig) return setIsLoadingPrompt(true) try { const response = await fetch(`${API_BASE}/api/strategies/preview-prompt`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ config: editingConfig, account_equity: 1000, prompt_variant: selectedVariant, }), }) if (!response.ok) throw new Error('Failed to fetch prompt preview') const data = await response.json() setPromptPreview(data) } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } finally { setIsLoadingPrompt(false) } } // Run AI test with real AI model const runAiTest = async () => { if (!token || !editingConfig || !selectedModelId) return setIsRunningAiTest(true) setAiTestResult(null) try { const response = await fetch(`${API_BASE}/api/strategies/test-run`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ config: editingConfig, prompt_variant: selectedVariant, ai_model_id: selectedModelId, run_real_ai: true, }), }) if (!response.ok) throw new Error('Failed to run AI test') const data = await response.json() setAiTestResult(data) } catch (err) { setAiTestResult({ error: err instanceof Error ? err.message : 'Unknown error', }) } finally { setIsRunningAiTest(false) } } const t = (key: string) => { const translations: Record> = { strategyStudio: { zh: '策略工作室', en: 'Strategy Studio' }, subtitle: { zh: '可视化配置和测试交易策略', en: 'Configure and test trading strategies' }, strategies: { zh: '策略', en: 'Strategies' }, newStrategy: { zh: '新建', en: 'New' }, coinSource: { zh: '币种来源', en: 'Coin Source' }, indicators: { zh: '技术指标', en: 'Indicators' }, riskControl: { zh: '风控参数', en: 'Risk Control' }, promptSections: { zh: 'Prompt 编辑', en: 'Prompt Editor' }, customPrompt: { zh: '附加提示', en: 'Extra Prompt' }, save: { zh: '保存', en: 'Save' }, saving: { zh: '保存中...', en: 'Saving...' }, activate: { zh: '激活', en: 'Activate' }, active: { zh: '激活中', en: 'Active' }, default: { zh: '默认', en: 'Default' }, promptPreview: { zh: 'Prompt 预览', en: 'Prompt Preview' }, aiTestRun: { zh: 'AI 测试', en: 'AI Test' }, systemPrompt: { zh: 'System Prompt', en: 'System Prompt' }, userPrompt: { zh: 'User Prompt', en: 'User Prompt' }, loadPrompt: { zh: '生成 Prompt', en: 'Generate Prompt' }, refreshPrompt: { zh: '刷新', en: 'Refresh' }, promptVariant: { zh: '风格', en: 'Style' }, balanced: { zh: '平衡', en: 'Balanced' }, aggressive: { zh: '激进', en: 'Aggressive' }, conservative: { zh: '保守', en: 'Conservative' }, selectModel: { zh: '选择 AI 模型', en: 'Select AI Model' }, runTest: { zh: '运行 AI 测试', en: 'Run AI Test' }, running: { zh: '运行中...', en: 'Running...' }, aiOutput: { zh: 'AI 输出', en: 'AI Output' }, reasoning: { zh: '思维链', en: 'Reasoning' }, decisions: { zh: '决策', en: 'Decisions' }, duration: { zh: '耗时', en: 'Duration' }, noModel: { zh: '请先配置 AI 模型', en: 'Please configure AI model first' }, testNote: { zh: '使用真实 AI 模型测试,不执行交易', en: 'Test with real AI, no trading' }, } return translations[key]?.[language] || key } if (isLoading) { return (
) } const configSections = [ { key: 'coinSource' as const, icon: Target, color: '#F0B90B', title: t('coinSource'), content: editingConfig && ( updateConfig('coin_source', coinSource)} disabled={selectedStrategy?.is_default} language={language} /> ), }, { key: 'indicators' as const, icon: BarChart3, color: '#0ECB81', title: t('indicators'), content: editingConfig && ( updateConfig('indicators', indicators)} disabled={selectedStrategy?.is_default} language={language} /> ), }, { key: 'riskControl' as const, icon: Shield, color: '#F6465D', title: t('riskControl'), content: editingConfig && ( updateConfig('risk_control', riskControl)} disabled={selectedStrategy?.is_default} language={language} /> ), }, { key: 'promptSections' as const, icon: FileText, color: '#a855f7', title: t('promptSections'), content: editingConfig && ( updateConfig('prompt_sections', promptSections)} disabled={selectedStrategy?.is_default} language={language} /> ), }, { key: 'customPrompt' as const, icon: Settings, color: '#60a5fa', title: t('customPrompt'), content: editingConfig && (

{language === 'zh' ? '附加在 System Prompt 末尾的额外提示,用于补充个性化交易风格' : 'Extra prompt appended to System Prompt for personalized trading style'}