Korzystanie z interfejsu Vercel AI SDK UI i elementów AI z interfejsem Prompt API

Opublikowano: 16 lipca 2026 r.

W artykule Korzystanie z wbudowanego interfejsu Prompt API z pakietem Vercel AI SDK przedstawiliśmy 4 podstawowe elementy generowania , czyli generateText(), streamText(), kod hybrydowy i uporządkowane dane wyjściowe za pomocą Output.object(), które są obsługiwane przez @browser-ai/core. Tym razem stworzymy coś bardziej interaktywnego – pełny interfejs czatu przesyłanego strumieniowo, który działa w całości w przeglądarce i automatycznie przełącza się na model w chmurze, gdy interfejs Prompt API jest niedostępny.

Co będziemy tworzyć

Interfejs czatu w React, który:

  • używa haka useChat z pakietu Vercel AI SDK do przesyłania strumieniowo rozmów wieloetapowych;
  • uruchamia pętlę modelu w przeglądarce bez konieczności korzystania z serwera backendu;
  • automatycznie przełącza się na Gemini 2.5 Flash, gdy interfejs Prompt API jest niedostępny;
  • renderuje odpowiedzi asystenta jako Markdown w stylu GitHub, obsługując niepełne tokeny podczas przesyłania strumieniowego;
  • wyświetla efekt „Myślę…” podczas oczekiwania na pierwszy token;
  • automatycznie przewija do nowych wiadomości i wyświetla przycisk przewijania do dołu, gdy użytkownik przewinie w górę.

Dodatkowe zależności

Oprócz ai, @browser-ai/core i @ai-sdk/google interfejs czatu potrzebuje React, powiązań React z pakietem AI SDK i kilku pakietów 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

W przypadku interfejsu dodaj też Tailwind CSS, kilka narzędzi do komponentów i Lucide do ikon:

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

Konfiguracja głosowania: kompilacja z wieloma punktami wejścia

Projekt ma już plik index.html. Dodaj chat.html jako drugi punkt wejścia i skonfiguruj Vite tak, aby kompilował oba pliki:

// 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 jest minimalny – zawiera tylko <div id="root"> i tag skryptu:

<!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>

Automatyczny wybór modelu

W chatbotcie aplikacja dokonuje wyboru automatycznie: najpierw próbuje użyć wbudowanego modelu, a jeśli interfejs Prompt API jest niedostępny, przełącza się na chmurę.

Dzieje się to podczas wczytywania modułu, zanim React zostanie zamontowany, więc agent jest gotowy, zanim użytkownik wpisze pierwszą wiadomość:

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.' });
})();

Nowym elementem jest ToolLoopAgent – abstrakcja pakietu AI SDK, która zarządza pętlą rozmowy wieloetapowej na dowolnym modelu. Pobiera model i prompt systemowy oraz obsługuje wewnętrznie komunikację dwukierunkową.

Łączenie agenta z useChat

Hak useChat z pakietu @ai-sdk/react zwykle komunikuje się z punktem końcowym HTTP. W przypadku wnioskowania po stronie przeglądarki użyj zamiast tego DirectChatTransport. Uruchamia on pętlę ToolLoopAgent w całości w przeglądarce bez udziału serwera:

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

useMemo jest ważny, ponieważ DirectChatTransport przechowuje stan rozmowy, więc musi być stabilnym odniesieniem. Ponowne tworzenie go przy każdym renderowaniu powoduje zresetowanie rozmowy.

useChat zapewnia:

  • Messages: pełna rozmowa jako UIMessage[], z których każda ma role i tablicę parts.
  • sendMessage({ text }): wysyła nową turę użytkownika i rozpoczyna przesyłanie strumieniowe odpowiedzi.
  • Status: 'idle' | 'submitted' | 'streaming' | 'error'
  • Stop: anuluje generowanie w trakcie.

Renderowanie wiadomości

Każda wiadomość ma tablicę parts. W przypadku tego chatbota interesują nas tylko części type: 'text'. Wiadomości użytkownika są wyświetlane jako dymek wyrównany do prawej, a wiadomości asystenta są wyrównane do lewej i mają 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 i Response to elementy AI. Są to komponenty źródłowe w stylu shadcn, które kopiujesz do projektu, zamiast instalować z npm. Response otacza react-markdown za pomocą remark-gfm dla Markdown w stylu GitHub (tabele, listy zadań, przekreślenie) i harden-react-markdown do czyszczenia linków i obrazów w danych wyjściowych AI.

Właściwość parseIncompleteMarkdown ma wartość true, gdy wiadomość jest nadal przesyłana strumieniowo. Podczas przesyłania strumieniowego model może napisać **bold i nie zamknąć jeszcze **, pozostawiając wiszący token, który byłby renderowany jako dosłowne gwiazdki. parseIncompleteMarkdown zamyka wszystkie otwarte **, __, `, ~~ i obcina wiszące początki linków [, dzięki czemu renderowane dane wyjściowe pozostają czyste w każdym przyrostowym fragmencie.

Stan „Myślę…”

Między wysłaniem wiadomości a otrzymaniem pierwszego tokena status ma wartość 'submitted'. W tym czasie aplikacja wyświetla animowany efekt:

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

Warunek messages.at(-1)?.role !== 'assistant' zapobiega ponownemu pojawianiu się efektu po rozpoczęciu przesyłania strumieniowego wiadomości asystenta.

ThinkingMessage używa komponentu Shimmer: <span> z ruchomym gradientem używającym background-clip: text, który nadaje tekstowi „Myślę…” efekt rozmytego podświetlenia.

Autoprzewijanie

Gdy pojawia się nowa treść, aplikacja przewija do dołu, ale tylko wtedy, gdy użytkownik jest już na dole. Przewijanie użytkownika z dala od czegoś, co czyta w trakcie rozmowy, byłoby irytujące.

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);
};

Gdy isAtBottom ma wartość false, pojawia się pływający przycisk przewijania do dołu, który znika, gdy użytkownik wróci na dół.

Obszar wprowadzania danych

Pole tekstowe automatycznie zmienia rozmiar podczas pisania, resetując wysokość do auto przy każdym zdarzeniu wprowadzania danych, a następnie ustawiając ją na scrollHeight. Wysyła się po naciśnięciu Enter (nie Shift+Enter), a podczas przesyłania strumieniowego odpowiedzi przycisk Wyślij jest zastępowany przyciskiem Zatrzymaj, który wywołuje 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>
  );
}

Montowanie ze stanem wczytywania

Ponieważ agentPromise jest asynchroniczny, poczekaj na jego zakończenie przed renderowaniem Chat. Otoczka App rozwiązuje obietnicę i w międzyczasie wyświetla spinner:

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} />;
}

Gdy agent rozwiąże obietnicę (niezależnie od tego, czy jest to natychmiastowe uruchomienie wbudowanego modelu, czy oczekiwanie na pobranie modelu), spinner znika i montuje się interfejs czatu.

Prezentacja

Wersja demonstracyjna na żywo to w pełni funkcjonalny chatbot działający w całości w przeglądarce. Wpisz wiadomość i naciśnij Enter. Jeśli interfejs Prompt API jest dostępny, odpowiedź jest przesyłana strumieniowo bezpośrednio z modelu na urządzeniu bez wysyłania żądania sieciowego. Jeśli Twoja przeglądarka nie obsługuje interfejsu Prompt API, automatycznie przełączy się na Gemini 2.5 Flash. Spróbuj poprosić go o wyjaśnienie czegoś na liście, napisanie fragmentu kodu lub użycie formatowania Markdown. Odpowiedzi są renderowane z sformatowanymi blokami kodu, tabelami i kodem wbudowanym.

Interfejs czatu przedstawiający rozmowę z asystentem AI, z polem wpisywania tekstu i obszarem przesyłania strumieniowego odpowiedzi.

Podsumowanie

W tych 2 artykułach przedstawiliśmy pełny zakres możliwości pakietu Vercel AI SDK z wbudowanym w przeglądarkę interfejsem Prompt API – od podstawowych elementów generowania po dopracowany interfejs czatu przesyłanego strumieniowo.

W artykule Korzystanie z wbudowanego interfejsu Prompt API z pakietem Vercel AI SDK dowiedzieliśmy się, jak używać generateText() i streamText() do generowania tekstu bez przesyłania strumieniowego i z przesyłaniem strumieniowym, jak żądać uporządkowanych danych wyjściowych JSON za pomocą Output.object() oraz jak pisać kod hybrydowy, który w czasie działania aplikacji wybiera między wbudowanym modelem a dostawcą usług w chmurze bez wprowadzania zmian w logice generowania.

W tym dokumencie użyliśmy tych samych elementów składowych i otoczyliśmy je pełnym interfejsem React: ToolLoopAgent do zarządzania pętlą rozmowy, useChat z DirectChatTransport do przesyłania strumieniowego odpowiedzi bezpośrednio w przeglądarce oraz komponenty elementów AI do czystego renderowania odpowiedzi Markdown w miarę ich przychodzenia. Wszystko to z automatycznym przełączaniem się na chmurę, gdy interfejs Prompt API jest niedostępny.

Wynikiem są 2 wersje demonstracyjne, które działają w całości w przeglądarce bez konieczności korzystania z backendu: