dashboard-chat-best-practices

$npx mdskill add vercel-labs/eve-chat-template/dashboard-chat-best-practices

Use this skill to keep the template's web chat feeling like a polished dashboard surface: stable, quiet, fast to first paint, and predictable under streaming state.

SKILL.md

.github/skills/dashboard-chat-best-practicesView on GitHub ↗
---
name: dashboard-chat-best-practices
description: Apply dashboard-grade chat UX patterns in the Eve chat template. Use when changing Next.js Cache Components/PPR behavior, static shell layout, Suspense bootstrap boundaries, theme hydration, auth display hydration, signed-in/signed-out static shell behavior, optimistic messages, streaming row identity, composer/sidebar interactions, thinking/tool rendering, responsive styling, visual polish, or Vercel dashboard chat comparisons.
---

# Dashboard Chat Best Practices

## Overview

Use this skill to keep the template's web chat feeling like a polished dashboard surface: stable, quiet, fast to first paint, and predictable under streaming state.

## Product Shape

- Build the actual chat surface, not a marketing page. The first screen should be a usable composer, sidebar, or chat route.
- Keep the aesthetic restrained and work-focused: dense but readable spacing, quiet borders, predictable controls, and minimal decoration.
- Prefer existing shadcn/Tailwind primitives and local components over new abstractions.
- Use icons for compact commands, with accessible labels/tooltips. Avoid explanatory in-app text for behavior that good controls can imply.

## Cache Components And PPR Basics

- Keep `cacheComponents: true` enabled in `next.config.ts`. Treat the chat route as a static shell with dynamic data streamed into Suspense boundaries.
- Keep the shell cheap and deterministic. Do not make `app/(chat)/layout.tsx` wait on auth, database, chat history, or per-chat data before rendering the shell.
- Pass cheap initial state into `AgentChatShell`: empty history, `initialNextCursor: null`, `viewer: null`, and `getInitialSetupStatus()`.
- Load expensive or request-bound data inside hidden Suspense children: `ResolvedChatBootstrap` for setup/viewer/sidebar history and `ExistingChat` for active chat data.
- Use sync components to hand resolved server data to the client shell. `AgentChatBootstrapSync` should update setup status, viewer, and history. `AgentChatRouteSync` should update the active chat for the current route.
- Keep Suspense fallbacks `null` for these sync boundaries when the visible shell already has stable placeholders. Do not add loading panels that push the chat frame around.
- Do not call `notFound()` until the dynamic boundary has enough real viewer/setup/chat data to prove the chat is inaccessible. The outer shell should still render while that decision is loading.
- Keep root and session routes visually distinct but structurally compatible. The root page can center the composer; the session page can pin it near the bottom. Both should live inside the same shell.
- Treat PPR/static shell work as a latency feature and a correctness feature. The goal is first paint without auth/database blocking, then precise data reconciliation after bootstrap.

## Static Shell And Hydration

- Preserve the shell-first strategy. `AgentChatShell` should paint the sidebar frame, top controls, and route body before auth, setup, or chat history finishes loading.
- Keep bootstrap data out of the blocking route path. Use hidden Suspense sync components to hydrate setup status, viewer, sidebar history, and active chat data.
- Preserve theme pre-hydration. Theme should be resolved before React paints so dark/light mode does not flash or mismatch.
- Preserve auth display pre-hydration. The `eve_logged_in` cookie is a display hint only; the head script sets `data-eve-auth-display` so CSS can hide the wrong signed-in/signed-out branch before viewer data resolves.
- Never treat the auth hint as authority. Server actions, chat loading, Eve auth, and rate limits must use the real Better Auth session.
- Keep `suppressHydrationWarning` only where it protects known pre-hydration document mutations such as theme/auth display hints.

## Same Route, Signed In Or Out

- Render one shared route shell for signed-in and signed-out states. Do not fork the page into separate auth routes just to show different controls.
- While bootstrap is loading, render both possible placeholders inside `AuthDisplayLoggedIn` and `AuthDisplayLoggedOut`; let the document dataset hide the wrong branch.
- For signed-out users, show the Sign in affordance immediately. For signed-in users, show a neutral user-menu placeholder instead of flashing Sign in.
- After bootstrap resolves, render from real `viewer` state: `viewer ? <UserMenu /> : <SidebarSignInButton />`.
- When a signed-out user submits text, save the draft to `sessionStorage`, open the auth modal, and restore the draft after sign-in.

## Optimistic Messaging And Row Identity

- Show an optimistic user bubble immediately on send, but keep persisted Eve events as the source of truth.
- Clear the optimistic user bubble only when the reduced event log contains the real latest user message.
- Keep live chat state centralized where possible. Avoid rendering from multiple competing sources in the same frame.
- Prefer stable row identities for streaming responses. A "thinking" or "streaming reply" row should become the assistant row without remounting into a different component path.
- Use a separate thinking presence only when there is no meaningful assistant text yet or a turn is still finalizing. Fade it out rather than abruptly removing it.
- Avoid letting final snapshot writes fight live state. During streaming, prefer the live source; after settle, reconcile with the canonical snapshot.

## Composer And Sidebar

- Keep `ChatComposer` controlled by the page: `value`, `onChange`, `onSubmit`, `disabled`, `disabledReason`, `isBusy`, and `isPreparing`.
- Enter should submit. Shift+Enter should insert a newline. Disabled states should expose actionable tooltip text.
- Busy state should show Stop. Preparing state should show a spinner. Empty or over-limit input should disable send without moving layout.
- Root and session pages can place the composer differently, but they should share the same component and control contract.
- Persist desktop sidebar open state in a cookie and write a pre-hydration document hint to avoid open-then-collapse flicker.
- Keep sidebar history cursor-paginated and optimistic. Use `touchChat`, `removeChat`, and title updates for sidebar state; do not make sidebar state the transcript source of truth.
- Keep mobile sidebar overlay behavior separate from desktop persistent state.

## Message Rendering

- Render user messages as compact right-aligned bubbles. Render assistant text as full-width left-aligned prose.
- Use Streamdown for assistant markdown. Keep code blocks, lists, and inline code readable in both themes.
- Group related dynamic tool parts into compact rows. Expand details only when there is useful input, output, error, or HITL control.
- Render reasoning as a collapsible block. Label active reasoning as "Thinking..." and settled reasoning as "Reasoning".
- Render connection authorization as a structured card, not prose embedded in assistant text.
- Keep text inside controls and cards from overflowing at mobile widths. Prefer stable dimensions for buttons, tool rows, counters, and composer controls.

## Styling Rules

- Use the existing token system: `background`, `foreground`, `muted`, `border`, `card`, `accent`, and local opacity patterns.
- Keep cards modest. Do not nest cards inside cards. Use cards for messages, modals, repeated items, or framed controls, not whole-page decoration.
- Keep radius consistent with the template. Avoid large pill shapes unless the local component already uses them.
- Avoid large decorative gradients, orbs, blobs, and marketing-style hero layouts in the chat app.
- Use lucide icons when an icon exists. Add `aria-label` for icon-only buttons and tooltip text where meaning is not obvious.
- Avoid layout shifts from loading, hover, or stream state. Reserve stable space for composer controls, sidebar rows, auth placeholders, and thinking/tool rows.

## Verification

- Run `pnpm typecheck` and `pnpm build` after UI or app-router changes.
- Manually check root signed-out, root signed-in, auth modal draft restore, first-message navigation, follow-up optimistic send, streaming assistant row, Connect/Skip auth cards, theme first paint, desktop sidebar cookie restore, and mobile sidebar overlay when touched.
- Use screenshots or browser checks for visual changes across desktop and mobile. Watch specifically for auth flicker, theme flash, composer jumps, sidebar jumps, and message remount flicker.

More from vercel-labs/eve-chat-template