trading-platform-frontend-v2/src/modules/assistant/components/AssistantSettingsPanel.tsx
Adrian Flores Cortes bfaf76ccf8 [OQI-007] feat: Add LLM assistant UX components
- ToolCallCard: Display tool execution results with expandable details
- MessageFeedback: Thumbs up/down with detailed feedback form
- StreamingIndicator: Multiple variants for thinking/analyzing/generating states
- AssistantSettingsPanel: Settings modal with risk profile, preferences

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 10:48:47 -06:00

454 lines
16 KiB
TypeScript

/**
* AssistantSettingsPanel Component
* Settings for customizing assistant behavior and preferences
*/
import React, { useState } from 'react';
import {
Settings,
X,
Save,
RotateCcw,
Bot,
Zap,
Shield,
Bell,
Sliders,
MessageSquare,
TrendingUp,
Clock,
Loader2,
} from 'lucide-react';
export interface AssistantSettings {
// Response preferences
responseStyle: 'concise' | 'detailed' | 'technical';
includeCharts: boolean;
includeSignals: boolean;
autoAnalyze: boolean;
// Risk profile
riskProfile: 'conservative' | 'moderate' | 'aggressive';
maxPositionSize: number;
preferredTimeframes: string[];
// Notification preferences
signalNotifications: boolean;
analysisNotifications: boolean;
priceAlerts: boolean;
// Advanced
streamResponses: boolean;
rememberContext: boolean;
maxHistoryMessages: number;
}
interface AssistantSettingsPanelProps {
settings: AssistantSettings;
onSave: (settings: AssistantSettings) => Promise<void>;
onClose: () => void;
isOpen: boolean;
}
const DEFAULT_SETTINGS: AssistantSettings = {
responseStyle: 'detailed',
includeCharts: true,
includeSignals: true,
autoAnalyze: false,
riskProfile: 'moderate',
maxPositionSize: 2,
preferredTimeframes: ['H1', 'H4', 'D1'],
signalNotifications: true,
analysisNotifications: false,
priceAlerts: true,
streamResponses: true,
rememberContext: true,
maxHistoryMessages: 50,
};
const TIMEFRAMES = ['M1', 'M5', 'M15', 'M30', 'H1', 'H4', 'D1', 'W1', 'MN'];
const AssistantSettingsPanel: React.FC<AssistantSettingsPanelProps> = ({
settings: initialSettings,
onSave,
onClose,
isOpen,
}) => {
const [settings, setSettings] = useState<AssistantSettings>(initialSettings);
const [isSaving, setIsSaving] = useState(false);
const [activeTab, setActiveTab] = useState<'general' | 'risk' | 'notifications' | 'advanced'>('general');
const handleSave = async () => {
setIsSaving(true);
try {
await onSave(settings);
onClose();
} catch (error) {
console.error('Failed to save settings:', error);
} finally {
setIsSaving(false);
}
};
const handleReset = () => {
setSettings(DEFAULT_SETTINGS);
};
const toggleTimeframe = (tf: string) => {
setSettings((prev) => ({
...prev,
preferredTimeframes: prev.preferredTimeframes.includes(tf)
? prev.preferredTimeframes.filter((t) => t !== tf)
: [...prev.preferredTimeframes, tf],
}));
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="w-full max-w-lg bg-gray-900 rounded-xl border border-gray-700 shadow-xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-700">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/20 rounded-lg">
<Settings className="w-5 h-5 text-blue-400" />
</div>
<div>
<h2 className="font-semibold text-white">Assistant Settings</h2>
<p className="text-sm text-gray-400">Customize your trading assistant</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-700">
{[
{ id: 'general', label: 'General', icon: Bot },
{ id: 'risk', label: 'Risk', icon: Shield },
{ id: 'notifications', label: 'Alerts', icon: Bell },
{ id: 'advanced', label: 'Advanced', icon: Sliders },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
activeTab === tab.id
? 'text-blue-400 border-b-2 border-blue-400 bg-blue-500/5'
: 'text-gray-400 hover:text-white hover:bg-gray-800/50'
}`}
>
<tab.icon className="w-4 h-4" />
{tab.label}
</button>
))}
</div>
{/* Content */}
<div className="p-4 space-y-4 max-h-[400px] overflow-y-auto">
{/* General Tab */}
{activeTab === 'general' && (
<div className="space-y-4">
{/* Response Style */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Response Style
</label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: 'concise', label: 'Concise', desc: 'Brief answers' },
{ id: 'detailed', label: 'Detailed', desc: 'Full explanations' },
{ id: 'technical', label: 'Technical', desc: 'Expert level' },
].map((style) => (
<button
key={style.id}
onClick={() =>
setSettings((prev) => ({
...prev,
responseStyle: style.id as AssistantSettings['responseStyle'],
}))
}
className={`p-3 rounded-lg border text-left transition-colors ${
settings.responseStyle === style.id
? 'border-blue-500 bg-blue-500/10'
: 'border-gray-700 hover:border-gray-600'
}`}
>
<p className="text-sm font-medium text-white">{style.label}</p>
<p className="text-xs text-gray-500">{style.desc}</p>
</button>
))}
</div>
</div>
{/* Toggles */}
<div className="space-y-3">
<Toggle
label="Include Charts"
description="Show visual charts in responses"
icon={TrendingUp}
checked={settings.includeCharts}
onChange={(v) => setSettings((prev) => ({ ...prev, includeCharts: v }))}
/>
<Toggle
label="Include Trading Signals"
description="Generate buy/sell signals when relevant"
icon={Zap}
checked={settings.includeSignals}
onChange={(v) => setSettings((prev) => ({ ...prev, includeSignals: v }))}
/>
<Toggle
label="Auto-Analyze Symbols"
description="Automatically analyze mentioned symbols"
icon={Bot}
checked={settings.autoAnalyze}
onChange={(v) => setSettings((prev) => ({ ...prev, autoAnalyze: v }))}
/>
</div>
</div>
)}
{/* Risk Tab */}
{activeTab === 'risk' && (
<div className="space-y-4">
{/* Risk Profile */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Risk Profile
</label>
<div className="grid grid-cols-3 gap-2">
{[
{ id: 'conservative', label: 'Conservative', color: 'green' },
{ id: 'moderate', label: 'Moderate', color: 'yellow' },
{ id: 'aggressive', label: 'Aggressive', color: 'red' },
].map((profile) => (
<button
key={profile.id}
onClick={() =>
setSettings((prev) => ({
...prev,
riskProfile: profile.id as AssistantSettings['riskProfile'],
}))
}
className={`p-3 rounded-lg border transition-colors ${
settings.riskProfile === profile.id
? `border-${profile.color}-500 bg-${profile.color}-500/10`
: 'border-gray-700 hover:border-gray-600'
}`}
>
<p className="text-sm font-medium text-white">{profile.label}</p>
</button>
))}
</div>
</div>
{/* Max Position Size */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Max Position Size (% of account)
</label>
<input
type="range"
min="0.5"
max="10"
step="0.5"
value={settings.maxPositionSize}
onChange={(e) =>
setSettings((prev) => ({
...prev,
maxPositionSize: parseFloat(e.target.value),
}))
}
className="w-full"
/>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>0.5%</span>
<span className="text-white font-medium">{settings.maxPositionSize}%</span>
<span>10%</span>
</div>
</div>
{/* Preferred Timeframes */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Preferred 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 ${
settings.preferredTimeframes.includes(tf)
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
>
{tf}
</button>
))}
</div>
</div>
</div>
)}
{/* Notifications Tab */}
{activeTab === 'notifications' && (
<div className="space-y-3">
<Toggle
label="Signal Notifications"
description="Get notified when new signals are generated"
icon={Zap}
checked={settings.signalNotifications}
onChange={(v) => setSettings((prev) => ({ ...prev, signalNotifications: v }))}
/>
<Toggle
label="Analysis Updates"
description="Get notified when analysis is complete"
icon={TrendingUp}
checked={settings.analysisNotifications}
onChange={(v) => setSettings((prev) => ({ ...prev, analysisNotifications: v }))}
/>
<Toggle
label="Price Alerts"
description="Get notified when prices hit targets"
icon={Bell}
checked={settings.priceAlerts}
onChange={(v) => setSettings((prev) => ({ ...prev, priceAlerts: v }))}
/>
</div>
)}
{/* Advanced Tab */}
{activeTab === 'advanced' && (
<div className="space-y-4">
<Toggle
label="Stream Responses"
description="Show responses as they're generated"
icon={MessageSquare}
checked={settings.streamResponses}
onChange={(v) => setSettings((prev) => ({ ...prev, streamResponses: v }))}
/>
<Toggle
label="Remember Context"
description="Keep conversation context between sessions"
icon={Clock}
checked={settings.rememberContext}
onChange={(v) => setSettings((prev) => ({ ...prev, rememberContext: v }))}
/>
{/* Max History Messages */}
<div>
<label className="block text-sm font-medium text-white mb-2">
Max History Messages
</label>
<select
value={settings.maxHistoryMessages}
onChange={(e) =>
setSettings((prev) => ({
...prev,
maxHistoryMessages: parseInt(e.target.value),
}))
}
className="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
>
<option value={20}>20 messages</option>
<option value={50}>50 messages</option>
<option value={100}>100 messages</option>
<option value={200}>200 messages</option>
</select>
<p className="text-xs text-gray-500 mt-1">
Older messages will be summarized to save context
</p>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between p-4 border-t border-gray-700 bg-gray-800/50">
<button
onClick={handleReset}
className="flex items-center gap-2 px-4 py-2 text-sm text-gray-400 hover:text-white transition-colors"
>
<RotateCcw className="w-4 h-4" />
Reset to Defaults
</button>
<div className="flex items-center gap-2">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-gray-300 hover:text-white transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
disabled={isSaving}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm transition-colors disabled:opacity-50"
>
{isSaving ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-4 h-4" />
Save Settings
</>
)}
</button>
</div>
</div>
</div>
</div>
);
};
// Toggle Component
interface ToggleProps {
label: string;
description: string;
icon: React.FC<{ className?: string }>;
checked: boolean;
onChange: (value: boolean) => void;
}
const Toggle: React.FC<ToggleProps> = ({
label,
description,
icon: Icon,
checked,
onChange,
}) => (
<div className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
<div className="flex items-center gap-3">
<Icon className="w-5 h-5 text-gray-400" />
<div>
<p className="text-sm font-medium text-white">{label}</p>
<p className="text-xs text-gray-500">{description}</p>
</div>
</div>
<button
onClick={() => onChange(!checked)}
className={`relative w-11 h-6 rounded-full transition-colors ${
checked ? 'bg-blue-600' : 'bg-gray-700'
}`}
>
<div
className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
checked ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
);
export default AssistantSettingsPanel;