Skip to content

Commit 9689177

Browse files
authored
Support sharing links to individual messages (#177)
1 parent 6c39ff9 commit 9689177

File tree

3 files changed

+113
-9
lines changed

3 files changed

+113
-9
lines changed

apps/web/src/app/(authenticated)/usage/Messages.tsx

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { useMemo, useEffect } from 'react';
1+
import { useMemo, useEffect, useState } from 'react';
22
import ReactMarkdown from 'react-markdown';
3+
import { Link2 } from 'lucide-react';
4+
import { toast } from 'sonner';
35

46
import type { Message } from '@/actions/analytics';
57
import { cn } from '@/lib/utils';
@@ -14,6 +16,8 @@ const PlainTextLink = ({ children }: { children?: React.ReactNode }) => {
1416

1517
type MessagesProps = {
1618
messages: Message[];
19+
enableMessageLinks?: boolean;
20+
shareToken?: string;
1721
};
1822

1923
type 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>&middot;</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 ? (

apps/web/src/components/task-sharing/SharedTaskView.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const SharedTaskView = ({
5151
sharedBy={sharedBy}
5252
sharedAt={sharedAt}
5353
showSharedInfo={true}
54+
enableMessageLinks={true}
55+
shareToken={shareToken}
5456
/>
5557
);
5658
};

apps/web/src/components/task-sharing/TaskDetails.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type TaskDetailsProps = {
2222
sharedAt?: Date;
2323
showSharedInfo?: boolean;
2424
headerActions?: React.ReactNode;
25+
enableMessageLinks?: boolean;
26+
shareToken?: string;
2527
};
2628

2729
export const TaskDetails = ({
@@ -31,6 +33,8 @@ export const TaskDetails = ({
3133
sharedAt,
3234
showSharedInfo = false,
3335
headerActions,
36+
enableMessageLinks = false,
37+
shareToken,
3438
}: TaskDetailsProps) => {
3539
const taskTitle = task.title || generateFallbackTitle(task);
3640

@@ -152,7 +156,11 @@ export const TaskDetails = ({
152156
<CardTitle>Conversation</CardTitle>
153157
</CardHeader>
154158
<CardContent>
155-
<Messages messages={messages} />
159+
<Messages
160+
messages={messages}
161+
enableMessageLinks={enableMessageLinks}
162+
shareToken={shareToken}
163+
/>
156164
</CardContent>
157165
</Card>
158166
) : (

0 commit comments

Comments
 (0)