diff --git a/src/features/ai/components/history/sidebar.tsx b/src/features/ai/components/history/sidebar.tsx index a12a0b0c..e32af09b 100644 --- a/src/features/ai/components/history/sidebar.tsx +++ b/src/features/ai/components/history/sidebar.tsx @@ -1,10 +1,9 @@ -import { Check, Search, Trash2 } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { AnimatePresence, motion } from "framer-motion"; +import { Check, Search, Trash2, X } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { getRelativeTime } from "@/features/ai/lib/formatting"; import type { Chat } from "@/features/ai/types/ai-chat"; -import { Button } from "@/ui/button"; -import { Dropdown } from "@/ui/dropdown"; -import Input from "@/ui/input"; import { cn } from "@/utils/cn"; import { ProviderIcon } from "../icons/provider-icons"; @@ -18,8 +17,6 @@ interface ChatHistoryDropdownProps { triggerRef: React.RefObject; } -const DROPDOWN_WIDTH = 340; - export default function ChatHistoryDropdown({ isOpen, onClose, @@ -29,116 +26,215 @@ export default function ChatHistoryDropdown({ onDeleteChat, triggerRef, }: ChatHistoryDropdownProps) { - const searchRef = useRef(null); + const inputRef = useRef(null); + const resultsRef = useRef(null); const [searchQuery, setSearchQuery] = useState(""); + const [selectedIndex, setSelectedIndex] = useState(0); const filteredChats = useMemo(() => { - if (!searchQuery.trim()) return chats; - return chats.filter((chat) => chat.title.toLowerCase().includes(searchQuery.toLowerCase())); + const query = searchQuery.trim().toLowerCase(); + if (!query) return chats; + return chats.filter((chat) => { + const titleMatch = chat.title.toLowerCase().includes(query); + const providerMatch = (chat.agentId ?? "custom").toLowerCase().includes(query); + return titleMatch || providerMatch; + }); }, [chats, searchQuery]); + const handleClose = useCallback(() => { + onClose(); + triggerRef.current?.focus(); + }, [onClose, triggerRef]); + useEffect(() => { - if (isOpen) { - setSearchQuery(""); - setTimeout(() => searchRef.current?.focus(), 0); - } + if (!isOpen) return; + setSearchQuery(""); + setSelectedIndex(0); + window.setTimeout(() => inputRef.current?.focus(), 0); }, [isOpen]); + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (event: KeyboardEvent) => { + switch (event.key) { + case "Escape": + event.preventDefault(); + handleClose(); + break; + case "ArrowDown": + event.preventDefault(); + setSelectedIndex((prev) => Math.min(prev + 1, filteredChats.length - 1)); + break; + case "ArrowUp": + event.preventDefault(); + setSelectedIndex((prev) => Math.max(prev - 1, 0)); + break; + case "Enter": + if (filteredChats[selectedIndex]) { + event.preventDefault(); + onSwitchToChat(filteredChats[selectedIndex].id); + handleClose(); + } + break; + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [filteredChats, handleClose, isOpen, onSwitchToChat, selectedIndex]); + + useEffect(() => { + setSelectedIndex(0); + }, [searchQuery]); + + useEffect(() => { + if (!resultsRef.current || filteredChats.length === 0) return; + const selectedElement = resultsRef.current.children[selectedIndex] as HTMLElement | undefined; + selectedElement?.scrollIntoView({ block: "nearest", behavior: "smooth" }); + }, [filteredChats.length, selectedIndex]); + return ( - -
- setSearchQuery(e.target.value)} - leftIcon={Search} - variant="ghost" - className="w-full" - /> -
- -
- {chats.length === 0 ? ( -
No chat history
- ) : filteredChats.length === 0 ? ( -
- No chats match "{searchQuery}" -
- ) : ( -
- {filteredChats.map((chat) => ( -
-
- {chat.id === currentChatId ? ( - - ) : ( - - )} -
- - +
+ +
- {chat.title} - - - {getRelativeTime(chat.lastMessageAt)} - - - - -
- ))} -
+ {chats.length === 0 ? ( +
+ No chat history yet +
+ ) : filteredChats.length === 0 ? ( +
+ No chats match "{searchQuery}" +
+ ) : ( + filteredChats.map((chat, index) => { + const isCurrent = chat.id === currentChatId; + const isSelected = index === selectedIndex; + + return ( +
{ + onSwitchToChat(chat.id); + handleClose(); + }} + onMouseEnter={() => setSelectedIndex(index)} + className={cn( + "group relative mb-0.5 flex cursor-pointer items-start gap-3 rounded-xl px-4 py-3 transition-colors", + isSelected ? "bg-hover/80" : "hover:bg-hover/40", + isCurrent && "bg-accent/5 hover:bg-accent/10", + )} + > + {isCurrent && ( +
+ )} + +
+ {isCurrent ? ( + + ) : ( + + )} +
+ +
+
+ + {chat.title} + + + {getRelativeTime(chat.lastMessageAt)} + +
+ +
+ + {(chat.agentId || "custom").replace(/-/g, " ")} + + {isCurrent && ( + <> + + Current chat + + )} +
+
+ + +
+ ); + }) + )} +
+ + +
+ )} - -
+ + ); }