'use client'; import { useExtractTOCItems } from "../../useTOCItems"; import { TableOfContents } from "./tableOfContents"; import { Button } from "@/components/ui/button"; import { TableOfContentsIcon, ThumbsDown, ThumbsUp } from "lucide-react"; import { Separator } from "@/components/ui/separator"; import { MarkdownRenderer } from "./markdownRenderer"; import { forwardRef, memo, useCallback, useImperativeHandle, useRef, useState } from "react"; import { Toggle } from "@/components/ui/toggle"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { CopyIconButton } from "@/app/[domain]/components/copyIconButton"; import { useToast } from "@/components/hooks/use-toast"; import { convertLLMOutputToPortableMarkdown } from "../../utils"; import { submitFeedback } from "../../actions"; import { isServiceError } from "@/lib/utils"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { LangfuseWeb } from "langfuse"; import { env } from "@sourcebot/shared/client"; import isEqual from "fast-deep-equal/react"; interface AnswerCardProps { answerText: string; messageId: string; chatId: string; traceId?: string; } const langfuseWeb = (env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT !== undefined && env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY) ? new LangfuseWeb({ publicKey: env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY, baseUrl: env.NEXT_PUBLIC_LANGFUSE_BASE_URL, }) : null; const AnswerCardComponent = forwardRef(({ answerText, messageId, chatId, traceId, }, forwardedRef) => { const markdownRendererRef = useRef(null); const { tocItems, activeId } = useExtractTOCItems({ target: markdownRendererRef.current }); const [isTOCButtonToggled, setIsTOCButtonToggled] = useState(false); const { toast } = useToast(); const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false); const [feedback, setFeedback] = useState<'like' | 'dislike' | undefined>(undefined); const captureEvent = useCaptureEvent(); useImperativeHandle( forwardedRef, () => markdownRendererRef.current as HTMLDivElement ); const onCopyAnswer = useCallback(() => { const markdownText = convertLLMOutputToPortableMarkdown(answerText); navigator.clipboard.writeText(markdownText); toast({ description: "✅ Copied to clipboard", }); return true; }, [answerText, toast]); const onFeedback = useCallback(async (feedbackType: 'like' | 'dislike') => { setIsSubmittingFeedback(true); const response = await submitFeedback({ chatId, messageId, feedbackType }); if (isServiceError(response)) { toast({ description: `❌ Failed to submit feedback: ${response.message}`, variant: "destructive" }); } else { toast({ description: `✅ Feedback submitted`, }); setFeedback(feedbackType); captureEvent('wa_chat_feedback_submitted', { feedback: feedbackType, chatId, messageId, }); langfuseWeb?.score({ traceId: traceId, name: 'user_feedback', value: feedbackType === 'like' ? 1 : 0, }) } setIsSubmittingFeedback(false); }, [chatId, messageId, toast, captureEvent, traceId]); return (
{(isTOCButtonToggled && tocItems.length > 0) && ( )}

Answer

Copy answer {tocItems.length > 0 && ( Toggle table of contents )}
) }) AnswerCardComponent.displayName = 'AnswerCard'; export const AnswerCard = memo(AnswerCardComponent, isEqual);