[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:
parent
7d9e8d2da9
commit
5ee7f14f25
495
src/modules/assistant/components/AnalysisRequestForm.tsx
Normal file
495
src/modules/assistant/components/AnalysisRequestForm.tsx
Normal 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;
|
||||
390
src/modules/assistant/components/ContextMemoryDisplay.tsx
Normal file
390
src/modules/assistant/components/ContextMemoryDisplay.tsx
Normal 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;
|
||||
505
src/modules/assistant/components/LLMConfigPanel.tsx
Normal file
505
src/modules/assistant/components/LLMConfigPanel.tsx
Normal 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;
|
||||
441
src/modules/assistant/components/StrategyTemplateSelector.tsx
Normal file
441
src/modules/assistant/components/StrategyTemplateSelector.tsx
Normal 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;
|
||||
@ -47,3 +47,18 @@ export { default as MessageSearch } from './MessageSearch';
|
||||
// Rendering
|
||||
export { default as MarkdownRenderer } 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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user