'use client' import { useMemo, useState } from 'react' import { UseChatHelpers } from '@ai-sdk/react' import { IconBookmark as Bookmark, IconCopy as Copy, IconThumbDown as ThumbsDown, IconThumbUp as ThumbsUp } from '@tabler/icons-react' import { toast } from 'sonner' import { saveNote } from '@/lib/actions/notes' import { captureClient } from '@/lib/analytics/posthog-client' import { stripSourceContextBlocks } from '@/lib/render/strip-source-context-blocks' import { stripSpecBlocks } from '@/lib/render/strip-spec-blocks' import type { SearchResultItem } from '@/lib/types' import type { UIDataTypes, UIMessage, UITools } from '@/lib/types/ai' import { cn } from '@/lib/utils' import { processCitations } from '@/lib/utils/citation' import { useLibrary } from './library/library-context' import { SaveNoteAuthDialog } from './library/save-note-auth-dialog' import { Button } from './ui/button' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip' import { ChatShare } from './chat-share' import { RetryButton } from './retry-button' interface MessageActionsProps { message: string messageId: string traceId?: string feedbackScore?: number | null reload?: () => Promise chatId?: string enableShare?: boolean isGuest?: boolean isCloudDeployment?: boolean libraryAvailable?: boolean className?: string status?: UseChatHelpers>['status'] visible?: boolean citationMaps?: Record> } export function MessageActions({ message, messageId, traceId, feedbackScore: initialFeedbackScore, reload, chatId, enableShare, isGuest = false, isCloudDeployment = false, libraryAvailable = true, className, status, visible = true, citationMaps }: MessageActionsProps) { const [feedbackScore, setFeedbackScore] = useState( initialFeedbackScore ?? null ) const [isSavingNote, setIsSavingNote] = useState(false) const [authPromptOpen, setAuthPromptOpen] = useState(false) const { openLibrary, upsertCachedNote } = useLibrary() const mappedMessage = useMemo(() => { if (!message) return '' return processCitations(message, citationMaps || {}) }, [message, citationMaps]) const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false) const isLoading = status === 'submitted' || status === 'streaming' const showSaveButton = libraryAvailable && (!isGuest || isCloudDeployment) // Keep the element mounted during loading to preserve layout; otherwise skip rendering. if (!visible && !isLoading) { return null } async function handleCopy() { await navigator.clipboard.writeText( stripSpecBlocks(stripSourceContextBlocks(mappedMessage)) ) toast.success('Message copied to clipboard') } async function handleSaveNote() { if (isSavingNote) return const content = stripSpecBlocks(stripSourceContextBlocks(mappedMessage)) captureClient('note_save_clicked', { source: 'button', chatId, chars: content.length, isGuest, isCloudDeployment }) if (isGuest) { setAuthPromptOpen(true) captureClient('library_auth_prompt_opened', { source: 'save_button', chatId }) return } setIsSavingNote(true) try { const result = await saveNote({ content, chatId, sourceMessageId: messageId }) if (!result.success) { captureClient('note_save_failed', { source: 'button', chatId, reason: result.error ?? 'unknown' }) toast.error(result.error ?? 'Failed to save note') return } if (result.note) { upsertCachedNote(result.note) } captureClient('note_saved', { source: 'button', chatId, chars: content.length }) toast.success('Saved to library', { action: { label: 'Open', onClick: () => { openLibrary() captureClient('library_opened', { source: 'toast' }) } } }) } catch (error) { console.error('Error saving note:', error) captureClient('note_save_failed', { source: 'button', chatId, reason: 'exception' }) toast.error('Failed to save note') } finally { setIsSavingNote(false) } } async function handleFeedback(score: number) { if (isSubmittingFeedback || !traceId) return captureClient('feedback_control_clicked', { score, chatId, isGuest }) setIsSubmittingFeedback(true) try { const response = await fetch('/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ traceId, score, messageId }) }) if (response.ok) { // The route answers 200 without writing a score when tracing is off, so // this records that the request succeeded, not that a score exists. captureClient('feedback_recorded', { score, chatId, isGuest }) setFeedbackScore(score) toast.success( score === 1 ? 'Thanks for the feedback!' : 'Thanks for letting us know!' ) } else { captureClient('feedback_failed', { score, chatId, isGuest, status: response.status, reason: 'response' }) console.error('Failed to submit feedback') toast.error('Failed to submit feedback') } } catch (error) { captureClient('feedback_failed', { score, chatId, isGuest, reason: 'exception' }) console.error('Error submitting feedback:', error) toast.error('Failed to submit feedback') } finally { setIsSubmittingFeedback(false) } } return ( <>
{reload && } {enableShare && chatId && } {traceId && ( <> {(feedbackScore === null || feedbackScore === 1) && ( )} {(feedbackScore === null || feedbackScore === -1) && ( )} )} {showSaveButton && ( Save to library )}
) }