diff --git a/src/components/features/MessageBubble.tsx b/src/components/features/MessageBubble.tsx index 901fd0c..8c9f3d9 100644 --- a/src/components/features/MessageBubble.tsx +++ b/src/components/features/MessageBubble.tsx @@ -43,11 +43,29 @@ export function MessageBubble({ message, user, thinkingContent, isStreaming, err if (isUser) { return ( -
-
+
+
{message.content}
+ {/* 悬停显示复制按钮 */} +
{user && }
diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 3f42d36..02d1061 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -2,14 +2,14 @@ import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; -import { Plus, PanelLeft, Trash2, MoreHorizontal, Loader2 } from 'lucide-react'; +import { Plus, PanelLeft, Trash2, MoreHorizontal, Loader2, Pencil, Check, X } from 'lucide-react'; import { UserMenu } from '@/components/ui/UserMenu'; import { cn } from '@/lib/utils'; import { useConversations } from '@/hooks/useConversations'; import { useSettings } from '@/hooks/useSettings'; import { useAuth } from '@/providers/AuthProvider'; import type { Conversation } from '@/drizzle/schema'; -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; interface SidebarProps { isOpen?: boolean; @@ -19,11 +19,39 @@ interface SidebarProps { export function Sidebar({ isOpen = true }: SidebarProps) { const pathname = usePathname(); const router = useRouter(); - const { conversations, loading, createConversation, deleteConversation } = useConversations(); + const { conversations, loading, createConversation, deleteConversation, updateConversation } = useConversations(); const { settings } = useSettings(); const { user } = useAuth(); const [creatingChat, setCreatingChat] = useState(false); const [menuOpen, setMenuOpen] = useState(null); + const [editingId, setEditingId] = useState(null); + const [editingTitle, setEditingTitle] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const inputRef = useRef(null); + const menuRef = useRef(null); + + // 聚焦输入框 + useEffect(() => { + if (editingId && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [editingId]); + + // 点击外部关闭下拉菜单 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setMenuOpen(null); + } + }; + if (menuOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [menuOpen]); // 创建新对话 const handleNewChat = async () => { @@ -60,6 +88,47 @@ export function Sidebar({ isOpen = true }: SidebarProps) { } }; + // 开始重命名 + const handleStartRename = (conversation: Conversation, e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setEditingId(conversation.conversationId); + setEditingTitle(conversation.title || ''); + setMenuOpen(null); + }; + + // 提交重命名 + const handleSubmitRename = async (conversationId: string) => { + if (!editingTitle.trim() || isSubmitting) return; + + try { + setIsSubmitting(true); + await updateConversation(conversationId, { title: editingTitle.trim() }); + setEditingId(null); + setEditingTitle(''); + } catch (error) { + console.error('Failed to rename conversation:', error); + } finally { + setIsSubmitting(false); + } + }; + + // 取消重命名 + const handleCancelRename = () => { + setEditingId(null); + setEditingTitle(''); + }; + + // 处理键盘事件 + const handleKeyDown = (e: React.KeyboardEvent, conversationId: string) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleSubmitRename(conversationId); + } else if (e.key === 'Escape') { + handleCancelRename(); + } + }; + // 按时间分组对话 const groupedConversations = groupConversationsByTime(conversations); @@ -111,47 +180,103 @@ export function Sidebar({ isOpen = true }: SidebarProps) { {items.map((conversation) => { const isActive = pathname === `/chat/${conversation.conversationId}`; + const isEditing = editingId === conversation.conversationId; return (
- - {conversation.title} - + {isEditing ? ( + // 编辑模式 - 显示输入框 +
+ setEditingTitle(e.target.value)} + onKeyDown={(e) => handleKeyDown(e, conversation.conversationId)} + onBlur={() => handleCancelRename()} + className="flex-1 px-2 py-1 text-sm bg-[var(--color-bg-secondary)] border border-[var(--color-border)] rounded text-[var(--color-text-primary)] focus:outline-none focus:border-[var(--color-primary)]" + disabled={isSubmitting} + /> + + +
+ ) : ( + // 正常模式 - 显示链接 + + {conversation.title} + + )} - {/* 更多操作按钮 */} - + {/* 更多操作按钮 - 非编辑模式时显示 */} + {!isEditing && ( + + )} {/* 下拉菜单 */} {menuOpen === conversation.conversationId && ( -
+
+
)}