תאריך פרסום: 16 ביולי 2026
במאמר Use the built-in Prompt API with the Vercel AI
SDK (שימוש ב-Prompt API המובנה עם Vercel AI SDK), ראיתם את ארבעת הפרימיטיבים העיקריים של יצירה, כלומר generateText(), streamText(), קוד היברידי ופלט מובנה באמצעות Output.object(), כולם מבוססים על @browser-ai/core. הפעם תיצרו משהו אינטראקטיבי יותר: ממשק משתמש מלא לצ'אט בסטרימינג שפועל כולו בדפדפן, עם מעבר אוטומטי למודל בענן כש-Prompt API לא זמין.
מה בונים
ממשק צ'אט של React שמבצע את הפעולות הבאות:
- משתמשים ב-hook
useChatשל Vercel AI SDK כדי להזרים שיחה רב-שלבית. - מריץ את לולאת המודל בדפדפן ללא צורך בשרת עורפי.
- אם Prompt API לא זמין, המערכת עוברת אוטומטית ל-Gemini 2.5 Flash.
- התשובות של ה-Assistant מוצגות בפורמט Markdown של GitHub, והמערכת מטפלת באסימונים לא שלמים במהלך הסטרימינג.
- מוצג אפקט מנצנץ עם הכיתוב 'חושב…' בזמן ההמתנה לטוקן הראשון.
- גלילה אוטומטית להודעות חדשות, עם לחצן גלילה לתחתית כשגוללים למעלה.
יחסי תלות נוספים
בנוסף ל-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
ה-hook של חבילת @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[], כל אחת עם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 הם רכיבי AI.
אלה רכיבי מקור בסגנון shadcn שמעתיקים לפרויקט במקום להתקין אותם מ-npm. Response עוטף את react-markdown ב-remark-gfm עבור Markdown בטעם GitHub (טבלאות, רשימות משימות, קו חוצה) וב-harden-react-markdown כדי לבצע סניטציה של קישורים ותמונות בפלט של ה-AI.
המאפיין parseIncompleteMarkdown הוא true בזמן שההודעה עדיין משודרת. במהלך הסטרימינג, יכול להיות שהמודל יכתוב **bold אבל עדיין לא יסגור את **, וישאיר טוקן תלוי שיוצג ככוכביות מילוליות. parseIncompleteMarkdown סוגר את כל **, __, `, ~~ פתוחים, ומקצר את ההתחלה של קישורי [ תלויים כדי שהפלט המעובד יישאר נקי בכל מקטע מצטבר.
המצב 'מתבצעת חשיבה…'
הזמן שעובר בין שליחת ההודעה לבין קבלת הטוקן הראשון הוא status.
'submitted' במהלך החלון הזה, האפליקציה מציגה נצנוץ אנימטיבי:
{status === 'submitted' && messages.at(-1)?.role !== 'assistant' && (
<ThinkingMessage />
)}
התנאי messages.at(-1)?.role !== 'assistant' מונע את ההופעה מחדש של האפקט אחרי שההודעה מה-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, מופיע לחצן צף למעבר לתחתית הדף, והוא נעלם בהדרגה כשהמשתמש חוזר לתחתית הדף.
אזור הקלט
הגודל של אזור הטקסט משתנה אוטומטית כשמקלידים בו. הגובה שלו מאופס ל-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>
);
}
רכיב Mount עם מצב טעינה
agentPromise הוא אסינכרוני, לכן צריך להמתין לו לפני שמבצעים רינדור של Chat. רכיב ה-wrapper 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 להזרמת תשובות ישירות בדפדפן, ורכיבי AI Elements לעיבוד תשובות ב-Markdown בצורה נקייה כשהן מגיעות. כל זאת עם מעבר אוטומטי לענן אם Prompt API לא זמין.
התוצאה היא שתי הדגמות שפועלות ישירות בדפדפן, ללא צורך בשרת עורפי:
- שימוש ב-Prompt API המובנה עם הדמו של Vercel AI SDK: יצירת טקסט, סטרימינג ופלט מובנה זה לצד זה
- שימוש בממשק המשתמש של Vercel AI SDK וב-AI Elements עם ההדגמה של Prompt API: צ'אט בוט מלא עם סטרימינג, עיבוד של Markdown ומצב כהה