将 Vercel AI SDK 界面和 AI 元素与 Prompt API 搭配使用

发布时间:2026 年 7 月 16 日

将内置 Prompt API 与 Vercel AI SDK 搭配使用中,您了解了四种核心生成原语,即 generateText()streamText()、混合代码和使用 Output.object() 的结构化输出,所有这些都由 @browser-ai/core 提供支持。这次,您将构建一个更具互动性的应用:一个完全在浏览器中运行的完整流式聊天界面,当 Prompt API 不可用时,该界面会自动回退到云模型。

您要构建的内容

一个执行以下操作的 React 聊天界面:

  • 使用 Vercel AI SDK 的 useChat 钩子进行流式多轮对话。
  • 在浏览器中运行模型循环,无需后端服务器。
  • 当 Prompt API 不可用时,自动回退到 Gemini 2.5 Flash。
  • 以 GitHub 风格的 Markdown 格式呈现助理回答,并在流式传输期间处理不完整的令牌。
  • 在等待第一个令牌时显示“正在思考…”微光效果。
  • 自动滚动到新消息,当您向上滚动时,会显示一个滚动到底部按钮。

其他依赖项

除了 ai@browser-ai/core@ai-sdk/google 之外,聊天界面还需要 React、AI SDK 的 React 绑定和一些 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

对于界面,还需添加 Tailwind CSS、一些组件实用程序和 Lucide 图标:

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

Vite 配置:多入口构建

项目已包含 index.html。添加 chat.html 作为第二个入口点,并配置 Vite 以构建这两个入口点:

// 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 是最小的,仅包含 <div id="root"> 和脚本标记:

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

自动模型选择

在聊天机器人中,应用会自动做出选择:它会先尝试使用内置模型,如果 Prompt API 不可用,则会回退到云端。

它会在模块加载时(在 React 安装之前)执行此操作,以便在用户输入第一条消息时,代理已准备就绪:

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

新组件是 ToolLoopAgent;AI SDK 抽象层,用于在任何模型之上管理多轮对话循环。它会获取模型和系统提示,并在内部处理来回对话。

连接代理以使用 useChat

@ai-sdk/react 软件包的 useChat 钩子通常与 HTTP 端点通信。 对于浏览器端推理,请改用 DirectChatTransport。它完全在浏览器中运行 ToolLoopAgent 循环,不涉及任何服务器:

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

useMemo 非常重要,因为 DirectChatTransport 保存对话状态,因此必须是稳定的引用。在每次渲染时重新创建它会重置对话。

useChat 可为您提供:

  • Messages:以 UIMessage[] 形式表示的完整对话,每个对话都包含 roleparts 数组
  • sendMessage({ text }):发送新的用户对话轮次并开始流式传输响应
  • Status'idle' | 'submitted' | 'streaming' | 'error'
  • Stop:取消正在进行的生成

呈现消息

每条消息都有一个 parts 数组。对于此聊天机器人,我们只关心 type: 'text' 部分。用户消息显示为右对齐的气泡;助理消息左对齐,并带有图标:

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

MessageContentResponseAI 元素。它们是 shadcn 样式的源组件,您可以将其复制到项目中,而不是从 npm 进行安装。Response 使用 remark-gfm 封装 react-markdown,以支持 GitHub 风格的 Markdown(表格、任务列表、删除线),并使用 harden-react-markdown 对 AI 输出中的链接和图片进行清理。

当消息仍在流式传输时,parseIncompleteMarkdown 属性为 true。在流式传输期间,模型可能会写入 **bold 但尚未关闭 **,从而留下一个悬空令牌,该令牌会呈现为字面星号。parseIncompleteMarkdown 会关闭所有打开的 **__`~~,并截断悬挂的 [ 链接开头,以便在每个增量块上保持干净的渲染输出。

“正在思考…”状态

从发送消息到接收第一个令牌,status'submitted'。在此窗口期间,应用会显示动画微光:

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

条件 messages.at(-1)?.role !== 'assistant' 可防止在助理消息开始流式传输后,微光效果再次出现。

ThinkingMessage 使用 Shimmer 组件:一个使用 background-clip: text<span>,可为“正在思考…”文字提供扫过式突出显示效果。

自动滚动

当有新内容到达时,应用会滚动到底部,但前提是用户已位于底部。如果用户正在阅读某内容,但您却在对话中途将该内容滚动到屏幕外,这会令用户感到烦恼。

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

isAtBottom 为 false 时,系统会显示一个浮动的“滚动到底部”按钮,当用户返回底部时,该按钮会淡出。

输入区域

当您输入内容时,textarea 会通过在每个输入事件中将其高度重置为 auto,然后将其设置为 scrollHeight 来自动调整大小。它会在按 Enter 键时提交(而不是按 Shift+Enter 键时),并且在流式传输响应时,“发送”按钮会被调用 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>
  );
}

装载并显示加载状态

由于 agentPromise 是异步的,因此在渲染 Chat 之前,请等待它完成。App 封装容器会解析 promise,并在同时显示旋转图标:

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

一旦代理解决问题(无论是内置模型立即启动还是等待模型下载),微调器就会消失,聊天界面会挂载。

演示

实时演示是一个功能完备的聊天机器人,完全在浏览器中运行。输入消息,然后按 Enter 键。如果 Prompt API 可用,则回复会直接从设备端模型流式传输,而无需网络请求。如果您的浏览器不支持 Prompt API,则会自动回退到 Gemini 2.5 Flash。您可以尝试让它以列表形式解释某件事、编写代码段或使用 Markdown 格式。开箱即用,可呈现包含格式化代码块、表格和内嵌代码的回答。

聊天界面,显示与 AI 助理的对话,其中包含文本输入字段和流式响应区域。

总结

在这两篇文章中,您已全面了解 Vercel AI SDK 如何利用浏览器的内置 Prompt API 实现各种功能,从原始生成基元到完善的流式聊天界面,应有尽有。

将内置 Prompt API 与 Vercel AI SDK 搭配使用中,您学习了如何使用 generateText()streamText() 进行非流式和流式文本生成,如何使用 Output.object() 请求结构化 JSON 输出,以及如何编写混合代码,以便在运行时在内置模型和云提供商之间进行选择,而无需更改生成逻辑。

在此文档中,您使用了相同的构建块,并将它们封装在一个完整的 React 界面中:一个用于管理对话循环的 ToolLoopAgent、一个包含 DirectChatTransportuseChat,用于直接在浏览器中流式传输回答,以及用于在 Markdown 回答到达时以清晰的方式呈现这些回答的 AI Elements 组件;所有这些都具有在 Prompt API 不可用时自动回退到云的功能。

最终生成了两个完全在浏览器中运行的演示,无需任何后端: