UI kit

ConversationHistory

ChatReact · Next.js

A 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

PropTypeDefaultDescription
threadsrequiredThread[]Threads to list ({ id, title, updatedAt }).
activeIdstringThe currently selected thread id.
onSelectrequired(id: string) => voidCalled when a thread is clicked.
onNew() => voidCalled 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>
  );
}