[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
|
// 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';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user