1
1
"use client" ;
2
2
3
- import { useLayoutEffect , useRef , useEffect , useCallback } from "react" ;
3
+ import React , { useLayoutEffect , useRef , useEffect , useCallback , useMemo , useState } from "react" ;
4
4
5
5
interface Message {
6
6
role : string ;
@@ -38,6 +38,31 @@ export default function MessageList({ messages }: MessageListProps) {
38
38
return scrollTop + clientHeight >= scrollHeight - 10 ; // 10px tolerance
39
39
} , [ ] ) ;
40
40
41
+ // Regex to find URLs
42
+ // https://stackoverflow.com/a/17773849
43
+ const urlRegex = useMemo < RegExp > ( ( ) => / ( h t t p s ? : \/ \/ (?: w w w \. | (? ! w w w ) ) [ a - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 - ] + [ a - z A - Z 0 - 9 ] \. [ ^ \s ] { 2 , } | w w w \. [ a - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 - ] + [ a - z A - Z 0 - 9 ] \. [ ^ \s ] { 2 , } | h t t p s ? : \/ \/ (?: w w w \. | (? ! w w w ) ) [ a - z A - Z 0 - 9 ] + \. [ ^ \s ] { 2 , } | w w w \. [ a - z A - Z 0 - 9 ] + \. [ ^ \s ] { 2 , } ) / g, [ ] ) ;
44
+
45
+ const [ modifierPressed , setModifierPressed ] = useState ( false ) ;
46
+
47
+ // Track Ctrl (Windows/Linux) or Cmd (Mac) key state
48
+ // This is so that underline is only visible when hover + cmd/ctrl
49
+ useEffect ( ( ) => {
50
+ const handleKeyDown = ( e : KeyboardEvent ) => {
51
+ if ( e . ctrlKey || e . metaKey ) setModifierPressed ( true ) ;
52
+ } ;
53
+ const handleKeyUp = ( e : KeyboardEvent ) => {
54
+ if ( ! e . ctrlKey && ! e . metaKey ) setModifierPressed ( false ) ;
55
+ } ;
56
+
57
+ window . addEventListener ( "keydown" , handleKeyDown ) ;
58
+ window . addEventListener ( "keyup" , handleKeyUp ) ;
59
+
60
+ return ( ) => {
61
+ window . removeEventListener ( "keydown" , handleKeyDown ) ;
62
+ window . removeEventListener ( "keyup" , handleKeyUp ) ;
63
+ } ;
64
+ } , [ ] ) ;
65
+
41
66
// Update isAtBottom on scroll
42
67
useEffect ( ( ) => {
43
68
const scrollContainer = scrollAreaRef . current ;
@@ -92,6 +117,40 @@ export default function MessageList({ messages }: MessageListProps) {
92
117
lastScrollHeightRef . current = currentScrollHeight ;
93
118
} , [ messages ] ) ;
94
119
120
+ const handleClick = ( e : React . MouseEvent < HTMLAnchorElement > , url : string ) => {
121
+ if ( e . metaKey || e . ctrlKey ) {
122
+ window . open ( url , "_blank" ) ;
123
+ } else {
124
+ e . preventDefault ( ) ; // disable normal click to emulate terminal behaviour
125
+ }
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
+
153
+
95
154
// If no messages, show a placeholder
96
155
if ( messages . length === 0 ) {
97
156
return (
@@ -107,7 +166,7 @@ export default function MessageList({ messages }: MessageListProps) {
107
166
className = "p-4 flex flex-col gap-4 max-w-4xl mx-auto"
108
167
style = { { minHeight : contentMinHeight . current } }
109
168
>
110
- { messages . map ( ( message ) => (
169
+ { messages . map ( ( message , index ) => (
111
170
< div
112
171
key = { message . id ?? "draft" }
113
172
className = { `${ message . role === "user" ? "text-right" : "" } ` }
@@ -127,7 +186,7 @@ export default function MessageList({ messages }: MessageListProps) {
127
186
{ message . role !== "user" && message . content === "" ? (
128
187
< LoadingDots />
129
188
) : (
130
- message . content . trimEnd ( )
189
+ buildClickableLinks ( message . content . trimEnd ( ) , index )
131
190
) }
132
191
</ div >
133
192
</ div >
0 commit comments