feat(聊天页面): 实现搜索结果跳转高亮
- 聊天页面支持通过 URL 参数定位消息 - MessageBubble 组件添加高亮状态支持 - 新增消息高亮动画样式,支持亮色/暗色主题 - 跳转后自动滚动到目标消息并高亮闪烁 - 3秒后自动清除高亮效果
This commit is contained in:
parent
57e8631e10
commit
0f8fd2ce1f
@ -30,14 +30,19 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const initialMessage = searchParams.get('message');
|
const initialMessage = searchParams.get('message');
|
||||||
|
const highlightParam = searchParams.get('highlight'); // 搜索高亮参数
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { setOptimizedPrompt } = usePromptOptimizer();
|
const { setOptimizedPrompt } = usePromptOptimizer();
|
||||||
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isNewChat, setIsNewChat] = useState(false);
|
const [isNewChat, setIsNewChat] = useState(false);
|
||||||
const [initialMessageSent, setInitialMessageSent] = useState(false);
|
const [initialMessageSent, setInitialMessageSent] = useState(false);
|
||||||
|
|
||||||
|
// 高亮消息状态
|
||||||
|
const [highlightedMessageId, setHighlightedMessageId] = useState<string | null>(null);
|
||||||
|
|
||||||
// 标题下拉菜单状态
|
// 标题下拉菜单状态
|
||||||
const [titleMenuOpen, setTitleMenuOpen] = useState(false);
|
const [titleMenuOpen, setTitleMenuOpen] = useState(false);
|
||||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||||
@ -128,6 +133,36 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// 处理搜索高亮:滚动到目标消息并高亮
|
||||||
|
useEffect(() => {
|
||||||
|
if (highlightParam && messages.length > 0) {
|
||||||
|
// 查找目标消息
|
||||||
|
const targetMessage = messages.find(m => m.id === highlightParam);
|
||||||
|
if (targetMessage) {
|
||||||
|
// 设置高亮状态
|
||||||
|
setHighlightedMessageId(highlightParam);
|
||||||
|
|
||||||
|
// 延迟滚动,确保 DOM 已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
const messageElement = document.querySelector(`[data-message-id="${highlightParam}"]`);
|
||||||
|
if (messageElement) {
|
||||||
|
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 清除 URL 中的 highlight 参数
|
||||||
|
router.replace(`/chat/${chatId}`, { scroll: false });
|
||||||
|
|
||||||
|
// 3秒后清除高亮状态
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setHighlightedMessageId(null);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [highlightParam, messages, chatId, router]);
|
||||||
|
|
||||||
// 聚焦标题输入框
|
// 聚焦标题输入框
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditingTitle && titleInputRef.current) {
|
if (isEditingTitle && titleInputRef.current) {
|
||||||
@ -595,6 +630,7 @@ export default function ChatPage({ params }: PageProps) {
|
|||||||
onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined}
|
onSaveToNote={message.role === 'assistant' && !isStreaming ? handleSaveToNote : undefined}
|
||||||
onLinkClick={handleLinkClick}
|
onLinkClick={handleLinkClick}
|
||||||
conversationId={chatId}
|
conversationId={chatId}
|
||||||
|
isHighlighted={highlightedMessageId === message.id}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -616,3 +616,36 @@ pre[class*="language-"] {
|
|||||||
[data-theme="dark"] .token.italic {
|
[data-theme="dark"] .token.italic {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========================================
|
||||||
|
消息搜索高亮动画
|
||||||
|
用于搜索跳转后高亮显示目标消息
|
||||||
|
风格:轻量闪烁,类似搜索引擎高亮
|
||||||
|
======================================== */
|
||||||
|
@keyframes messageHighlight {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(255, 235, 120, 0.6);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-highlight {
|
||||||
|
animation: messageHighlight 2.5s ease-out forwards;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色模式下使用更暗的黄色 */
|
||||||
|
[data-theme="dark"] .message-highlight {
|
||||||
|
animation-name: messageHighlightDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes messageHighlightDark {
|
||||||
|
0% {
|
||||||
|
background-color: rgba(255, 200, 50, 0.25);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -56,6 +56,8 @@ interface MessageBubbleProps {
|
|||||||
onLinkClick?: (url: string) => void;
|
onLinkClick?: (url: string) => void;
|
||||||
/** 对话 ID(用于关联笔记来源) */
|
/** 对话 ID(用于关联笔记来源) */
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
|
/** 是否高亮显示(搜索跳转时使用) */
|
||||||
|
isHighlighted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化文件大小
|
// 格式化文件大小
|
||||||
@ -75,7 +77,7 @@ function getDocumentIcon(type: string) {
|
|||||||
return FileText;
|
return FileText;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, onRegenerate, onSaveToNote, onLinkClick, conversationId }: MessageBubbleProps) {
|
export function MessageBubble({ message, user, thinkingContent, isStreaming, error, images, searchImages, searchVideos, uploadedImages, uploadedDocuments, usedTools, pyodideStatus, onRegenerate, onSaveToNote, onLinkClick, conversationId, isHighlighted }: MessageBubbleProps) {
|
||||||
const isUser = message.role === 'user';
|
const isUser = message.role === 'user';
|
||||||
const [thinkingExpanded, setThinkingExpanded] = useState(false);
|
const [thinkingExpanded, setThinkingExpanded] = useState(false);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
@ -138,7 +140,13 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
|||||||
|
|
||||||
if (isUser) {
|
if (isUser) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-end items-start gap-3 mb-8 animate-fade-in group">
|
<div
|
||||||
|
data-message-id={message.id}
|
||||||
|
className={cn(
|
||||||
|
"flex justify-end items-start gap-3 mb-8 animate-fade-in group",
|
||||||
|
isHighlighted && "message-highlight"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="max-w-[70%] relative">
|
<div className="max-w-[70%] relative">
|
||||||
{/* 用户上传的图片 */}
|
{/* 用户上传的图片 */}
|
||||||
{uploadedImages && uploadedImages.length > 0 && (
|
{uploadedImages && uploadedImages.length > 0 && (
|
||||||
@ -227,7 +235,13 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-4 mb-8 animate-fade-in">
|
<div
|
||||||
|
data-message-id={message.id}
|
||||||
|
className={cn(
|
||||||
|
"flex items-start gap-4 mb-8 animate-fade-in",
|
||||||
|
isHighlighted && "message-highlight"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{/* AI 图标 */}
|
{/* AI 图标 */}
|
||||||
<div className="flex-shrink-0 mt-4">
|
<div className="flex-shrink-0 mt-4">
|
||||||
<AILogo size={28} />
|
<AILogo size={28} />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user