استخدام واجهة مستخدم Vercel AI SDK وعناصر الذكاء الاصطناعي مع Prompt API

تم النشر في: 16 يوليو 2026

في مقالة استخدام واجهة برمجة التطبيقات المضمّنة Prompt API مع حزمة Vercel AI SDK، اطّلعت على أربع وحدات أساسية للإنشاء ، وهي generateText() وstreamText() والرمز المختلط والناتج المنظَّم باستخدام Output.object()، وكلها مدعومة من @browser-ai/core. في هذه المقالة، ستنشئ واجهة مستخدم أكثر تفاعلاً للمحادثة المباشرة تعمل بالكامل في المتصفّح، مع الرجوع تلقائيًا إلى نموذج سحابي عندما لا تتوفّر واجهة برمجة التطبيقات Prompt API.

ما الذي ستنشئه؟

واجهة مستخدم للدردشة في React تنفّذ ما يلي:

  • تستخدم أداة useChat في حزمة Vercel AI SDK للبث في محادثة مترابطة.
  • تُشغّل حلقة النموذج في المتصفّح بدون الحاجة إلى خادم في الخلفية.
  • ترجع تلقائيًا إلى Gemini 2.5 Flash عندما لا تتوفّر واجهة برمجة التطبيقات Prompt API.
  • تعرض ردود المساعد بتنسيق 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

إعداد 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، وهو تجريد حزمة تطوير البرامج (SDK) للذكاء الاصطناعي (AI) الذي يدير حلقة محادثة مترابطة فوق أي نموذج. يأخذ النموذج وطلب النظام، ويتعامل مع الردود داخليًا.

ربط الوكيل بـ 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 تغلق أي علامات مفتوحة ** و__ و` و~~، وتقتطع علامات بدء الروابط المتدلّية [ حتى يظل الناتج المعروض نظيفًا في كل جزء إضافي.

حالة "جارٍ التفكير…"

بين إرسال رسالة وتلقّي الرمز الأول، 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 غير صحيحة، ويختفي عندما يعود المستخدم إلى الأسفل.

مساحة الإدخال

يتم تغيير حجم منطقة النص تلقائيًا أثناء الكتابة من خلال إعادة ضبط ارتفاعها على 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 الوعد ويعرض مؤشر تحميل في هذه الأثناء:

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 في المتصفّح، بدءًا من وحدات الإنشاء الأساسية وصولاً إلى واجهة مستخدم المحادثة المباشرة المصقولة.

في مقالة استخدام واجهة برمجة التطبيقات المضمّنة Prompt API مع حزمة Vercel AI SDK، تعلّمت كيفية استخدام generateText() و streamText() لإنشاء نص غير مباشر ومباشر ، وكيفية طلب ناتج JSON منظَّم باستخدام Output.object()، و كيفية كتابة رمز مختلط يختار بين النموذج المضمّن ومزوّد سحابي في وقت التشغيل بدون إجراء أي تغييرات على منطق الإنشاء.

في هذا المستند، استخدمت وحدات الإنشاء نفسها وغلّفتها في واجهة مستخدم كاملة في React: ToolLoopAgent لإدارة حلقة المحادثة، وuseChat مع DirectChatTransport لعرض الردود مباشرةً في المتصفّح، ومكوّنات عناصر الذكاء الاصطناعي لعرض ردود Markdown بشكلٍ نظيف عند وصولها، وكل ذلك مع الرجوع تلقائيًا إلى النموذج السحابي عندما لا تتوفّر واجهة برمجة التطبيقات Prompt API.

النتيجة هي عرضان توضيحيان يعملان بالكامل في المتصفّح، بدون الحاجة إلى أي خادم في الخلفية: