UI kit
ConversationHistory
ChatReact · Next.jsA sidebar list of past conversations with an active state and a 'new conversation' action. Pair it with RagChat for multi-thread history; persistence (localStorage, DB) is up to you.
<ConversationHistory />Live preview
Conversations
Installation
No dependencies to install — just React + Tailwind CSS. It's a presentational component (no data fetching), so the same source works unchanged in React and Next.js (App or Pages Router) — there's no separate variant. Copy it into your project (e.g. components/ConversationHistory.tsx) and import it.
Usage
usage.tsx
import { ConversationHistory } from "@/components/ConversationHistory";
export default function Example() {
return (
<ConversationHistory threads={threads} activeId={id} onSelect={open} onNew={create} />
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
threadsrequired | Thread[] | — | Threads to list ({ id, title, updatedAt }). |
activeId | string | — | The currently selected thread id. |
onSelectrequired | (id: string) => void | — | Called when a thread is clicked. |
onNew | () => void | — | Called when the new-conversation button is clicked. |
Component source copy-paste ready
Presentational component — no data fetching, so the same source works unchanged in React and Next.js. There's no separate Next.js variant to copy.
ConversationHistory.tsx
"use client";
export interface Thread {
id: string;
title: string;
/** ISO string or a pre-formatted display label. */
updatedAt?: string;
}
interface ConversationHistoryProps {
threads: Thread[];
activeId?: string;
onSelect: (id: string) => void;
onNew?: () => void;
className?: string;
}
/**
* ConversationHistory — a sidebar list of past chat threads with a "new
* conversation" action. Pair it with RagChat to give your app multi-thread
* history; persistence is up to you (localStorage, DB, …).
*/
export function ConversationHistory({ threads, activeId, onSelect, onNew, className = "" }: ConversationHistoryProps) {
return (
<div className={`flex h-full flex-col rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950 ${className}`}>
<div className="flex items-center justify-between border-b border-zinc-200 px-3 py-2.5 dark:border-zinc-800">
<span className="text-xs font-semibold uppercase tracking-wide text-zinc-500">Conversations</span>
{onNew && (
<button
type="button"
onClick={onNew}
aria-label="New conversation"
className="flex h-6 w-6 items-center justify-center rounded-md text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 5v14M5 12h14" /></svg>
</button>
)}
</div>
<ul className="flex-1 overflow-y-auto p-1.5">
{threads.length === 0 && (
<li className="px-2 py-6 text-center text-xs text-zinc-400">No conversations yet.</li>
)}
{threads.map((t) => {
const active = t.id === activeId;
return (
<li key={t.id}>
<button
type="button"
onClick={() => onSelect(t.id)}
className={`flex w-full flex-col gap-0.5 rounded-lg px-2.5 py-2 text-left transition ${
active ? "bg-zinc-100 dark:bg-zinc-800" : "hover:bg-zinc-50 dark:hover:bg-zinc-900"
}`}
>
<span className="truncate text-[13px] font-medium text-zinc-700 dark:text-zinc-200">{t.title}</span>
{t.updatedAt && <span className="text-[11px] text-zinc-400">{t.updatedAt}</span>}
</button>
</li>
);
})}
</ul>
</div>
);
}