mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
Support sharing links to individual messages (#177)
This commit is contained in:
parent
6c39ff9c2a
commit
9689177a70
3 changed files with 113 additions and 9 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import { useMemo, useEffect } from 'react';
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Link2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import type { Message } from '@/actions/analytics';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -14,6 +16,8 @@ const PlainTextLink = ({ children }: { children?: React.ReactNode }) => {
|
|||
|
||||
type MessagesProps = {
|
||||
messages: Message[];
|
||||
enableMessageLinks?: boolean;
|
||||
shareToken?: string;
|
||||
};
|
||||
|
||||
type SuggestionItem = string | { answer: string };
|
||||
|
|
@ -40,7 +44,13 @@ const parseQuestionData = (text: string): QuestionData | null => {
|
|||
return null;
|
||||
};
|
||||
|
||||
export const Messages = ({ messages }: MessagesProps) => {
|
||||
export const Messages = ({
|
||||
messages,
|
||||
enableMessageLinks = false,
|
||||
}: MessagesProps) => {
|
||||
const [hoveredMessageId, setHoveredMessageId] = useState<string | null>(null);
|
||||
const [clickedMessageId, setClickedMessageId] = useState<string | null>(null);
|
||||
|
||||
const { containerRef, scrollToBottom, autoScrollToBottom, userHasScrolled } =
|
||||
useAutoScroll<HTMLDivElement>({
|
||||
enabled: true,
|
||||
|
|
@ -69,6 +79,59 @@ export const Messages = ({ messages }: MessagesProps) => {
|
|||
);
|
||||
}, [messages]);
|
||||
|
||||
// Handle anchor link clicks
|
||||
const handleAnchorClick = (messageId: string) => {
|
||||
// Add click animation
|
||||
setClickedMessageId(messageId);
|
||||
setTimeout(() => setClickedMessageId(null), 200);
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
url.hash = `#${messageId}`;
|
||||
|
||||
// Update URL without reload
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
|
||||
// Copy to clipboard with enhanced feedback
|
||||
navigator.clipboard
|
||||
.writeText(url.toString())
|
||||
.then(() => {
|
||||
toast.success('Message link copied to clipboard!', {
|
||||
description: 'Share this link to highlight this specific message',
|
||||
duration: 3000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Failed to copy link', {
|
||||
description: 'Please try again or copy the URL manually',
|
||||
duration: 4000,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Handle URL hash on mount and highlight message
|
||||
useEffect(() => {
|
||||
if (enableMessageLinks && window.location.hash) {
|
||||
const messageId = window.location.hash.substring(1);
|
||||
const element = document.getElementById(messageId);
|
||||
if (element) {
|
||||
// Small delay to ensure the component is fully rendered
|
||||
// Add simple highlight effect immediately
|
||||
element.style.transition = 'background-color 0.3s ease-in-out';
|
||||
element.style.backgroundColor = 'rgba(59, 130, 246, 0.1)';
|
||||
|
||||
// Fade out the highlight after 2 seconds
|
||||
setTimeout(() => {
|
||||
element.style.backgroundColor = '';
|
||||
}, 2000);
|
||||
|
||||
// Delay scroll to allow things to load
|
||||
setTimeout(() => {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}, [enableMessageLinks]);
|
||||
|
||||
// Auto-scroll when new messages arrive or content changes (only if user is at bottom)
|
||||
useEffect(() => {
|
||||
autoScrollToBottom();
|
||||
|
|
@ -92,13 +155,23 @@ export const Messages = ({ messages }: MessagesProps) => {
|
|||
const questionData =
|
||||
isQuestion && message.text ? parseQuestionData(message.text) : null;
|
||||
|
||||
const messageId = `message-${message.id}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
id={messageId}
|
||||
className={cn(
|
||||
'flex flex-col gap-3 rounded-lg p-4',
|
||||
'flex flex-col gap-3 rounded-lg p-4 relative transition-all duration-200',
|
||||
message.role === 'user' ? 'bg-primary/10' : 'bg-secondary/10',
|
||||
enableMessageLinks && 'hover:shadow-sm hover:bg-opacity-80',
|
||||
)}
|
||||
onMouseEnter={() =>
|
||||
enableMessageLinks && setHoveredMessageId(messageId)
|
||||
}
|
||||
onMouseLeave={() =>
|
||||
enableMessageLinks && setHoveredMessageId(null)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between gap-2 text-xs font-medium text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -106,11 +179,32 @@ export const Messages = ({ messages }: MessagesProps) => {
|
|||
<div>·</div>
|
||||
<div>{message.timestamp}</div>
|
||||
</div>
|
||||
{message.mode && (
|
||||
<div className="px-2 py-1 bg-muted rounded text-xs font-medium">
|
||||
{message.mode}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Anchor Link Button */}
|
||||
{enableMessageLinks && hoveredMessageId === messageId && (
|
||||
<button
|
||||
onClick={() => handleAnchorClick(messageId)}
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-muted transition-colors duration-200 cursor-pointer',
|
||||
clickedMessageId === messageId && 'bg-primary/10',
|
||||
)}
|
||||
title="Copy link to this message"
|
||||
type="button"
|
||||
>
|
||||
<Link2
|
||||
className={cn(
|
||||
'h-3 w-3 text-muted-foreground hover:text-primary transition-colors duration-200',
|
||||
clickedMessageId === messageId && 'text-primary',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{message.mode && (
|
||||
<div className="px-2 py-1 bg-muted rounded text-xs font-medium">
|
||||
{message.mode}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isQuestion && questionData ? (
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ export const SharedTaskView = ({
|
|||
sharedBy={sharedBy}
|
||||
sharedAt={sharedAt}
|
||||
showSharedInfo={true}
|
||||
enableMessageLinks={true}
|
||||
shareToken={shareToken}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ type TaskDetailsProps = {
|
|||
sharedAt?: Date;
|
||||
showSharedInfo?: boolean;
|
||||
headerActions?: React.ReactNode;
|
||||
enableMessageLinks?: boolean;
|
||||
shareToken?: string;
|
||||
};
|
||||
|
||||
export const TaskDetails = ({
|
||||
|
|
@ -31,6 +33,8 @@ export const TaskDetails = ({
|
|||
sharedAt,
|
||||
showSharedInfo = false,
|
||||
headerActions,
|
||||
enableMessageLinks = false,
|
||||
shareToken,
|
||||
}: TaskDetailsProps) => {
|
||||
const taskTitle = task.title || generateFallbackTitle(task);
|
||||
|
||||
|
|
@ -152,7 +156,11 @@ export const TaskDetails = ({
|
|||
<CardTitle>Conversation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Messages messages={messages} />
|
||||
<Messages
|
||||
messages={messages}
|
||||
enableMessageLinks={enableMessageLinks}
|
||||
shareToken={shareToken}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue