From bfaf76ccf89f6516aae83cf2ce204b12d51210cd Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 10:48:47 -0600 Subject: [PATCH] [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 --- .../components/AssistantSettingsPanel.tsx | 453 ++++++++++++++++++ .../assistant/components/MessageFeedback.tsx | 261 ++++++++++ .../components/StreamingIndicator.tsx | 222 +++++++++ .../assistant/components/ToolCallCard.tsx | 304 ++++++++++++ src/modules/assistant/components/index.ts | 20 +- 5 files changed, 1259 insertions(+), 1 deletion(-) create mode 100644 src/modules/assistant/components/AssistantSettingsPanel.tsx create mode 100644 src/modules/assistant/components/MessageFeedback.tsx create mode 100644 src/modules/assistant/components/StreamingIndicator.tsx create mode 100644 src/modules/assistant/components/ToolCallCard.tsx diff --git a/src/modules/assistant/components/AssistantSettingsPanel.tsx b/src/modules/assistant/components/AssistantSettingsPanel.tsx new file mode 100644 index 0000000..f345bbd --- /dev/null +++ b/src/modules/assistant/components/AssistantSettingsPanel.tsx @@ -0,0 +1,453 @@ +/** + * 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; + 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 = ({ + settings: initialSettings, + onSave, + onClose, + isOpen, +}) => { + const [settings, setSettings] = useState(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 ( +
+
+ {/* Header */} +
+
+
+ +
+
+

Assistant Settings

+

Customize your trading assistant

+
+
+ +
+ + {/* Tabs */} +
+ {[ + { 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) => ( + + ))} +
+ + {/* Content */} +
+ {/* General Tab */} + {activeTab === 'general' && ( +
+ {/* Response Style */} +
+ +
+ {[ + { id: 'concise', label: 'Concise', desc: 'Brief answers' }, + { id: 'detailed', label: 'Detailed', desc: 'Full explanations' }, + { id: 'technical', label: 'Technical', desc: 'Expert level' }, + ].map((style) => ( + + ))} +
+
+ + {/* Toggles */} +
+ setSettings((prev) => ({ ...prev, includeCharts: v }))} + /> + setSettings((prev) => ({ ...prev, includeSignals: v }))} + /> + setSettings((prev) => ({ ...prev, autoAnalyze: v }))} + /> +
+
+ )} + + {/* Risk Tab */} + {activeTab === 'risk' && ( +
+ {/* Risk Profile */} +
+ +
+ {[ + { id: 'conservative', label: 'Conservative', color: 'green' }, + { id: 'moderate', label: 'Moderate', color: 'yellow' }, + { id: 'aggressive', label: 'Aggressive', color: 'red' }, + ].map((profile) => ( + + ))} +
+
+ + {/* Max Position Size */} +
+ + + setSettings((prev) => ({ + ...prev, + maxPositionSize: parseFloat(e.target.value), + })) + } + className="w-full" + /> +
+ 0.5% + {settings.maxPositionSize}% + 10% +
+
+ + {/* Preferred Timeframes */} +
+ +
+ {TIMEFRAMES.map((tf) => ( + + ))} +
+
+
+ )} + + {/* Notifications Tab */} + {activeTab === 'notifications' && ( +
+ setSettings((prev) => ({ ...prev, signalNotifications: v }))} + /> + setSettings((prev) => ({ ...prev, analysisNotifications: v }))} + /> + setSettings((prev) => ({ ...prev, priceAlerts: v }))} + /> +
+ )} + + {/* Advanced Tab */} + {activeTab === 'advanced' && ( +
+ setSettings((prev) => ({ ...prev, streamResponses: v }))} + /> + setSettings((prev) => ({ ...prev, rememberContext: v }))} + /> + + {/* Max History Messages */} +
+ + +

+ Older messages will be summarized to save context +

+
+
+ )} +
+ + {/* Footer */} +
+ +
+ + +
+
+
+
+ ); +}; + +// Toggle Component +interface ToggleProps { + label: string; + description: string; + icon: React.FC<{ className?: string }>; + checked: boolean; + onChange: (value: boolean) => void; +} + +const Toggle: React.FC = ({ + label, + description, + icon: Icon, + checked, + onChange, +}) => ( +
+
+ +
+

{label}

+

{description}

+
+
+ +
+); + +export default AssistantSettingsPanel; diff --git a/src/modules/assistant/components/MessageFeedback.tsx b/src/modules/assistant/components/MessageFeedback.tsx new file mode 100644 index 0000000..10d8619 --- /dev/null +++ b/src/modules/assistant/components/MessageFeedback.tsx @@ -0,0 +1,261 @@ +/** + * MessageFeedback Component + * Allows users to rate and provide feedback on assistant messages + */ + +import React, { useState } from 'react'; +import { + ThumbsUp, + ThumbsDown, + MessageSquare, + X, + Send, + Loader2, + Check, + Flag, +} from 'lucide-react'; + +interface MessageFeedbackProps { + messageId: string; + onFeedback?: (messageId: string, feedback: FeedbackData) => Promise; + compact?: boolean; +} + +export interface FeedbackData { + rating: 'positive' | 'negative'; + category?: string; + comment?: string; +} + +const FEEDBACK_CATEGORIES = [ + { id: 'accurate', label: 'Accurate analysis' }, + { id: 'helpful', label: 'Helpful response' }, + { id: 'inaccurate', label: 'Inaccurate information' }, + { id: 'unclear', label: 'Unclear explanation' }, + { id: 'incomplete', label: 'Incomplete answer' }, + { id: 'wrong_signal', label: 'Wrong trading signal' }, + { id: 'other', label: 'Other' }, +]; + +const MessageFeedback: React.FC = ({ + messageId, + onFeedback, + compact = false, +}) => { + const [rating, setRating] = useState<'positive' | 'negative' | null>(null); + const [showForm, setShowForm] = useState(false); + const [category, setCategory] = useState(''); + const [comment, setComment] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitted, setSubmitted] = useState(false); + + const handleRating = async (value: 'positive' | 'negative') => { + setRating(value); + + // For positive feedback, submit immediately without form + if (value === 'positive') { + await submitFeedback(value); + } else { + // For negative feedback, show the form for more details + setShowForm(true); + } + }; + + const submitFeedback = async (feedbackRating: 'positive' | 'negative') => { + if (!onFeedback) { + setSubmitted(true); + return; + } + + setIsSubmitting(true); + try { + await onFeedback(messageId, { + rating: feedbackRating, + category: category || undefined, + comment: comment || undefined, + }); + setSubmitted(true); + setShowForm(false); + } catch (error) { + console.error('Failed to submit feedback:', error); + } finally { + setIsSubmitting(false); + } + }; + + const handleSubmit = async () => { + if (rating) { + await submitFeedback(rating); + } + }; + + const handleCancel = () => { + setShowForm(false); + setRating(null); + setCategory(''); + setComment(''); + }; + + // Already submitted state + if (submitted) { + return ( +
+ + Thanks for your feedback +
+ ); + } + + // Compact mode - just icons + if (compact && !showForm) { + return ( +
+ + +
+ ); + } + + return ( +
+ {/* Rating Buttons */} + {!showForm && ( +
+ Was this helpful? +
+ + +
+
+ )} + + {/* Detailed Feedback Form (for negative feedback) */} + {showForm && ( +
+
+
+ + Tell us what went wrong +
+ +
+ + {/* Categories */} +
+ +
+ {FEEDBACK_CATEGORIES.filter(c => + rating === 'negative' + ? !['accurate', 'helpful'].includes(c.id) + : !['inaccurate', 'unclear', 'incomplete', 'wrong_signal'].includes(c.id) + ).map((cat) => ( + + ))} +
+
+ + {/* Comment */} +
+ +
+ +