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

Opublikowano: 16 lipca 2026 r.

W sekcji Korzystanie z wbudowanego interfejsu Prompt API z pakietem Vercel AI SDK przedstawiliśmy 4 podstawowe elementy generowania: generateText(), streamText(), kod hybrydowy i dane wyjściowe w formacie strukturalnym za pomocą Output.object(), które są oparte na @browser-ai/core. Tym razem stworzysz coś bardziej interaktywnego: pełny interfejs czatu strumieniowego, 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 tworzysz

Interfejs czatu w React, który:

  • Wykorzystuje hook useChat pakietu Vercel AI SDK do strumieniowego przesyłania wieloetapowej rozmowy.
  • 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 w formacie Markdown używanym w GitHubie, obsługując niekompletne tokeny podczas przesyłania strumieniowego.
  • Podczas oczekiwania na pierwszy token wyświetla migoczący tekst „Myślę…”.
  • Automatyczne przewijanie do nowych wiadomości z przyciskiem przewijania do dołu, gdy przewiniesz w górę.

Dodatkowe zależności

Oprócz ai, @browser-ai/core@ai-sdk/google interfejs czatu wymaga React, powiązań React pakietu 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, niektóre narzędzia komponentów i Lucide do ikon:

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

Konfiguracja Vite: kompilacja z wieloma punktami wejścia

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

// 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 chatbotach aplikacja dokonuje tego 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 w czasie 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.' });
})();

Nowy element to ToolLoopAgent, czyli abstrakcja pakietu AI SDK, która zarządza wieloetapową pętlą konwersacji na dowolnym modelu. Przyjmuje model i prompt systemowy oraz wewnętrznie obsługuje wymianę informacji.

Połącz agenta z funkcją useChat

Element zaczepienia useChat pakietu @ai-sdk/react zwykle komunikuje się z punktem końcowym HTTP. W przypadku wnioskowania po stronie przeglądarki użyj DirectChatTransport. Pętla ToolLoopAgent jest wykonywana 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żne, ponieważ DirectChatTransport zawiera stan rozmowy, więc musi być stabilnym odwołaniem. Ponowne tworzenie go przy każdym renderowaniu resetuje rozmowę.

useChat zapewnia:

  • Messages: cała rozmowa w formie UIMessage[], z których każdy zawiera tablicę roleparts
  • sendMessage({ text }): wysyła turę nowego 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 type: 'text'. Wiadomości użytkownika pojawiają się w dymku wyrównanym do prawej strony, a wiadomości asystenta są wyrównane do lewej strony 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>
  );
};

MessageContentResponse 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 znakiem remark-gfm w przypadku Markdown w stylu GitHub (tabele, listy zadań, przekreślenie) i harden-react-markdown w celu oczyszczenia 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 zapisać **bold, ale 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 linki [, aby renderowane dane wyjściowe były czyste w każdym przyroście.

Stan „Myślenie…”

Od wysłania wiadomości do otrzymania pierwszego tokena upływa status'submitted'. W tym czasie w aplikacji wyświetla się animowany połysk:

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

Warunek messages.at(-1)?.role !== 'assistant' zapobiega ponownemu pojawieniu się efektu migotania, gdy tylko zacznie się przesyłanie strumieniowe 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 przesuwającego się podświetlenia.

Autoprzewijanie

Gdy pojawią się nowe treści, aplikacja przewija się do dołu, ale tylko wtedy, gdy użytkownik jest już na dole. Przewijanie treści, które czytają, 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);
};

Pływający przycisk przewijania do dołu pojawia się, gdy wartość isAtBottom jest fałszywa, i zanika, gdy użytkownik wróci na dół.

Obszar wprowadzania

Pole tekstowe automatycznie zmienia rozmiar podczas pisania, resetując wysokość do auto przy każdym zdarzeniu wejścia, a następnie ustawiając ją na scrollHeight. Wysyła się po naciśnięciu Entera (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ż funkcja agentPromise jest asynchroniczna, przed renderowaniem Chat poczekaj na jej wykonanie. Element 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 zostanie uruchomiony (czy to wbudowany model, który uruchamia się natychmiast, czy model, który wymaga pobrania), spinner znika i wyświetla się interfejs czatu.

Prezentacja

Wersja demonstracyjna 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 żą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ś w formie listy, napisanie fragmentu kodu lub użycie formatowania Markdown. Odpowiedzi są od razu 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 pokazaliśmy pełen zakres możliwości pakietu Vercel AI SDK w połączeniu z wbudowanym w przeglądarkę interfejsem Prompt API – od podstawowych elementów generowania po dopracowany interfejs czatu strumieniowego.

W artykule Korzystanie z wbudowanego interfejsu Prompt API z pakietem Vercel AI SDK dowiesz się, jak używać funkcji generateText()streamText() do generowania tekstu bez przesyłania strumieniowego i z przesyłaniem strumieniowym, jak prosić o strukturalne dane wyjściowe w formacie JSON za pomocą funkcji Output.object() oraz jak pisać kod hybrydowy, który w czasie działania wybiera między wbudowanym modelem a dostawcą usług w chmurze bez zmiany logiki generowania.

W tym dokumencie wykorzystaliśmy te same elementy i zastosowaliśmy je w pełnym interfejsie React: ToolLoopAgent do zarządzania pętlą rozmowy, useChatDirectChatTransport do przesyłania strumieniowego odpowiedzi bezpośrednio w przeglądarce oraz komponenty AI Elements do czystego renderowania odpowiedzi w formacie Markdown w miarę ich otrzymywania. Wszystko to z automatycznym przełączaniem na chmurę, gdy interfejs Prompt API jest niedostępny.

W rezultacie powstały 2 wersje demonstracyjne, które działają w całości w przeglądarce i nie wymagają zaplecza: