Skip to content

Commit 2deb633

Browse files
Memoize components and improve performance
1 parent 318927e commit 2deb633

File tree

12 files changed

+3703
-4254
lines changed

12 files changed

+3703
-4254
lines changed

components/block-actions.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { cn } from '@/lib/utils';
2+
import { CopyIcon, DeltaIcon, RedoIcon, UndoIcon } from './icons';
3+
import { Button } from './ui/button';
4+
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
5+
import { useCopyToClipboard } from 'usehooks-ts';
6+
import { toast } from 'sonner';
7+
import { UIBlock } from './block';
8+
import { memo } from 'react';
9+
10+
interface BlockActionsProps {
11+
block: UIBlock;
12+
handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void;
13+
currentVersionIndex: number;
14+
isCurrentVersion: boolean;
15+
mode: 'read-only' | 'edit' | 'diff';
16+
}
17+
18+
export function PureBlockActions({
19+
block,
20+
handleVersionChange,
21+
currentVersionIndex,
22+
isCurrentVersion,
23+
mode,
24+
}: BlockActionsProps) {
25+
const [_, copyToClipboard] = useCopyToClipboard();
26+
27+
return (
28+
<div className="flex flex-row gap-1">
29+
<Tooltip>
30+
<TooltipTrigger asChild>
31+
<Button
32+
variant="outline"
33+
className="p-2 h-fit dark:hover:bg-zinc-700"
34+
onClick={() => {
35+
copyToClipboard(block.content);
36+
toast.success('Copied to clipboard!');
37+
}}
38+
disabled={block.status === 'streaming'}
39+
>
40+
<CopyIcon size={18} />
41+
</Button>
42+
</TooltipTrigger>
43+
<TooltipContent>Copy to clipboard</TooltipContent>
44+
</Tooltip>
45+
<Tooltip>
46+
<TooltipTrigger asChild>
47+
<Button
48+
variant="outline"
49+
className="p-2 h-fit dark:hover:bg-zinc-700 !pointer-events-auto"
50+
onClick={() => {
51+
handleVersionChange('prev');
52+
}}
53+
disabled={currentVersionIndex === 0 || block.status === 'streaming'}
54+
>
55+
<UndoIcon size={18} />
56+
</Button>
57+
</TooltipTrigger>
58+
<TooltipContent>View Previous version</TooltipContent>
59+
</Tooltip>
60+
<Tooltip>
61+
<TooltipTrigger asChild>
62+
<Button
63+
variant="outline"
64+
className="p-2 h-fit dark:hover:bg-zinc-700 !pointer-events-auto"
65+
onClick={() => {
66+
handleVersionChange('next');
67+
}}
68+
disabled={isCurrentVersion || block.status === 'streaming'}
69+
>
70+
<RedoIcon size={18} />
71+
</Button>
72+
</TooltipTrigger>
73+
<TooltipContent>View Next version</TooltipContent>
74+
</Tooltip>
75+
<Tooltip>
76+
<TooltipTrigger asChild>
77+
<Button
78+
variant="outline"
79+
className={cn(
80+
'p-2 h-fit !pointer-events-auto dark:hover:bg-zinc-700',
81+
{
82+
'bg-muted': mode === 'diff',
83+
},
84+
)}
85+
onClick={() => {
86+
handleVersionChange('toggle');
87+
}}
88+
disabled={block.status === 'streaming' || currentVersionIndex === 0}
89+
>
90+
<DeltaIcon size={18} />
91+
</Button>
92+
</TooltipTrigger>
93+
<TooltipContent>View changes</TooltipContent>
94+
</Tooltip>
95+
</div>
96+
);
97+
}
98+
99+
export const BlockActions = memo(PureBlockActions, (prevProps, nextProps) => {
100+
if (
101+
prevProps.block.status === 'streaming' &&
102+
nextProps.block.status === 'streaming'
103+
) {
104+
return true;
105+
}
106+
107+
return false;
108+
});

components/block-close-button.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { memo, SetStateAction } from 'react';
2+
import { CrossIcon } from './icons';
3+
import { Button } from './ui/button';
4+
import { UIBlock } from './block';
5+
import equal from 'fast-deep-equal';
6+
7+
interface BlockCloseButtonProps {
8+
setBlock: (value: SetStateAction<UIBlock>) => void;
9+
}
10+
11+
export function PureBlockCloseButton({ setBlock }: BlockCloseButtonProps) {
12+
return (
13+
<Button
14+
variant="outline"
15+
className="h-fit p-2 dark:hover:bg-zinc-700"
16+
onClick={() => {
17+
setBlock((currentBlock) => ({
18+
...currentBlock,
19+
isVisible: false,
20+
}));
21+
}}
22+
>
23+
<CrossIcon size={18} />
24+
</Button>
25+
);
26+
}
27+
28+
export const BlockCloseButton = memo(PureBlockCloseButton, () => true);

components/block.tsx

Lines changed: 17 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { formatDistance } from 'date-fns';
99
import { AnimatePresence, motion } from 'framer-motion';
1010
import {
1111
type Dispatch,
12+
memo,
1213
type SetStateAction,
1314
useCallback,
1415
useEffect,
@@ -36,6 +37,9 @@ import { Button } from './ui/button';
3637
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
3738
import { useScrollToBottom } from './use-scroll-to-bottom';
3839
import { VersionFooter } from './version-footer';
40+
import { Markdown } from './markdown';
41+
import { BlockActions } from './block-actions';
42+
import { BlockCloseButton } from './block-close-button';
3943
export interface UIBlock {
4044
title: string;
4145
documentId: string;
@@ -50,7 +54,7 @@ export interface UIBlock {
5054
};
5155
}
5256

53-
export function Block({
57+
export function PureBlock({
5458
chatId,
5559
input,
5660
setInput,
@@ -247,8 +251,6 @@ export function Block({
247251
const { width: windowWidth, height: windowHeight } = useWindowSize();
248252
const isMobile = windowWidth ? windowWidth < 768 : false;
249253

250-
const [_, copyToClipboard] = useCopyToClipboard();
251-
252254
return (
253255
<motion.div
254256
className="flex flex-row h-dvh w-dvw fixed top-0 left-0 z-50 bg-muted"
@@ -401,18 +403,7 @@ export function Block({
401403
>
402404
<div className="p-2 flex flex-row justify-between items-start">
403405
<div className="flex flex-row gap-4 items-start">
404-
<Button
405-
variant="outline"
406-
className="h-fit p-2 dark:hover:bg-zinc-700"
407-
onClick={() => {
408-
setBlock((currentBlock) => ({
409-
...currentBlock,
410-
isVisible: false,
411-
}));
412-
}}
413-
>
414-
<CrossIcon size={18} />
415-
</Button>
406+
<BlockCloseButton setBlock={setBlock} />
416407

417408
<div className="flex flex-col">
418409
<div className="font-medium">
@@ -439,78 +430,13 @@ export function Block({
439430
</div>
440431
</div>
441432

442-
<div className="flex flex-row gap-1">
443-
<Tooltip>
444-
<TooltipTrigger asChild>
445-
<Button
446-
variant="outline"
447-
className="p-2 h-fit dark:hover:bg-zinc-700"
448-
onClick={() => {
449-
copyToClipboard(block.content);
450-
toast.success('Copied to clipboard!');
451-
}}
452-
disabled={block.status === 'streaming'}
453-
>
454-
<CopyIcon size={18} />
455-
</Button>
456-
</TooltipTrigger>
457-
<TooltipContent>Copy to clipboard</TooltipContent>
458-
</Tooltip>
459-
<Tooltip>
460-
<TooltipTrigger asChild>
461-
<Button
462-
variant="outline"
463-
className="p-2 h-fit dark:hover:bg-zinc-700 !pointer-events-auto"
464-
onClick={() => {
465-
handleVersionChange('prev');
466-
}}
467-
disabled={
468-
currentVersionIndex === 0 || block.status === 'streaming'
469-
}
470-
>
471-
<UndoIcon size={18} />
472-
</Button>
473-
</TooltipTrigger>
474-
<TooltipContent>View Previous version</TooltipContent>
475-
</Tooltip>
476-
<Tooltip>
477-
<TooltipTrigger asChild>
478-
<Button
479-
variant="outline"
480-
className="p-2 h-fit dark:hover:bg-zinc-700 !pointer-events-auto"
481-
onClick={() => {
482-
handleVersionChange('next');
483-
}}
484-
disabled={isCurrentVersion || block.status === 'streaming'}
485-
>
486-
<RedoIcon size={18} />
487-
</Button>
488-
</TooltipTrigger>
489-
<TooltipContent>View Next version</TooltipContent>
490-
</Tooltip>
491-
<Tooltip>
492-
<TooltipTrigger asChild>
493-
<Button
494-
variant="outline"
495-
className={cx(
496-
'p-2 h-fit !pointer-events-auto dark:hover:bg-zinc-700',
497-
{
498-
'bg-muted': mode === 'diff',
499-
},
500-
)}
501-
onClick={() => {
502-
handleVersionChange('toggle');
503-
}}
504-
disabled={
505-
block.status === 'streaming' || currentVersionIndex === 0
506-
}
507-
>
508-
<DeltaIcon size={18} />
509-
</Button>
510-
</TooltipTrigger>
511-
<TooltipContent>View changes</TooltipContent>
512-
</Tooltip>
513-
</div>
433+
<BlockActions
434+
block={block}
435+
currentVersionIndex={currentVersionIndex}
436+
handleVersionChange={handleVersionChange}
437+
isCurrentVersion={isCurrentVersion}
438+
mode={mode}
439+
/>
514440
</div>
515441

516442
<div className="prose dark:prose-invert dark:bg-muted bg-background h-full overflow-y-scroll px-4 py-8 md:p-20 !max-w-full pb-40 items-center">
@@ -570,3 +496,7 @@ export function Block({
570496
</motion.div>
571497
);
572498
}
499+
500+
export const Block = memo(PureBlock, (prevProps, nextProps) => {
501+
return false;
502+
});

components/chat-header.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import { Button } from '@/components/ui/button';
1010
import { BetterTooltip } from '@/components/ui/tooltip';
1111
import { PlusIcon, VercelIcon } from './icons';
1212
import { useSidebar } from './ui/sidebar';
13+
import { memo } from 'react';
1314

14-
export function ChatHeader({ selectedModelId }: { selectedModelId: string }) {
15+
export function PureChatHeader({
16+
selectedModelId,
17+
}: {
18+
selectedModelId: string;
19+
}) {
1520
const router = useRouter();
1621
const { open } = useSidebar();
1722

@@ -54,3 +59,7 @@ export function ChatHeader({ selectedModelId }: { selectedModelId: string }) {
5459
</header>
5560
);
5661
}
62+
63+
export const ChatHeader = memo(PureChatHeader, (prevProps, nextProps) => {
64+
return prevProps.selectedModelId === nextProps.selectedModelId;
65+
});

components/chat.tsx

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ import useSWR, { useSWRConfig } from 'swr';
88
import { useWindowSize } from 'usehooks-ts';
99

1010
import { ChatHeader } from '@/components/chat-header';
11-
import { PreviewMessage, ThinkingMessage } from '@/components/message';
12-
import { useScrollToBottom } from '@/components/use-scroll-to-bottom';
1311
import type { Vote } from '@/lib/db/schema';
1412
import { fetcher } from '@/lib/utils';
1513

1614
import { Block, type UIBlock } from './block';
1715
import { BlockStreamHandler } from './block-stream-handler';
1816
import { MultimodalInput } from './multimodal-input';
19-
import { Overview } from './overview';
17+
import { Messages } from './messages';
2018

2119
export function Chat({
2220
id,
@@ -69,48 +67,22 @@ export function Chat({
6967
fetcher,
7068
);
7169

72-
const [messagesContainerRef, messagesEndRef] =
73-
useScrollToBottom<HTMLDivElement>();
74-
7570
const [attachments, setAttachments] = useState<Array<Attachment>>([]);
7671

7772
return (
7873
<>
7974
<div className="flex flex-col min-w-0 h-dvh bg-background">
8075
<ChatHeader selectedModelId={selectedModelId} />
81-
<div
82-
ref={messagesContainerRef}
83-
className="flex flex-col min-w-0 gap-6 flex-1 overflow-y-scroll pt-4"
84-
>
85-
{messages.length === 0 && <Overview />}
86-
87-
{messages.map((message, index) => (
88-
<PreviewMessage
89-
key={message.id}
90-
chatId={id}
91-
message={message}
92-
block={block}
93-
setBlock={setBlock}
94-
isLoading={isLoading && messages.length - 1 === index}
95-
vote={
96-
votes
97-
? votes.find((vote) => vote.messageId === message.id)
98-
: undefined
99-
}
100-
/>
101-
))}
10276

103-
{isLoading &&
104-
messages.length > 0 &&
105-
messages[messages.length - 1].role === 'user' && (
106-
<ThinkingMessage />
107-
)}
77+
<Messages
78+
chatId={id}
79+
block={block}
80+
setBlock={setBlock}
81+
isLoading={isLoading}
82+
votes={votes}
83+
messages={messages}
84+
/>
10885

109-
<div
110-
ref={messagesEndRef}
111-
className="shrink-0 min-w-[24px] min-h-[24px]"
112-
/>
113-
</div>
11486
<form className="flex mx-auto px-4 bg-background pb-4 md:pb-6 gap-2 w-full md:max-w-3xl">
11587
<MultimodalInput
11688
chatId={id}

0 commit comments

Comments
 (0)