استفاده از رابط کاربری و عناصر هوش مصنوعی Vercel AI SDK به همراه Prompt API

منتشر شده: ۱۶ ژوئیه ۲۰۲۶

در بخش «استفاده از API داخلی Prompt با Vercel AI SDK» ، چهار هسته اولیه تولید کد، یعنی generateText() ، streamText() ، کد ترکیبی و خروجی ساختاریافته با استفاده از Output.object() را مشاهده کردید که همگی توسط @browser-ai/core پشتیبانی می‌شوند. این بار شما چیزی تعاملی‌تر می‌سازید: یک رابط کاربری چت استریمینگ کامل که کاملاً در مرورگر اجرا می‌شود و در صورت عدم دسترسی به API Prompt، به طور خودکار به یک مدل ابری برمی‌گردد.

چیزی که در حال ساختن آن هستید

یک رابط چت React که موارد زیر را انجام می‌دهد:

  • Uses the Vercel AI SDK's useChat hook for streaming multi-turn conversation.
  • حلقه مدل را در مرورگر و بدون نیاز به سرور backend اجرا می‌کند.
  • وقتی Prompt API در دسترس نباشد، به طور خودکار به Gemini 2.5 Flash برمی‌گردد.
  • دستیار رندرها به صورت Markdown با طعم GitHub پاسخ می‌دهد و توکن‌های ناقص را در حین پخش مدیریت می‌کند.
  • در حالی که منتظر اولین نشانه است، یک چشمک «در حال فکر کردن...» نشان می‌دهد.
  • به صورت خودکار به پیام‌های جدید اسکرول می‌کند، و وقتی به بالا اسکرول می‌کنید، یک دکمه اسکرول به پایین نمایش داده می‌شود.

وابستگی‌های اضافی

رابط کاربری چت علاوه بر ai ، @browser-ai/core و @ai-sdk/google به React، اتصالات React مربوط به AI SDK و چند بسته 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 است؛ انتزاعی از کیت توسعه نرم‌افزار هوش مصنوعی که یک حلقه مکالمه چند نوبتی را بر روی هر مدلی مدیریت می‌کند. این قطعه، مدل و اعلان سیستم را دریافت کرده و رفت و برگشت‌ها را به صورت داخلی مدیریت می‌کند.

اتصال اپراتور به useChat

هوک useChat پکیج @ai-sdk/react معمولاً با یک نقطه پایانی 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>
  );
};

MessageContent و Response عناصر هوش مصنوعی هستند. آن‌ها کامپوننت‌های منبع به سبک shadcn هستند که شما به جای نصب از npm، آن‌ها را در پروژه خود کپی می‌کنید. Response react-markdown را با remark-gfm برای Markdown با طعم GitHub (جداول، لیست وظایف، خط خورده) و harden-react-markdown را برای پاکسازی لینک‌ها و تصاویر در خروجی هوش مصنوعی، پوشش می‌دهد.

مقدار parseIncompleteMarkdown تا زمانی که پیام هنوز در حال پخش است، true است. در حین پخش، مدل ممکن است **bold بنویسد اما هنوز ** را نبندد و یک توکن آویزان باقی بگذارد که به صورت ستاره‌های واقعی رندر می‌شود. parseIncompleteMarkdown هر ** باز، __ ، ` ، ~~ را می‌بندد و شروع dangling [ link را کوتاه می‌کند تا خروجی رندر شده در هر بخش افزایشی تمیز بماند.

حالت «فکر کردن…»

بین ارسال پیام و دریافت اولین توکن، status 'submitted' است. در طول این پنجره، برنامه یک چشمک زن متحرک نشان می‌دهد:

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

شرط messages.at(-1)?.role !== 'assistant' مانع از نمایش مجدد لرزش پس از شروع ارسال پیام دستیار می‌شود.

ThinkingMessage از یک کامپوننت Shimmer استفاده می‌کند: یک <span> با یک گرادیان متحرک با استفاده از background-clip: text که به متن "Thinking…" یک جلوه هایلایت فراگیر می‌دهد.

اسکرول خودکار

وقتی محتوای جدید می‌رسد، برنامه به پایین اسکرول می‌کند، اما فقط در صورتی که کاربر از قبل در پایین صفحه باشد. اسکرول کردن آنها از چیزی که در حال خواندن آن در حین مکالمه است، آزاردهنده خواهد بود.

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) که 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 را حل می‌کند و در عین حال یک 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} />;
}

به محض اینکه عامل مشکل را حل کرد، چه مدل داخلی فوراً بوت شود و چه منتظر دانلود مدل باشد، چرخنده ناپدید می‌شود و رابط کاربری چت نصب می‌شود.

نسخه آزمایشی

نسخه آزمایشی زنده یک چت‌بات کاملاً کاربردی است که کاملاً در مرورگر شما اجرا می‌شود. یک پیام تایپ کنید و Enter را فشار دهید. اگر Prompt API در دسترس باشد، پاسخ مستقیماً از مدل روی دستگاه و بدون درخواست شبکه پخش می‌شود. اگر مرورگر شما از Prompt API پشتیبانی نمی‌کند، به طور خودکار به Gemini 2.5 Flash برمی‌گردد. سعی کنید از آن بخواهید چیزی را در یک لیست توضیح دهد، یک قطعه کد بنویسد یا از قالب‌بندی Markdown استفاده کند. پاسخ‌ها با بلوک‌های کد قالب‌بندی شده، جداول و کد درون‌خطی به صورت پیش‌فرض رندر می‌شوند.

رابط چتی که مکالمه‌ای را با یک دستیار هوش مصنوعی نشان می‌دهد، شامل یک فیلد ورودی متن و یک ناحیه پاسخ‌دهی در حال پخش.

نتیجه‌گیری

در طول این دو مقاله، شما طیف کاملی از امکاناتی که Vercel AI SDK با Prompt API داخلی مرورگر فراهم می‌کند را مشاهده کردید، از تولید داده‌های خام اولیه گرفته تا یک رابط چت استریمینگ حرفه‌ای.

در بخش «استفاده از API داخلی Prompt با Vercel AI SDK» ، یاد گرفتید که چگونه generateText() و streamText() برای تولید متن غیر استریمینگ و استریمینگ استفاده کنید، چگونه با Output.object() خروجی JSON ساختاریافته درخواست کنید، و چگونه کد ترکیبی بنویسید که در زمان اجرا بین مدل داخلی و یک ارائه‌دهنده ابری، بدون هیچ تغییری در منطق تولید، یکی را انتخاب کند.

در این سند، شما همان بلوک‌های سازنده را برداشته و آنها را در یک رابط کاربری کامل React قرار داده‌اید: یک ToolLoopAgent برای مدیریت حلقه مکالمه، useChat با DirectChatTransport برای پخش مستقیم پاسخ‌ها در مرورگر، و کامپوننت‌های AI Elements برای رندر کردن تمیز پاسخ‌های Markdown به محض دریافت؛ همه اینها با قابلیت پشتیبان‌گیری خودکار ابری در صورت عدم دسترسی به Prompt API.

نتیجه دو نسخه نمایشی است که کاملاً در مرورگر کار می‌کنند و نیازی به backend ندارند: