trading-platform-frontend-v2/src/modules/assistant/components/MessageFeedback.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

262 lines
8.2 KiB
TypeScript

/**
* 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;