Skip to main content
The dashboard provides an intuitive interface for viewing all chat conversations captured by your widget or headless API.

Conversation List

The main dashboard page (/) displays all conversations in chronological order, with the most recently updated conversations at the top.

What You See

Each conversation card shows:
  • Session ID: The unique identifier for the chat session
  • Last Updated: Timestamp of the most recent message
  • Last Message Preview: A snippet of the most recent message content
From dashboard/app/page.tsx:38:
conversations.map((conversation) => (
  <Link
    key={conversation._id}
    className="card"
    href={`/conversations/${conversation._id}`}
  >
    <div className="card-title">Session: {conversation.sessionId}</div>
    <div className="card-time">Updated {formatDate(conversation.updatedAt)}</div>
    <div className="card-last">
      {conversation.lastMessage?.trim() || "No messages in this conversation"}
    </div>
  </Link>
))

Empty State

If no conversations exist yet, you’ll see a helpful message:
No conversations yet Messages from the widget will appear here.

Session ID Grouping

Conversations are organized by sessionId, which is provided by:
  • The widget (automatically generated or custom)
  • Your headless API implementation
Each unique sessionId creates a separate conversation thread. This allows you to:
  • Track individual user journeys
  • Group messages from the same session
  • Identify returning users (if you persist sessionId)

Example Session IDs

// Auto-generated by widget
"session_abc123xyz"

// Custom session from your app
"user_12345_chat"

// Anonymous visitor
"anon_visitor_xyz"

Individual Conversation View

Click any conversation card to view the full message thread.

Conversation Details

The conversation detail page shows:
  • Session ID: Displayed in the page header
  • Last Updated: Timestamp of most recent activity
  • Full Message Thread: All messages in chronological order

Message Threading

Messages are displayed with:
  • Role: User or Assistant
  • Timestamp: When the message was created
  • Content: Full message text
From dashboard/app/conversations/[id]/page.tsx:44:
<section className="thread">
  {thread.messages.map((message) => (
    <article
      key={message._id}
      className={`thread-message ${message.role === "user" ? "user" : "assistant"}`}
    >
      <div className="thread-meta">
        {message.role === "user" ? "User" : "Assistant"}{formatDate(message.createdAt)}
      </div>
      <div>{message.content}</div>
    </article>
  ))}
</section>

Visual Design

Messages are styled differently based on role:
  • User messages: Styled with the user class for visual distinction
  • Assistant messages: Styled with the assistant class
This makes it easy to follow the conversation flow at a glance.

Data Source

The dashboard fetches data directly from Convex using server-side queries:

List Conversations

From dashboard/lib/convex.ts:38:
export async function listConversations(): Promise<ConversationSummary[]> {
  const client = getClient();
  const conversations = await client.query(anyApi.conversations.listConversations, {});

  return conversations as ConversationSummary[];
}

Get Conversation Thread

From dashboard/lib/convex.ts:45:
export async function getConversationThread(
  conversationId: string
): Promise<ConversationThread | null> {
  const client = getClient();
  const thread = await client.query(anyApi.conversations.getConversationThread, {
    conversationId
  });

  return thread as ConversationThread | null;
}
When viewing an individual conversation, a back link is provided:
<Link className="back-link" href="/">
  ← Back to conversations
</Link>
This allows quick navigation back to the main list.

URL Structure

/                              # Main conversation list
/conversations/:conversationId # Individual conversation view
Conversation IDs are Convex document IDs, ensuring stable URLs.

Data Types

The dashboard works with strongly-typed data:

ConversationSummary

type ConversationSummary = {
  _id: string;          // Convex document ID
  sessionId: string;    // User-provided session identifier
  createdAt: number;    // Unix timestamp (ms)
  updatedAt: number;    // Unix timestamp (ms)
  lastMessage: string;  // Preview of most recent message
};

ConversationMessage

type ConversationMessage = {
  _id: string;                    // Convex document ID
  role: "user" | "assistant";      // Message author
  content: string;                // Message text
  createdAt: number;              // Unix timestamp (ms)
};

ConversationThread

type ConversationThread = {
  conversation: ConversationSummary;
  messages: ConversationMessage[];
};

Timestamp Formatting

All timestamps are formatted using the Intl.DateTimeFormat API:
function formatDate(timestamp: number): string {
  return new Intl.DateTimeFormat("en-US", {
    dateStyle: "medium",
    timeStyle: "short"
  }).format(new Date(timestamp));
}
Example output: Mar 3, 2026, 2:30 PM

Real-Time Updates

The dashboard uses Next.js Server Components, which means:
  • Data is fetched on every page load
  • Refreshing the page shows the latest conversations
  • No client-side polling or websockets needed
To see new messages, simply refresh the page or navigate back to the conversation list.

Error Handling

Conversation Not Found

If you navigate to a conversation that doesn’t exist, Next.js shows a 404 page:
dashboard/app/conversations/[id]/page.tsx
const thread = await getConversationThread(id);

if (!thread) {
  notFound(); // Returns 404 page
}

Missing Convex Configuration

If CONVEX_URL is not configured, an error is thrown when trying to fetch data:
if (!url) {
  throw new Error("CONVEX_URL or NEXT_PUBLIC_CONVEX_URL is required for dashboard");
}
Ensure this environment variable is set in your deployment.

Build docs developers (and LLMs) love