shadimage
Blocks.so
blockai-05source: ok · refreshed

AI Elements Chat

A ai elements chat block.

npx shadcn add "https://blocks.so/r/ai-05.json"
free

Where it goes

chat

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@tabler/icons-reactai

Registry dependencies

button

Files1 · source cached 10/8/2026, 2:20:48 AM

content/components/ai/ai-05.tsx
'use client';

// Install AI Elements components:
// npx ai-elements@latest add conversation message prompt-input

import {
  IconAdjustmentsHorizontal,
  IconBolt,
  IconPaperclip,
  IconPlus,
} from '@tabler/icons-react';
import type { ChatStatus } from 'ai';
import { useState } from 'react';
import {
  Conversation,
  ConversationContent,
  ConversationScrollButton,
} from '@/components/ai-elements/conversation';
import {
  Message,
  MessageContent,
  MessageResponse,
} from '@/components/ai-elements/message';
import {
  PromptInput,
  PromptInputButton,
  PromptInputFooter,
  PromptInputSubmit,
  PromptInputTextarea,
  PromptInputTools,
} from '@/components/ai-elements/prompt-input';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';

interface DemoMessage {
  id: string;
  role: 'user' | 'assistant';
  content: string;
}

const INITIAL_MESSAGES: DemoMessage[] = [
  {
    id: 'intro',
    role: 'assistant',
    content:
      'Hi, I am your workspace assistant. I can draft copy, summarize documents, and turn notes into tasks. What are you working on?',
  },
  {
    id: 'question',
    role: 'user',
    content: 'Summarize the Q3 launch plan in three bullets.',
  },
  {
    id: 'answer',
    role: 'assistant',
    content:
      '- **Ship the new onboarding** by Sept 18 with an A/B test on pricing.\n- **Open EU and APAC** with localized checkout.\n- **Launch the partner API** in private beta with 12 partners.',
  },
];

const SUGGESTIONS = [
  'Draft a launch announcement',
  'List open risks',
  'Turn this into tasks',
];

const RESPONSES = [
  'Here is a quick outline you can reuse:\n\n1. Swap the mock response with a real API call.\n2. Stream tokens into `MessageResponse`.\n3. Keep the layout as-is for a consistent UI.',
  'If you want multi-model support, add a small model selector to the prompt footer and pass the selection to your backend.',
  'You can also inject tools like file upload or voice input by adding buttons to the prompt footer.',
];

export default function Ai05() {
  const [messages, setMessages] = useState<DemoMessage[]>(INITIAL_MESSAGES);
  const [inputValue, setInputValue] = useState('');
  const [status, setStatus] = useState<ChatStatus>('ready');

  const handleSend = (text: string) => {
    const trimmed = text.trim();
    if (!trimmed || status !== 'ready') {
      return;
    }

    setMessages((prev) => [
      ...prev,
      { id: `user-${Date.now()}`, role: 'user', content: trimmed },
    ]);
    setInputValue('');
    setStatus('submitted');

    window.setTimeout(() => {
      setMessages((prev) => [
        ...prev,
        {
          id: `assistant-${Date.now()}`,
          role: 'assistant',
          content: RESPONSES[messages.length % RESPONSES.length],
        },
      ]);
      setStatus('ready');
    }, 1200);
  };

  const handleReset = () => {
    setMessages(INITIAL_MESSAGES.slice(0, 1));
    setInputValue('');
    setStatus('ready');
  };

  return (
    <div className="w-full px-4">
      <div className="mx-auto flex h-[560px] w-full max-w-2xl flex-col overflow-hidden rounded-3xl bg-card shadow-[0_0_0_1px_oklch(0_0_0/0.06),0_1px_2px_oklch(0_0_0/0.04),0_12px_32px_-12px_oklch(0_0_0/0.18)] dark:shadow-[0_0_0_1px_oklch(1_0_0/0.1),0_12px_32px_-12px_oklch(0_0_0/0.6)]">
        <header className="flex items-center justify-between gap-4 px-5 py-4">
          <div className="flex flex-col">
            <span className="font-semibold text-sm leading-tight">
              Assistant
            </span>
            <span className="inline-flex items-center gap-1.5 text-muted-foreground text-xs">
              <span className="size-1.5 rounded-full bg-emerald-500" />
              Online
            </span>
          </div>
          <div className="flex items-center gap-1">
            <Button
              className="h-8 gap-1.5 px-2.5 text-xs"
              onClick={handleReset}
              size="sm"
              variant="ghost"
            >
              <IconPlus className="size-4" stroke={1.5} />
              New chat
            </Button>
            <Button
              aria-label="Settings"
              className="size-8"
              size="icon"
              title="Settings"
              variant="ghost"
            >
              <IconAdjustmentsHorizontal className="size-4" stroke={1.5} />
            </Button>
          </div>
        </header>

        <Conversation className="border-border/60 border-t">
          <ConversationContent className="gap-5 px-5 py-5">
            {messages.map((message) => (
              <Message from={message.role} key={message.id}>
                <MessageContent
                  className={cn(
                    'leading-relaxed',
                    message.role === 'assistant' && 'max-w-prose',
                    message.role === 'user' &&
                      'group-[.is-user]:rounded-2xl group-[.is-user]:rounded-br-md group-[.is-user]:bg-primary group-[.is-user]:text-primary-foreground'
                  )}
                >
                  {message.role === 'assistant' ? (
                    <MessageResponse>{message.content}</MessageResponse>
                  ) : (
                    <p className="whitespace-pre-wrap text-pretty">
                      {message.content}
                    </p>
                  )}
                </MessageContent>
              </Message>
            ))}
            {status === 'submitted' && (
              <Message from="assistant">
                <output
                  aria-label="Assistant is typing"
                  className="flex h-7 items-center gap-1"
                >
                  {[0, 1, 2].map((dot) => (
                    <span
                      className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60"
                      key={dot}
                      style={{ animationDelay: `${dot * 150}ms` }}
                    />
                  ))}
                </output>
              </Message>
            )}
          </ConversationContent>
          <ConversationScrollButton />
        </Conversation>

        <div className="flex flex-col gap-3 p-3">
          <div className="flex flex-wrap gap-2 px-1">
            {SUGGESTIONS.map((suggestion) => (
              <button
                className="rounded-full border border-border/70 bg-background px-3 py-1.5 text-muted-foreground text-xs transition-[color,background-color,scale] hover:bg-muted hover:text-foreground active:scale-[0.96] disabled:pointer-events-none disabled:opacity-50"
                disabled={status !== 'ready'}
                key={suggestion}
                onClick={() => handleSend(suggestion)}
                type="button"
              >
                {suggestion}
              </button>
            ))}
          </div>
          <PromptInput
            className="[&>[data-slot=input-group]]:rounded-xl [&>[data-slot=input-group]]:border-border [&>[data-slot=input-group]]:bg-muted/40 [&>[data-slot=input-group]]:shadow-none"
            onSubmit={(message) => handleSend(message.text)}
          >
            <PromptInputTextarea
              onChange={(event) => setInputValue(event.currentTarget.value)}
              placeholder="Message Assistant..."
              value={inputValue}
            />
            <PromptInputFooter>
              <PromptInputTools>
                <PromptInputButton aria-label="Attach file">
                  <IconPaperclip className="size-4" stroke={1.5} />
                </PromptInputButton>
                <PromptInputButton aria-label="Quick prompt">
                  <IconBolt className="size-4" stroke={1.5} />
                </PromptInputButton>
              </PromptInputTools>
              <PromptInputSubmit
                className="rounded-full transition-[scale,opacity] active:scale-[0.96]"
                disabled={!inputValue.trim() || status !== 'ready'}
                status={status}
              />
            </PromptInputFooter>
          </PromptInput>
        </div>
      </div>
    </div>
  );
}

More from Blocks.so

Browse all components in this library →