เผยแพร่เมื่อ: 16 กรกฎาคม 2026
ในบทความเรื่องใช้ Prompt API ในตัวกับ Vercel AI
SDK คุณได้เห็น Primitive หลัก 4 รายการสำหรับการสร้าง ได้แก่ generateText(), streamText(), โค้ดแบบไฮบริด และเอาต์พุตที่มีโครงสร้างโดยใช้
Output.object() ซึ่งทั้งหมดนี้ขับเคลื่อนโดย @browser-ai/core คราวนี้คุณจะสร้างสิ่งที่โต้ตอบได้มากขึ้น นั่นคือ UI แชทแบบสตรีมมิงเต็มรูปแบบที่ทำงานในเบราว์เซอร์ทั้งหมด โดยจะเปลี่ยนไปใช้โมเดลในระบบคลาวด์โดยอัตโนมัติเมื่อ Prompt API ไม่พร้อมใช้งาน
สิ่งที่คุณจะสร้าง
อินเทอร์เฟซแชท React ที่ทำสิ่งต่อไปนี้
- ใช้ฮุก
useChatของ Vercel AI SDK สำหรับการสนทนาไปมาแบบสตรีมมิง - เรียกใช้ลูปโมเดลในเบราว์เซอร์ โดยไม่จำเป็นต้องใช้เซิร์ฟเวอร์แบ็กเอนด์
- เปลี่ยนไปใช้ Gemini 2.5 Flash โดยอัตโนมัติเมื่อ Prompt API ไม่พร้อมใช้งาน
- แสดงการตอบกลับของ Assistant เป็น Markdown รูปแบบ GitHub โดยจัดการโทเค็นที่ไม่สมบูรณ์ระหว่างการสตรีม
- แสดงการโหลดแบบ Shimmer ที่มีข้อความ "กำลังคิด…" ขณะรอโทเค็นแรก
- เลื่อนไปที่ข้อความใหม่โดยอัตโนมัติ พร้อมปุ่มเลื่อนไปด้านล่างเมื่อคุณเลื่อนขึ้น
ทรัพยากร Dependency เพิ่มเติม
นอกเหนือจาก ai, @browser-ai/core และ @ai-sdk/google แล้ว UI แชทต้องใช้
React, การผูก React ของ AI SDK และแพ็กเกจ Markdown 2-3 รายการ
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
สำหรับ UI ให้เพิ่ม Tailwind CSS, ยูทิลิตีของคอมโพเนนต์บางรายการ และ Lucide สำหรับไอคอนด้วย
npm install -D tailwindcss postcss autoprefixer
npm install clsx tailwind-merge lucide-react
การกำหนดค่า Vite: บิลด์แบบหลายรายการ
โปรเจ็กต์มี index.html อยู่แล้ว เพิ่ม chat.html เป็นจุดเริ่มต้นที่ 2 และกำหนดค่า Vite ให้สร้างทั้ง 2 รายการ
// 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 ที่จัดการลูปการสนทนาไปมาบนโมเดลใดก็ได้ โดยจะใช้โมเดลและพรอมต์ของระบบ และจัดการการโต้ตอบภายใน
เชื่อมต่อตัวแทนเพื่อใช้ Chat
ฮุก 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และอาร์เรย์partssendMessage({ text }): ส่งข้อความใหม่ของผู้ใช้และเริ่มสตรีมการตอบกลับStatus:'idle'|'submitted'|'streaming'|'error'Stop: ยกเลิกการสร้างที่กำลังดำเนินการ
แสดงข้อความ
แต่ละข้อความจะมีอาร์เรย์ parts สำหรับแชทบอตนี้ เราสนใจเฉพาะส่วน type:
'text' ข้อความของผู้ใช้จะปรากฏเป็นฟองข้อความที่จัดชิดขวา ส่วนข้อความของ Assistant จะจัดชิดซ้ายพร้อมไอคอน
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' ในช่วงเวลานี้ แอปจะแสดงการโหลดแบบ Shimmer ที่เคลื่อนไหว
{status === 'submitted' && messages.at(-1)?.role !== 'assistant' && (
<ThinkingMessage />
)}
เงื่อนไข messages.at(-1)?.role !== 'assistant' จะป้องกันไม่ให้การโหลดแบบ Shimmer
ปรากฏขึ้นอีกครั้งเมื่อข้อความของ 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 จะแก้ปัญหา 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} />;
}
เมื่อตัวแทนแก้ปัญหาได้ ไม่ว่าจะเป็นการบูตโมเดลในตัวทันทีหรือรอการดาวน์โหลดโมเดล ตัวหมุนจะหายไปและ UI แชทจะติดตั้ง
สาธิต
การสาธิตแบบสด เป็นแชทบอตที่ทำงานได้อย่างสมบูรณ์ซึ่งทำงานในเบราว์เซอร์ทั้งหมด พิมพ์ข้อความแล้วกด Enter หาก Prompt API พร้อมใช้งาน การตอบกลับจะสตรีมโดยตรงจากโมเดลในอุปกรณ์โดยไม่มีคำขอเครือข่าย หากเบราว์เซอร์ไม่รองรับ Prompt API ระบบจะเปลี่ยนไปใช้ Gemini 2.5 Flash โดยอัตโนมัติ ลองขอให้แชทบอตอธิบายบางสิ่งในรายการ เขียนข้อมูลโค้ด หรือใช้การจัดรูปแบบ Markdown การตอบกลับจะแสดงผลด้วยบล็อกโค้ด ตาราง และโค้ดแบบอินไลน์ที่จัดรูปแบบแล้ว

บทสรุป
ตลอดทั้ง 2 บทความนี้ คุณได้เห็นความสามารถทั้งหมดของ Vercel AI SDK ที่ใช้ Prompt API ในตัวของเบราว์เซอร์ได้ ตั้งแต่ Primitive การสร้างแบบดิบไปจนถึงอินเทอร์เฟซแชทแบบสตรีมมิงที่ขัดเกลาแล้ว
ในบทความเรื่องใช้ Prompt API ในตัวกับ Vercel AI SDK คุณได้เรียนรู้วิธี
ใช้ generateText() และ streamText() สำหรับการสร้างข้อความแบบไม่สตรีมมิงและการสร้างข้อความแบบสตรีมมิง
วิธีขอเอาต์พุต JSON ที่มีโครงสร้างด้วย Output.object() และ
วิธีเขียนโค้ดแบบไฮบริดที่เลือกระหว่างโมเดลในตัวกับผู้ให้บริการระบบคลาวด์
ในรันไทม์โดยไม่ต้องเปลี่ยนแปลงตรรกะการสร้าง
ในเอกสารนี้ คุณได้ใช้บล็อกการสร้างเดียวกันและรวมไว้ใน UI React แบบเต็มรูปแบบ ได้แก่ ToolLoopAgent เพื่อจัดการลูปการสนทนา, useChat กับ DirectChatTransport เพื่อสตรีมการตอบกลับโดยตรงในเบราว์เซอร์ และคอมโพเนนต์องค์ประกอบ AI เพื่อแสดงผลการตอบกลับ Markdown อย่างชัดเจนเมื่อได้รับ โดยทั้งหมดนี้จะเปลี่ยนไปใช้ระบบคลาวด์โดยอัตโนมัติเมื่อ Prompt API ไม่พร้อมใช้งาน
ผลลัพธ์ที่ได้คือการสาธิต 2 รายการที่ทำงานในเบราว์เซอร์ทั้งหมดโดยไม่จำเป็นต้องใช้แบ็กเอนด์
- การสาธิตการใช้ Prompt API ในตัวกับ Vercel AI SDK: การสร้างข้อความ การสตรีม และเอาต์พุตที่มีโครงสร้างแบบเคียงข้างกัน
- การสาธิตการใช้ UI และองค์ประกอบ AI ของ Vercel AI SDK กับ Prompt API: แชทบอตแบบสตรีมมิงเต็มรูปแบบพร้อมการแสดงผล Markdown และโหมดมืด