[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>
This commit is contained in:
parent
423be4062c
commit
bfaf76ccf8
453
src/modules/assistant/components/AssistantSettingsPanel.tsx
Normal file
453
src/modules/assistant/components/AssistantSettingsPanel.tsx
Normal file
@ -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<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;
|
||||||
261
src/modules/assistant/components/MessageFeedback.tsx
Normal file
261
src/modules/assistant/components/MessageFeedback.tsx
Normal file
@ -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<void>;
|
||||||
|
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<MessageFeedbackProps> = ({
|
||||||
|
messageId,
|
||||||
|
onFeedback,
|
||||||
|
compact = false,
|
||||||
|
}) => {
|
||||||
|
const [rating, setRating] = useState<'positive' | 'negative' | null>(null);
|
||||||
|
const [showForm, setShowForm] = useState(false);
|
||||||
|
const [category, setCategory] = useState<string>('');
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||||
|
<Check className="w-4 h-4 text-green-400" />
|
||||||
|
<span>Thanks for your feedback</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact mode - just icons
|
||||||
|
if (compact && !showForm) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => handleRating('positive')}
|
||||||
|
disabled={rating === 'positive'}
|
||||||
|
className={`p-1.5 rounded transition-colors ${
|
||||||
|
rating === 'positive'
|
||||||
|
? 'bg-green-500/20 text-green-400'
|
||||||
|
: 'text-gray-500 hover:text-gray-300 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
title="Good response"
|
||||||
|
>
|
||||||
|
<ThumbsUp className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRating('negative')}
|
||||||
|
disabled={rating === 'negative'}
|
||||||
|
className={`p-1.5 rounded transition-colors ${
|
||||||
|
rating === 'negative'
|
||||||
|
? 'bg-red-500/20 text-red-400'
|
||||||
|
: 'text-gray-500 hover:text-gray-300 hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
title="Bad response"
|
||||||
|
>
|
||||||
|
<ThumbsDown className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Rating Buttons */}
|
||||||
|
{!showForm && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-gray-500">Was this helpful?</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => handleRating('positive')}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors ${
|
||||||
|
rating === 'positive'
|
||||||
|
? 'bg-green-500/20 text-green-400 border border-green-500/30'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ThumbsUp className="w-4 h-4" />
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRating('negative')}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm transition-colors ${
|
||||||
|
rating === 'negative'
|
||||||
|
? 'bg-red-500/20 text-red-400 border border-red-500/30'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ThumbsDown className="w-4 h-4" />
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Detailed Feedback Form (for negative feedback) */}
|
||||||
|
{showForm && (
|
||||||
|
<div className="p-4 bg-gray-800 rounded-lg border border-gray-700 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Flag className="w-4 h-4 text-red-400" />
|
||||||
|
<span className="font-medium text-white">Tell us what went wrong</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="p-1 text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Categories */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">Category (optional)</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{FEEDBACK_CATEGORIES.filter(c =>
|
||||||
|
rating === 'negative'
|
||||||
|
? !['accurate', 'helpful'].includes(c.id)
|
||||||
|
: !['inaccurate', 'unclear', 'incomplete', 'wrong_signal'].includes(c.id)
|
||||||
|
).map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => setCategory(category === cat.id ? '' : cat.id)}
|
||||||
|
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
|
||||||
|
category === cat.id
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comment */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm text-gray-400">Additional comments (optional)</label>
|
||||||
|
<div className="relative">
|
||||||
|
<MessageSquare className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
|
||||||
|
<textarea
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
placeholder="Help us improve by sharing more details..."
|
||||||
|
className="w-full pl-10 pr-4 py-2 bg-gray-900 border border-gray-700 rounded-lg text-white placeholder-gray-500 text-sm focus:outline-none focus:border-blue-500 resize-none"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
className="px-4 py-2 text-sm text-gray-300 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
Submitting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
Submit Feedback
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageFeedback;
|
||||||
222
src/modules/assistant/components/StreamingIndicator.tsx
Normal file
222
src/modules/assistant/components/StreamingIndicator.tsx
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* StreamingIndicator Component
|
||||||
|
* Shows various loading and streaming states for the assistant
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Loader2,
|
||||||
|
Bot,
|
||||||
|
Brain,
|
||||||
|
Sparkles,
|
||||||
|
Zap,
|
||||||
|
Search,
|
||||||
|
BarChart3,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
type IndicatorVariant =
|
||||||
|
| 'thinking'
|
||||||
|
| 'analyzing'
|
||||||
|
| 'generating'
|
||||||
|
| 'searching'
|
||||||
|
| 'loading'
|
||||||
|
| 'typing';
|
||||||
|
|
||||||
|
interface StreamingIndicatorProps {
|
||||||
|
variant?: IndicatorVariant;
|
||||||
|
text?: string;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VARIANT_CONFIG: Record<IndicatorVariant, {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
bgColor: string;
|
||||||
|
}> = {
|
||||||
|
thinking: {
|
||||||
|
icon: <Brain className="w-4 h-4" />,
|
||||||
|
text: 'Thinking...',
|
||||||
|
color: 'text-purple-400',
|
||||||
|
bgColor: 'bg-purple-500/20',
|
||||||
|
},
|
||||||
|
analyzing: {
|
||||||
|
icon: <BarChart3 className="w-4 h-4" />,
|
||||||
|
text: 'Analyzing market data...',
|
||||||
|
color: 'text-blue-400',
|
||||||
|
bgColor: 'bg-blue-500/20',
|
||||||
|
},
|
||||||
|
generating: {
|
||||||
|
icon: <Sparkles className="w-4 h-4" />,
|
||||||
|
text: 'Generating response...',
|
||||||
|
color: 'text-yellow-400',
|
||||||
|
bgColor: 'bg-yellow-500/20',
|
||||||
|
},
|
||||||
|
searching: {
|
||||||
|
icon: <Search className="w-4 h-4" />,
|
||||||
|
text: 'Searching...',
|
||||||
|
color: 'text-cyan-400',
|
||||||
|
bgColor: 'bg-cyan-500/20',
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
icon: <Loader2 className="w-4 h-4 animate-spin" />,
|
||||||
|
text: 'Loading...',
|
||||||
|
color: 'text-gray-400',
|
||||||
|
bgColor: 'bg-gray-500/20',
|
||||||
|
},
|
||||||
|
typing: {
|
||||||
|
icon: <Bot className="w-4 h-4" />,
|
||||||
|
text: 'Assistant is typing...',
|
||||||
|
color: 'text-green-400',
|
||||||
|
bgColor: 'bg-green-500/20',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const StreamingIndicator: React.FC<StreamingIndicatorProps> = ({
|
||||||
|
variant = 'thinking',
|
||||||
|
text,
|
||||||
|
compact = false,
|
||||||
|
}) => {
|
||||||
|
const config = VARIANT_CONFIG[variant];
|
||||||
|
const displayText = text || config.text;
|
||||||
|
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<div className={`inline-flex items-center gap-1.5 px-2 py-1 rounded ${config.bgColor}`}>
|
||||||
|
<div className={`${config.color} animate-pulse`}>
|
||||||
|
{config.icon}
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs ${config.color}`}>{displayText}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-start gap-3 p-4">
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className={`flex-shrink-0 w-8 h-8 rounded-full ${config.bgColor} flex items-center justify-center`}>
|
||||||
|
<Bot className={`w-4 h-4 ${config.color}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium text-white">Assistant</span>
|
||||||
|
<div className={`flex items-center gap-1.5 px-2 py-0.5 rounded-full ${config.bgColor}`}>
|
||||||
|
<div className={`${config.color} animate-pulse`}>
|
||||||
|
{config.icon}
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs ${config.color}`}>{displayText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Animated Dots */}
|
||||||
|
<div className="mt-2 flex items-center gap-1">
|
||||||
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full ${config.bgColor} animate-bounce`}
|
||||||
|
style={{ animationDelay: '0ms' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full ${config.bgColor} animate-bounce`}
|
||||||
|
style={{ animationDelay: '150ms' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-2 h-2 rounded-full ${config.bgColor} animate-bounce`}
|
||||||
|
style={{ animationDelay: '300ms' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Typing Dots Animation Component
|
||||||
|
export const TypingDots: React.FC<{ className?: string }> = ({ className = '' }) => (
|
||||||
|
<div className={`flex items-center gap-1 ${className}`}>
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="w-1.5 h-1.5 rounded-full bg-gray-400 animate-bounce"
|
||||||
|
style={{ animationDelay: `${i * 150}ms` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pulse Animation Component
|
||||||
|
export const PulseIndicator: React.FC<{
|
||||||
|
color?: string;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
}> = ({ color = 'bg-green-500', size = 'md' }) => {
|
||||||
|
const sizes = {
|
||||||
|
sm: 'w-2 h-2',
|
||||||
|
md: 'w-3 h-3',
|
||||||
|
lg: 'w-4 h-4',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="relative flex">
|
||||||
|
<span
|
||||||
|
className={`animate-ping absolute inline-flex h-full w-full rounded-full ${color} opacity-75`}
|
||||||
|
/>
|
||||||
|
<span className={`relative inline-flex rounded-full ${sizes[size]} ${color}`} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Processing Steps Component
|
||||||
|
interface ProcessingStep {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: 'pending' | 'active' | 'complete';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProcessingSteps: React.FC<{
|
||||||
|
steps: ProcessingStep[];
|
||||||
|
}> = ({ steps }) => (
|
||||||
|
<div className="flex items-center gap-3 p-3 bg-gray-800/50 rounded-lg">
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<React.Fragment key={step.id}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={`w-5 h-5 rounded-full flex items-center justify-center text-xs font-medium ${
|
||||||
|
step.status === 'complete'
|
||||||
|
? 'bg-green-500 text-white'
|
||||||
|
: step.status === 'active'
|
||||||
|
? 'bg-blue-500 text-white'
|
||||||
|
: 'bg-gray-700 text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{step.status === 'complete' ? (
|
||||||
|
<Zap className="w-3 h-3" />
|
||||||
|
) : step.status === 'active' ? (
|
||||||
|
<Loader2 className="w-3 h-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
index + 1
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-sm ${
|
||||||
|
step.status === 'complete'
|
||||||
|
? 'text-green-400'
|
||||||
|
: step.status === 'active'
|
||||||
|
? 'text-white'
|
||||||
|
: 'text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{step.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{index < steps.length - 1 && (
|
||||||
|
<div
|
||||||
|
className={`flex-1 h-px ${
|
||||||
|
step.status === 'complete' ? 'bg-green-500' : 'bg-gray-700'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default StreamingIndicator;
|
||||||
304
src/modules/assistant/components/ToolCallCard.tsx
Normal file
304
src/modules/assistant/components/ToolCallCard.tsx
Normal file
@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* ToolCallCard Component
|
||||||
|
* Displays tool execution results from LLM agent
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Wrench,
|
||||||
|
Check,
|
||||||
|
X,
|
||||||
|
Loader2,
|
||||||
|
Copy,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
TrendingUp,
|
||||||
|
TrendingDown,
|
||||||
|
DollarSign,
|
||||||
|
BarChart3,
|
||||||
|
Search,
|
||||||
|
AlertTriangle,
|
||||||
|
Clock,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export interface ToolCall {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
arguments: Record<string, unknown>;
|
||||||
|
result?: unknown;
|
||||||
|
status: 'pending' | 'running' | 'success' | 'error';
|
||||||
|
error?: string;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolCallCardProps {
|
||||||
|
toolCall: ToolCall;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOOL_ICONS: Record<string, React.ReactNode> = {
|
||||||
|
get_price: <DollarSign className="w-4 h-4" />,
|
||||||
|
get_quote: <DollarSign className="w-4 h-4" />,
|
||||||
|
analyze_symbol: <BarChart3 className="w-4 h-4" />,
|
||||||
|
get_signal: <TrendingUp className="w-4 h-4" />,
|
||||||
|
create_order: <TrendingUp className="w-4 h-4" />,
|
||||||
|
close_position: <TrendingDown className="w-4 h-4" />,
|
||||||
|
get_portfolio: <BarChart3 className="w-4 h-4" />,
|
||||||
|
search_symbols: <Search className="w-4 h-4" />,
|
||||||
|
backtest: <Clock className="w-4 h-4" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOOL_LABELS: Record<string, string> = {
|
||||||
|
get_price: 'Get Price',
|
||||||
|
get_quote: 'Get Quote',
|
||||||
|
analyze_symbol: 'Analyze Symbol',
|
||||||
|
get_signal: 'Get Signal',
|
||||||
|
create_order: 'Create Order',
|
||||||
|
close_position: 'Close Position',
|
||||||
|
get_portfolio: 'Get Portfolio',
|
||||||
|
search_symbols: 'Search Symbols',
|
||||||
|
backtest: 'Backtest Strategy',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolCallCard: React.FC<ToolCallCardProps> = ({
|
||||||
|
toolCall,
|
||||||
|
compact = false,
|
||||||
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const icon = TOOL_ICONS[toolCall.name] || <Wrench className="w-4 h-4" />;
|
||||||
|
const label = TOOL_LABELS[toolCall.name] || toolCall.name;
|
||||||
|
|
||||||
|
const statusColors = {
|
||||||
|
pending: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
|
||||||
|
running: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||||
|
success: 'bg-green-500/20 text-green-400 border-green-500/30',
|
||||||
|
error: 'bg-red-500/20 text-red-400 border-red-500/30',
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusIcons = {
|
||||||
|
pending: <Clock className="w-3 h-3" />,
|
||||||
|
running: <Loader2 className="w-3 h-3 animate-spin" />,
|
||||||
|
success: <Check className="w-3 h-3" />,
|
||||||
|
error: <X className="w-3 h-3" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
const content = JSON.stringify(toolCall.result, null, 2);
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatResult = (result: unknown): string => {
|
||||||
|
if (result === null || result === undefined) return 'No result';
|
||||||
|
if (typeof result === 'string') return result;
|
||||||
|
if (typeof result === 'number') return result.toLocaleString();
|
||||||
|
return JSON.stringify(result, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderResultPreview = () => {
|
||||||
|
const result = toolCall.result;
|
||||||
|
if (!result) return null;
|
||||||
|
|
||||||
|
// Handle price result
|
||||||
|
if (typeof result === 'object' && result !== null) {
|
||||||
|
const r = result as Record<string, unknown>;
|
||||||
|
|
||||||
|
if ('price' in r || 'bid' in r || 'ask' in r) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
{r.symbol && (
|
||||||
|
<span className="font-medium text-white">{String(r.symbol)}</span>
|
||||||
|
)}
|
||||||
|
{r.price && (
|
||||||
|
<span className="font-mono text-green-400">{Number(r.price).toFixed(5)}</span>
|
||||||
|
)}
|
||||||
|
{r.bid && r.ask && (
|
||||||
|
<span className="text-gray-400">
|
||||||
|
<span className="text-red-400">{Number(r.bid).toFixed(5)}</span>
|
||||||
|
{' / '}
|
||||||
|
<span className="text-green-400">{Number(r.ask).toFixed(5)}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{r.change !== undefined && (
|
||||||
|
<span className={Number(r.change) >= 0 ? 'text-green-400' : 'text-red-400'}>
|
||||||
|
{Number(r.change) >= 0 ? '+' : ''}{Number(r.change).toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle signal result
|
||||||
|
if ('signal' in r || 'direction' in r) {
|
||||||
|
const direction = r.direction || r.signal;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 text-sm">
|
||||||
|
<span className={`px-2 py-0.5 rounded font-medium ${
|
||||||
|
direction === 'BUY' || direction === 'LONG'
|
||||||
|
? 'bg-green-500/20 text-green-400'
|
||||||
|
: direction === 'SELL' || direction === 'SHORT'
|
||||||
|
? 'bg-red-500/20 text-red-400'
|
||||||
|
: 'bg-gray-500/20 text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{String(direction)}
|
||||||
|
</span>
|
||||||
|
{r.confidence && (
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Confidence: <span className="text-white">{Number(r.confidence).toFixed(0)}%</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{r.entry && (
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Entry: <span className="font-mono text-white">{Number(r.entry).toFixed(5)}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle portfolio result
|
||||||
|
if ('balance' in r || 'equity' in r) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
{r.balance && (
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Balance: <span className="text-white font-medium">${Number(r.balance).toLocaleString()}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{r.equity && (
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Equity: <span className={Number(r.equity) >= Number(r.balance || 0) ? 'text-green-400' : 'text-red-400'}>
|
||||||
|
${Number(r.equity).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{r.positions !== undefined && (
|
||||||
|
<span className="text-gray-400">
|
||||||
|
Positions: <span className="text-white">{String(r.positions)}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle array results (e.g., search results)
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return (
|
||||||
|
<span className="text-sm text-gray-400">
|
||||||
|
{result.length} result{result.length !== 1 ? 's' : ''} found
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for simple results
|
||||||
|
return (
|
||||||
|
<span className="text-sm text-gray-300 truncate max-w-[300px]">
|
||||||
|
{formatResult(result).substring(0, 100)}
|
||||||
|
{formatResult(result).length > 100 ? '...' : ''}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (compact) {
|
||||||
|
return (
|
||||||
|
<div className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs ${statusColors[toolCall.status]}`}>
|
||||||
|
{icon}
|
||||||
|
<span>{label}</span>
|
||||||
|
{statusIcons[toolCall.status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`rounded-lg border ${statusColors[toolCall.status]} overflow-hidden`}>
|
||||||
|
{/* Header */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
className="w-full flex items-center justify-between p-3 hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-1.5 rounded ${statusColors[toolCall.status]}`}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium text-white">{label}</span>
|
||||||
|
<span className={`flex items-center gap-1 text-xs px-1.5 py-0.5 rounded ${statusColors[toolCall.status]}`}>
|
||||||
|
{statusIcons[toolCall.status]}
|
||||||
|
{toolCall.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{toolCall.status === 'success' && toolCall.result && (
|
||||||
|
<div className="mt-1">{renderResultPreview()}</div>
|
||||||
|
)}
|
||||||
|
{toolCall.status === 'error' && toolCall.error && (
|
||||||
|
<div className="flex items-center gap-1.5 mt-1 text-sm text-red-400">
|
||||||
|
<AlertTriangle className="w-3 h-3" />
|
||||||
|
<span>{toolCall.error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{toolCall.duration && (
|
||||||
|
<span className="text-xs text-gray-500">{toolCall.duration}ms</span>
|
||||||
|
)}
|
||||||
|
{isExpanded ? (
|
||||||
|
<ChevronUp className="w-4 h-4 text-gray-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Expanded Content */}
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="border-t border-gray-700 p-3 space-y-3">
|
||||||
|
{/* Arguments */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-xs text-gray-500 uppercase tracking-wide">Arguments</span>
|
||||||
|
</div>
|
||||||
|
<pre className="p-2 bg-gray-900/50 rounded text-xs text-gray-300 overflow-x-auto">
|
||||||
|
{JSON.stringify(toolCall.arguments, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Result */}
|
||||||
|
{toolCall.result && (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-xs text-gray-500 uppercase tracking-wide">Result</span>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="flex items-center gap-1 text-xs text-gray-400 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
Copied
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="w-3 h-3" />
|
||||||
|
Copy
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="p-2 bg-gray-900/50 rounded text-xs text-gray-300 overflow-x-auto max-h-40">
|
||||||
|
{formatResult(toolCall.result)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolCallCard;
|
||||||
@ -3,14 +3,32 @@
|
|||||||
* Export all LLM copilot components
|
* Export all LLM copilot components
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Core Chat Components
|
||||||
export { default as ChatMessage } from './ChatMessage';
|
export { default as ChatMessage } from './ChatMessage';
|
||||||
export type { Message } from './ChatMessage';
|
export type { Message } from './ChatMessage';
|
||||||
|
|
||||||
export { default as ChatInput } from './ChatInput';
|
export { default as ChatInput } from './ChatInput';
|
||||||
|
|
||||||
|
// Signal Components
|
||||||
export { default as SignalCard } from './SignalCard';
|
export { default as SignalCard } from './SignalCard';
|
||||||
export type { TradingSignal } from './SignalCard';
|
export type { TradingSignal } from './SignalCard';
|
||||||
|
|
||||||
|
// Conversation Management
|
||||||
export { default as ConversationHistory } from './ConversationHistory';
|
export { default as ConversationHistory } from './ConversationHistory';
|
||||||
|
|
||||||
export { default as ContextPanel } from './ContextPanel';
|
export { default as ContextPanel } from './ContextPanel';
|
||||||
|
|
||||||
|
// Tool Call Display
|
||||||
|
export { default as ToolCallCard } from './ToolCallCard';
|
||||||
|
export type { ToolCall } from './ToolCallCard';
|
||||||
|
|
||||||
|
// Feedback System
|
||||||
|
export { default as MessageFeedback } from './MessageFeedback';
|
||||||
|
export type { FeedbackData } from './MessageFeedback';
|
||||||
|
|
||||||
|
// Loading & Streaming Indicators
|
||||||
|
export { default as StreamingIndicator } from './StreamingIndicator';
|
||||||
|
export { TypingDots, PulseIndicator, ProcessingSteps } from './StreamingIndicator';
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
export { default as AssistantSettingsPanel } from './AssistantSettingsPanel';
|
||||||
|
export type { AssistantSettings } from './AssistantSettingsPanel';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user