将 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 hook 进行流式多轮对话。
  • 在浏览器中 运行模型循环,无需后端服务器。
  • 当 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

投票配置:多条目构建

该项目已包含 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 hook 通常与 HTTP 端点通信。对于浏览器端推理,请改用 DirectChatTransport。它完全在浏览器中运行 ToolLoopAgent 循环,不涉及任何服务器:

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

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

useChat 为您提供:

  • Messages:以 UIMessage[] 形式呈现的完整对话,每个对话都包含一个 role 和一个 parts 数组
  • 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 使用 react-markdown 封装了 remark-gfm,用于 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 组件:一个 <span>,其中包含使用 background-clip: text 的移动渐变,为“正在思考…”文本提供扫掠突出显示效果。

自动滚动

当有新内容到达时,应用会滚动到底部,但前提是用户已位于底部。在对话过程中,如果用户正在阅读某些内容,滚动到其他位置会很烦人。

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 是异步的,因此请先等待它,然后再渲染 ChatApp 封装容器会解析该 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 来直接在浏览器中流式传输响应,以及 AI 元素组件来在 Markdown 响应到达时对其进行清晰地渲染;所有这些都具有在 Prompt API 不可用时自动回退到云的功能。

结果是两个完全在浏览器中运行的演示,无需后端: