Gemini Clone

Open-source Gemini clone in React with a centered greeting over an ambient glow, a single-row pill composer, avatar-free assistant replies, and disabled, ready, and stop send states.

Gemini can make mistakes, so double-check it.

Overview

The Gemini Clone shows how to restyle assistant-ui to match Google Gemini's current interface. The empty state centers a single-line greeting over a soft blue glow, and a single-row pill composer is shared by both the empty and chat states. Assistant replies render as full-width markdown with no avatar, and user turns sit in a rounded grey bubble.

Features

  • Centered greeting: one "How can I help you today?" headline, centered over a blurred radial glow.
  • Single-row composer: a 32px pill that holds the + menu, the input, the model picker, the mic, and send on one line.
  • Combined + menu: "Add photos & files" sits next to the Deep Research, Canvas, Create image, and Guided Learning tools.
  • Model picker: Fast and Thinking, each with a short description and a check mark on the active model.
  • Send states: a disabled grey arrow when empty, a blue arrow when ready, and a stop square while a response streams.
  • Avatar-free replies: assistant messages are full-width markdown, with the action bar revealed on hover.
  • Grey user bubble: a rounded-3xl warm-grey bubble, right-aligned, with copy and edit actions on hover.

Quick Start

npx assistant-ui add thread

Code

The empty and chat states render the same Composer. It is a flex column: an optional attachment row on top, then a single action row with the input as the flex-1 middle and the controls bottom-aligned around it.

import {
  AuiIf,
  ComposerPrimitive,
  ThreadPrimitive,
} from "@assistant-ui/react";

export const Gemini = () => (
  <ThreadPrimitive.Root className="bg-[#fdfcfc] dark:bg-[#131314]">
    <AuiIf condition={(s) => s.thread.isEmpty}>
      <EmptyState />
    </AuiIf>
    <AuiIf condition={(s) => !s.thread.isEmpty}>
      <ThreadPrimitive.Viewport>
        <ThreadPrimitive.Messages components={{ Message: ChatMessage }} />
        <ThreadPrimitive.ViewportFooter className="sticky bottom-0">
          <Composer />
        </ThreadPrimitive.ViewportFooter>
      </ThreadPrimitive.Viewport>
    </AuiIf>
  </ThreadPrimitive.Root>
);

const Composer = () => (
  <ComposerPrimitive.Root className="flex flex-col rounded-4xl bg-white p-3 shadow-[0_2px_10px_-2px_rgba(0,0,0,0.18)] dark:bg-[#1e1f20]">
    <AuiIf condition={(s) => s.composer.attachments.length > 0}>
      <ComposerPrimitive.Attachments
        components={{ Attachment: GeminiAttachment }}
      />
    </AuiIf>
    <div className="flex items-end gap-1">
      <PlusMenu />
      <ComposerPrimitive.Input
        placeholder="Ask Gemini"
        className="flex-1 resize-none bg-transparent text-[17px]"
      />
      <ModelPicker />
      <VoiceButton />
      <SendButton />
    </div>
  </ComposerPrimitive.Root>
);

Send states

The trailing slot swaps between Send and Cancel with AuiIf. ComposerPrimitive.Send disables itself while the composer is empty, so the disabled, ready, and running looks are all driven by state instead of manual class toggling:

{/* Send: disabled grey when empty, blue when there is text */}
<AuiIf condition={(s) => !s.thread.isRunning}>
  <ComposerPrimitive.Send className="bg-[#d3e3fd] text-[#062e6f] disabled:bg-[#e8eaed] disabled:text-[#1f1f1f]/40">
    <ArrowUpIcon />
  </ComposerPrimitive.Send>
</AuiIf>
{/* Cancel: stop square while a response streams */}
<AuiIf condition={(s) => s.thread.isRunning}>
  <ComposerPrimitive.Cancel className="bg-[#d3e3fd] text-[#062e6f]">
    <span className="size-3 rounded-[3px] bg-current" />
  </ComposerPrimitive.Cancel>
</AuiIf>

Combined + menu

The + button opens one menu for both attachments and tools. ComposerPrimitive.AddAttachment is rendered as the first item via asChild, so picking it opens the file dialog and closes the menu:

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/shared/dropdown-menu";

<DropdownMenu>
  <DropdownMenuTrigger>
    <PlusIcon />
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start" side="bottom">
    <DropdownMenuItem asChild>
      <ComposerPrimitive.AddAttachment>
        <Paperclip /> Add photos & files
      </ComposerPrimitive.AddAttachment>
    </DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem icon={<Telescope />}>Deep Research</DropdownMenuItem>
    <DropdownMenuItem icon={<PencilRuler />}>Canvas</DropdownMenuItem>
    {/* Create image, Guided Learning ... */}
  </DropdownMenuContent>
</DropdownMenu>

Ambient glow

The empty state places a single blurred ellipse behind the greeting and composer. It is decorative, so it is pointer-events-none and aria-hidden:

<div className="relative flex grow items-center justify-center">
  <div
    aria-hidden="true"
    className="pointer-events-none absolute top-1/2 left-1/2 h-[330px] w-[720px] -translate-x-1/2 -translate-y-1/2 bg-[radial-gradient(closest-side,#a9d1fb,transparent)] opacity-70 blur-[55px] dark:bg-[radial-gradient(closest-side,#1d4068,transparent)] dark:opacity-65"
  />
  <div className="relative z-10">
    <h1 className="text-center text-4xl">How can I help you today?</h1>
    <Composer />
  </div>
</div>

Color palette

ElementLightDark
Background#fdfcfc#131314
Composer surface#ffffff#1e1f20
Primary text#1f1f1f#e3e3e3
Muted text & icons#444746#c4c7c5
User bubble#f2f0f0#333537
Send (ready)#d3e3fd#1f3760
Ambient glow#a9d1fb#1d4068

Messages

Assistant replies have no avatar; the markdown spans the full content width and the action bar (feedback, copy, regenerate, more) fades in on hover. User turns sit in a rounded grey bubble, right-aligned, with copy and edit actions to its left:

<div className="max-w-[75%] rounded-3xl bg-[#f2f0f0] px-5 py-3 dark:bg-[#333537]">
  <MessagePrimitive.Parts components={{ Text: MarkdownText }} />
</div>

Source

View full source on GitHub