diff --git a/src/modules/assistant/components/AnalysisRequestForm.tsx b/src/modules/assistant/components/AnalysisRequestForm.tsx new file mode 100644 index 0000000..66634cc --- /dev/null +++ b/src/modules/assistant/components/AnalysisRequestForm.tsx @@ -0,0 +1,495 @@ +/** + * AnalysisRequestForm Component + * Structured request builder for complex LLM analysis tasks + * OQI-007: LLM Strategy Agent + */ + +import React, { useState, useMemo } from 'react'; +import { + ChartBarIcon, + CalendarIcon, + AdjustmentsHorizontalIcon, + BeakerIcon, + DocumentTextIcon, + ChevronDownIcon, + ChevronUpIcon, + PaperAirplaneIcon, + BookmarkIcon, + XMarkIcon, +} from '@heroicons/react/24/outline'; +import { SparklesIcon } from '@heroicons/react/24/solid'; + +// ============================================================================ +// Types +// ============================================================================ + +export interface AnalysisRequest { + symbol: string; + timeframes: string[]; + dateRange: { start: string; end: string }; + indicators: string[]; + strategyType: string; + riskParams: { + maxDrawdown: number; + riskPerTrade: number; + targetRR: number; + }; + includeContext: { + recentSignals: boolean; + portfolioContext: boolean; + marketRegime: boolean; + }; + notes?: string; +} + +export interface AnalysisTemplate { + id: string; + name: string; + description: string; + config: Partial; +} + +interface AnalysisRequestFormProps { + onSubmit: (request: AnalysisRequest) => void; + onSaveTemplate?: (template: Omit) => void; + savedTemplates?: AnalysisTemplate[]; + onLoadTemplate?: (templateId: string) => void; + initialSymbol?: string; + isLoading?: boolean; + estimatedTokens?: number; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const TIMEFRAMES = ['1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w']; +const INDICATORS = [ + 'SMA', 'EMA', 'RSI', 'MACD', 'Bollinger Bands', 'ATR', 'Stochastic', + 'VWAP', 'Fibonacci', 'Ichimoku', 'Volume Profile', 'OBV', +]; +const STRATEGY_TYPES = [ + 'Trend Following', 'Mean Reversion', 'Breakout', 'Scalping', + 'Swing Trading', 'Position Trading', 'Custom', +]; + +// ============================================================================ +// Component +// ============================================================================ + +export const AnalysisRequestForm: React.FC = ({ + onSubmit, + onSaveTemplate, + savedTemplates = [], + onLoadTemplate, + initialSymbol = '', + isLoading = false, + estimatedTokens, +}) => { + const [expandedSection, setExpandedSection] = useState('symbol'); + const [showSaveDialog, setShowSaveDialog] = useState(false); + const [templateName, setTemplateName] = useState(''); + + const [request, setRequest] = useState({ + symbol: initialSymbol, + timeframes: ['1h', '4h'], + dateRange: { + start: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], + end: new Date().toISOString().split('T')[0], + }, + indicators: ['EMA', 'RSI', 'MACD'], + strategyType: 'Trend Following', + riskParams: { + maxDrawdown: 10, + riskPerTrade: 2, + targetRR: 2, + }, + includeContext: { + recentSignals: true, + portfolioContext: false, + marketRegime: true, + }, + notes: '', + }); + + const toggleSection = (section: string) => { + setExpandedSection(expandedSection === section ? '' : section); + }; + + const updateRequest = ( + key: K, + value: AnalysisRequest[K] + ) => { + setRequest((prev) => ({ ...prev, [key]: value })); + }; + + const toggleTimeframe = (tf: string) => { + setRequest((prev) => ({ + ...prev, + timeframes: prev.timeframes.includes(tf) + ? prev.timeframes.filter((t) => t !== tf) + : [...prev.timeframes, tf], + })); + }; + + const toggleIndicator = (indicator: string) => { + setRequest((prev) => ({ + ...prev, + indicators: prev.indicators.includes(indicator) + ? prev.indicators.filter((i) => i !== indicator) + : [...prev.indicators, indicator], + })); + }; + + const isValid = useMemo(() => { + return ( + request.symbol.trim() !== '' && + request.timeframes.length > 0 && + request.indicators.length > 0 + ); + }, [request]); + + const handleSubmit = () => { + if (isValid && !isLoading) { + onSubmit(request); + } + }; + + const handleSaveTemplate = () => { + if (templateName.trim() && onSaveTemplate) { + onSaveTemplate({ + name: templateName, + description: `${request.strategyType} analysis for ${request.symbol}`, + config: request, + }); + setShowSaveDialog(false); + setTemplateName(''); + } + }; + + const SectionHeader: React.FC<{ + id: string; + icon: React.ReactNode; + title: string; + badge?: string; + }> = ({ id, icon, title, badge }) => ( + + ); + + return ( +
+ {/* Header */} +
+
+
+ +

Analysis Request

+
+ {savedTemplates.length > 0 && ( + + )} +
+
+ + {/* Form Sections */} +
+ {/* Symbol & Timeframes */} +
+ } + title="Symbol & Timeframes" + badge={request.symbol || undefined} + /> + {expandedSection === 'symbol' && ( +
+
+ + updateRequest('symbol', e.target.value.toUpperCase())} + placeholder="e.g., EURUSD, BTCUSD" + className="w-full px-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-gray-900 dark:text-white placeholder-gray-500 focus:ring-2 focus:ring-primary-500" + /> +
+
+ +
+ {TIMEFRAMES.map((tf) => ( + + ))} +
+
+
+ )} +
+ + {/* Date Range */} +
+ } + title="Date Range" + /> + {expandedSection === 'dateRange' && ( +
+
+ + updateRequest('dateRange', { ...request.dateRange, start: e.target.value })} + className="w-full px-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-gray-900 dark:text-white" + /> +
+
+ + updateRequest('dateRange', { ...request.dateRange, end: e.target.value })} + className="w-full px-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-gray-900 dark:text-white" + /> +
+
+ )} +
+ + {/* Indicators */} +
+ } + title="Indicators" + badge={`${request.indicators.length} selected`} + /> + {expandedSection === 'indicators' && ( +
+
+ {INDICATORS.map((indicator) => ( + + ))} +
+
+ )} +
+ + {/* Strategy & Risk */} +
+ } + title="Strategy & Risk" + /> + {expandedSection === 'strategy' && ( +
+
+ + +
+
+
+ + updateRequest('riskParams', { ...request.riskParams, maxDrawdown: Number(e.target.value) })} + className="w-full px-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-gray-900 dark:text-white text-sm" + /> +
+
+ + updateRequest('riskParams', { ...request.riskParams, riskPerTrade: Number(e.target.value) })} + className="w-full px-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-gray-900 dark:text-white text-sm" + /> +
+
+ + updateRequest('riskParams', { ...request.riskParams, targetRR: Number(e.target.value) })} + className="w-full px-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-gray-900 dark:text-white text-sm" + /> +
+
+
+ )} +
+ + {/* Context Options */} +
+ } + title="Include Context" + /> + {expandedSection === 'context' && ( +
+ {[ + { key: 'recentSignals' as const, label: 'Recent Signals', desc: 'Include last 5 generated signals' }, + { key: 'portfolioContext' as const, label: 'Portfolio Context', desc: 'Include current positions' }, + { key: 'marketRegime' as const, label: 'Market Regime', desc: 'Include volatility analysis' }, + ].map((item) => ( + + ))} +
+ +