Skip to content

Commit 0733ac6

Browse files
authored
Merge branch 'main' into fix/pr-preview-duplicate-comments
2 parents ced842c + ef11f19 commit 0733ac6

File tree

1 file changed

+72
-4
lines changed

1 file changed

+72
-4
lines changed

chat/src/components/message-list.tsx

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useLayoutEffect, useRef, useEffect, useCallback } from "react";
3+
import React, {useLayoutEffect, useRef, useEffect, useCallback, useMemo} from "react";
44

55
interface Message {
66
role: string;
@@ -18,7 +18,12 @@ 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+
}
25+
26+
export default function MessageList({messages}: MessageListProps) {
2227
const scrollAreaRef = useRef<HTMLDivElement>(null);
2328

2429
// Track if user is at bottom - default to true for initial scroll
@@ -32,6 +37,27 @@ export default function MessageList({ messages }: MessageListProps) {
3237
return scrollTop + clientHeight >= scrollHeight - 10; // 10px tolerance
3338
}, []);
3439

40+
// Track Ctrl (Windows/Linux) or Cmd (Mac) key state
41+
// This is so that underline is only visible when hover + cmd/ctrl
42+
useEffect(() => {
43+
const handleKeyDown = (e: KeyboardEvent) => {
44+
if (e.ctrlKey || e.metaKey) document.documentElement.classList.add('modifier-pressed');
45+
};
46+
const handleKeyUp = (e: KeyboardEvent) => {
47+
if (!e.ctrlKey && !e.metaKey) document.documentElement.classList.remove('modifier-pressed');
48+
};
49+
50+
window.addEventListener("keydown", handleKeyDown);
51+
window.addEventListener("keyup", handleKeyUp);
52+
53+
return () => {
54+
window.removeEventListener("keydown", handleKeyDown);
55+
window.removeEventListener("keyup", handleKeyUp);
56+
document.documentElement.classList.remove('modifier-pressed');
57+
58+
};
59+
}, []);
60+
3561
// Update isAtBottom on scroll
3662
useEffect(() => {
3763
const scrollContainer = scrollAreaRef.current;
@@ -94,7 +120,7 @@ export default function MessageList({ messages }: MessageListProps) {
94120
<div className="overflow-y-auto flex-1" ref={scrollAreaRef}>
95121
<div
96122
className="p-4 flex flex-col gap-4 max-w-4xl mx-auto transition-all duration-300 ease-in-out min-h-0">
97-
{messages.map((message) => (
123+
{messages.map((message, index) => (
98124
<div
99125
key={message.id ?? "draft"}
100126
className={`${message.role === "user" ? "text-right" : ""}`}
@@ -114,7 +140,10 @@ export default function MessageList({ messages }: MessageListProps) {
114140
{message.role !== "user" && message.content === "" ? (
115141
<LoadingDots />
116142
) : (
117-
message.content.trimEnd()
143+
<ProcessedMessage
144+
messageContent={message.content}
145+
index={index}
146+
/>
118147
)}
119148
</div>
120149
</div>
@@ -142,3 +171,42 @@ const LoadingDots = () => (
142171
<span className="sr-only">Loading...</span>
143172
</div>
144173
);
174+
175+
176+
const ProcessedMessage = React.memo(function ProcessedMessage({
177+
messageContent,
178+
index,
179+
}: ProcessedMessageProps) {
180+
// Regex to find URLs
181+
// https://stackoverflow.com/a/17773849
182+
const urlRegex = useMemo<RegExp>(() => /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/g, []);
183+
184+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, url: string) => {
185+
if (e.metaKey || e.ctrlKey) {
186+
window.open(url, "_blank");
187+
} else {
188+
e.preventDefault(); // disable normal click to emulate terminal behaviour
189+
}
190+
}
191+
192+
const linkedContent = useMemo(() => {
193+
return messageContent.split(urlRegex).map((content, idx) => {
194+
console.log(content)
195+
if (urlRegex.test(content)) {
196+
return (
197+
<a
198+
key={`${index}-${idx}`}
199+
href={content}
200+
onClick={(e) => handleClick(e, content)}
201+
className="cursor-default [.modifier-pressed_&]:hover:underline [.modifier-pressed_&]:hover:cursor-pointer"
202+
>
203+
{content}
204+
</a>
205+
);
206+
}
207+
return <span key={`${index}-${idx}`}>{content}</span>;
208+
});
209+
}, [index, messageContent, urlRegex]);
210+
211+
return <>{linkedContent}</>;
212+
});

0 commit comments

Comments
 (0)