Skip to content

Commit c15436c

Browse files
committed
feat: add memoized component ProcessedMessage
1 parent b4aec03 commit c15436c

File tree

1 file changed

+52
-33
lines changed

1 file changed

+52
-33
lines changed

chat/src/components/message-list.tsx

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ interface MessageListProps {
1818
messages: (Message | DraftMessage)[];
1919
}
2020

21-
export default function MessageList({ messages }: MessageListProps) {
21+
interface ProcessedMessageProps {
22+
messageContent: string;
23+
index: number;
24+
modifierPressed: boolean;
25+
urlRegex: RegExp;
26+
handleClick: (e: React.MouseEvent<HTMLAnchorElement>, url: string) => void;
27+
}
28+
29+
export default function MessageList({messages}: MessageListProps) {
2230
const scrollAreaRef = useRef<HTMLDivElement>(null);
2331
// Avoid the message list to change its height all the time. It causes some
2432
// flickering in the screen because some messages, as the ones displaying
@@ -34,7 +42,7 @@ export default function MessageList({ messages }: MessageListProps) {
3442

3543
const checkIfAtBottom = useCallback(() => {
3644
if (!scrollAreaRef.current) return false;
37-
const { scrollTop, scrollHeight, clientHeight } = scrollAreaRef.current;
45+
const {scrollTop, scrollHeight, clientHeight} = scrollAreaRef.current;
3846
return scrollTop + clientHeight >= scrollHeight - 10; // 10px tolerance
3947
}, []);
4048

@@ -117,39 +125,13 @@ export default function MessageList({ messages }: MessageListProps) {
117125
lastScrollHeightRef.current = currentScrollHeight;
118126
}, [messages]);
119127

120-
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, url: string) => {
128+
const handleClick = useCallback(() => (e: React.MouseEvent<HTMLAnchorElement>, url: string) => {
121129
if (e.metaKey || e.ctrlKey) {
122130
window.open(url, "_blank");
123131
} else {
124132
e.preventDefault(); // disable normal click to emulate terminal behaviour
125133
}
126-
};
127-
128-
const buildClickableLinks = useCallback((message: string, msg_index: number) => {
129-
const linkedContent = message.split(urlRegex).map((content, index) => {
130-
if (urlRegex.test(content)) {
131-
return (
132-
<a
133-
key={`${msg_index}-${index}`}
134-
href={content}
135-
onClick={(e) => handleClick(e, content)}
136-
className={`${
137-
modifierPressed ? "hover:underline cursor-pointer" : "cursor-default"
138-
}`}
139-
>
140-
{content}
141-
</a>
142-
);
143-
}
144-
return <span key={`${msg_index}-${index}`}>{content}</span>;
145-
})
146-
147-
return <>
148-
{linkedContent}
149-
</>
150-
}, [modifierPressed, urlRegex])
151-
152-
134+
}, []);
153135

154136
// If no messages, show a placeholder
155137
if (messages.length === 0) {
@@ -164,7 +146,7 @@ export default function MessageList({ messages }: MessageListProps) {
164146
<div className="overflow-y-auto flex-1" ref={scrollAreaRef}>
165147
<div
166148
className="p-4 flex flex-col gap-4 max-w-4xl mx-auto"
167-
style={{ minHeight: contentMinHeight.current }}
149+
style={{minHeight: contentMinHeight.current}}
168150
>
169151
{messages.map((message, index) => (
170152
<div
@@ -184,9 +166,15 @@ export default function MessageList({ messages }: MessageListProps) {
184166
}`}
185167
>
186168
{message.role !== "user" && message.content === "" ? (
187-
<LoadingDots />
169+
<LoadingDots/>
188170
) : (
189-
buildClickableLinks(message.content.trimEnd(), index)
171+
<ProcessedMessage
172+
messageContent={message.content}
173+
index={index}
174+
modifierPressed={modifierPressed}
175+
urlRegex={urlRegex}
176+
handleClick={handleClick}
177+
/>
190178
)}
191179
</div>
192180
</div>
@@ -214,3 +202,34 @@ const LoadingDots = () => (
214202
<span className="sr-only">Loading...</span>
215203
</div>
216204
);
205+
206+
207+
const ProcessedMessage = React.memo(function ProcessedMessage({
208+
messageContent,
209+
index,
210+
modifierPressed,
211+
urlRegex,
212+
handleClick
213+
}: ProcessedMessageProps) {
214+
const linkedContent = useMemo(() => {
215+
return messageContent.split(urlRegex).map((content, idx) => {
216+
if (urlRegex.test(content)) {
217+
return (
218+
<a
219+
key={`${index}-${idx}`}
220+
href={content}
221+
onClick={(e) => handleClick(e, content)}
222+
className={`${
223+
modifierPressed ? "hover:underline cursor-pointer" : "cursor-default"
224+
}`}
225+
>
226+
{content}
227+
</a>
228+
);
229+
}
230+
return <span key={`${index}-${idx}`}>{content}</span>;
231+
});
232+
}, [handleClick, index, messageContent, modifierPressed, urlRegex]);
233+
234+
return <>{linkedContent}</>;
235+
});

0 commit comments

Comments
 (0)