- 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>
262 lines
8.2 KiB
TypeScript
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;
|