# AI Chat with Model Selection

> A ai chat with model selection block.

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

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

## Install

```bash
npx shadcn@latest add https://blocks.so/r/ai-02.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`, `select`, `textarea`

## Files

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

```tsx
'use client';

import {
  IconAlertTriangle,
  IconArrowUp,
  IconCloud,
  IconFileSpark,
  IconGauge,
  IconPhotoScan,
} from '@tabler/icons-react';
import { useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';

const PROMPTS = [
  {
    icon: IconFileSpark,
    text: 'Write documentation',
    prompt:
      'Write comprehensive documentation for this codebase, including setup instructions, API references, and usage examples.',
  },
  {
    icon: IconGauge,
    text: 'Optimize performance',
    prompt:
      'Analyze the codebase for performance bottlenecks and suggest optimizations to improve loading times and runtime efficiency.',
  },
  {
    icon: IconAlertTriangle,
    text: 'Find and fix 3 bugs',
    prompt:
      'Scan through the codebase to identify and fix 3 critical bugs, providing detailed explanations for each fix.',
  },
];

const MODELS = [
  {
    value: 'gpt-5',
    name: 'GPT-5',
    description: 'Most advanced model',
    max: true,
  },
  {
    value: 'gpt-4o',
    name: 'GPT-4o',
    description: 'Fast and capable',
  },
  {
    value: 'claude-3.5',
    name: 'Claude 3.5 Sonnet',
    description: 'Great for coding tasks',
  },
];

export default function Ai02() {
  const [inputValue, setInputValue] = useState('');
  const [modelValue, setModelValue] = useState(MODELS[0].value);
  const selectedModel = MODELS.find((m) => m.value === modelValue) ?? MODELS[0];
  const inputRef = useRef<HTMLTextAreaElement>(null);

  const handlePromptClick = (prompt: string) => {
    if (inputRef.current) {
      inputRef.current.value = prompt;
      setInputValue(prompt);
      inputRef.current.focus();
    }
  };

  const maxBadge = (
    <span className="inline-flex h-[15px] items-center rounded-[4px] border border-border/80 bg-muted/40 px-[5px]">
      <span
        className="font-bold text-[9px] uppercase leading-none tracking-[0.06em]"
        style={{
          background:
            'linear-gradient(to right, rgb(129, 161, 193), rgb(125, 124, 155))',
          WebkitBackgroundClip: 'text',
          WebkitTextFillColor: 'transparent',
        }}
      >
        MAX
      </span>
    </span>
  );

  return (
    <div className="flex w-[calc(42rem-5rem)] flex-col gap-4">
      <div className="flex min-h-[120px] cursor-text flex-col rounded-2xl border border-border bg-card shadow-lg">
        <div className="relative max-h-[258px] flex-1 overflow-y-auto">
          <Textarea
            className="min-h-[48.4px] w-full resize-none whitespace-pre-wrap break-words border-0 bg-transparent! p-3 text-[16px] text-foreground shadow-none outline-none transition-[padding] duration-200 ease-in-out focus-visible:ring-0 focus-visible:ring-offset-0"
            onChange={(e) => setInputValue(e.target.value)}
            placeholder="Ask anything"
            ref={inputRef}
            value={inputValue}
          />
        </div>

        <div className="flex min-h-[40px] items-center gap-2 p-2 pb-1">
          <div className="flex size-7 items-center justify-center rounded-full bg-muted">
            <IconCloud className="size-4 text-muted-foreground" />
          </div>

          <Select
            onValueChange={(value) => value && setModelValue(value)}
            value={modelValue}
          >
            <SelectTrigger
              className="h-7 rounded-full border-transparent bg-transparent px-2 font-medium text-foreground text-sm shadow-none transition-[background-color,color] hover:bg-muted aria-expanded:bg-muted dark:bg-transparent dark:hover:bg-muted"
              size="sm"
            >
              <SelectValue>
                <span className="flex items-center gap-1.5">
                  <span>{selectedModel.name}</span>
                  {selectedModel.max && maxBadge}
                </span>
              </SelectValue>
            </SelectTrigger>
            <SelectContent
              align="start"
              alignItemWithTrigger={false}
              className="w-64 p-1"
              sideOffset={6}
            >
              {MODELS.map((model) => (
                <SelectItem
                  className="rounded-md py-1.5"
                  key={model.value}
                  value={model.value}
                >
                  <span className="flex flex-col gap-0.5">
                    <span className="flex items-center gap-1.5 font-medium">
                      {model.name}
                      {model.max && maxBadge}
                    </span>
                    <span className="text-muted-foreground text-xs">
                      {model.description}
                    </span>
                  </span>
                </SelectItem>
              ))}
            </SelectContent>
          </Select>

          <div className="ml-auto flex items-center gap-1.5">
            <Button
              aria-label="Attach images"
              className="rounded-full text-muted-foreground transition-[background-color,color] hover:text-foreground"
              size="icon-sm"
              title="Attach images"
              variant="ghost"
            >
              <IconPhotoScan className="size-4.5" />
            </Button>

            <Button
              aria-label="Send message"
              className="rounded-full transition-[background-color,scale,opacity] active:not-disabled:scale-[0.96]"
              disabled={!inputValue}
              size="icon-sm"
            >
              <IconArrowUp className="size-4" />
            </Button>
          </div>
        </div>
      </div>

      <div className="flex flex-wrap justify-center gap-2">
        {PROMPTS.map((button) => {
          const IconComponent = button.icon;
          return (
            <Button
              className="h-8 gap-1.5 rounded-full border-border bg-background pr-3.5 pl-2.5 font-normal text-foreground text-sm shadow-[0_1px_2px_oklch(0_0_0/0.05)] transition-[background-color,color,scale,box-shadow] duration-150 ease-out hover:bg-muted active:scale-[0.96] dark:bg-muted/40 dark:hover:bg-muted"
              key={button.text}
              onClick={() => handlePromptClick(button.prompt)}
              variant="outline"
            >
              <IconComponent
                className="size-4 text-muted-foreground transition-colors duration-150 group-hover/button:text-foreground"
                strokeWidth={1.75}
              />
              {button.text}
            </Button>
          );
        })}
      </div>
    </div>
  );
}
```
