# AI Chat with Voice Input

> A ai chat with voice input block.

A shadcn/ui block from the [@blocks-so](https://blocks.so) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/blocks-so/ai-01).

- Page, with a live preview: https://shadcn.exchange/r/blocks-so/ai-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/blocks-so/ai-01-light-e7ab5dd85981ce7d.png
- Registry item: https://blocks.so/r/ai-01.json
- Categories: ai
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://blocks.so/r/ai-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tabler/icons-react`
- Registry items: `button`, `dropdown-menu`, `textarea`

## Files

### `content/components/ai/ai-01.tsx`

```tsx
'use client';

import {
  IconMicrophone,
  IconPaperclip,
  IconPlus,
  IconSearch,
  IconSend,
  IconSparkles,
  IconWaveSine,
} from '@tabler/icons-react';
import type React from 'react';
import { useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';

export default function Ai01() {
  const [message, setMessage] = useState('');
  const [isExpanded, setIsExpanded] = useState(false);
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const send = () => {
    if (!message.trim()) {
      return;
    }

    setMessage('');
    setIsExpanded(false);

    if (textareaRef.current) {
      textareaRef.current.style.height = 'auto';
    }
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    send();
  };

  const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setMessage(e.target.value);

    if (textareaRef.current) {
      textareaRef.current.style.height = 'auto';
      textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
    }

    setIsExpanded(e.target.value.length > 100 || e.target.value.includes('\n'));
  };

  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      send();
    }
  };

  return (
    <div className="w-full">
      <h1 className="mx-auto mb-6 max-w-2xl text-balance text-center font-semibold text-3xl text-foreground leading-tight tracking-tight">
        How can I help you today?
      </h1>

      <form className="group/composer w-full" onSubmit={handleSubmit}>
        <input className="sr-only" multiple ref={fileInputRef} type="file" />

        <div
          className={cn(
            'mx-auto w-full max-w-2xl cursor-text overflow-clip bg-background bg-clip-padding p-2.5 shadow-[0_0_0_1px_oklch(0_0_0/0.08),0_1px_2px_-1px_oklch(0_0_0/0.08),0_4px_12px_-2px_oklch(0_0_0/0.08)] dark:bg-muted/50 dark:shadow-[0_0_0_1px_oklch(1_0_0/0.1)]',
            isExpanded
              ? "grid rounded-[28px] [grid-template-areas:'header'_'primary'_'footer'] [grid-template-columns:1fr] [grid-template-rows:auto_1fr_auto]"
              : "grid rounded-full [grid-template-areas:'header_header_header'_'leading_primary_trailing'_'._footer_.'] [grid-template-columns:auto_1fr_auto] [grid-template-rows:auto_1fr_auto]"
          )}
        >
          <div
            className={cn(
              'flex overflow-x-hidden',
              isExpanded
                ? 'px-2 pt-1.5 pb-2'
                : '-my-2.5 min-h-14 items-center px-1.5'
            )}
            style={{ gridArea: 'primary' }}
          >
            <div className="max-h-52 flex-1 overflow-auto">
              <Textarea
                className="scrollbar-thin min-h-0 resize-none rounded-none border-0 p-0 text-base placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0 md:text-base dark:bg-transparent"
                onChange={handleTextareaChange}
                onKeyDown={handleKeyDown}
                placeholder="Ask anything"
                ref={textareaRef}
                rows={1}
                value={message}
              />
            </div>
          </div>

          <div
            className={cn('flex items-center', { hidden: isExpanded })}
            style={{ gridArea: 'leading' }}
          >
            <DropdownMenu>
              <DropdownMenuTrigger
                render={
                  <Button
                    aria-label="Add attachments"
                    className="rounded-full transition-[scale,background-color] duration-150 ease-out active:scale-[0.96]"
                    size="icon-lg"
                    type="button"
                    variant="ghost"
                  />
                }
              >
                <IconPlus
                  className="size-5 text-muted-foreground"
                  stroke={1.5}
                />
              </DropdownMenuTrigger>

              <DropdownMenuContent
                align="start"
                className="w-64 rounded-[16px] p-1.5"
              >
                <DropdownMenuGroup className="space-y-1">
                  <DropdownMenuItem
                    className="rounded-[10px] px-2.5 py-2"
                    onClick={() => fileInputRef.current?.click()}
                  >
                    <IconPaperclip
                      className="opacity-60"
                      size={20}
                      stroke={1.5}
                    />
                    Add photos & files
                  </DropdownMenuItem>
                  <DropdownMenuItem
                    className="rounded-[10px] px-2.5 py-2"
                    onClick={() => {}}
                  >
                    <IconSparkles
                      className="opacity-60"
                      size={20}
                      stroke={1.5}
                    />
                    Agent mode
                  </DropdownMenuItem>
                  <DropdownMenuItem
                    className="rounded-[10px] px-2.5 py-2"
                    onClick={() => {}}
                  >
                    <IconSearch className="opacity-60" size={20} stroke={1.5} />
                    Deep Research
                  </DropdownMenuItem>
                </DropdownMenuGroup>
              </DropdownMenuContent>
            </DropdownMenu>
          </div>

          <div
            className="flex items-center gap-2"
            style={{ gridArea: isExpanded ? 'footer' : 'trailing' }}
          >
            <div className="ms-auto flex items-center gap-1.5">
              <Button
                aria-label="Record audio message"
                className="rounded-full transition-[scale,background-color] duration-150 ease-out active:scale-[0.96]"
                size="icon-lg"
                type="button"
                variant="ghost"
              >
                <IconMicrophone
                  className="size-5 text-muted-foreground"
                  stroke={1.5}
                />
              </Button>

              <Button
                aria-label="Audio visualization"
                className="rounded-full transition-[scale,background-color] duration-150 ease-out active:scale-[0.96]"
                size="icon-lg"
                type="button"
                variant="ghost"
              >
                <IconWaveSine
                  className="size-5 text-muted-foreground"
                  stroke={1.5}
                />
              </Button>

              <Button
                aria-label="Send message"
                className="rounded-full transition-[scale,opacity,background-color] duration-150 ease-out active:scale-[0.96] disabled:opacity-40"
                disabled={!message.trim()}
                size="icon-lg"
                type="submit"
              >
                <IconSend className="size-4.5" stroke={1.5} />
              </Button>
            </div>
          </div>
        </div>
      </form>
    </div>
  );
}
```
