ساخت پیامهای TanStack AI و شبیهسازی مکالمههای از پیش تعریفشده با استفاده از useChat، بدون نیاز به مدل، API Route، درخواست شبکه یا API Key.
@farsiui/helpers/tanstack-ai به شما اجازه میدهد یک مکالمهٔ AI را مستقیماً در کد تعریف کنید و آن را از طریق useChat در TanStack AI بهصورت Stream دریافت کنید.
این Helper مقادیر Native از نوع UIMessage[] مربوط به TanStack را ایجاد میکند و پاسخهای Assistant را از طریق یک Connection Adapter محلی، بهصورت رویدادهای واقعی AG-UI بازپخش میکند. بنابراین کامپوننتها همان رفتاری را خواهند داشت که در محیط واقعی دارند؛ Text و Reasoning بهصورت کلمهبهکلمه Stream میشوند و Tool Callها از Input تا Result پیش میروند.
import { createChat } from "@farsiui/helpers/tanstack-ai"
const chat = createChat()
.user("What changed in this release?")
.assistant("The release adds keyboard shortcuts and faster search.")
.user("Can you show me the shortcuts?")
.assistant("Press ⌘K to search and ⌘Enter to submit.")Chat را همراه با پیامهای اولیه و Connection محلی به useChat بدهید. TanStack AI همان رویدادهای AG-UI را دریافت میکند که در حالت استفاده از Server دریافت میکرد.
import { useChat } from "@tanstack/ai-react"
function Chat() {
const { messages, append } = useChat({
initialMessages: chat.get(0),
connection: chat.transport(),
})
const nextMessage = chat.next(messages)
return (
<button
disabled={!nextMessage}
onClick={() => {
if (nextMessage) {
void append(nextMessage)
}
}}
>
Send next message
</button>
)
}get(0) مکالمه را بدون پیام شروع میکند. append(nextMessage) پیام بعدی از پیش تعریفشدهٔ کاربر را اضافه میکند و Connection پاسخ Assistant را بهصورت رویدادهای AG-UI Stream میکند.
این Helper رابط کاربری Chat را از مدل و Backend جدا میکند تا بتوانید Frontend را بهتنهایی توسعه دهید. همهچیز بهصورت آفلاین، سریع و قابل تکرار اجرا میشود.
پکیج Helper را نصب کنید:
pnpm add @farsiui/helpersیک مکالمه بسازید، پیامها و Connection آن را به useChat بدهید و سپس هر پیام از پیش تعریفشدهٔ کاربر را با next() ارسال کنید.
"use client"
import { createChat } from "@farsiui/helpers/tanstack-ai"
import { useChat } from "@tanstack/ai-react"
const chat = createChat()
.user("What changed in this release?")
.assistant("The release adds keyboard shortcuts and faster search.")
.user("Can you show me the shortcuts?")
.assistant("Press ⌘K to search and ⌘Enter to submit.")
const initialMessages = chat.get(0)
const connection = chat.transport()
export function Chat() {
const { messages, append, status } = useChat({
initialMessages,
connection,
})
const nextMessage = chat.next(messages)
const isBusy = status === "submitted" || status === "streaming"
return (
<div>
{messages.map((message) => (
<div key={message.id}>{/* Render the message */}</div>
))}
<button
disabled={!nextMessage || isBusy}
onClick={() => {
if (nextMessage && !isBusy) {
void append(nextMessage)
}
}}
>
Send
</button>
</div>
)
}پیامهایی که با این Helper ساخته میشوند از نقشهای user و assistant استفاده میکنند.
برای اضافه کردن پیام کاربر از user() استفاده کنید. اگر مکالمه را با createChat({ messages }) شروع کنید، پیامهای موجود TanStack نیز حفظ میشوند.
const chat = createChat()
.user("What changed in this release?")
const [message] = chat.get()
message.role // "user"
message.parts // [{ type: "text", content: "What changed in this release?" }]اگر به id یا Timestamp مربوط به createdAt نیاز دارید، آنها را در آرگومان دوم مشخص کنید:
chat.user("What changed in this release?", {
id: "user-release-question",
metadata: {
createdAt: "2026-01-01T10:00:00.000Z",
},
})پیامهای کاربر میتوانند فایل نیز داشته باشند. برای جزئیات، بخش Files را ببینید.
برای اضافه کردن پیام Assistant از assistant() استفاده کنید:
const chat = createChat()
.assistant("The release adds keyboard shortcuts and faster search.")
const [message] = chat.get()
message.role // "assistant"
message.parts // [{ type: "text", content: "The release adds..." }]یک String یک Text Part ایجاد میکند. میتوانید آرایهای از Partهای TanStack را نیز مستقیماً ارسال کنید:
chat.assistant([
{ type: "thinking", content: "I should summarize the release." },
{ type: "text", content: "The release adds keyboard shortcuts." },
])اگر میخواهید پیام در چند مرحله Stream شود، از Writer Callback استفاده کنید:
chat.assistant(({ writer }) => {
writer.reasoning("I should summarize the release.")
writer.text("The release adds keyboard shortcuts and faster search.")
})Writer، Partها را دقیقاً به همان ترتیبی که فراخوانی میکنید اضافه میکند.
Writer در TanStack، Partهایی را پشتیبانی میکند که میتوانند از طریق Connection مربوط به AG-UI رفتوبرگشت داشته باشند: Text، Reasoning و Tool Call.
همچنین Timing و Error نیز پشتیبانی میشوند.
chat.assistant(({ writer }) => {
writer.reasoning("I should answer directly.")
writer.text("Hello.")
})برای Partهایی که معادل مستقیمی در این Connection ندارند، بخش Adapter Differences را ببینید.
برای اضافه کردن متن از text() استفاده کنید:
chat.assistant(({ writer }) => {
writer.text("The release adds keyboard shortcuts.")
writer.text(" Search is faster too.")
})وقتی Chat با chat.transport() استفاده شود، Text بهصورت کلمهبهکلمه Stream میشود.
فراخوانیهای متوالی text() در get() بهصورت Partهای جداگانه باقی میمانند، اما Stream Processor تناستک هنگام Playback آنها را در یک Text Part ترکیب میکند.
با mode: "instant" میتوانید کل مقدار را یکجا ارسال کنید و با delayMs فاصلهٔ بین Deltaهای Text را تغییر دهید:
writer.text("Done.", { mode: "instant" })
writer.text("This part streams more slowly.", { delayMs: 100 })برای اضافه کردن Reasoning از reasoning() استفاده کنید. این مقدار به یک Part از نوع thinking در TanStack تبدیل میشود:
chat.assistant(({ writer }) => {
writer.reasoning("I should check the latest conditions first.")
writer.text("Let me check the weather.")
})Reasoning همان Options مربوط به delayMs و mode را مانند Text پشتیبانی میکند:
writer.reasoning("Checking the forecast.", { mode: "instant" })برای اضافه کردن یک Tool Call از tool() استفاده کنید. این متد یک Handle برمیگرداند که چرخهٔ Tool را از Input تا Output دنبال میکند:
chat.assistant(({ writer }) => {
writer
.tool("getWeather", {
input: { city: "San Francisco" },
})
.sleep(900)
.output({
city: "San Francisco",
temperature: 18,
condition: "Breezy",
})
writer.text("It is 18°C and breezy in San Francisco.")
})Tool میتواند با output() یا error() به پایان برسد:
writer
.tool("getWeather", {
input: { city: "San Francisco" },
})
.error()پیام نهایی TanStack شامل یک Part از نوع tool-call و یک Part مستقل از نوع tool-result خواهد بود.
برای Type کردن نام Tool و Input و Output، همان Tuple مربوط به Client Toolهای برنامهٔ خود را استفاده کنید:
import { clientTools } from "@tanstack/ai-client"
import { getWeatherTool } from "@/lib/tools"
const tools = clientTools(getWeatherTool.client())
const chat = createChat<typeof tools>()
chat.assistant(({ writer }) => {
writer
.tool("getWeather", {
input: { city: "San Francisco" },
})
.output({
city: "San Francisco",
temperature: 18,
condition: "Breezy",
})
})برای اضافه کردن فایل به پیام کاربر، از گزینهٔ files استفاده کنید:
chat.user("Describe this image.", {
files: [
{
mediaType: "image/png",
url: "https://example.com/screenshot.png",
},
],
})Helper بر اساس Media Type، هر فایل را به یک Media Part مربوط به TanStack تبدیل میکند:
این Partها از طریق get() در دسترس هستند.
از آنجا که next() کل UIMessage کاربر را برمیگرداند، append() متن و Media Partهای آن را نیز حفظ میکند:
const nextMessage = chat.next(messages)
if (nextMessage) {
void append(nextMessage)
}Media Partهای Assistant را میتوان در یک آرایهٔ Static Parts قرار داد و با get() خواند، اما Connection مربوط به AG-UI فایلهای Assistant را Stream نمیکند.
TanStack AI و AI SDK از مدلهای متفاوتی برای پیام و Streaming استفاده میکنند. این Adapter فقط قابلیتهایی را ارائه میکند که بتوانند از ابتدا تا انتها در این Connection نمایش داده شوند.
اگر Preview شما به مدل کامل Message Partهای AI SDK نیاز دارد، از AI SDK Helper استفاده کنید.
با get() میتوانید پیامها را از ابتدای مکالمه دریافت کنید:
chat.get() // همهٔ پیامها
chat.get(2) // دو پیام اول
chat.get(0) // مکالمهٔ اولیه بدون پیامget() مقادیر Cloneشدهٔ UIMessage[] مربوط به TanStack را برمیگرداند و تغییری در Chat ایجاد نمیکند.
برای پیدا کردن پیام بعدی کاربر در مکالمهٔ از پیش تعریفشده، از next() استفاده کنید:
const initialMessages = chat.get(2)
const nextMessage = chat.next(initialMessages)next() همیشه یک Transcript از پیامها دریافت میکند، نه یک Index. پیام بعدی کاربر را برمیگرداند و اگر پیام دیگری باقی نمانده باشد، null برمیگرداند.
برای ادامه دادن یک مکالمهٔ ذخیرهشده یا Fixture، مقدار messages را به createChat بدهید:
import { createChat } from "@farsiui/helpers/tanstack-ai"
import type { UIMessage } from "@tanstack/ai-client"
declare const savedMessages: UIMessage[]
const chat = createChat({
messages: savedMessages,
})
.user("What should we do next?")
.assistant("Turn the open questions into a checklist.")IDها، Timestampها و Partهای موجود حفظ میشوند.
متد transport() یک ConnectConnectionAdapter از TanStack ایجاد میکند که باید آن را بهعنوان connection به useChat بدهید:
const connection = chat.transport()
const { messages, append, sendMessage } = useChat({
initialMessages: chat.get(0),
connection,
})
const nextMessage = chat.next(messages)
// بازپخش پیام بعدی از پیش تعریفشده
if (nextMessage) {
void append(nextMessage)
}
// یا ارسال ورودی معمولی کاربر
void sendMessage("Tell me more.")وقتی append() یا sendMessage() یک Run را شروع میکند، Connection پیام Assistant بعد از Transcript فعلی را پیدا کرده و رویدادهای AG-UI را از طریق Stream Processor معمول TanStack ارسال میکند.
ابتدا Message IDها بررسی میشوند و در صورت نیاز، بر اساس Role و متن آخرین پیام تطبیق انجام میشود.
از append(nextMessage) برای بازپخش یک پیام کاربر از پیش تعریفشده استفاده کنید. ID و Media Partهای پیام حفظ میشوند.
برای ورودی معمولی کاربر از sendMessage() استفاده کنید. این متد String یا محتوای Multimodal دریافت میکند، نه یک UIMessage کامل.
یک پاسخ میتواند این ترتیب از رویدادها را تولید کند:
RUN_STARTED
REASONING_MESSAGE_START
→ REASONING_MESSAGE_CONTENT
→ REASONING_MESSAGE_END
TOOL_CALL_START
→ TOOL_CALL_ARGS
→ TOOL_CALL_END
→ TOOL_CALL_RESULT
TEXT_MESSAGE_START
→ TEXT_MESSAGE_CONTENT
→ TEXT_MESSAGE_END
RUN_FINISHEDخطاهای از پیش تعریفشده، رویداد RUN_ERROR تولید میکنند.
TanStack مقدارهای threadId و runId مورد استفاده در هر Run را تأمین میکند.
delayMs در سطح Connection مقدار پیشفرض تمام Text و Reasoningهای Streamشده است. برای یک Part خاص میتوانید آن را با writer.text(..., { delayMs }) یا writer.reasoning(..., { delayMs }) تغییر دهید.
const connection = chat.transport({
delayMs: 25,
})وقتی مکالمه دیگر پاسخ Assistant از پیش تعریفشدهای ندارد، از fallback استفاده کنید:
const connection = chat.transport({
fallback: "This demo has no more predefined replies.",
})Fallback میتواند آرایهای از Partهای TanStack یا یک Writer Callback نیز باشد. Callback، Transcript ورودی را دریافت میکند و میتواند بر اساس وضعیت فعلی پاسخ بسازد:
const connection = chat.transport({
fallback: ({ writer, messages }) => {
writer.text(
`This example already has ${messages.length} messages.`,
{
mode: "instant",
}
)
},
})Fallbackها مانند پاسخهای Assistant Stream میشوند، اما به مکالمهٔ از پیش تعریفشده اضافه نمیشوند.
اگر Fallback تعریف نشده باشد، پس از تمام شدن مکالمه این خطا ایجاد میشود:
"No assistant response found for this transcript."
فراخوانی stop() از useChat، Stream فعال Connection را میبندد.
برای شبیهسازی سرعت یک پاسخ واقعی، میتوانید از Delay استفاده کنید:
const chat = createChat()
.user("Give me a project update.")
.sleep(800)
.assistant(({ writer }) => {
writer.reasoning("I should lead with the completed milestone.")
writer.sleep(500)
writer.text("The first milestone is complete.", {
mode: "instant",
})
})
const connection = chat.transport({
delayMs: 50,
})chat.sleep(ms) قبل از شروع پاسخ بعدی Assistant منتظر میماند.writer.sleep(ms) بین Partهای یک پاسخ Assistant مکث میکند.tool.sleep(ms) بین Input و Result یک Tool مکث میکند.transport({ delayMs }) تأخیر پیشفرض بین Deltaهای Text و Reasoning را تعیین میکند.writer.text(text, { delayMs }) و writer.reasoning(text, { delayMs }) این تأخیر را برای یک Part مشخص تغییر میدهند.mode: "instant" کل Text یا Reasoning را در یک Delta ارسال میکند.برای تستهای سریع، از chat.transport({ delayMs: 0 }) همراه با Textهای instant استفاده کنید.
اگر میخواهید کل پاسخ Assistant با خطا مواجه شود، از error() روی Chat استفاده کنید:
const chat = createChat()
.user("Load the report.")
.error("The report could not be loaded.")این کار رویداد RUN_ERROR را در TanStack ایجاد میکند.
برای ایجاد خطا پس از Stream شدن محتوای قبلی، از writer.error() استفاده کنید:
chat.assistant(({ writer }) => {
writer.text("I found the report.")
writer.error("The connection closed before it could be read.")
})پیامهای TanStack از Timestamp مربوط به createdAt پشتیبانی میکنند. این مقدار را بهعنوان Metadata پیام مشخص کنید تا از طریق get() و next() حفظ شود.
Connection مربوط به AG-UI، Metadata پیام را Stream نمیکند.
const chat = createChat()
.user("Hello", {
metadata: {
createdAt: "2026-01-01T10:00:00.000Z",
},
})
.assistant("Hi.", {
id: "assistant-welcome",
metadata: {
createdAt: new Date("2026-01-01T10:00:01.000Z"),
},
})اگر Fixture به Prefix سفارشی یا زمان ثابت نیاز دارد، از Options مربوط به Chat استفاده کنید:
const chat = createChat({
messageIdPrefix: "demo-message",
toolCallIdPrefix: "demo-tool",
now: "2026-01-01T00:00:00.000Z",
})بخشهای قبلی جریانهای اصلی استفاده را پوشش میدهند. این بخش تمام Exportها و Options عمومی را فهرست میکند.
createChat()یک مکالمهٔ Typed ایجاد میکند و رابط Fluent مربوط به TanStack Chat را برمیگرداند:
function createChat<
TOOLS extends ReadonlyArray<AnyClientTool> = AnyClientTool[],
DATA = unknown,
>(
options?: CreateChatOptions<TOOLS, DATA>
): TanStackChat<TOOLS, DATA>Writer مربوط به Helper، Partهای structured-output ایجاد نمیکند. مقدار DATA فقط Type آنها را هنگام شروع Chat از پیامهای موجود حفظ میکند.
IDهای موجود در messages رزرو میشوند و IDهای جدید بعد از Transcript موجود تولید خواهند شد.
TanStackChatهر متدی که محتوا اضافه میکند، همان Chat را برمیگرداند؛ بنابراین میتوانید متدها را بهصورت زنجیرهای استفاده کنید.
فراخوانی user() یا assistant() بدون محتوا از مقدار "Summarize the uploaded receipt." استفاده میکند.
فراخوانی error() بدون پیام از "An error occurred." استفاده میکند.
get(count) در صورت منفی بودن count یا عدد صحیح نبودن آن، خطا ایجاد میکند.
user()ساختار FilePayload:
type FilePayload = {
type?: "file"
mediaType: string
url: string
filename?: string
providerMetadata?: Record<string, unknown>
}Media Partهای TanStack، URL و Media Type را حفظ میکنند، اما ساختار پیام آنها filename و providerMetadata را حفظ نمیکند.
assistant()assistant()transport()برای جزئیات Matching، Fallback، خطاها و رفتار AG-UI، بخش Transport را ببینید.
writer داخل assistant(({ writer }) => {}) و Callbackهای Fallback در دسترس است.
فراخوانی text() بدون محتوا از "Summarize the uploaded receipt." استفاده میکند.
فراخوانی reasoning() بدون محتوا از "I need to inspect the available context before answering." استفاده میکند.
فراخوانی error() بدون پیام از "An error occurred." استفاده میکند.
tool() یک Handle با متدهای sleep(delayMs)، output(value) و error(errorText?) برمیگرداند.
وقتی چرخهٔ Tool بین Input و Result به رویدادهای بیشتری نیاز دارد، از این Handle استفاده کنید.
فراخوانی tool.error() بدون پیام از "Tool call failed." استفاده میکند.