diff --git a/chat/src/components/message-list.tsx b/chat/src/components/message-list.tsx index c97e803..ff6f85c 100644 --- a/chat/src/components/message-list.tsx +++ b/chat/src/components/message-list.tsx @@ -1,6 +1,6 @@ "use client"; -import { useLayoutEffect, useRef, useEffect, useCallback } from "react"; +import React, {useLayoutEffect, useRef, useEffect, useCallback, useMemo} from "react"; interface Message { role: string; @@ -18,7 +18,12 @@ interface MessageListProps { messages: (Message | DraftMessage)[]; } -export default function MessageList({ messages }: MessageListProps) { +interface ProcessedMessageProps { + messageContent: string; + index: number; +} + +export default function MessageList({messages}: MessageListProps) { const scrollAreaRef = useRef(null); // Track if user is at bottom - default to true for initial scroll @@ -32,6 +37,27 @@ export default function MessageList({ messages }: MessageListProps) { return scrollTop + clientHeight >= scrollHeight - 10; // 10px tolerance }, []); + // Track Ctrl (Windows/Linux) or Cmd (Mac) key state + // This is so that underline is only visible when hover + cmd/ctrl + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey || e.metaKey) document.documentElement.classList.add('modifier-pressed'); + }; + const handleKeyUp = (e: KeyboardEvent) => { + if (!e.ctrlKey && !e.metaKey) document.documentElement.classList.remove('modifier-pressed'); + }; + + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + document.documentElement.classList.remove('modifier-pressed'); + + }; + }, []); + // Update isAtBottom on scroll useEffect(() => { const scrollContainer = scrollAreaRef.current; @@ -94,7 +120,7 @@ export default function MessageList({ messages }: MessageListProps) {
- {messages.map((message) => ( + {messages.map((message, index) => (
) : ( - message.content.trimEnd() + )}
@@ -142,3 +171,42 @@ const LoadingDots = () => ( Loading...
); + + +const ProcessedMessage = React.memo(function ProcessedMessage({ + messageContent, + index, + }: ProcessedMessageProps) { + // Regex to find URLs + // https://stackoverflow.com/a/17773849 + const urlRegex = useMemo(() => /(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, []); + + const handleClick = (e: React.MouseEvent, url: string) => { + if (e.metaKey || e.ctrlKey) { + window.open(url, "_blank"); + } else { + e.preventDefault(); // disable normal click to emulate terminal behaviour + } + } + + const linkedContent = useMemo(() => { + return messageContent.split(urlRegex).map((content, idx) => { + console.log(content) + if (urlRegex.test(content)) { + return ( + handleClick(e, content)} + className="cursor-default [.modifier-pressed_&]:hover:underline [.modifier-pressed_&]:hover:cursor-pointer" + > + {content} + + ); + } + return {content}; + }); + }, [index, messageContent, urlRegex]); + + return <>{linkedContent}; +}); \ No newline at end of file