import type { Attachment, ChatRequestOptions, CreateMessage, Message, } from 'ai'; import { formatDistance } from 'date-fns'; import { AnimatePresence, motion } from 'framer-motion'; import { type Dispatch, memo, type SetStateAction, useCallback, useEffect, useState, } from 'react'; import useSWR, { useSWRConfig } from 'swr'; import { useDebounceCallback, useWindowSize } from 'usehooks-ts'; import type { Document, Suggestion, Vote } from '@/lib/db/schema'; import { cn, fetcher } from '@/lib/utils'; import { DiffView } from './diffview'; import { DocumentSkeleton } from './document-skeleton'; import { Editor } from './editor'; import { MultimodalInput } from './multimodal-input'; import { Toolbar } from './toolbar'; import { VersionFooter } from './version-footer'; import { BlockActions } from './block-actions'; import { BlockCloseButton } from './block-close-button'; import { BlockMessages } from './block-messages'; import { CodeEditor } from './code-editor'; import { Console } from './console'; import { useSidebar } from './ui/sidebar'; import { useBlock } from '@/hooks/use-block'; import equal from 'fast-deep-equal'; export type BlockKind = 'text' | 'code'; export interface UIBlock { title: string; documentId: string; kind: BlockKind; content: string; isVisible: boolean; status: 'streaming' | 'idle'; boundingBox: { top: number; left: number; width: number; height: number; }; } export interface ConsoleOutput { id: string; status: 'in_progress' | 'completed' | 'failed'; content: string | null; } function PureBlock({ chatId, input, setInput, handleSubmit, isLoading, stop, attachments, setAttachments, append, messages, setMessages, reload, votes, isReadonly, }: { chatId: string; input: string; setInput: (input: string) => void; isLoading: boolean; stop: () => void; attachments: Array; setAttachments: Dispatch>>; messages: Array; setMessages: Dispatch>>; votes: Array | undefined; append: ( message: Message | CreateMessage, chatRequestOptions?: ChatRequestOptions, ) => Promise; handleSubmit: ( event?: { preventDefault?: () => void; }, chatRequestOptions?: ChatRequestOptions, ) => void; reload: ( chatRequestOptions?: ChatRequestOptions, ) => Promise; isReadonly: boolean; }) { const { block, setBlock } = useBlock(); const { data: documents, isLoading: isDocumentsFetching, mutate: mutateDocuments, } = useSWR>( block.documentId !== 'init' && block.status !== 'streaming' ? `/api/document?id=${block.documentId}` : null, fetcher, ); const { data: suggestions } = useSWR>( documents && block && block.status !== 'streaming' ? `/api/suggestions?documentId=${block.documentId}` : null, fetcher, { dedupingInterval: 5000, }, ); const [mode, setMode] = useState<'edit' | 'diff'>('edit'); const [document, setDocument] = useState(null); const [currentVersionIndex, setCurrentVersionIndex] = useState(-1); const [consoleOutputs, setConsoleOutputs] = useState>( [], ); const { open: isSidebarOpen } = useSidebar(); useEffect(() => { if (documents && documents.length > 0) { const mostRecentDocument = documents.at(-1); if (mostRecentDocument) { setDocument(mostRecentDocument); setCurrentVersionIndex(documents.length - 1); setBlock((currentBlock) => ({ ...currentBlock, content: mostRecentDocument.content ?? '', })); } } }, [documents, setBlock]); useEffect(() => { mutateDocuments(); }, [block.status, mutateDocuments]); const { mutate } = useSWRConfig(); const [isContentDirty, setIsContentDirty] = useState(false); const handleContentChange = useCallback( (updatedContent: string) => { if (!block) return; mutate>( `/api/document?id=${block.documentId}`, async (currentDocuments) => { if (!currentDocuments) return undefined; const currentDocument = currentDocuments.at(-1); if (!currentDocument || !currentDocument.content) { setIsContentDirty(false); return currentDocuments; } if (currentDocument.content !== updatedContent) { await fetch(`/api/document?id=${block.documentId}`, { method: 'POST', body: JSON.stringify({ title: block.title, content: updatedContent, kind: block.kind, }), }); setIsContentDirty(false); const newDocument = { ...currentDocument, content: updatedContent, createdAt: new Date(), }; return [...currentDocuments, newDocument]; } return currentDocuments; }, { revalidate: false }, ); }, [block, mutate], ); const debouncedHandleContentChange = useDebounceCallback( handleContentChange, 2000, ); const saveContent = useCallback( (updatedContent: string, debounce: boolean) => { if (document && updatedContent !== document.content) { setIsContentDirty(true); if (debounce) { debouncedHandleContentChange(updatedContent); } else { handleContentChange(updatedContent); } } }, [document, debouncedHandleContentChange, handleContentChange], ); function getDocumentContentById(index: number) { if (!documents) return ''; if (!documents[index]) return ''; return documents[index].content ?? ''; } const handleVersionChange = (type: 'next' | 'prev' | 'toggle' | 'latest') => { if (!documents) return; if (type === 'latest') { setCurrentVersionIndex(documents.length - 1); setMode('edit'); } if (type === 'toggle') { setMode((mode) => (mode === 'edit' ? 'diff' : 'edit')); } if (type === 'prev') { if (currentVersionIndex > 0) { setCurrentVersionIndex((index) => index - 1); } } else if (type === 'next') { if (currentVersionIndex < documents.length - 1) { setCurrentVersionIndex((index) => index + 1); } } }; const [isToolbarVisible, setIsToolbarVisible] = useState(false); /* * NOTE: if there are no documents, or if * the documents are being fetched, then * we mark it as the current version. */ const isCurrentVersion = documents && documents.length > 0 ? currentVersionIndex === documents.length - 1 : true; const { width: windowWidth, height: windowHeight } = useWindowSize(); const isMobile = windowWidth ? windowWidth < 768 : false; return ( {block.isVisible && ( {!isMobile && ( )} {!isMobile && ( {!isCurrentVersion && ( )}
)}
{document?.title ?? block.title}
{isContentDirty ? (
Saving changes...
) : document ? (
{`Updated ${formatDistance( new Date(document.createdAt), new Date(), { addSuffix: true, }, )}`}
) : (
)}
{isDocumentsFetching && !block.content ? ( ) : block.kind === 'code' ? ( ) : block.kind === 'text' ? ( mode === 'edit' ? ( ) : ( ) ) : null} {suggestions ? (
) : null} {isCurrentVersion && ( )}
{!isCurrentVersion && ( )} )} ); } export const Block = memo(PureBlock, (prevProps, nextProps) => { if (prevProps.isLoading !== nextProps.isLoading) return false; if (!equal(prevProps.votes, nextProps.votes)) return false; if (prevProps.input !== nextProps.input) return false; if (!equal(prevProps.messages, nextProps.messages.length)) return false; return true; });