import React, { useState, useCallback, useMemo, useEffect } from 'react'; import * as Ariakit from '@ariakit/react'; import { TFeedback, TFeedbackTag, getTagsForRating } from 'librechat-data-provider'; import { Button, OGDialog, OGDialogContent, OGDialogTitle, ThumbUpIcon, ThumbDownIcon, } from '@librechat/client'; import { AlertCircle, PenTool, ImageOff, Ban, HelpCircle, CheckCircle, Lightbulb, Search, } from 'lucide-react'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; interface FeedbackProps { handleFeedback: ({ feedback }: { feedback: TFeedback | undefined }) => void; feedback?: TFeedback; isLast?: boolean; } const ICONS = { AlertCircle, PenTool, ImageOff, Ban, HelpCircle, CheckCircle, Lightbulb, Search, ThumbsUp: ThumbUpIcon, ThumbsDown: ThumbDownIcon, }; function FeedbackOptionButton({ tag, active, onClick, }: { tag: TFeedbackTag; active?: boolean; onClick: (e: React.MouseEvent) => void; }) { const localize = useLocalize(); const Icon = ICONS[tag.icon as keyof typeof ICONS] || AlertCircle; const label = localize(tag.label as Parameters[0]); return ( ); } function FeedbackButtons({ isLast, feedback, onFeedback, onOther, }: { isLast: boolean; feedback?: TFeedback; onFeedback: (fb: TFeedback | undefined) => void; onOther?: () => void; }) { const localize = useLocalize(); const upStore = Ariakit.usePopoverStore({ placement: 'bottom' }); const downStore = Ariakit.usePopoverStore({ placement: 'bottom' }); const positiveTags = useMemo(() => getTagsForRating('thumbsUp'), []); const negativeTags = useMemo(() => getTagsForRating('thumbsDown'), []); const upActive = feedback?.rating === 'thumbsUp' ? feedback.tag?.key : undefined; const downActive = feedback?.rating === 'thumbsDown' ? feedback.tag?.key : undefined; const handleThumbsUpClick = useCallback( (e: React.MouseEvent) => { e.preventDefault(); if (feedback?.rating !== 'thumbsUp') { upStore.toggle(); return; } onFeedback(undefined); }, [feedback, onFeedback, upStore], ); const handleUpOption = useCallback( (tag: TFeedbackTag) => (e: React.MouseEvent) => { e.preventDefault(); upStore.hide(); onFeedback({ rating: 'thumbsUp', tag }); if (tag.key === 'other') { onOther?.(); } }, [onFeedback, onOther, upStore], ); const handleThumbsDownClick = useCallback( (e: React.MouseEvent) => { e.preventDefault(); if (feedback?.rating !== 'thumbsDown') { downStore.toggle(); return; } onOther?.(); }, [feedback, onOther, downStore], ); const handleDownOption = useCallback( (tag: TFeedbackTag) => (e: React.MouseEvent) => { e.preventDefault(); downStore.hide(); onFeedback({ rating: 'thumbsDown', tag }); if (tag.key === 'other') { onOther?.(); } }, [onFeedback, onOther, downStore], ); return ( <> } />
{positiveTags.map((tag) => ( ))}
} />
{negativeTags.map((tag) => ( ))}
); } function buttonClasses(isActive: boolean, isLast: boolean) { return cn( 'hover-button rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200', 'hover:text-text-primary hover:bg-surface-hover', 'md:group-hover:visible md:group-focus-within:visible md:group-[.final-completion]:visible', !isLast && 'md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100', 'focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white focus-visible:outline-none', isActive && 'active text-text-primary bg-surface-hover', ); } export default function Feedback({ isLast = false, handleFeedback, feedback: initialFeedback, }: FeedbackProps) { const localize = useLocalize(); const [openDialog, setOpenDialog] = useState(false); const [feedback, setFeedback] = useState(initialFeedback); useEffect(() => { setFeedback(initialFeedback); }, [initialFeedback]); const propagateMinimal = useCallback( (fb: TFeedback | undefined) => { setFeedback(fb); handleFeedback({ feedback: fb }); }, [handleFeedback], ); const handleButtonFeedback = useCallback( (fb: TFeedback | undefined) => { if (fb?.tag?.key === 'other') setOpenDialog(true); else setOpenDialog(false); propagateMinimal(fb); }, [propagateMinimal], ); const handleOtherOpen = useCallback(() => setOpenDialog(true), []); const handleTextChange = (e: React.ChangeEvent) => { setFeedback((prev) => (prev ? { ...prev, text: e.target.value } : undefined)); }; const handleDialogSave = useCallback(() => { if (feedback?.tag?.key === 'other' && !feedback?.text?.trim()) { return; } propagateMinimal(feedback); setOpenDialog(false); }, [feedback, propagateMinimal]); const handleDialogClear = useCallback(() => { setFeedback(undefined); handleFeedback({ feedback: undefined }); setOpenDialog(false); }, [handleFeedback]); const renderSingleFeedbackButton = () => { if (!feedback) return null; const isThumbsUp = feedback.rating === 'thumbsUp'; const Icon = isThumbsUp ? ThumbUpIcon : ThumbDownIcon; const label = isThumbsUp ? localize('com_ui_feedback_positive') : localize('com_ui_feedback_negative'); return ( ); }; return ( <> {feedback ? ( renderSingleFeedbackButton() ) : ( )} {localize('com_ui_feedback_more_information')}