import { useState, useMemo, useEffect, useRef } from "react" import { Link } from "react-router-dom" import { Plus, Trash2, MessageSquare, Loader2, Search, Edit2, X, Sparkles, } from "lucide-react" import { useConversationsQuery, useCreateConversationMutation, useDeleteConversationMutation, } from "@/hooks/useAiChatQuery" import { useChatPage } from "./ChatPageContext" import type { ConversationResponse } from "@/client/model" import { t } from "@/i18n/T" import { Badge } from "@/components/ui/badge" import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@/components/ui/empty" interface ChatConversationListProps { selectedId: string | null onSelect: (id: string) => void onNew: () => void } export function ChatConversationList({ selectedId, onSelect, onNew, }: ChatConversationListProps) { const { scope, projectId, projectName, selectedModel } = useChatPage() const { data, isLoading } = useConversationsQuery( scope === "project" ? projectId : null ) const createMutation = useCreateConversationMutation() const deleteMutation = useDeleteConversationMutation() const [deletingId, setDeletingId] = useState(null) const [searchQuery, setSearchQuery] = useState("") const [isSearchOpen, setIsSearchOpen] = useState(false) const searchInputRef = useRef(null) const getConversationLink = (id: string) => { if (scope === "project" && projectName) return `/${projectName}/chat/${id}` return `/me/chat/${id}` } const conversations = useMemo( () => data?.conversations || [], [data?.conversations] ) // Filter conversations by search query const filteredConversations = useMemo(() => { if (!searchQuery.trim()) return conversations const q = searchQuery.toLowerCase() return conversations.filter((c) => (c.title || "Untitled Chat").toLowerCase().includes(q) ) }, [conversations, searchQuery]) // Group conversations by date const groupedConversations = useMemo(() => { const now = new Date() const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()) const yesterday = new Date(today) yesterday.setDate(yesterday.getDate() - 1) const weekAgo = new Date(today) weekAgo.setDate(weekAgo.getDate() - 7) const monthAgo = new Date(today) monthAgo.setMonth(monthAgo.getMonth() - 1) const groups: { label: string; items: ConversationResponse[] }[] = [] const todayItems: ConversationResponse[] = [] const yesterdayItems: ConversationResponse[] = [] const thisWeekItems: ConversationResponse[] = [] const thisMonthItems: ConversationResponse[] = [] const earlierItems: ConversationResponse[] = [] for (const c of filteredConversations) { const date = new Date(c.created_at) if (date >= today) { todayItems.push(c) } else if (date >= yesterday) { yesterdayItems.push(c) } else if (date >= weekAgo) { thisWeekItems.push(c) } else if (date >= monthAgo) { thisMonthItems.push(c) } else { earlierItems.push(c) } } if (todayItems.length > 0) groups.push({ label: "Today", items: todayItems }) if (yesterdayItems.length > 0) groups.push({ label: "Yesterday", items: yesterdayItems }) if (thisWeekItems.length > 0) groups.push({ label: "This Week", items: thisWeekItems }) if (thisMonthItems.length > 0) groups.push({ label: "This Month", items: thisMonthItems }) if (earlierItems.length > 0) groups.push({ label: "Earlier", items: earlierItems }) return groups }, [filteredConversations]) const handleNew = async () => { onNew() try { const conversation = await createMutation.mutateAsync({ project_id: scope === "project" ? projectId : null, title: "New Chat", model: selectedModel?.model_name ?? null, model_config: null, }) if (conversation?.id) { onSelect(conversation.id) } } catch (err) { console.error("Failed to create conversation:", err) } } const handleDelete = async (e: React.MouseEvent, id: string) => { e.stopPropagation() e.preventDefault() setDeletingId(id) try { await deleteMutation.mutateAsync(id) if (selectedId === id) { onNew() } } catch (err) { console.error("Failed to delete conversation:", err) } finally { setDeletingId(null) } } // Cmd/Ctrl+K to focus search useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault() setIsSearchOpen(true) setTimeout(() => searchInputRef.current?.focus(), 0) } if (e.key === "Escape" && isSearchOpen) { setIsSearchOpen(false) setSearchQuery("") } } window.addEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown) }, [isSearchOpen]) return (
{/* Header */}
{t("chat.conversations.chat_history")}

{conversations.length} conversations

{/* Search Input */} {isSearchOpen && (
setSearchQuery(e.target.value)} className="min-w-0 flex-1 bg-transparent text-sm outline-none" style={{ color: "var(--text-primary)" }} /> {searchQuery && ( )}
)} {/* New Chat Button */}
{/* List */}
{isLoading ? (
) : filteredConversations.length === 0 ? ( {searchQuery ? t("chat.conversations.no_matching") : t("chat.conversations.no_conversations")} {searchQuery ? t("chat.conversations.try_different_search") : t("chat.conversations.start_new_chat")} ) : (
{groupedConversations.map((group) => (
{group.label}
{group.items.map((conversation) => ( handleDelete(e, conversation.id)} /> ))}
))}
)}
) } function ConversationItem({ conversation, isSelected, isDeleting, linkTo, onDelete, }: { conversation: ConversationResponse isSelected: boolean isDeleting: boolean linkTo: string onDelete: (e: React.MouseEvent) => void }) { const date = new Date(conversation.created_at) const now = new Date() const isToday = date.getDate() === now.getDate() && date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear() const timeLabel = isToday ? date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : date.toLocaleDateString([], { month: "short", day: "numeric" }) return (

{conversation.title || t("chat.conversations.untitled_chat")}

{timeLabel}
) }