import React, { useState } from 'react'; import { Button } from './ui/button'; import { Copy, ThumbsUp, ThumbsDown, ChevronDown, ChevronUp, Check, X, } from 'lucide-react'; import { Badge } from './ui/badge'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible'; import { Textarea } from './ui/textarea'; import type { Message as MessageType } from '../App'; import { toast } from 'sonner'; import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png'; // ✅ Markdown rendering (NEW) import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; // ✅ NEW: call backend feedback API import { apiFeedback, type User as ApiUser, type LearningMode as ApiLearningMode, type FileType as ApiFileType, type FeedbackRating, } from '../lib/api'; interface MessageProps { message: MessageType; showSenderInfo?: boolean; // For group chat mode // ✅ NEW (recommended) — for logging feedback correctly user?: ApiUser | null; // context (optional but useful for metadata) learningMode?: ApiLearningMode; docType?: ApiFileType | string; // optional: supply refs (if your message.references is not the same as backend refs) refs?: string[]; /** * Optional: provide the user question that led to this assistant message. * Best practice: parent passes a function that finds previous user message. */ getContextUserText?: () => string; } // 反馈标签选项 const FEEDBACK_TAGS: Record = { not_helpful: [ 'Code was incorrect', "Shouldn't have used Memory", "Don't like the personality", "Don't like the style", 'Not factually correct', ], helpful: [ 'Accurate and helpful', 'Clear explanation', 'Good examples', 'Solved my problem', 'Well structured', ], }; export function Message({ message, showSenderInfo = false, // NEW user = null, learningMode, docType, refs, getContextUserText, }: MessageProps) { const [feedback, setFeedback] = useState(null); const [copied, setCopied] = useState(false); const [referencesOpen, setReferencesOpen] = useState(false); const [showFeedbackArea, setShowFeedbackArea] = useState(false); // ✅ unify to backend enum const [feedbackType, setFeedbackType] = useState(null); const [feedbackText, setFeedbackText] = useState(''); const [selectedTags, setSelectedTags] = useState([]); const [submitting, setSubmitting] = useState(false); const isUser = message.role === 'user'; const handleCopy = async () => { await navigator.clipboard.writeText(message.content); setCopied(true); toast.success('Message copied to clipboard'); setTimeout(() => setCopied(false), 2000); }; const handleFeedbackClick = (type: FeedbackRating) => { if (feedback === type) { // clicked same state -> collapse + reset detail setFeedback(null); setShowFeedbackArea(false); setFeedbackType(null); setFeedbackText(''); setSelectedTags([]); return; } // open feedback area setFeedback(type); setFeedbackType(type); setShowFeedbackArea(true); }; const handleFeedbackClose = () => { setShowFeedbackArea(false); setFeedbackType(null); setFeedbackText(''); setSelectedTags([]); // do NOT reset feedback (keeps thumbs state) }; const handleTagToggle = (tag: string) => { setSelectedTags((prev) => prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag], ); }; const handleFeedbackSubmit = async () => { if (!feedbackType) return; // If not logged in / no user, we cannot attribute feedback to a student_id if (!user?.email) { toast.error('Please login to submit feedback.'); return; } const assistantText = (message.content || '').trim(); const userText = (getContextUserText ? getContextUserText() : '').trim(); // refs: prefer explicit refs prop; fallback to message.references const refsToSend = refs && refs.length > 0 ? refs : (message.references ?? []); setSubmitting(true); try { await apiFeedback({ user, rating: feedbackType, assistantMessageId: message.id, // recommended assistantText, userText, tags: selectedTags, comment: feedbackText, refs: refsToSend, learningMode, docType, timestampMs: Date.now(), }); toast.success('Thanks — feedback recorded.'); handleFeedbackClose(); // keep thumbs state } catch (e: any) { console.error('[feedback] submit failed:', e); toast.error(e?.message ? `Feedback failed: ${e.message}` : 'Feedback failed.'); } finally { setSubmitting(false); } }; return (
{/* Avatar */} {showSenderInfo && message.sender ? (
{message.sender.isAI ? ( Clare ) : ( {message.sender.name .split(' ') .map((n) => n[0]) .join('') .toUpperCase()} )}
) : !isUser ? (
Clare
) : null}
{/* Sender name in group chat */} {showSenderInfo && message.sender && (
{message.sender.name} {message.sender.isAI && ( AI )}
)}
{/* ✅ KEY FIX: assistant uses Markdown renderer */} {isUser ? (

{message.content}

) : (

{children}

, // code blocks + inline code styling code: ({ className, children }) => ( {children} ), }} > {message.content}
)}
{/* References */} {message.references && message.references.length > 0 && ( {message.references.map((ref, index) => ( {ref} ))} )} {/* Message Actions */}
{!isUser && ( <> )}
{/* Feedback Area */} {!isUser && showFeedbackArea && feedbackType && (

Tell us more:

{/* Tags */}
{FEEDBACK_TAGS[feedbackType].map((tag) => ( ))}
{/* Comment box */}