1- import { useMemo , useEffect } from 'react' ;
1+ import { useMemo , useEffect , useState } from 'react' ;
22import ReactMarkdown from 'react-markdown' ;
3+ import { Link2 } from 'lucide-react' ;
4+ import { toast } from 'sonner' ;
35
46import type { Message } from '@/actions/analytics' ;
57import { cn } from '@/lib/utils' ;
@@ -14,6 +16,8 @@ const PlainTextLink = ({ children }: { children?: React.ReactNode }) => {
1416
1517type MessagesProps = {
1618 messages : Message [ ] ;
19+ enableMessageLinks ?: boolean ;
20+ shareToken ?: string ;
1721} ;
1822
1923type SuggestionItem = string | { answer : string } ;
@@ -40,7 +44,13 @@ const parseQuestionData = (text: string): QuestionData | null => {
4044 return null ;
4145} ;
4246
43- export const Messages = ( { messages } : MessagesProps ) => {
47+ export const Messages = ( {
48+ messages,
49+ enableMessageLinks = false ,
50+ } : MessagesProps ) => {
51+ const [ hoveredMessageId , setHoveredMessageId ] = useState < string | null > ( null ) ;
52+ const [ clickedMessageId , setClickedMessageId ] = useState < string | null > ( null ) ;
53+
4454 const { containerRef, scrollToBottom, autoScrollToBottom, userHasScrolled } =
4555 useAutoScroll < HTMLDivElement > ( {
4656 enabled : true ,
@@ -69,6 +79,59 @@ export const Messages = ({ messages }: MessagesProps) => {
6979 ) ;
7080 } , [ messages ] ) ;
7181
82+ // Handle anchor link clicks
83+ const handleAnchorClick = ( messageId : string ) => {
84+ // Add click animation
85+ setClickedMessageId ( messageId ) ;
86+ setTimeout ( ( ) => setClickedMessageId ( null ) , 200 ) ;
87+
88+ const url = new URL ( window . location . href ) ;
89+ url . hash = `#${ messageId } ` ;
90+
91+ // Update URL without reload
92+ window . history . replaceState ( null , '' , url . toString ( ) ) ;
93+
94+ // Copy to clipboard with enhanced feedback
95+ navigator . clipboard
96+ . writeText ( url . toString ( ) )
97+ . then ( ( ) => {
98+ toast . success ( 'Message link copied to clipboard!' , {
99+ description : 'Share this link to highlight this specific message' ,
100+ duration : 3000 ,
101+ } ) ;
102+ } )
103+ . catch ( ( ) => {
104+ toast . error ( 'Failed to copy link' , {
105+ description : 'Please try again or copy the URL manually' ,
106+ duration : 4000 ,
107+ } ) ;
108+ } ) ;
109+ } ;
110+
111+ // Handle URL hash on mount and highlight message
112+ useEffect ( ( ) => {
113+ if ( enableMessageLinks && window . location . hash ) {
114+ const messageId = window . location . hash . substring ( 1 ) ;
115+ const element = document . getElementById ( messageId ) ;
116+ if ( element ) {
117+ // Small delay to ensure the component is fully rendered
118+ // Add simple highlight effect immediately
119+ element . style . transition = 'background-color 0.3s ease-in-out' ;
120+ element . style . backgroundColor = 'rgba(59, 130, 246, 0.1)' ;
121+
122+ // Fade out the highlight after 2 seconds
123+ setTimeout ( ( ) => {
124+ element . style . backgroundColor = '' ;
125+ } , 2000 ) ;
126+
127+ // Delay scroll to allow things to load
128+ setTimeout ( ( ) => {
129+ element . scrollIntoView ( { behavior : 'smooth' , block : 'start' } ) ;
130+ } , 1000 ) ;
131+ }
132+ }
133+ } , [ enableMessageLinks ] ) ;
134+
72135 // Auto-scroll when new messages arrive or content changes (only if user is at bottom)
73136 useEffect ( ( ) => {
74137 autoScrollToBottom ( ) ;
@@ -92,25 +155,56 @@ export const Messages = ({ messages }: MessagesProps) => {
92155 const questionData =
93156 isQuestion && message . text ? parseQuestionData ( message . text ) : null ;
94157
158+ const messageId = `message-${ message . id } ` ;
159+
95160 return (
96161 < div
97162 key = { message . id }
163+ id = { messageId }
98164 className = { cn (
99- 'flex flex-col gap-3 rounded-lg p-4' ,
165+ 'flex flex-col gap-3 rounded-lg p-4 relative transition-all duration-200 ' ,
100166 message . role === 'user' ? 'bg-primary/10' : 'bg-secondary/10' ,
167+ enableMessageLinks && 'hover:shadow-sm hover:bg-opacity-80' ,
101168 ) }
169+ onMouseEnter = { ( ) =>
170+ enableMessageLinks && setHoveredMessageId ( messageId )
171+ }
172+ onMouseLeave = { ( ) =>
173+ enableMessageLinks && setHoveredMessageId ( null )
174+ }
102175 >
103176 < div className = "flex flex-row items-center justify-between gap-2 text-xs font-medium text-muted-foreground" >
104177 < div className = "flex items-center gap-2" >
105178 < div > { message . name } </ div >
106179 < div > ·</ div >
107180 < div > { message . timestamp } </ div >
108181 </ div >
109- { message . mode && (
110- < div className = "px-2 py-1 bg-muted rounded text-xs font-medium" >
111- { message . mode }
112- </ div >
113- ) }
182+ < div className = "flex items-center gap-2" >
183+ { /* Anchor Link Button */ }
184+ { enableMessageLinks && hoveredMessageId === messageId && (
185+ < button
186+ onClick = { ( ) => handleAnchorClick ( messageId ) }
187+ className = { cn (
188+ 'p-1 rounded hover:bg-muted transition-colors duration-200 cursor-pointer' ,
189+ clickedMessageId === messageId && 'bg-primary/10' ,
190+ ) }
191+ title = "Copy link to this message"
192+ type = "button"
193+ >
194+ < Link2
195+ className = { cn (
196+ 'h-3 w-3 text-muted-foreground hover:text-primary transition-colors duration-200' ,
197+ clickedMessageId === messageId && 'text-primary' ,
198+ ) }
199+ />
200+ </ button >
201+ ) }
202+ { message . mode && (
203+ < div className = "px-2 py-1 bg-muted rounded text-xs font-medium" >
204+ { message . mode }
205+ </ div >
206+ ) }
207+ </ div >
114208 </ div >
115209
116210 { isQuestion && questionData ? (
0 commit comments