
Conversation history sidebar with a New conversation button, a search field and a list whose rows rename inline or delete from a dropdown menu. Shows an empty state when nothing matches.
blocks/chat/chat-history-01/index.tsx
"use client"
import { useState } from "react"
import {
DotsThreeIcon,
PencilSimpleIcon,
PlusIcon,
TrashIcon,
} from "@phosphor-icons/react/dist/ssr"
import {
Section,
SectionContent,
} from "@/components/editorial/section"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from "@/components/ui/empty"
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
SidebarInput,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
} from "@/components/ui/sidebar"
// The copy: change text here, not in the markup.
const COPY = {
create: "New conversation",
search: "Search conversations",
actions: "More",
rename: "Rename",
remove: "Delete",
untitled: "Untitled",
emptyTitle: "No conversations yet",
emptyBody: "Start one and it will show up here.",
} as const
// when is a string the app already formatted; the list never tells time.
const CONVERSATIONS = [
{ id: "c1", title: "Which hero for a product page", when: "an hour ago" },
{ id: "c2", title: "Type scale questions", when: "yesterday" },
{ id: "c3", title: "Composable vs ready-made", when: "3 days ago" },
]
export default function ChatHistory01() {
const [conversations, setConversations] = useState(CONVERSATIONS)
const [currentId, setCurrentId] = useState("c1")
const [query, setQuery] = useState("")
const [editingId, setEditingId] = useState<string | null>(null)
const shown = conversations.filter(
(c) => !query || c.title.toLowerCase().includes(query.toLowerCase())
)
const rename = (id: string, title: string) => {
setConversations((prev) =>
prev.map((c) =>
c.id === id ? { ...c, title: title.trim() || c.title } : c
)
)
setEditingId(null)
}
return (
<Section>
<SectionContent>
<SidebarProvider className="min-h-0">
<Sidebar
collapsible="none"
className="h-104 max-w-xs overflow-hidden rounded-xl ring-1 ring-border"
>
<SidebarHeader>
<Button
onClick={() =>
setConversations((prev) => [
{
id: `c-${Date.now()}`,
title: COPY.untitled,
when: "now",
},
...prev,
])
}
>
<PlusIcon weight="light" data-icon="inline-start" />
{COPY.create}
</Button>
<SidebarInput
value={query}
placeholder={COPY.search}
aria-label={COPY.search}
onChange={(event) => setQuery(event.target.value)}
/>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
{shown.length === 0 ? (
<Empty>
<EmptyHeader>
<EmptyTitle>{COPY.emptyTitle}</EmptyTitle>
<EmptyDescription>{COPY.emptyBody}</EmptyDescription>
</EmptyHeader>
</Empty>
) : (
<SidebarMenu>
{shown.map((conversation) => (
<SidebarMenuItem key={conversation.id}>
{editingId === conversation.id ? (
<SidebarInput
autoFocus
defaultValue={conversation.title}
aria-label={COPY.rename}
onBlur={(event) =>
rename(conversation.id, event.target.value)
}
onKeyDown={(event) => {
if (event.key === "Enter")
rename(
conversation.id,
event.currentTarget.value
)
if (event.key === "Escape") setEditingId(null)
}}
/>
) : (
<>
<SidebarMenuButton
size="lg"
isActive={conversation.id === currentId}
onClick={() => setCurrentId(conversation.id)}
>
<span className="grid">
<span className="truncate">
{conversation.title}
</span>
<span className="text-xs text-muted-foreground">
{conversation.when}
</span>
</span>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger
render={<SidebarMenuAction showOnHover />}
aria-label={COPY.actions}
>
<DotsThreeIcon weight="bold" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuItem
onClick={() =>
setEditingId(conversation.id)
}
>
<PencilSimpleIcon weight="light" />
{COPY.rename}
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onClick={() =>
setConversations((prev) =>
prev.filter(
(c) => c.id !== conversation.id
)
)
}
>
<TrashIcon weight="light" />
{COPY.remove}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
)}
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
</SidebarProvider>
</SectionContent>
</Section>
)
}