Components / AI Chat Interfaces
AI Chat Sidebar
A conversation history sidebar listing previous AI chat sessions with search, date grouping, and active state indicators.
Components / AI Chat Interfaces
A conversation history sidebar listing previous AI chat sessions with search, date grouping, and active state indicators.
Install the core libraries required for this component.
npm install reactAdd the necessary Shadcn UI primitives.
npx shadcn-ui@latest add card input button badge separatorimport { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
interface Conversation {
id: number;
title: string;
lastMessage: string;
date: string; // YYYY-MM-DD
unread: boolean;
}
export default function AIChatSidebar01() {
const [search, setSearch] = useState("");
const [activeConversation, setActiveConversation] = useState<number | null>(1);
// Self-contained conversation data
const conversations: Conversation[] = [
{
id: 1,
title: "ChatGPT - Daily Summary",
lastMessage: "Here’s the summary of today’s tech news.",
date: "2026-02-19",
unread: false,
},
{
id: 2,
title: "GPT-4 - Project Planning",
lastMessage: "We should break the tasks into milestones.",
date: "2026-02-18",
unread: true,
},
{
id: 3,
title: "Custom LLM - Experiment",
lastMessage: "Testing new streaming AI responses.",
date: "2026-02-17",
unread: false,
},
{
id: 4,
title: "Custom LLM 2 - Preview",
lastMessage: "Testing new version of AI responses.",
date: "2026-02-19",
unread: false,
},
];
// Group conversations by date
const groupedConversations: Record<string, Conversation[]> = conversations.reduce(
(acc, conv) => {
if (!acc[conv.date]) acc[conv.date] = [];
acc[conv.date].push(conv);
return acc;
},
{} as Record<string, Conversation[]>
);
// Filtered by search
const filteredGroupedConversations = Object.fromEntries(
Object.entries(groupedConversations).map(([date, convs]) => [
date,
convs.filter((c) => c.title.toLowerCase().includes(search.toLowerCase())),
])
);
return (
<Card className="w-[280px] p-2 flex flex-col h-full">
{/* Search bar */}
<CardContent className="p-0">
<Input
placeholder="Search conversations..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</CardContent>
{/* Conversations list */}
<CardContent className="flex-1 overflow-y-auto p-2 pt-0! space-y-3">
{Object.entries(filteredGroupedConversations).map(([date, convs]) =>
convs.length > 0 ? (
<div key={date} className="space-y-3">
<div className="text-xs text-muted-foreground font-semibold">{date}</div>
<div className="flex flex-col gap-1">
{convs.map((conv) => (
<Button
key={conv.id}
variant={activeConversation === conv.id ? "default" : "ghost"}
className="w-full justify-between items-center px-2 h-10 py-2"
onClick={() => setActiveConversation(conv.id)}
>
<div className="flex flex-col text-left overflow-hidden">
<span className="text-xs font-medium truncate">{conv.title}</span>
<span className="text-[10px] opacity-60 truncate">
{conv.lastMessage}
</span>
</div>
{conv.unread && (
<Badge variant="secondary" className="flex-shrink-0 ml-2">
New
</Badge>
)}
</Button>
))}
</div>
</div>
) : null
)}
</CardContent>
<Separator className="mt-12" />
{/* Footer actions */}
<CardContent className="p-2 flex flex-col gap-2">
<Button size="sm" className="w-full">
+ New Conversation
</Button>
<Button size="sm" className="w-full">
Export Conversations
</Button>
</CardContent>
</Card>
);
} import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
interface Conversation {
id: number;
title: string;
lastMessage: string;
date: string; // YYYY-MM-DD
unread: boolean;
}
export default function AIChatSidebar01() {
const [search, setSearch] = useState("");
const [activeConversation, setActiveConversation] = useState<number | null>(1);
// Self-contained conversation data
const conversations: Conversation[] = [
{
id: 1,
title: "ChatGPT - Daily Summary",
lastMessage: "Here’s the summary of today’s tech news.",
date: "2026-02-19",
unread: false,
},
{
id: 2,
title: "GPT-4 - Project Planning",
lastMessage: "We should break the tasks into milestones.",
date: "2026-02-18",
unread: true,
},
{
id: 3,
title: "Custom LLM - Experiment",
lastMessage: "Testing new streaming AI responses.",
date: "2026-02-17",
unread: false,
},
{
id: 4,
title: "Custom LLM 2 - Preview",
lastMessage: "Testing new version of AI responses.",
date: "2026-02-19",
unread: false,
},
];
// Group conversations by date
const groupedConversations: Record<string, Conversation[]> = conversations.reduce(
(acc, conv) => {
if (!acc[conv.date]) acc[conv.date] = [];
acc[conv.date].push(conv);
return acc;
},
{} as Record<string, Conversation[]>
);
// Filtered by search
const filteredGroupedConversations = Object.fromEntries(
Object.entries(groupedConversations).map(([date, convs]) => [
date,
convs.filter((c) => c.title.toLowerCase().includes(search.toLowerCase())),
])
);
return (
<Card className="w-[280px] p-2 flex flex-col h-full">
{/* Search bar */}
<CardContent className="p-0">
<Input
placeholder="Search conversations..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</CardContent>
{/* Conversations list */}
<CardContent className="flex-1 overflow-y-auto p-2 pt-0! space-y-3">
{Object.entries(filteredGroupedConversations).map(([date, convs]) =>
convs.length > 0 ? (
<div key={date} className="space-y-3">
<div className="text-xs text-muted-foreground font-semibold">{date}</div>
<div className="flex flex-col gap-1">
{convs.map((conv) => (
<Button
key={conv.id}
variant={activeConversation === conv.id ? "default" : "ghost"}
className="w-full justify-between items-center px-2 h-10 py-2"
onClick={() => setActiveConversation(conv.id)}
>
<div className="flex flex-col text-left overflow-hidden">
<span className="text-xs font-medium truncate">{conv.title}</span>
<span className="text-[10px] opacity-60 truncate">
{conv.lastMessage}
</span>
</div>
{conv.unread && (
<Badge variant="secondary" className="flex-shrink-0 ml-2">
New
</Badge>
)}
</Button>
))}
</div>
</div>
) : null
)}
</CardContent>
<Separator className="mt-12" />
{/* Footer actions */}
<CardContent className="p-2 flex flex-col gap-2">
<Button size="sm" className="w-full">
+ New Conversation
</Button>
<Button size="sm" className="w-full">
Export Conversations
</Button>
</CardContent>
</Card>
);
}
Primitives required in your
@/components/ui
directory.
| Component | Path |
|---|---|
| card | @/components/ui/card |
| input | @/components/ui/input |
| button | @/components/ui/button |
| badge | @/components/ui/badge |
| separator | @/components/ui/separator |
Types and interfaces defined in this component.
interface Conversation {
id: number;
title: string;
lastMessage: string;
date: string; // YYYY-MM-DD
unread: boolean;
} interface Conversation {
id: number;
title: string;
lastMessage: string;
date: string; // YYYY-MM-DD
unread: boolean;
} Internal functions used to handle component logic.
| Function | Parameters |
|---|---|
| AIChatSidebar01() | None |
React state variables managed within this component.
| Variable | Initial Value |
|---|---|
| search | "" |
All variable declarations found in this component.
| Name | Kind | Value |
|---|---|---|
| filteredGroupedConversations | | Object.fromEntries( |
Every JSX/HTML tag used in this component, with usage count and source.
| Tag | Count | Type | Source |
|---|---|---|---|
| <Badge> | 1× | | @/components/ui/badge |
| <Card> | 1× | | @/components/ui/card |
| <CardContent> | 3× | | @/components/ui/card |
| <Separator> | 1× | | @/components/ui/separator |
| <Button> | 3× | | Native HTML |
| <div> | 4× | | Native HTML |
| <Input> | 1× | | Native HTML |
| <number> | 1× | | Native HTML |
| <span> | 2× | | Native HTML |
| <string> | 2× | | Native HTML |