AI

Assistant Welcome

A first-run assistant hero — greeting, capability cards and starter chips that prefill the host's composer instead of sending, over a token-only animated accent.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { ArrowUpRight, Check } from "lucide-react"
import { cn } from "@/lib/utils"

export interface AssistantCapability {
  /**
   * Unique inside `capabilities` only — the pick echo is keyed by
   * `capability:<id>`, so a starter chip may reuse the same id without the two
   * lighting up together.
   */
  id: string
  title: React.ReactNode

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/assistant-welcome.json

Prompt

Build a React + TypeScript + Tailwind "AssistantWelcome" block (lucide-react
ArrowUpRight + Check). It is the first screen of a chat surface: a greeting, a
grid of capability cards, a row of starter chips, a decorative token gradient,
and a bottom slot the host fills with its own composer.

Contract
- forwardRef<HTMLElement, AssistantWelcomeProps> over a <section>, spreading the
  remaining native section props; cn() merges className.
- Props: greeting (required node), subtitle?, eyebrow?, capabilities?:
  { id, title, example, prompt?, icon? }[], starters?: { id, text, prompt? }[],
  onPick?: (pick: { source: "capability" | "starter"; id: string; text: string })
  => void, children (the composer slot), footnote?, capabilitiesLabel?,
  startersLabel?, density?: "comfortable" | "compact", accent? (default true),
  disabled?, headingLevel?: 1 | 2, echoDuration? (ms, default 2400).
- The block owns no text state. `greeting` is a prop, never derived: time of
  day, the user's first name and the workspace are resolved by the host, so the
  component stays timezone- and identity-free.
- `pick.text` is pre-resolved (`prompt ?? example` for a card, `prompt ?? text`
  for a chip), so a handler is one line: setDraft(pick.text).

Behavior
- Prefill, not send. onPick hands the text over; the host drops it into its
  composer and the reader may edit, extend or discard it. Nothing latches, and
  the same card may be picked again — the opposite of a click-to-send strip
  where the first pick is irreversible and the group locks for good.
- Transient echo: the picked item is marked (primary border, tinted icon chip,
  Check swapped in for the hover arrow) and a polite live region announces
  "Added to the composer: <text>". A single setTimeout clears it after
  echoDuration; the effect stores a fresh state object per pick so picking the
  SAME item twice restarts the timer instead of inheriting the first one's
  remaining time, and the cleanup clears the timer on unmount and on every
  re-pick. echoDuration is clamped 600–10000 and NaN falls back to the default.
- Echo keys are `${source}:${id}` — a card and a chip may share an id without
  lighting up together.
- No onPick → honest degrade: cards render as <div>, chips as <span>, with no
  pointer cursor, no hover, no button role and no hover arrow. A control that
  looks clickable and does nothing is worse than a static screenshot.
- disabled (session connecting, restoring, out of quota): every card and chip
  keeps aria-disabled — never the native `disabled` attribute, which would blur
  whatever the reader had tabbed to — and the click still reaches the handler,
  which refuses it. The echo mark is derived away while disabled; the pending
  timer still clears the state on its own. The composer slot keeps rendering:
  what sits inside it is the host's call.
- Empty arrays render nothing at all: no label row, no gap, no zero-height
  shell. Same for the composer block when neither children nor footnote exist.
- Keyboard: cards and chips are ordinary buttons in document order (a welcome
  screen has a handful, and a roving group would hide the rest from sequential
  navigation); focus-visible rings on both. The live region never steals focus —
  after a pick the host is the one that should focus its composer.

Rendering & styling
- Semantic tokens only: bg-card surfaces, border/border-primary edges, bg-muted
  icon chips, text-muted-foreground supporting copy, primary for the picked
  state, ring for focus. The accent is three blurred radial blobs painted from
  var(--chart-1), var(--chart-4) and var(--chart-2) — no hex anywhere, so a
  theme swap re-tints it and dark mode is free.
- Accent motion: each blob carries `animate-pulse` with an inline
  animation-duration/-delay (7s / 11s / 9s, staggered) so it beats the utility
  shorthand in the cascade, wrapped in a fixed `opacity-*` parent so the breath
  stays subtle. `motion-reduce:animate-none` drops the keyframes and leaves the
  identical gradient perfectly still — decoration off, nothing else changes.
- The accent is aria-hidden, absolutely positioned and pointer-events-none; the
  content column is merely `relative`, so positioned siblings paint in DOM order
  and no negative z-index can slide the accent under the host's background. The
  section is `isolate` to contain the whole thing.
- Capability grid is auto-fit: grid-cols-[repeat(auto-fit,minmax(min(15rem,100%),1fr))]
  so three cards fill a row and seven wrap, with no hardcoded column count.
- Overflow discipline: greeting and every label use wrap-anywhere (not
  break-words) so an unbreakable pasted token lowers min-content instead of
  widening the block past the viewport; chips wrap with items-start so a short
  chip never inherits a tall neighbour's height.
- The hover arrow is always mounted at opacity-0 and revealed on
  hover/focus-visible, so it cannot reflow the title row.

Customization levers
- Density: `density="compact"` halves the paddings, drops one type step and
  narrows the grid track — same anatomy, side-panel scale. Both maps live in one
  DENSITY const; add a third row for a "roomy" variant.
- Anatomy: every block is independently omissible — drop capabilities for a
  chips-only returning-user screen, drop starters for a pure capability grid,
  drop children for a marketing screenshot, set capabilitiesLabel="" /
  startersLabel="" to lose the eyebrows (the lists fall back to aria-label).
- Motion strength: accent={false} removes the gradient entirely; otherwise tune
  ACCENT_BLOBS (count, size, opacity, duration) — it is data, not JSX.
- Palette: swap the three chart tokens for var(--primary) for a monochrome
  accent, or re-point the picked state from primary to a chart token.
- Reach: headingLevel switches h1/h2 for pages that already own an h1;
  echoDuration tunes how long the "added" mark lingers; the icon slot accepts
  any node and the chip normalises svg children to 1rem.
- Composer: the slot is plain children, so a full prompt-input with attachments,
  model picker and stop button drops in without touching this block.

Concepts

  • Prefill, not send — a pick only seeds the draft, so it is reversible: the reader edits or deletes it, and the same card stays live. That is why nothing here latches the way a click-to-send suggestion strip has to.
  • Transient echo — the "added" mark is a receipt with a lifetime, not state. A fresh state object per pick restarts its timer, so picking one card twice reads as two acknowledgements rather than one that expires early.
  • Honest degrade — with no onPick the cards lose their button role, cursor and hover entirely. The component would rather look static than render an affordance that does nothing.
  • aria-disabled over native disabled — while the session is connecting the click still reaches the handler and is refused there; the native attribute would blur the item the reader had just tabbed to and hide it from assistive tech.
  • Composer as a slot — the block never owns the input. It shapes the screen around whatever composer the host already ships, so prefill is a one-line handler instead of a second source of draft state.
  • Decoration that can be switched off — the gradient is token-painted, aria-hidden and pointer-events-none, and reduced motion freezes it in place: the screen loses its breathing accent and not one interaction.

On This Page