diff --git a/components/block-actions.tsx b/components/block-actions.tsx new file mode 100644 index 000000000..2eb1a24ee --- /dev/null +++ b/components/block-actions.tsx @@ -0,0 +1,108 @@ +import { cn } from '@/lib/utils'; +import { CopyIcon, DeltaIcon, RedoIcon, UndoIcon } from './icons'; +import { Button } from './ui/button'; +import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; +import { useCopyToClipboard } from 'usehooks-ts'; +import { toast } from 'sonner'; +import { UIBlock } from './block'; +import { memo } from 'react'; + +interface BlockActionsProps { + block: UIBlock; + handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void; + currentVersionIndex: number; + isCurrentVersion: boolean; + mode: 'read-only' | 'edit' | 'diff'; +} + +function PureBlockActions({ + block, + handleVersionChange, + currentVersionIndex, + isCurrentVersion, + mode, +}: BlockActionsProps) { + const [_, copyToClipboard] = useCopyToClipboard(); + + return ( +
+ + + + + Copy to clipboard + + + + + + View Previous version + + + + + + View Next version + + + + + + View changes + +
+ ); +} + +export const BlockActions = memo(PureBlockActions, (prevProps, nextProps) => { + if ( + prevProps.block.status === 'streaming' && + nextProps.block.status === 'streaming' + ) { + return true; + } + + return false; +}); diff --git a/components/block-close-button.tsx b/components/block-close-button.tsx new file mode 100644 index 000000000..620c15187 --- /dev/null +++ b/components/block-close-button.tsx @@ -0,0 +1,28 @@ +import { memo, SetStateAction } from 'react'; +import { CrossIcon } from './icons'; +import { Button } from './ui/button'; +import { UIBlock } from './block'; +import equal from 'fast-deep-equal'; + +interface BlockCloseButtonProps { + setBlock: (value: SetStateAction) => void; +} + +function PureBlockCloseButton({ setBlock }: BlockCloseButtonProps) { + return ( + + ); +} + +export const BlockCloseButton = memo(PureBlockCloseButton, () => true); diff --git a/components/block-messages.tsx b/components/block-messages.tsx new file mode 100644 index 000000000..47f263ff9 --- /dev/null +++ b/components/block-messages.tsx @@ -0,0 +1,71 @@ +import { Dispatch, memo, SetStateAction } from 'react'; +import { UIBlock } from './block'; +import { PreviewMessage } from './message'; +import { useScrollToBottom } from './use-scroll-to-bottom'; +import { Vote } from '@/lib/db/schema'; +import { Message } from 'ai'; + +interface BlockMessagesProps { + chatId: string; + block: UIBlock; + setBlock: Dispatch>; + isLoading: boolean; + votes: Array | undefined; + messages: Array; +} + +function PureBlockMessages({ + chatId, + block, + setBlock, + isLoading, + votes, + messages, +}: BlockMessagesProps) { + const [messagesContainerRef, messagesEndRef] = + useScrollToBottom(); + + return ( +
+ {messages.map((message, index) => ( + vote.messageId === message.id) + : undefined + } + /> + ))} + +
+
+ ); +} + +function areEqual( + prevProps: BlockMessagesProps, + nextProps: BlockMessagesProps, +) { + if ( + prevProps.block.status === 'streaming' && + nextProps.block.status === 'streaming' + ) { + return true; + } + + return false; +} + +export const BlockMessages = memo(PureBlockMessages, areEqual); diff --git a/components/block-stream-handler.tsx b/components/block-stream-handler.tsx index 5402b2c2b..87c648827 100644 --- a/components/block-stream-handler.tsx +++ b/components/block-stream-handler.tsx @@ -9,7 +9,7 @@ interface BlockStreamHandlerProps { streamingData: JSONValue[] | undefined; } -export function PureBlockStreamHandler({ +function PureBlockStreamHandler({ setBlock, streamingData, }: BlockStreamHandlerProps) { diff --git a/components/block.tsx b/components/block.tsx index 9e3f3ac76..7f8cd758e 100644 --- a/components/block.tsx +++ b/components/block.tsx @@ -4,23 +4,18 @@ import type { CreateMessage, Message, } from 'ai'; -import cx from 'classnames'; import { formatDistance } from 'date-fns'; import { AnimatePresence, motion } from 'framer-motion'; import { type Dispatch, + memo, type SetStateAction, useCallback, useEffect, useState, } from 'react'; -import { toast } from 'sonner'; import useSWR, { useSWRConfig } from 'swr'; -import { - useCopyToClipboard, - useDebounceCallback, - useWindowSize, -} from 'usehooks-ts'; +import { useDebounceCallback, useWindowSize } from 'usehooks-ts'; import type { Document, Suggestion, Vote } from '@/lib/db/schema'; import { fetcher } from '@/lib/utils'; @@ -28,14 +23,13 @@ import { fetcher } from '@/lib/utils'; import { DiffView } from './diffview'; import { DocumentSkeleton } from './document-skeleton'; import { Editor } from './editor'; -import { CopyIcon, CrossIcon, DeltaIcon, RedoIcon, UndoIcon } from './icons'; -import { PreviewMessage } from './message'; import { MultimodalInput } from './multimodal-input'; import { Toolbar } from './toolbar'; -import { Button } from './ui/button'; -import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; -import { useScrollToBottom } from './use-scroll-to-bottom'; import { VersionFooter } from './version-footer'; +import { BlockActions } from './block-actions'; +import { BlockCloseButton } from './block-close-button'; +import { BlockMessages } from './block-messages'; + export interface UIBlock { title: string; documentId: string; @@ -50,7 +44,7 @@ export interface UIBlock { }; } -export function Block({ +function PureBlock({ chatId, input, setInput, @@ -89,9 +83,6 @@ export function Block({ chatRequestOptions?: ChatRequestOptions, ) => void; }) { - const [messagesContainerRef, messagesEndRef] = - useScrollToBottom(); - const { data: documents, isLoading: isDocumentsFetching, @@ -247,8 +238,6 @@ export function Block({ const { width: windowWidth, height: windowHeight } = useWindowSize(); const isMobile = windowWidth ? windowWidth < 768 : false; - const [_, copyToClipboard] = useCopyToClipboard(); - return (
-
- {messages.map((message, index) => ( - vote.messageId === message.id) - : undefined - } - /> - ))} - -
-
+
- +
@@ -439,78 +400,13 @@ export function Block({
-
- - - - - Copy to clipboard - - - - - - View Previous version - - - - - - View Next version - - - - - - View changes - -
+
@@ -570,3 +466,7 @@ export function Block({ ); } + +export const Block = memo(PureBlock, (prevProps, nextProps) => { + return false; +}); diff --git a/components/chat-header.tsx b/components/chat-header.tsx index 2e6be7cd3..27e9e7529 100644 --- a/components/chat-header.tsx +++ b/components/chat-header.tsx @@ -10,8 +10,9 @@ import { Button } from '@/components/ui/button'; import { BetterTooltip } from '@/components/ui/tooltip'; import { PlusIcon, VercelIcon } from './icons'; import { useSidebar } from './ui/sidebar'; +import { memo } from 'react'; -export function ChatHeader({ selectedModelId }: { selectedModelId: string }) { +function PureChatHeader({ selectedModelId }: { selectedModelId: string }) { const router = useRouter(); const { open } = useSidebar(); @@ -54,3 +55,7 @@ export function ChatHeader({ selectedModelId }: { selectedModelId: string }) { ); } + +export const ChatHeader = memo(PureChatHeader, (prevProps, nextProps) => { + return prevProps.selectedModelId === nextProps.selectedModelId; +}); diff --git a/components/chat.tsx b/components/chat.tsx index 425ea1513..ad65857e6 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -8,15 +8,13 @@ import useSWR, { useSWRConfig } from 'swr'; import { useWindowSize } from 'usehooks-ts'; import { ChatHeader } from '@/components/chat-header'; -import { PreviewMessage, ThinkingMessage } from '@/components/message'; -import { useScrollToBottom } from '@/components/use-scroll-to-bottom'; import type { Vote } from '@/lib/db/schema'; import { fetcher } from '@/lib/utils'; import { Block, type UIBlock } from './block'; import { BlockStreamHandler } from './block-stream-handler'; import { MultimodalInput } from './multimodal-input'; -import { Overview } from './overview'; +import { Messages } from './messages'; export function Chat({ id, @@ -69,48 +67,22 @@ export function Chat({ fetcher, ); - const [messagesContainerRef, messagesEndRef] = - useScrollToBottom(); - const [attachments, setAttachments] = useState>([]); return ( <>
-
- {messages.length === 0 && } - - {messages.map((message, index) => ( - vote.messageId === message.id) - : undefined - } - /> - ))} - {isLoading && - messages.length > 0 && - messages[messages.length - 1].role === 'user' && ( - - )} + -
-
) => void; } -export function DocumentToolResult({ +function PureDocumentToolResult({ type, result, setBlock, @@ -73,17 +73,15 @@ export function DocumentToolResult({ ); } +export const DocumentToolResult = memo(PureDocumentToolResult, () => true); + interface DocumentToolCallProps { type: 'create' | 'update' | 'request-suggestions'; args: { title: string }; setBlock: (value: SetStateAction) => void; } -export function DocumentToolCall({ - type, - args, - setBlock, -}: DocumentToolCallProps) { +function PureDocumentToolCall({ type, args, setBlock }: DocumentToolCallProps) { return ( - - ))} -
+ )} ); } + +export const MultimodalInput = memo( + PureMultimodalInput, + (prevProps, currentProps) => { + if (prevProps.input !== currentProps.input) return false; + if (prevProps.isLoading !== currentProps.isLoading) return false; + + return true; + }, +); diff --git a/components/sidebar-history.tsx b/components/sidebar-history.tsx index 3147041d5..7b5d5b929 100644 --- a/components/sidebar-history.tsx +++ b/components/sidebar-history.tsx @@ -4,7 +4,7 @@ import { isToday, isYesterday, subMonths, subWeeks } from 'date-fns'; import Link from 'next/link'; import { useParams, usePathname, useRouter } from 'next/navigation'; import type { User } from 'next-auth'; -import { useEffect, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { toast } from 'sonner'; import useSWR from 'swr'; @@ -36,6 +36,7 @@ import { } from '@/components/ui/sidebar'; import type { Chat } from '@/lib/db/schema'; import { fetcher } from '@/lib/utils'; +import equal from 'fast-deep-equal'; type GroupedChats = { today: Chat[]; @@ -45,7 +46,7 @@ type GroupedChats = { older: Chat[]; }; -const ChatItem = ({ +const PureChatItem = ({ chat, isActive, onDelete, @@ -62,6 +63,7 @@ const ChatItem = ({ {chat.title} + ); +export const ChatItem = memo(PureChatItem, (prevProps, nextProps) => { + if (prevProps.isActive !== nextProps.isActive) return false; + return true; +}); + export function SidebarHistory({ user }: { user: User | undefined }) { const { setOpenMobile } = useSidebar(); const { id } = useParams(); diff --git a/components/suggested-actions.tsx b/components/suggested-actions.tsx new file mode 100644 index 000000000..c5614752d --- /dev/null +++ b/components/suggested-actions.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { Button } from './ui/button'; +import { ChatRequestOptions, CreateMessage, Message } from 'ai'; +import { memo } from 'react'; + +interface SuggestedActionsProps { + chatId: string; + append: ( + message: Message | CreateMessage, + chatRequestOptions?: ChatRequestOptions, + ) => Promise; +} + +function PureSuggestedActions({ chatId, append }: SuggestedActionsProps) { + const suggestedActions = [ + { + title: 'What is the weather', + label: 'in San Francisco?', + action: 'What is the weather in San Francisco?', + }, + { + title: 'Help me draft an essay', + label: 'about Silicon Valley', + action: 'Help me draft a short essay about Silicon Valley', + }, + ]; + + return ( +
+ {suggestedActions.map((suggestedAction, index) => ( + 1 ? 'hidden sm:block' : 'block'} + > + + + ))} +
+ ); +} + +export const SuggestedActions = memo(PureSuggestedActions, () => true); diff --git a/components/toolbar.tsx b/components/toolbar.tsx index f129f8d47..571882443 100644 --- a/components/toolbar.tsx +++ b/components/toolbar.tsx @@ -10,6 +10,7 @@ import { } from 'framer-motion'; import { type Dispatch, + memo, type SetStateAction, useEffect, useRef, @@ -32,6 +33,7 @@ import { StopIcon, SummarizeIcon, } from './icons'; +import equal from 'fast-deep-equal'; type ToolProps = { type: 'final-polish' | 'request-suggestions' | 'adjust-reading-level'; @@ -324,7 +326,7 @@ export const Tools = ({ ); }; -export const Toolbar = ({ +const PureToolbar = ({ isToolbarVisible, setIsToolbarVisible, append, @@ -465,3 +467,7 @@ export const Toolbar = ({ ); }; + +export const Toolbar = memo(PureToolbar, (prevProps, nextProps) => { + return equal(prevProps, nextProps); +}); diff --git a/package.json b/package.json index 5df304008..f7a2093d7 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "diff-match-patch": "^1.0.5", "dotenv": "^16.4.5", "drizzle-orm": "^0.34.0", + "fast-deep-equal": "^3.1.3", "framer-motion": "^11.3.19", "geist": "^1.3.1", "lucide-react": "^0.446.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8300147ee..6c76a2467 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: drizzle-orm: specifier: ^0.34.0 version: 0.34.1(@neondatabase/serverless@0.9.5)(@opentelemetry/api@1.9.0)(@types/pg@8.11.6)(@types/react@18.3.12)(@vercel/postgres@0.10.0)(postgres@3.4.5)(react@19.0.0-rc-45804af1-20241021) + fast-deep-equal: + specifier: ^3.1.3 + version: 3.1.3 framer-motion: specifier: ^11.3.19 version: 11.11.10(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) @@ -197,7 +200,7 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-import-resolver-typescript: specifier: ^3.6.3 - version: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1) + version: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-plugin-tailwindcss: specifier: ^3.17.5 version: 3.17.5(tailwindcss@3.4.14) @@ -5233,8 +5236,8 @@ snapshots: '@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.2(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -5257,37 +5260,37 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.3.7 enhanced-resolve: 5.17.1 eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5298,7 +5301,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3