'use client' import { useEffect, useRef, useState } from 'react' import Link from 'next/link' import { UseChatHelpers } from '@ai-sdk/react' import { IconBookmark as Bookmark, IconMessageCirclePlus as MessageCirclePlus } from '@tabler/icons-react' import { ChatRequestOptions } from 'ai' import { toast } from 'sonner' import { saveNote } from '@/lib/actions/notes' import { captureClient } from '@/lib/analytics/posthog-client' import { useAuthCapabilities } from '@/lib/contexts/app-user-context' import { stripSourceContextBlocks } from '@/lib/render/strip-source-context-blocks' import type { SearchResultItem } from '@/lib/types' import type { UIDataTypes, UIMessage, UIMessageMetadata, UITools } from '@/lib/types/ai' import { useLibrary } from './library/library-context' import { Button } from './ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog' import { CollapsibleMessage } from './collapsible-message' import { MarkdownMessage } from './message' import { MessageActions } from './message-actions' export type AnswerSectionProps = { content: string isOpen: boolean onOpenChange: (open: boolean) => void chatId?: string showActions?: boolean messageId: string metadata?: UIMessageMetadata status?: UseChatHelpers>['status'] reload?: ( messageId: string, options?: ChatRequestOptions ) => Promise citationMaps?: Record> isGuest?: boolean isCloudDeployment?: boolean libraryAvailable?: boolean onQuoteContext?: (text: string) => void } export function AnswerSection({ content, isOpen, onOpenChange, chatId, showActions = true, // Default to true for backward compatibility messageId, metadata, status, reload, citationMaps, isGuest = false, isCloudDeployment = false, libraryAvailable = true, onQuoteContext }: AnswerSectionProps) { const displayContent = stripSourceContextBlocks(content) const contentRef = useRef(null) const [selection, setSelection] = useState<{ text: string top: number left: number } | null>(null) const [isSavingSelection, setIsSavingSelection] = useState(false) const [authPromptOpen, setAuthPromptOpen] = useState(false) const lastTrackedSelectionKeyRef = useRef(null) const { openLibrary, upsertCachedNote } = useLibrary() // Sharing records the chat under the signed-in user, so it needs a real // auth provider (supabase or better-auth), not the anonymous one. The // opt-in flag (ENABLE_SHARE) is folded into this capability server-side // by the root layout, so a prebuilt image can still enable sharing at // runtime; shareChat enforces the same flag again. const capabilities = useAuthCapabilities() const enableShare = capabilities.share && !isGuest const showSelectionSaveButton = libraryAvailable && (!isGuest || isCloudDeployment) const showSelectionDeepDiveButton = Boolean(onQuoteContext) useEffect(() => { if (!selection) return const clearSelection = () => { setSelection(null) lastTrackedSelectionKeyRef.current = null } const handleSelectionChange = () => { const text = window.getSelection()?.toString().trim() if (!text) { clearSelection() } } const handleMouseDown = (event: MouseEvent) => { const target = event.target if (!(target instanceof Node)) return if (contentRef.current?.contains(target)) return if ( target instanceof Element && target.closest('[data-selection-actions]') ) { return } clearSelection() } document.addEventListener('selectionchange', handleSelectionChange) document.addEventListener('mousedown', handleMouseDown) window.addEventListener('scroll', clearSelection, true) return () => { document.removeEventListener('selectionchange', handleSelectionChange) document.removeEventListener('mousedown', handleMouseDown) window.removeEventListener('scroll', clearSelection, true) } }, [selection]) const handleReload = () => { if (reload) { return reload(messageId) } return Promise.resolve(undefined) } const updateSelection = () => { const currentSelection = window.getSelection() const text = currentSelection?.toString().trim() if (!text || !currentSelection || currentSelection.rangeCount === 0) { setSelection(null) lastTrackedSelectionKeyRef.current = null return } const range = currentSelection.getRangeAt(0) const container = contentRef.current if ( !container || !container.contains(range.commonAncestorContainer) || text.length < 2 ) { setSelection(null) lastTrackedSelectionKeyRef.current = null return } const rect = range.getBoundingClientRect() const trackingKey = `${messageId}:${text.length}:${Math.round(rect.top)}:${Math.round(rect.left)}` if (lastTrackedSelectionKeyRef.current !== trackingKey) { lastTrackedSelectionKeyRef.current = trackingKey captureClient('selection_note_actions_shown', { chatId, chars: text.length, isGuest, isCloudDeployment }) } setSelection({ text, top: Math.max(8, rect.top - 42), left: Math.max(8, Math.min(window.innerWidth - 220, rect.left)) }) } async function handleSaveSelection() { if (!selection || isSavingSelection) return captureClient('note_save_clicked', { source: 'selection', chatId, chars: selection.text.length, isGuest, isCloudDeployment }) if (isGuest) { setAuthPromptOpen(true) captureClient('library_auth_prompt_opened', { source: 'selection_save', chatId }) return } setIsSavingSelection(true) try { const result = await saveNote({ content: selection.text, chatId, sourceMessageId: messageId }) if (!result.success) { captureClient('note_save_failed', { source: 'selection', chatId, reason: result.error ?? 'unknown' }) toast.error(result.error ?? 'Failed to save note') return } if (result.note) { upsertCachedNote(result.note) } captureClient('note_saved', { source: 'selection', chatId, chars: selection.text.length }) toast.success('Saved to library', { action: { label: 'Open', onClick: () => { openLibrary() captureClient('library_opened', { source: 'toast' }) } } }) setSelection(null) window.getSelection()?.removeAllRanges() } catch (error) { console.error('Error saving selected note:', error) captureClient('note_save_failed', { source: 'selection', chatId, reason: 'exception' }) toast.error('Failed to save note') } finally { setIsSavingSelection(false) } } function handleQuoteSelection() { if (!selection) return onQuoteContext?.(selection.text) captureClient('selection_followup', { chatId, chars: selection.text.length }) setSelection(null) window.getSelection()?.removeAllRanges() } return ( {displayContent && (
{selection && (showSelectionSaveButton || showSelectionDeepDiveButton) && (
event.preventDefault()} > {showSelectionSaveButton && ( )} {showSelectionDeepDiveButton && ( )}
)}
)}
Save notes to your library Sign in or create an account to save selected text to your Library.
) }