[OQI-007] feat: Add LLM strategy agent advanced components

- AnalysisRequestForm: Structured request builder for complex analysis tasks
- StrategyTemplateSelector: Pre-built strategy templates with AI recommendations
- LLMConfigPanel: Model selection and inference parameters (Claude models)
- ContextMemoryDisplay: Conversation context and memory visualization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 12:31:08 -06:00
parent 7d9e8d2da9
commit 5ee7f14f25
5 changed files with 1846 additions and 0 deletions

View File

@ -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<AnalysisRequest>;
}
interface AnalysisRequestFormProps {
onSubmit: (request: AnalysisRequest) => void;
onSaveTemplate?: (template: Omit<AnalysisTemplate, 'id'>) => 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<AnalysisRequestFormProps> = ({
onSubmit,
onSaveTemplate,
savedTemplates = [],
onLoadTemplate,
initialSymbol = '',
isLoading = false,
estimatedTokens,
}) => {
const [expandedSection, setExpandedSection] = useState<string>('symbol');
const [showSaveDialog, setShowSaveDialog] = useState(false);
const [templateName, setTemplateName] = useState('');
const [request, setRequest] = useState<AnalysisRequest>({
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 = <K extends keyof AnalysisRequest>(
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 }) => (
<button
onClick={() => toggleSection(id)}
className="w-full flex items-center justify-between p-3 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
>
<div className="flex items-center gap-2">
<span className="text-gray-500 dark:text-gray-400">{icon}</span>
<span className="font-medium text-gray-900 dark:text-white">{title}</span>
{badge && (
<span className="px-2 py-0.5 bg-primary-500/20 text-primary-400 text-xs rounded-full">
{badge}
</span>
)}
</div>
{expandedSection === id ? (
<ChevronUpIcon className="w-5 h-5 text-gray-400" />
) : (
<ChevronDownIcon className="w-5 h-5 text-gray-400" />
)}
</button>
);
return (
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<SparklesIcon className="w-5 h-5 text-primary-500" />
<h3 className="font-semibold text-gray-900 dark:text-white">Analysis Request</h3>
</div>
{savedTemplates.length > 0 && (
<select
onChange={(e) => onLoadTemplate?.(e.target.value)}
className="text-sm bg-gray-100 dark:bg-gray-800 border-0 rounded-lg px-3 py-1.5 text-gray-700 dark:text-gray-300"
>
<option value="">Load Template...</option>
{savedTemplates.map((t) => (
<option key={t.id} value={t.id}>{t.name}</option>
))}
</select>
)}
</div>
</div>
{/* Form Sections */}
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{/* Symbol & Timeframes */}
<div>
<SectionHeader
id="symbol"
icon={<ChartBarIcon className="w-5 h-5" />}
title="Symbol & Timeframes"
badge={request.symbol || undefined}
/>
{expandedSection === 'symbol' && (
<div className="p-4 pt-0 space-y-4">
<div>
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">Symbol</label>
<input
type="text"
value={request.symbol}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-2">Timeframes</label>
<div className="flex flex-wrap gap-2">
{TIMEFRAMES.map((tf) => (
<button
key={tf}
onClick={() => toggleTimeframe(tf)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
request.timeframes.includes(tf)
? 'bg-primary-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{tf}
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* Date Range */}
<div>
<SectionHeader
id="dateRange"
icon={<CalendarIcon className="w-5 h-5" />}
title="Date Range"
/>
{expandedSection === 'dateRange' && (
<div className="p-4 pt-0 grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">Start Date</label>
<input
type="date"
value={request.dateRange.start}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">End Date</label>
<input
type="date"
value={request.dateRange.end}
onChange={(e) => 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"
/>
</div>
</div>
)}
</div>
{/* Indicators */}
<div>
<SectionHeader
id="indicators"
icon={<AdjustmentsHorizontalIcon className="w-5 h-5" />}
title="Indicators"
badge={`${request.indicators.length} selected`}
/>
{expandedSection === 'indicators' && (
<div className="p-4 pt-0">
<div className="flex flex-wrap gap-2">
{INDICATORS.map((indicator) => (
<button
key={indicator}
onClick={() => toggleIndicator(indicator)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
request.indicators.includes(indicator)
? 'bg-emerald-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{indicator}
</button>
))}
</div>
</div>
)}
</div>
{/* Strategy & Risk */}
<div>
<SectionHeader
id="strategy"
icon={<BeakerIcon className="w-5 h-5" />}
title="Strategy & Risk"
/>
{expandedSection === 'strategy' && (
<div className="p-4 pt-0 space-y-4">
<div>
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">Strategy Type</label>
<select
value={request.strategyType}
onChange={(e) => updateRequest('strategyType', 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"
>
{STRATEGY_TYPES.map((type) => (
<option key={type} value={type}>{type}</option>
))}
</select>
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs text-gray-500 mb-1">Max DD %</label>
<input
type="number"
value={request.riskParams.maxDrawdown}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Risk/Trade %</label>
<input
type="number"
value={request.riskParams.riskPerTrade}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">Target R:R</label>
<input
type="number"
step="0.5"
value={request.riskParams.targetRR}
onChange={(e) => 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"
/>
</div>
</div>
</div>
)}
</div>
{/* Context Options */}
<div>
<SectionHeader
id="context"
icon={<DocumentTextIcon className="w-5 h-5" />}
title="Include Context"
/>
{expandedSection === 'context' && (
<div className="p-4 pt-0 space-y-3">
{[
{ 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) => (
<label key={item.key} className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={request.includeContext[item.key]}
onChange={(e) => updateRequest('includeContext', {
...request.includeContext,
[item.key]: e.target.checked,
})}
className="mt-1 w-4 h-4 rounded border-gray-300 text-primary-500 focus:ring-primary-500"
/>
<div>
<div className="text-sm font-medium text-gray-900 dark:text-white">{item.label}</div>
<div className="text-xs text-gray-500">{item.desc}</div>
</div>
</label>
))}
<div className="pt-2">
<label className="block text-sm text-gray-600 dark:text-gray-400 mb-1">Additional Notes</label>
<textarea
value={request.notes}
onChange={(e) => updateRequest('notes', e.target.value)}
placeholder="Any specific questions or focus areas..."
rows={2}
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 resize-none"
/>
</div>
</div>
)}
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
{estimatedTokens && (
<div className="text-xs text-gray-500 mb-3">
Estimated context: ~{estimatedTokens.toLocaleString()} tokens
</div>
)}
<div className="flex items-center gap-2">
{onSaveTemplate && (
<button
onClick={() => setShowSaveDialog(true)}
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors flex items-center gap-2"
>
<BookmarkIcon className="w-4 h-4" />
Save
</button>
)}
<button
onClick={handleSubmit}
disabled={!isValid || isLoading}
className="flex-1 px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{isLoading ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Analyzing...
</>
) : (
<>
<PaperAirplaneIcon className="w-4 h-4" />
Run Analysis
</>
)}
</button>
</div>
</div>
{/* Save Template Dialog */}
{showSaveDialog && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-900 rounded-xl p-6 w-96 shadow-xl">
<div className="flex items-center justify-between mb-4">
<h4 className="font-semibold text-gray-900 dark:text-white">Save Template</h4>
<button onClick={() => setShowSaveDialog(false)}>
<XMarkIcon className="w-5 h-5 text-gray-400" />
</button>
</div>
<input
type="text"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder="Template name..."
className="w-full px-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-gray-900 dark:text-white mb-4"
/>
<button
onClick={handleSaveTemplate}
disabled={!templateName.trim()}
className="w-full px-4 py-2 bg-primary-600 text-white rounded-lg font-medium disabled:opacity-50"
>
Save Template
</button>
</div>
</div>
)}
</div>
);
};
export default AnalysisRequestForm;

View File

@ -0,0 +1,390 @@
/**
* ContextMemoryDisplay Component
* Visualize conversation context and summarization
* OQI-007: LLM Strategy Agent
*/
import React, { useState, useMemo } from 'react';
import {
CircleStackIcon,
DocumentTextIcon,
ClockIcon,
ChevronDownIcon,
ChevronUpIcon,
TrashIcon,
BookmarkIcon,
ArrowDownTrayIcon,
InformationCircleIcon,
ExclamationTriangleIcon,
} from '@heroicons/react/24/outline';
// ============================================================================
// Types
// ============================================================================
export interface ContextMessage {
id: string;
role: 'user' | 'assistant';
preview: string;
timestamp: string;
tokenCount: number;
isSummarized?: boolean;
}
export interface ContextSummary {
id: string;
createdAt: string;
messageCount: number;
content: string;
tokenCount: number;
}
export interface ContextMemoryState {
messages: ContextMessage[];
summaries: ContextSummary[];
maxContextTokens: number;
currentTokens: number;
lastCheckpoint?: string;
}
interface ContextMemoryDisplayProps {
state: ContextMemoryState;
onClearOldContext?: () => void;
onSaveCheckpoint?: () => void;
onExportConversation?: () => void;
onRestoreCheckpoint?: (checkpointId: string) => void;
compact?: boolean;
}
// ============================================================================
// Helper Functions
// ============================================================================
const formatTimestamp = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString();
};
const formatTokens = (tokens: number) => {
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`;
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`;
return tokens.toString();
};
// ============================================================================
// Component
// ============================================================================
export const ContextMemoryDisplay: React.FC<ContextMemoryDisplayProps> = ({
state,
onClearOldContext,
onSaveCheckpoint,
onExportConversation,
onRestoreCheckpoint,
compact = false,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [showSummaryContent, setShowSummaryContent] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'messages' | 'summaries'>('messages');
const usagePercent = useMemo(() => {
return Math.min((state.currentTokens / state.maxContextTokens) * 100, 100);
}, [state.currentTokens, state.maxContextTokens]);
const usageColor = useMemo(() => {
if (usagePercent >= 90) return 'bg-red-500';
if (usagePercent >= 70) return 'bg-yellow-500';
return 'bg-emerald-500';
}, [usagePercent]);
const recentMessages = state.messages.filter((m) => !m.isSummarized);
const summarizedMessages = state.messages.filter((m) => m.isSummarized);
if (compact) {
return (
<div className="p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<CircleStackIcon className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-600 dark:text-gray-400">Context</span>
</div>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{isExpanded ? (
<ChevronUpIcon className="w-4 h-4" />
) : (
<ChevronDownIcon className="w-4 h-4" />
)}
</button>
</div>
{/* Progress Bar */}
<div className="h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full ${usageColor} transition-all`}
style={{ width: `${usagePercent}%` }}
/>
</div>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>{formatTokens(state.currentTokens)} tokens</span>
<span>{usagePercent.toFixed(0)}%</span>
</div>
{isExpanded && (
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 space-y-2">
<div className="text-xs text-gray-500">
{recentMessages.length} messages in context
{state.summaries.length > 0 && ` + ${state.summaries.length} summaries`}
</div>
<div className="flex gap-2">
{onClearOldContext && (
<button
onClick={onClearOldContext}
className="flex-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
>
Clear Old
</button>
)}
{onSaveCheckpoint && (
<button
onClick={onSaveCheckpoint}
className="flex-1 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
>
Checkpoint
</button>
)}
</div>
</div>
)}
</div>
);
}
return (
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<CircleStackIcon className="w-5 h-5 text-primary-500" />
<h3 className="font-semibold text-gray-900 dark:text-white">Context Memory</h3>
</div>
<div className="flex items-center gap-2">
{onExportConversation && (
<button
onClick={onExportConversation}
className="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg"
title="Export conversation"
>
<ArrowDownTrayIcon className="w-4 h-4" />
</button>
)}
{onSaveCheckpoint && (
<button
onClick={onSaveCheckpoint}
className="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg"
title="Save checkpoint"
>
<BookmarkIcon className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* Usage Gauge */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Context Usage</span>
<span className="font-medium text-gray-900 dark:text-white">
{formatTokens(state.currentTokens)} / {formatTokens(state.maxContextTokens)}
</span>
</div>
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full ${usageColor} transition-all duration-300`}
style={{ width: `${usagePercent}%` }}
/>
</div>
{usagePercent >= 80 && (
<div className="flex items-center gap-1 text-xs text-yellow-600 dark:text-yellow-400">
<ExclamationTriangleIcon className="w-3.5 h-3.5" />
<span>Context filling up. Old messages may be summarized.</span>
</div>
)}
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-3 mt-3">
<div className="text-center p-2 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-lg font-bold text-gray-900 dark:text-white">{recentMessages.length}</div>
<div className="text-xs text-gray-500">Active Messages</div>
</div>
<div className="text-center p-2 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-lg font-bold text-gray-900 dark:text-white">{state.summaries.length}</div>
<div className="text-xs text-gray-500">Summaries</div>
</div>
<div className="text-center p-2 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-lg font-bold text-gray-900 dark:text-white">{summarizedMessages.length}</div>
<div className="text-xs text-gray-500">Summarized</div>
</div>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setActiveTab('messages')}
className={`flex-1 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'messages'
? 'border-primary-500 text-primary-500'
: 'border-transparent text-gray-500'
}`}
>
Messages ({recentMessages.length})
</button>
<button
onClick={() => setActiveTab('summaries')}
className={`flex-1 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'summaries'
? 'border-primary-500 text-primary-500'
: 'border-transparent text-gray-500'
}`}
>
Summaries ({state.summaries.length})
</button>
</div>
{/* Content */}
<div className="max-h-64 overflow-y-auto">
{activeTab === 'messages' && (
<div className="p-2 space-y-1">
{recentMessages.length === 0 ? (
<div className="text-center py-6 text-gray-500 text-sm">
No messages in context yet
</div>
) : (
recentMessages.map((message) => (
<div
key={message.id}
className="flex items-start gap-2 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
>
<div
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium ${
message.role === 'user'
? 'bg-blue-500/20 text-blue-400'
: 'bg-purple-500/20 text-purple-400'
}`}
>
{message.role === 'user' ? 'U' : 'A'}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-700 dark:text-gray-300 truncate">
{message.preview}
</p>
<div className="flex items-center gap-2 text-xs text-gray-500 mt-0.5">
<span>{formatTimestamp(message.timestamp)}</span>
<span></span>
<span>{message.tokenCount} tokens</span>
</div>
</div>
</div>
))
)}
</div>
)}
{activeTab === 'summaries' && (
<div className="p-2 space-y-2">
{state.summaries.length === 0 ? (
<div className="text-center py-6 text-gray-500 text-sm">
No summaries created yet
</div>
) : (
state.summaries.map((summary) => (
<div
key={summary.id}
className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<DocumentTextIcon className="w-4 h-4 text-gray-400" />
<span className="text-sm font-medium text-gray-900 dark:text-white">
{summary.messageCount} messages summarized
</span>
</div>
<button
onClick={() =>
setShowSummaryContent(
showSummaryContent === summary.id ? null : summary.id
)
}
className="text-xs text-primary-500 hover:text-primary-600"
>
{showSummaryContent === summary.id ? 'Hide' : 'Show'}
</button>
</div>
<div className="flex items-center gap-2 text-xs text-gray-500">
<ClockIcon className="w-3.5 h-3.5" />
<span>{formatTimestamp(summary.createdAt)}</span>
<span></span>
<span>{summary.tokenCount} tokens</span>
</div>
{showSummaryContent === summary.id && (
<div className="mt-2 p-2 bg-white dark:bg-gray-900 rounded text-sm text-gray-600 dark:text-gray-400">
{summary.content}
</div>
)}
</div>
))
)}
</div>
)}
</div>
{/* Footer Actions */}
<div className="p-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div className="flex items-center gap-2">
{onClearOldContext && (
<button
onClick={onClearOldContext}
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 text-sm text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-lg transition-colors"
>
<TrashIcon className="w-4 h-4" />
Clear Old Context
</button>
)}
{onSaveCheckpoint && (
<button
onClick={onSaveCheckpoint}
className="flex-1 flex items-center justify-center gap-1 px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<BookmarkIcon className="w-4 h-4" />
Save Checkpoint
</button>
)}
</div>
{/* Info */}
<div className="mt-3 p-2 bg-blue-50 dark:bg-blue-500/10 rounded-lg">
<div className="flex items-start gap-2">
<InformationCircleIcon className="w-4 h-4 text-blue-500 mt-0.5" />
<p className="text-xs text-blue-700 dark:text-blue-300">
Recent messages are sent in full. Older messages are summarized to save tokens.
The AI uses summaries to maintain conversation context.
</p>
</div>
</div>
</div>
</div>
);
};
export default ContextMemoryDisplay;

View File

@ -0,0 +1,505 @@
/**
* LLMConfigPanel Component
* Model selection and inference parameters
* OQI-007: LLM Strategy Agent
*/
import React, { useState } from 'react';
import {
CpuChipIcon,
AdjustmentsHorizontalIcon,
SparklesIcon,
CurrencyDollarIcon,
ClockIcon,
LightBulbIcon,
BookmarkIcon,
XMarkIcon,
InformationCircleIcon,
CheckIcon,
} from '@heroicons/react/24/outline';
// ============================================================================
// Types
// ============================================================================
export type ModelId = 'claude-3-5-sonnet' | 'claude-3-opus' | 'claude-3-haiku';
export type ReasoningStyle = 'concise' | 'detailed' | 'technical' | 'exploratory';
export type AnalysisDepth = 'quick' | 'standard' | 'deep';
export interface LLMConfig {
modelId: ModelId;
temperature: number;
reasoningStyle: ReasoningStyle;
analysisDepth: AnalysisDepth;
showThinking: boolean;
maxTokens: number;
}
export interface ModelInfo {
id: ModelId;
name: string;
description: string;
contextWindow: string;
speed: 'fast' | 'medium' | 'slow';
intelligence: 'good' | 'great' | 'best';
costPerMToken: number;
}
export interface ConfigPreset {
id: string;
name: string;
description: string;
config: LLMConfig;
}
interface LLMConfigPanelProps {
config: LLMConfig;
onConfigChange: (config: LLMConfig) => void;
onSavePreset?: (name: string, config: LLMConfig) => void;
savedPresets?: ConfigPreset[];
onLoadPreset?: (presetId: string) => void;
estimatedCost?: number;
isOpen: boolean;
onClose: () => void;
}
// ============================================================================
// Constants
// ============================================================================
const MODELS: ModelInfo[] = [
{
id: 'claude-3-5-sonnet',
name: 'Claude 3.5 Sonnet',
description: 'Fast and capable, great for most analysis tasks',
contextWindow: '200K',
speed: 'fast',
intelligence: 'great',
costPerMToken: 3.0,
},
{
id: 'claude-3-opus',
name: 'Claude 3 Opus',
description: 'Most intelligent, best for complex reasoning',
contextWindow: '200K',
speed: 'slow',
intelligence: 'best',
costPerMToken: 15.0,
},
{
id: 'claude-3-haiku',
name: 'Claude 3 Haiku',
description: 'Budget-friendly, good for simple queries',
contextWindow: '200K',
speed: 'fast',
intelligence: 'good',
costPerMToken: 0.25,
},
];
const REASONING_STYLES: Array<{ value: ReasoningStyle; label: string; desc: string }> = [
{ value: 'concise', label: 'Concise', desc: 'Brief, actionable insights' },
{ value: 'detailed', label: 'Detailed', desc: 'Comprehensive explanations' },
{ value: 'technical', label: 'Technical', desc: 'In-depth technical analysis' },
{ value: 'exploratory', label: 'Exploratory', desc: 'Consider multiple scenarios' },
];
const ANALYSIS_DEPTHS: Array<{ value: AnalysisDepth; label: string; desc: string; tools: string }> = [
{ value: 'quick', label: 'Quick', desc: 'Fast signal check', tools: '1 tool call' },
{ value: 'standard', label: 'Standard', desc: 'Balanced analysis', tools: '2-3 tool calls' },
{ value: 'deep', label: 'Deep', desc: 'Thorough investigation', tools: '5+ tool calls' },
];
const DEFAULT_PRESETS: ConfigPreset[] = [
{
id: 'quick-signal',
name: 'Quick Signal Check',
description: 'Fast, low-cost signal verification',
config: {
modelId: 'claude-3-haiku',
temperature: 0.3,
reasoningStyle: 'concise',
analysisDepth: 'quick',
showThinking: false,
maxTokens: 1024,
},
},
{
id: 'deep-analysis',
name: 'Deep Analysis',
description: 'Comprehensive market analysis',
config: {
modelId: 'claude-3-opus',
temperature: 0.7,
reasoningStyle: 'detailed',
analysisDepth: 'deep',
showThinking: true,
maxTokens: 4096,
},
},
{
id: 'balanced',
name: 'Balanced',
description: 'Good balance of speed and quality',
config: {
modelId: 'claude-3-5-sonnet',
temperature: 0.5,
reasoningStyle: 'detailed',
analysisDepth: 'standard',
showThinking: false,
maxTokens: 2048,
},
},
];
// ============================================================================
// Component
// ============================================================================
export const LLMConfigPanel: React.FC<LLMConfigPanelProps> = ({
config,
onConfigChange,
onSavePreset,
savedPresets = [],
onLoadPreset,
estimatedCost,
isOpen,
onClose,
}) => {
const [showSaveDialog, setShowSaveDialog] = useState(false);
const [presetName, setPresetName] = useState('');
const [activeTab, setActiveTab] = useState<'model' | 'style' | 'presets'>('model');
const allPresets = [...DEFAULT_PRESETS, ...savedPresets];
const updateConfig = <K extends keyof LLMConfig>(key: K, value: LLMConfig[K]) => {
onConfigChange({ ...config, [key]: value });
};
const selectedModel = MODELS.find((m) => m.id === config.modelId) || MODELS[0];
const getSpeedBadge = (speed: ModelInfo['speed']) => {
switch (speed) {
case 'fast':
return 'bg-emerald-500/20 text-emerald-400';
case 'medium':
return 'bg-yellow-500/20 text-yellow-400';
case 'slow':
return 'bg-red-500/20 text-red-400';
}
};
const getIntelligenceBadge = (intel: ModelInfo['intelligence']) => {
switch (intel) {
case 'good':
return 'bg-gray-500/20 text-gray-400';
case 'great':
return 'bg-blue-500/20 text-blue-400';
case 'best':
return 'bg-purple-500/20 text-purple-400';
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-900 rounded-xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div className="flex items-center gap-2">
<CpuChipIcon className="w-5 h-5 text-primary-500" />
<h3 className="font-semibold text-gray-900 dark:text-white">LLM Configuration</h3>
</div>
<button onClick={onClose} className="p-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg">
<XMarkIcon className="w-5 h-5 text-gray-400" />
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200 dark:border-gray-700">
{[
{ id: 'model', label: 'Model', icon: CpuChipIcon },
{ id: 'style', label: 'Style', icon: AdjustmentsHorizontalIcon },
{ id: 'presets', label: 'Presets', icon: BookmarkIcon },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
className={`flex-1 flex items-center justify-center gap-2 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-primary-500 text-primary-500'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{/* Model Tab */}
{activeTab === 'model' && (
<div className="space-y-4">
<div className="space-y-3">
{MODELS.map((model) => (
<button
key={model.id}
onClick={() => updateConfig('modelId', model.id)}
className={`w-full p-4 rounded-xl border-2 text-left transition-all ${
config.modelId === model.id
? 'border-primary-500 bg-primary-500/5'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold text-gray-900 dark:text-white">{model.name}</span>
{config.modelId === model.id && (
<CheckIcon className="w-4 h-4 text-primary-500" />
)}
</div>
<p className="text-sm text-gray-500 mt-1">{model.description}</p>
<div className="flex gap-2 mt-2">
<span className={`px-2 py-0.5 text-xs rounded ${getSpeedBadge(model.speed)}`}>
{model.speed}
</span>
<span className={`px-2 py-0.5 text-xs rounded ${getIntelligenceBadge(model.intelligence)}`}>
{model.intelligence}
</span>
<span className="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 rounded">
{model.contextWindow} context
</span>
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-gray-900 dark:text-white">
${model.costPerMToken.toFixed(2)}
</div>
<div className="text-xs text-gray-500">/1M tokens</div>
</div>
</div>
</button>
))}
</div>
</div>
)}
{/* Style Tab */}
{activeTab === 'style' && (
<div className="space-y-6">
{/* Temperature */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-900 dark:text-white">
Temperature
</label>
<span className="text-sm text-gray-500">{config.temperature.toFixed(1)}</span>
</div>
<input
type="range"
min="0"
max="1"
step="0.1"
value={config.temperature}
onChange={(e) => updateConfig('temperature', parseFloat(e.target.value))}
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>Deterministic (0)</span>
<span>Creative (1)</span>
</div>
</div>
{/* Reasoning Style */}
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Reasoning Style
</label>
<div className="grid grid-cols-2 gap-2">
{REASONING_STYLES.map((style) => (
<button
key={style.value}
onClick={() => updateConfig('reasoningStyle', style.value)}
className={`p-3 rounded-lg border-2 text-left transition-all ${
config.reasoningStyle === style.value
? 'border-primary-500 bg-primary-500/5'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300'
}`}
>
<div className="font-medium text-gray-900 dark:text-white text-sm">{style.label}</div>
<div className="text-xs text-gray-500">{style.desc}</div>
</button>
))}
</div>
</div>
{/* Analysis Depth */}
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Analysis Depth
</label>
<div className="flex gap-2">
{ANALYSIS_DEPTHS.map((depth) => (
<button
key={depth.value}
onClick={() => updateConfig('analysisDepth', depth.value)}
className={`flex-1 p-3 rounded-lg border-2 text-center transition-all ${
config.analysisDepth === depth.value
? 'border-primary-500 bg-primary-500/5'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300'
}`}
>
<div className="font-medium text-gray-900 dark:text-white text-sm">{depth.label}</div>
<div className="text-xs text-gray-500">{depth.tools}</div>
</button>
))}
</div>
</div>
{/* Show Thinking */}
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex items-center gap-3">
<LightBulbIcon className="w-5 h-5 text-yellow-500" />
<div>
<div className="font-medium text-gray-900 dark:text-white text-sm">Show Thinking</div>
<div className="text-xs text-gray-500">Display Claude's reasoning process</div>
</div>
</div>
<button
onClick={() => updateConfig('showThinking', !config.showThinking)}
className={`relative w-11 h-6 rounded-full transition-colors ${
config.showThinking ? 'bg-primary-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<span
className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
config.showThinking ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{/* Max Tokens */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-gray-900 dark:text-white">
Max Response Tokens
</label>
<span className="text-sm text-gray-500">{config.maxTokens.toLocaleString()}</span>
</div>
<input
type="range"
min="512"
max="8192"
step="256"
value={config.maxTokens}
onChange={(e) => updateConfig('maxTokens', parseInt(e.target.value))}
className="w-full"
/>
</div>
</div>
)}
{/* Presets Tab */}
{activeTab === 'presets' && (
<div className="space-y-3">
{allPresets.map((preset) => (
<button
key={preset.id}
onClick={() => onConfigChange(preset.config)}
className="w-full p-4 bg-gray-50 dark:bg-gray-800 rounded-lg text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-gray-900 dark:text-white">{preset.name}</div>
<div className="text-sm text-gray-500">{preset.description}</div>
</div>
<div className="text-xs text-gray-400">
{MODELS.find((m) => m.id === preset.config.modelId)?.name}
</div>
</div>
</button>
))}
{onSavePreset && (
<button
onClick={() => setShowSaveDialog(true)}
className="w-full p-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg text-gray-500 hover:border-primary-500 hover:text-primary-500 transition-colors"
>
+ Save Current as Preset
</button>
)}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 text-sm text-gray-500">
<div className="flex items-center gap-1">
<CpuChipIcon className="w-4 h-4" />
<span>{selectedModel.name}</span>
</div>
{estimatedCost !== undefined && (
<div className="flex items-center gap-1">
<CurrencyDollarIcon className="w-4 h-4" />
<span>~${estimatedCost.toFixed(4)}/request</span>
</div>
)}
</div>
<button
onClick={onClose}
className="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg font-medium transition-colors"
>
Apply
</button>
</div>
</div>
{/* Save Preset Dialog */}
{showSaveDialog && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white dark:bg-gray-900 rounded-xl p-6 w-80">
<h4 className="font-semibold text-gray-900 dark:text-white mb-4">Save Preset</h4>
<input
type="text"
value={presetName}
onChange={(e) => setPresetName(e.target.value)}
placeholder="Preset name..."
className="w-full px-3 py-2 bg-gray-100 dark:bg-gray-800 border-0 rounded-lg text-gray-900 dark:text-white mb-4"
/>
<div className="flex gap-2">
<button
onClick={() => setShowSaveDialog(false)}
className="flex-1 px-4 py-2 text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg"
>
Cancel
</button>
<button
onClick={() => {
if (presetName.trim()) {
onSavePreset?.(presetName, config);
setShowSaveDialog(false);
setPresetName('');
}
}}
disabled={!presetName.trim()}
className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg font-medium disabled:opacity-50"
>
Save
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default LLMConfigPanel;

View File

@ -0,0 +1,441 @@
/**
* StrategyTemplateSelector Component
* Pre-built strategy templates with AI quick-suggestions
* OQI-007: LLM Strategy Agent
*/
import React, { useState } from 'react';
import {
SparklesIcon,
ChartBarIcon,
ArrowTrendingUpIcon,
ArrowsRightLeftIcon,
BoltIcon,
ClockIcon,
ScaleIcon,
CheckCircleIcon,
XMarkIcon,
InformationCircleIcon,
} from '@heroicons/react/24/outline';
import { StarIcon } from '@heroicons/react/24/solid';
// ============================================================================
// Types
// ============================================================================
export interface StrategyTemplate {
id: string;
name: string;
description: string;
category: 'trend' | 'reversal' | 'breakout' | 'scalping' | 'swing' | 'position';
riskLevel: 'low' | 'medium' | 'high';
metrics: {
winRate: number;
avgRR: number;
maxDrawdown: number;
sharpeRatio: number;
};
bestTimeframes: string[];
indicators: string[];
marketConditions: string[];
isAIRecommended?: boolean;
popularity?: number;
}
interface StrategyTemplateSelectorProps {
templates: StrategyTemplate[];
aiRecommendations?: string[];
onSelectTemplate: (template: StrategyTemplate) => void;
onCompare?: (templates: StrategyTemplate[]) => void;
selectedTemplateId?: string;
compact?: boolean;
}
// ============================================================================
// Helper Functions
// ============================================================================
const getCategoryIcon = (category: StrategyTemplate['category']) => {
switch (category) {
case 'trend':
return ArrowTrendingUpIcon;
case 'reversal':
return ArrowsRightLeftIcon;
case 'breakout':
return BoltIcon;
case 'scalping':
return BoltIcon;
case 'swing':
return ClockIcon;
case 'position':
return ScaleIcon;
default:
return ChartBarIcon;
}
};
const getCategoryColor = (category: StrategyTemplate['category']) => {
switch (category) {
case 'trend':
return 'bg-blue-500/20 text-blue-400';
case 'reversal':
return 'bg-purple-500/20 text-purple-400';
case 'breakout':
return 'bg-amber-500/20 text-amber-400';
case 'scalping':
return 'bg-red-500/20 text-red-400';
case 'swing':
return 'bg-emerald-500/20 text-emerald-400';
case 'position':
return 'bg-cyan-500/20 text-cyan-400';
default:
return 'bg-gray-500/20 text-gray-400';
}
};
const getRiskColor = (riskLevel: StrategyTemplate['riskLevel']) => {
switch (riskLevel) {
case 'low':
return 'text-emerald-400';
case 'medium':
return 'text-yellow-400';
case 'high':
return 'text-red-400';
default:
return 'text-gray-400';
}
};
// ============================================================================
// Component
// ============================================================================
export const StrategyTemplateSelector: React.FC<StrategyTemplateSelectorProps> = ({
templates,
aiRecommendations = [],
onSelectTemplate,
onCompare,
selectedTemplateId,
compact = false,
}) => {
const [filterCategory, setFilterCategory] = useState<StrategyTemplate['category'] | 'all'>('all');
const [compareMode, setCompareMode] = useState(false);
const [compareList, setCompareList] = useState<string[]>([]);
const [showDetails, setShowDetails] = useState<string | null>(null);
const categories: Array<{ value: StrategyTemplate['category'] | 'all'; label: string }> = [
{ value: 'all', label: 'All' },
{ value: 'trend', label: 'Trend' },
{ value: 'reversal', label: 'Reversal' },
{ value: 'breakout', label: 'Breakout' },
{ value: 'scalping', label: 'Scalping' },
{ value: 'swing', label: 'Swing' },
{ value: 'position', label: 'Position' },
];
const filteredTemplates = templates.filter(
(t) => filterCategory === 'all' || t.category === filterCategory
);
const recommendedTemplates = templates.filter((t) => aiRecommendations.includes(t.id));
const toggleCompare = (templateId: string) => {
setCompareList((prev) =>
prev.includes(templateId)
? prev.filter((id) => id !== templateId)
: prev.length < 3
? [...prev, templateId]
: prev
);
};
const handleCompare = () => {
const templatesToCompare = templates.filter((t) => compareList.includes(t.id));
onCompare?.(templatesToCompare);
};
const TemplateCard: React.FC<{ template: StrategyTemplate }> = ({ template }) => {
const Icon = getCategoryIcon(template.category);
const isSelected = selectedTemplateId === template.id;
const isInCompare = compareList.includes(template.id);
const isRecommended = aiRecommendations.includes(template.id);
return (
<div
className={`relative p-4 bg-white dark:bg-gray-800 rounded-xl border-2 transition-all cursor-pointer ${
isSelected
? 'border-primary-500 ring-2 ring-primary-500/20'
: isInCompare
? 'border-emerald-500'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
onClick={() => !compareMode && onSelectTemplate(template)}
>
{/* AI Recommended Badge */}
{isRecommended && (
<div className="absolute -top-2 -right-2 flex items-center gap-1 px-2 py-0.5 bg-gradient-to-r from-purple-500 to-pink-500 text-white text-xs font-medium rounded-full">
<SparklesIcon className="w-3 h-3" />
AI Pick
</div>
)}
{/* Compare Checkbox */}
{compareMode && (
<div
onClick={(e) => {
e.stopPropagation();
toggleCompare(template.id);
}}
className="absolute top-3 right-3"
>
<div
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
isInCompare
? 'bg-emerald-500 border-emerald-500'
: 'border-gray-300 dark:border-gray-600'
}`}
>
{isInCompare && <CheckCircleIcon className="w-4 h-4 text-white" />}
</div>
</div>
)}
{/* Header */}
<div className="flex items-start gap-3 mb-3">
<div className={`p-2 rounded-lg ${getCategoryColor(template.category)}`}>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-gray-900 dark:text-white truncate">
{template.name}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
{template.description}
</p>
</div>
</div>
{/* Metrics */}
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="text-center p-2 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div className="text-lg font-bold text-emerald-400">{template.metrics.winRate}%</div>
<div className="text-xs text-gray-500">Win Rate</div>
</div>
<div className="text-center p-2 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
<div className="text-lg font-bold text-blue-400">{template.metrics.avgRR}:1</div>
<div className="text-xs text-gray-500">Avg R:R</div>
</div>
</div>
{/* Tags */}
<div className="flex flex-wrap gap-1">
<span className={`px-2 py-0.5 text-xs rounded capitalize ${getRiskColor(template.riskLevel)}`}>
{template.riskLevel} risk
</span>
{template.bestTimeframes.slice(0, 2).map((tf) => (
<span key={tf} className="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded">
{tf}
</span>
))}
</div>
{/* Info Button */}
<button
onClick={(e) => {
e.stopPropagation();
setShowDetails(showDetails === template.id ? null : template.id);
}}
className="absolute bottom-3 right-3 p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<InformationCircleIcon className="w-4 h-4" />
</button>
</div>
);
};
return (
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<ChartBarIcon className="w-5 h-5 text-primary-500" />
<h3 className="font-semibold text-gray-900 dark:text-white">Strategy Templates</h3>
</div>
{onCompare && (
<button
onClick={() => {
if (compareMode && compareList.length >= 2) {
handleCompare();
}
setCompareMode(!compareMode);
if (!compareMode) setCompareList([]);
}}
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
compareMode
? compareList.length >= 2
? 'bg-emerald-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{compareMode
? compareList.length >= 2
? `Compare (${compareList.length})`
: `Select ${2 - compareList.length} more`
: 'Compare'}
</button>
)}
</div>
{/* Category Filter */}
<div className="flex gap-1 overflow-x-auto pb-1">
{categories.map((cat) => (
<button
key={cat.value}
onClick={() => setFilterCategory(cat.value)}
className={`px-3 py-1 text-sm rounded-lg whitespace-nowrap transition-colors ${
filterCategory === cat.value
? 'bg-primary-500 text-white'
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
>
{cat.label}
</button>
))}
</div>
</div>
{/* AI Recommendations */}
{recommendedTemplates.length > 0 && filterCategory === 'all' && (
<div className="p-4 bg-gradient-to-r from-purple-500/10 to-pink-500/10 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2 mb-3">
<SparklesIcon className="w-4 h-4 text-purple-400" />
<span className="text-sm font-medium text-purple-400">AI Recommended for Current Market</span>
</div>
<div className={`grid ${compact ? 'grid-cols-1' : 'grid-cols-2 lg:grid-cols-3'} gap-3`}>
{recommendedTemplates.slice(0, 3).map((template) => (
<TemplateCard key={template.id} template={template} />
))}
</div>
</div>
)}
{/* All Templates */}
<div className="p-4">
<div className={`grid ${compact ? 'grid-cols-1' : 'grid-cols-2 lg:grid-cols-3'} gap-3`}>
{filteredTemplates
.filter((t) => !aiRecommendations.includes(t.id) || filterCategory !== 'all')
.map((template) => (
<TemplateCard key={template.id} template={template} />
))}
</div>
{filteredTemplates.length === 0 && (
<div className="text-center py-8 text-gray-500">
No templates found for this category
</div>
)}
</div>
{/* Details Modal */}
{showDetails && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-900 rounded-xl max-w-lg w-full max-h-[80vh] overflow-y-auto">
{(() => {
const template = templates.find((t) => t.id === showDetails);
if (!template) return null;
const Icon = getCategoryIcon(template.category);
return (
<>
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${getCategoryColor(template.category)}`}>
<Icon className="w-5 h-5" />
</div>
<h4 className="font-semibold text-gray-900 dark:text-white">{template.name}</h4>
</div>
<button onClick={() => setShowDetails(null)}>
<XMarkIcon className="w-5 h-5 text-gray-400" />
</button>
</div>
<div className="p-4 space-y-4">
<p className="text-gray-600 dark:text-gray-400">{template.description}</p>
<div>
<h5 className="text-sm font-medium text-gray-900 dark:text-white mb-2">Performance Metrics</h5>
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-xl font-bold text-emerald-400">{template.metrics.winRate}%</div>
<div className="text-xs text-gray-500">Win Rate</div>
</div>
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-xl font-bold text-blue-400">{template.metrics.avgRR}:1</div>
<div className="text-xs text-gray-500">Avg R:R</div>
</div>
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-xl font-bold text-red-400">{template.metrics.maxDrawdown}%</div>
<div className="text-xs text-gray-500">Max Drawdown</div>
</div>
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-xl font-bold text-purple-400">{template.metrics.sharpeRatio}</div>
<div className="text-xs text-gray-500">Sharpe Ratio</div>
</div>
</div>
</div>
<div>
<h5 className="text-sm font-medium text-gray-900 dark:text-white mb-2">Best Timeframes</h5>
<div className="flex flex-wrap gap-2">
{template.bestTimeframes.map((tf) => (
<span key={tf} className="px-3 py-1 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-lg text-sm">
{tf}
</span>
))}
</div>
</div>
<div>
<h5 className="text-sm font-medium text-gray-900 dark:text-white mb-2">Indicators Used</h5>
<div className="flex flex-wrap gap-2">
{template.indicators.map((ind) => (
<span key={ind} className="px-3 py-1 bg-primary-500/20 text-primary-400 rounded-lg text-sm">
{ind}
</span>
))}
</div>
</div>
<div>
<h5 className="text-sm font-medium text-gray-900 dark:text-white mb-2">Best Market Conditions</h5>
<div className="flex flex-wrap gap-2">
{template.marketConditions.map((cond) => (
<span key={cond} className="px-3 py-1 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 rounded-lg text-sm">
{cond}
</span>
))}
</div>
</div>
<button
onClick={() => {
onSelectTemplate(template);
setShowDetails(null);
}}
className="w-full py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-lg font-medium transition-colors"
>
Use This Template
</button>
</div>
</>
);
})()}
</div>
</div>
)}
</div>
);
};
export default StrategyTemplateSelector;

View File

@ -47,3 +47,18 @@ export { default as MessageSearch } from './MessageSearch';
// Rendering // Rendering
export { default as MarkdownRenderer } from './MarkdownRenderer'; export { default as MarkdownRenderer } from './MarkdownRenderer';
export { CodeBlock, AlertBox, SignalCard as MarkdownSignalCard, parseMarkdown } from './MarkdownRenderer'; export { CodeBlock, AlertBox, SignalCard as MarkdownSignalCard, parseMarkdown } from './MarkdownRenderer';
// Analysis & Strategy (OQI-007)
export { default as AnalysisRequestForm } from './AnalysisRequestForm';
export type { AnalysisRequest, AnalysisTemplate } from './AnalysisRequestForm';
export { default as StrategyTemplateSelector } from './StrategyTemplateSelector';
export type { StrategyTemplate } from './StrategyTemplateSelector';
// LLM Configuration (OQI-007)
export { default as LLMConfigPanel } from './LLMConfigPanel';
export type { LLMConfig, ModelInfo, ConfigPreset, ModelId, ReasoningStyle, AnalysisDepth } from './LLMConfigPanel';
// Context Memory (OQI-007)
export { default as ContextMemoryDisplay } from './ContextMemoryDisplay';
export type { ContextMessage, ContextSummary, ContextMemoryState } from './ContextMemoryDisplay';