Menggunakan UI Vercel AI SDK dan Elemen AI dengan Prompt API

Dipublikasikan: 16 Juli 2026

Di artikel Menggunakan Prompt API bawaan dengan Vercel AI SDK, Anda telah melihat empat primitif pembuatan inti, yaitu generateText(), streamText(), kode hybrid, dan output terstruktur menggunakan Output.object(), yang semuanya didukung oleh @browser-ai/core. Kali ini, Anda akan membuat sesuatu yang lebih interaktif: UI chat streaming lengkap yang berjalan sepenuhnya di browser, dengan otomatis beralih ke model cloud jika Prompt API tidak tersedia.

Yang akan Anda buat

Antarmuka chat React yang melakukan hal berikut:

  • Menggunakan hook useChat Vercel AI SDK untuk percakapan multi-turn streaming.
  • Menjalankan loop model di browser tanpa memerlukan server backend.
  • Otomatis beralih ke Gemini 2.5 Flash jika Prompt API tidak tersedia.
  • Merender balasan asisten sebagai Markdown yang disesuaikan dengan GitHub, menangani token yang tidak lengkap selama streaming.
  • Menampilkan shimmer "Thinking…" saat menunggu token pertama.
  • Men-scroll otomatis ke pesan baru, dengan tombol scroll ke bawah saat Anda men-scroll ke atas.

Dependensi tambahan

Selain ai, @browser-ai/core, dan @ai-sdk/google, UI chat memerlukan React, binding React AI SDK, dan beberapa paket Markdown:

npm install react react-dom @ai-sdk/react
npm install react-markdown remark-gfm harden-react-markdown
npm install -D @types/react @types/react-dom

Untuk UI, tambahkan juga Tailwind CSS, beberapa utilitas komponen, dan Lucide untuk ikon:

npm install -D tailwindcss postcss autoprefixer
npm install clsx tailwind-merge lucide-react

Konfigurasi Vite: build multi-entri

Project ini sudah memiliki index.html. Tambahkan chat.html sebagai titik entri kedua dan konfigurasi Vite untuk membuat keduanya:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: { alias: { '@': resolve(__dirname, './src') } },
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        chat: resolve(__dirname, 'chat.html'),
      },
    },
  },
});

chat.html bersifat minimal, hanya <div id="root"> dan tag skrip:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Built-in AI Chatbot</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/chat.tsx"></script>
  </body>
</html>

Pemilihan model otomatis

Di chatbot, aplikasi membuat pilihan tersebut secara otomatis: aplikasi mencoba model bawaan terlebih dahulu, dan beralih ke cloud jika Prompt API tidak tersedia.

Hal ini dilakukan pada waktu pemuatan modul, sebelum React dipasang, sehingga agen siap saat pengguna mengetik pesan pertamanya:

const agentPromise: Promise<ToolLoopAgent> = (async () => {
  const builtIn = browserAI();
  let model: any = builtIn;

  if (typeof builtIn.availability === 'function') {
    const availability = await builtIn.availability();
    if (availability === 'unavailable') {
      const { createGoogleGenerativeAI } = await import('@ai-sdk/google');
      model = createGoogleGenerativeAI({ apiKey })('gemini-2.5-flash');
    } else if (availability === 'downloadable') {
      await builtIn.createSessionWithProgress(() => {});
    }
  }

  return new ToolLoopAgent({ model, instructions: 'You are a helpful assistant.' });
})();

Bagian baru ini adalah ToolLoopAgent; abstraksi AI SDK yang mengelola loop percakapan multi-turn di atas model apa pun. Bagian ini menggunakan model dan perintah sistem, serta menangani bolak-balik secara internal.

Menghubungkan agen ke useChat

Hook useChat paket @ai-sdk/react biasanya berkomunikasi dengan endpoint HTTP. Untuk inferensi sisi browser, gunakan DirectChatTransport. Bagian ini menjalankan loop ToolLoopAgent sepenuhnya di browser tanpa melibatkan server:

const transport = useMemo(() => new DirectChatTransport({ agent }), [agent]);
const { messages, sendMessage, status, stop } = useChat({ transport });

useMemo penting karena DirectChatTransport menyimpan status percakapan, sehingga harus menjadi referensi yang stabil. Membuat ulang pada setiap render akan mereset percakapan.

useChat memberi Anda:

  • Messages: percakapan lengkap sebagai UIMessage[], masing-masing dengan role dan array parts
  • sendMessage({ text }): mengirim giliran pengguna baru dan mulai melakukan streaming respons
  • Status: 'idle' | 'submitted' | 'streaming' | 'error'
  • Stop: membatalkan pembuatan yang sedang berlangsung

Merender pesan

Setiap pesan memiliki array parts. Untuk chatbot ini, kita hanya memperhatikan bagian type: 'text'. Pesan pengguna muncul sebagai gelembung yang diratakan ke kanan; pesan asisten diratakan ke kiri dengan ikon:

const ChatMessage = ({ message, isStreaming }: { message: UIMessage; isStreaming: boolean }) => {
  const isUser = message.role === 'user';

  const textParts = message.parts.map((part, i) => {
    if (part.type !== 'text') return null;
    if (isUser) return <span key={i}>{part.text}</span>;
    return <Response key={i} parseIncompleteMarkdown={isStreaming}>{part.text}</Response>;
  });

  if (isUser) {
    return (
      <div className="flex flex-col items-end gap-2 animate-fade-up">
        <MessageContent className="w-fit max-w-[min(80%,56ch)] ...">
          {textParts}
        </MessageContent>
      </div>
    );
  }

  return (
    <div className="flex items-start gap-3">
      <AIIcon />
      <MessageContent className="text-[13px] leading-[1.65]">{textParts}</MessageContent>
    </div>
  );
};

MessageContent dan Response adalah Elemen AI. Keduanya adalah komponen sumber gaya shadcn yang Anda salin ke project, bukan diinstal dari npm. Response menggabungkan react-markdown dengan remark-gfm untuk Markdown yang disesuaikan dengan GitHub (tabel, daftar tugas, coretan) dan harden-react-markdown untuk membersihkan link dan gambar dalam output AI.

Properti parseIncompleteMarkdown adalah true saat pesan masih di-streaming. Selama streaming, model dapat menulis **bold tetapi belum menutup **, sehingga menyisakan token yang tidak lengkap yang akan dirender sebagai tanda bintang literal. parseIncompleteMarkdown menutup **, __, `, ~~ yang terbuka, dan memangkas awal link [ yang tidak lengkap sehingga output yang dirender tetap bersih pada setiap bagian tambahan.

Status "Thinking…"

Antara mengirim pesan dan menerima token pertama, status adalah 'submitted'. Selama periode ini, aplikasi menampilkan shimmer animasi:

{status === 'submitted' && messages.at(-1)?.role !== 'assistant' && (
  <ThinkingMessage />
)}

Kondisi messages.at(-1)?.role !== 'assistant' mencegah shimmer muncul kembali setelah pesan asisten mulai di-streaming.

ThinkingMessage menggunakan komponen Shimmer: <span> dengan gradien bergerak menggunakan background-clip: text yang memberikan efek sorot menyapu pada teks "Thinking…".

Scroll otomatis

Saat konten baru tiba, aplikasi akan men-scroll ke bagian bawah, tetapi hanya jika pengguna sudah berada di bagian bawah. Men-scroll mereka dari sesuatu yang sedang mereka baca di tengah percakapan akan mengganggu.

const [isAtBottom, setIsAtBottom] = useState(true);

useEffect(() => {
  if (isAtBottom) endRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, status, isAtBottom]);

const handleScroll = () => {
  const el = containerRef.current;
  if (!el) return;
  setIsAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 50);
};

Tombol scroll ke bawah mengambang akan muncul jika isAtBottom salah (false), dan memudar saat pengguna kembali ke bagian bawah.

Area input

Area teks akan otomatis diubah ukurannya saat Anda mengetik dengan mereset tingginya ke auto pada setiap peristiwa input, lalu menetapkannya ke scrollHeight. Area teks akan dikirim saat Enter ditekan (bukan Shift+Enter), dan saat respons di-streaming, tombol Kirim akan diganti dengan tombol Berhenti yang memanggil stop():

<textarea
  onInput={(e) => {
    const el = e.currentTarget;
    el.style.height = 'auto';
    el.style.height = `${el.scrollHeight}px`;
  }}
  onKeyDown={(e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      if (input.trim() && !isStreaming) {
        sendMessage({ text: input });
        setInput('');
      }
    }
  }}
/>;
{
  isStreaming ? (
    <Button variant="outline" onClick={stop}>
      Stop
    </Button>
  ) : (
    <Button type="submit" disabled={!input.trim()}>
      Send
    </Button>
  );
}

Memasang dengan status pemuatan

Karena agentPromise bersifat asinkron, tunggu hingga selesai sebelum merender Chat. Wrapper App akan menyelesaikan promise dan menampilkan pemintal sementara:

function App() {
  const [agent, setAgent] = (useState < ToolLoopAgent) | (null > null);

  useEffect(() => {
    agentPromise.then(setAgent);
  }, []);

  if (!agent) {
    return (
      <div className="flex h-dvh items-center justify-center">
        <Loader size={20} />
      </div>
    );
  }

  return <Chat agent={agent} />;
}

Setelah agen menyelesaikan, baik itu model bawaan yang langsung di-booting atau menunggu download model, pemintal akan menghilang dan UI chat akan dipasang.

Demo

Demo langsung adalah chatbot yang berfungsi penuh dan berjalan sepenuhnya di browser Anda. Ketik pesan, lalu tekan Enter. Jika Prompt API tersedia, streaming balasan akan langsung berasal dari model di perangkat tanpa permintaan jaringan. Jika browser Anda tidak mendukung Prompt API, browser akan otomatis beralih ke Gemini 2.5 Flash. Coba minta chatbot untuk menjelaskan sesuatu dalam daftar, menulis cuplikan kode, atau menggunakan pemformatan Markdown. Respons akan dirender dengan blok kode, tabel, dan kode inline yang diformat.

Antarmuka chat yang menampilkan percakapan dengan asisten AI, yang menampilkan kolom input teks dan area respons streaming.

Kesimpulan

Selama dua artikel ini, Anda telah melihat berbagai kemungkinan yang dapat dilakukan Vercel AI SDK dengan Prompt API bawaan browser, mulai dari primitif pembuatan mentah hingga antarmuka chat streaming yang disempurnakan.

Di artikel Menggunakan Prompt API bawaan dengan Vercel AI SDK, Anda telah mempelajari cara menggunakan generateText() dan streamText() untuk pembuatan teks non-streaming dan streaming, cara meminta output JSON terstruktur dengan Output.object(), dan cara menulis kode hybrid yang memilih antara model bawaan dan penyedia cloud saat runtime tanpa mengubah logika pembuatan.

Dalam dokumen ini, Anda telah menggunakan blok penyusun yang sama dan menggabungkannya dalam UI React lengkap: ToolLoopAgent untuk mengelola loop percakapan, useChat dengan DirectChatTransport untuk melakukan streaming respons langsung di browser, dan komponen Elemen AI untuk merender respons Markdown secara bersih saat respons tiba; semuanya dengan otomatis beralih ke cloud jika Prompt API tidak tersedia.

Hasilnya adalah dua demo yang berfungsi sepenuhnya di browser, tanpa memerlukan backend: