e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
155 lines
5.3 KiB
Plaintext
155 lines
5.3 KiB
Plaintext
---
|
|
title: Grok Clone
|
|
description: Open-source Grok clone in React — pill composer with paperclip, animated Mic↔Send, functional model picker dropdown, and message timing tooltip.
|
|
---
|
|
|
|
import { Grok } from "@/components/examples/grok";
|
|
|
|
<div className="not-prose h-[600px]">
|
|
<Grok />
|
|
</div>
|
|
|
|
## Overview
|
|
|
|
The Grok Clone demonstrates how to customize assistant-ui to match xAI's Grok interface. The empty state centers the Grok wordmark above a pill composer with an animated trailing button that swaps between Mic, Send, and Cancel based on composer state. Models are picked from a real dropdown that animates open/collapse along with the composer.
|
|
|
|
## Features
|
|
|
|
- **Centered Grok Wordmark**: Empty state shows the SVG wordmark above the composer
|
|
- **Pill Composer**: `rounded-4xl` with thin ring border, paperclip on the leading edge
|
|
- **Functional Model Picker**: Fast / Grok 4.1 / Think dropdown with descriptions, plus "Subscribe to SuperGrok"
|
|
- **Expand/Collapse Model Pill**: When the composer is empty the pill shows the model name; when typing it collapses to just the model icon
|
|
- **Animated Send Slot**: Mic when empty, Send (white-on-dark) when typing, Cancel while running — all via `group-data-[empty]` / `group-data-[running]`
|
|
- **Inverted Primary**: Dark button on light mode, light button on dark mode
|
|
- **Message Timing**: Streaming responses show a hover tooltip with first-token, total time, tokens/sec, chunk count
|
|
- **Hover-only Action Bars**: Edit/Copy on user messages, Reload/Copy/👍/👎 on assistant messages
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
npx assistant-ui add thread
|
|
```
|
|
|
|
## Code
|
|
|
|
The composer carries `data-empty` and `data-running` attributes so the trailing button transitions smoothly:
|
|
|
|
```tsx
|
|
import {
|
|
AuiIf,
|
|
ThreadPrimitive,
|
|
ComposerPrimitive,
|
|
useAuiState,
|
|
} from "@assistant-ui/react";
|
|
|
|
export const Grok = () => (
|
|
<ThreadPrimitive.Root className="bg-[#fdfdfd] px-4 dark:bg-[#141414]">
|
|
<AuiIf condition={(s) => s.thread.isEmpty}>
|
|
<div className="flex h-full flex-col items-center justify-center">
|
|
<GrokLogo className="mb-6 h-10 text-[#0d0d0d] dark:text-white" />
|
|
<Composer />
|
|
</div>
|
|
</AuiIf>
|
|
<AuiIf condition={(s) => !s.thread.isEmpty}>
|
|
<ThreadPrimitive.Viewport>
|
|
<ThreadPrimitive.Messages>
|
|
{() => <ChatMessage />}
|
|
</ThreadPrimitive.Messages>
|
|
</ThreadPrimitive.Viewport>
|
|
<Composer />
|
|
</AuiIf>
|
|
</ThreadPrimitive.Root>
|
|
);
|
|
|
|
const Composer = () => {
|
|
const isEmpty = useAuiState((s) => s.composer.isEmpty);
|
|
const isRunning = useAuiState((s) => s.thread.isRunning);
|
|
return (
|
|
<ComposerPrimitive.Root
|
|
className="group/composer mx-auto mb-3 w-full max-w-3xl"
|
|
data-empty={isEmpty}
|
|
data-running={isRunning}
|
|
>
|
|
<div className="rounded-4xl bg-[#f8f8f8] ring-1 ring-[#e5e5e5] ring-inset dark:bg-[#212121] dark:ring-[#2a2a2a]">
|
|
<ComposerPrimitive.AddAttachment><Paperclip /></ComposerPrimitive.AddAttachment>
|
|
<ComposerPrimitive.Input placeholder="What do you want to know?" />
|
|
<GrokModelPicker />
|
|
<SendOrMic />
|
|
</div>
|
|
</ComposerPrimitive.Root>
|
|
);
|
|
};
|
|
```
|
|
|
|
### Functional Model Picker
|
|
|
|
```tsx
|
|
const MODELS = [
|
|
{ id: "fast", name: "Fast", description: "Default. Quick responses", Icon: Zap },
|
|
{ id: "grok-4.1", name: "Grok 4.1", description: "Standard reasoning", Icon: Moon },
|
|
{ id: "think", name: "Think", description: "Multi-step reasoning", Icon: Moon },
|
|
];
|
|
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger>
|
|
<CurrentIcon />
|
|
{/* Name + chevron collapse to nothing while typing */}
|
|
<div className="group-data-[empty=false]/composer:max-w-0 group-data-[empty=true]/composer:max-w-32">
|
|
<span>{current.name}</span>
|
|
<ChevronDown />
|
|
</div>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
{MODELS.map((m) => (
|
|
<DropdownMenuItem onSelect={() => setModel(m.id)}>
|
|
{m.id === model ? <Check /> : <m.Icon />}
|
|
{m.name} — {m.description}
|
|
</DropdownMenuItem>
|
|
))}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
```
|
|
|
|
### Animated Send Slot
|
|
|
|
```tsx
|
|
<div className="relative h-9 w-9 rounded-full bg-[#0d0d0d] dark:bg-white">
|
|
<button className="group-data-[empty=false]/composer:scale-0">
|
|
<Mic /> {/* Voice when empty */}
|
|
</button>
|
|
<ComposerPrimitive.Send className="group-data-[empty=true]/composer:scale-0">
|
|
<ArrowUp /> {/* Send when typing */}
|
|
</ComposerPrimitive.Send>
|
|
<ComposerPrimitive.Cancel className="group-data-[running=false]/composer:scale-0">
|
|
<Square /> {/* Stop while running */}
|
|
</ComposerPrimitive.Cancel>
|
|
</div>
|
|
```
|
|
|
|
### Color Palette
|
|
|
|
| Element | Light | Dark |
|
|
|---------|-------|------|
|
|
| Background | `#fdfdfd` | `#141414` |
|
|
| Composer surface | `#f8f8f8` | `#212121` |
|
|
| Ring border | `#e5e5e5` | `#2a2a2a` |
|
|
| Primary text | `#0d0d0d` | `white` |
|
|
| Muted text | `#9a9a9a` | `#6b6b6b` |
|
|
| Send button (inverted) | `#0d0d0d` | `white` |
|
|
|
|
### Animation Technique
|
|
|
|
The composer uses `data-*` attributes for state-based animations:
|
|
|
|
```tsx
|
|
<ComposerPrimitive.Root data-empty={isEmpty} data-running={isRunning}>
|
|
<button className="group-data-[empty=false]/composer:scale-0 group-data-[empty=false]/composer:opacity-0" />
|
|
</ComposerPrimitive.Root>
|
|
```
|
|
|
|
This drives the Mic↔Send transition and the model pill expand/collapse on the same gesture.
|
|
|
|
## Source
|
|
|
|
<SourceLink href="https://github.com/assistant-ui/assistant-ui/blob/main/apps/docs/components/examples/grok.tsx" />
|