کامپوننت MessageScroller برای ساخت رابطهای پیامرسان، چت و خروجیهای Streaming.
کامپوننت MessageScroller برای ساخت رابطهای پیامرسان، چت و خروجیهای Streaming طراحی شده است.
MessageScroller در پکیج @farsiui/react بهصورت یک primitive بدون استایل ارائه میشود. این primitive مدیریت اسکرول، حفظ موقعیت کاربر هنگام اضافه شدن پیامهای قدیمی، دنبال کردن خروجی در حال Streaming، لنگر کردن پیامها و تشخیص پیامهای قابل مشاهده را بر عهده دارد.
ظاهر و استایل کامپوننت را میتوانید کاملاً مطابق نیاز پروژه خود پیادهسازی کنید.
برای راهنمای رفتار و مثالهای زنده، صفحهٔ Message Scroller را ببینید.
پکیج @farsiui/react را نصب کنید:
pnpm add @farsiui/reactimport {
MessageScroller,
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
} from "@farsiui/react/message-scroller"MessageScroller بهصورت یک namespace ارائه میشود و بخشهای مختلف آن از طریق همین namespace در دسترس هستند.
<MessageScroller.Provider>
<MessageScroller.Root>
<MessageScroller.Viewport>
<MessageScroller.Content>
{messages.map((message) => (
<MessageScroller.Item
key={message.id}
messageId={message.id}
scrollAnchor={message.role === "user"}
>
{/* رابط کاربری پیام */}
</MessageScroller.Item>
))}
</MessageScroller.Content>
</MessageScroller.Viewport>
<MessageScroller.Button />
</MessageScroller.Root>
</MessageScroller.Provider>اگر قبلاً از نسخهٔ استایلدار استفاده کردهاید، معادل بخشهای آن در نسخهٔ بدون استایل به شکل زیر است:
Hookها نیز از همان Provider استفاده میکنند:
import {
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
} from "@farsiui/react/message-scroller"در این مثال، MessageScroller همراه با AI SDK استفاده شده است:
"use client"
import { useChat } from "@ai-sdk/react"
import { DefaultChatTransport } from "ai"
import { MessageScroller } from "@farsiui/react/message-scroller"
import { ChatInput } from "@/components/chat-input"
export function Chat() {
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: "/api/chat",
}),
})
return (
<div className="flex h-svh w-full flex-col">
<MessageScroller.Provider>
<MessageScroller.Root className="relative flex flex-1 flex-col overflow-hidden">
<MessageScroller.Viewport className="flex flex-1 flex-col overflow-y-auto">
<MessageScroller.Content className="flex flex-col gap-4 p-6 text-base">
{messages.map((message, index) => (
<MessageScroller.Item
key={message.id}
messageId={`message-${index}`}
scrollAnchor={message.role === "user"}
>
<div className="rounded-lg bg-muted p-4">
{message.parts.map((part, i) =>
part.type === "text" ? (
<span key={i}>{part.text}</span>
) : null
)}
</div>
</MessageScroller.Item>
))}
</MessageScroller.Content>
</MessageScroller.Viewport>
<MessageScroller.Button className="absolute bottom-2 left-1/2 z-10 -translate-x-1/2 rounded-full border bg-background px-3 py-1 text-sm font-medium inert:opacity-0">
رفتن به آخرین پیام
</MessageScroller.Button>
</MessageScroller.Root>
</MessageScroller.Provider>
<ChatInput
onSend={sendMessage}
disabled={status !== "ready"}
/>
</div>
)
}یک container اسکرولشونده در اولین رندر به ابتدای محتوا میرود. بنابراین اگر Transcript از سمت سرور رندر شده باشد، ابتدا پیامهای قدیمی نمایش داده میشوند و بعد از اجرای JavaScript، موقعیت اسکرول تغییر میکند.
اگر مقدار defaultScrollPosition برابر "end" یا "last-anchor" باشد، Viewport تا زمان اعمال موقعیت اولیه دارای ویژگی data-pending-scroll است.
برای جلوگیری از نمایش این پرش، Viewport را تا زمان آماده شدن موقعیت مخفی کنید:
#messages[data-pending-scroll] {
visibility: hidden;
}شناسه را روی Viewport قرار دهید:
<MessageScroller.Viewport id="messages">
<MessageScroller.Content>
{/* پیامها */}
</MessageScroller.Content>
</MessageScroller.Viewport>اگر میخواهید موقعیت "end" از همان اولین Paint قابل مشاهده باشد، میتوانید یک اسکریپت کوچک بعد از Viewport اجرا کنید:
const scrollToEndScript = `
(function () {
var viewport = document.getElementById("messages")
if (!viewport) {
return
}
viewport.scrollTop = viewport.scrollHeight
viewport.removeAttribute("data-pending-scroll")
})()
`
<MessageScroller.Root>
<MessageScroller.Viewport
id="messages"
suppressHydrationWarning
>
<MessageScroller.Content>
{/* transcript */}
</MessageScroller.Content>
</MessageScroller.Viewport>
<script
dangerouslySetInnerHTML={{
__html: scrollToEndScript,
}}
/>
<MessageScroller.Button />
</MessageScroller.Root>این روش فقط برای "end" مناسب است و زمانی کاربرد دارد که پیامها از ابتدا در HTML وجود داشته باشند.
برای "last-anchor" از این اسکریپت استفاده نکنید. همچنین اگر پیامها در سمت کلاینت بارگذاری میشوند، نیازی به این روش نیست.
اگر پروژه شما Content Security Policy دارد، برای اسکریپت nonce در نظر بگیرید.
ریشهٔ بدون استایل MessageScroller است. وضعیت اسکرول و تنظیمات رفتاری را مدیریت میکند و آنها را در اختیار سایر بخشها و Hookها قرار میدهد.
خود Provider هیچ DOMای رندر نمیکند.
defaultScrollPositionمقدار "last-anchor" Viewport را روی آخرین پیام دارای scrollAnchor قرار میدهد و در صورت نبود Anchor یا مناسب بودن اندازهٔ محتوا، به "end" برمیگردد.
برای "end" و "last-anchor"، ویژگی data-pending-scroll تا زمان اعمال موقعیت اولیه روی Viewport باقی میماند. جزئیات بیشتر در جلوگیری از پرش هنگام بارگذاری مجدد.
قاب و container اصلی layout است. فضای والد خود را پر میکند؛ بنابراین آن را داخل یک layout با ارتفاع مشخص و درون MessageScroller.Provider قرار دهید.
Root نیز attributeهای وضعیت اسکرول زیر را منعکس میکند (Viewport هم آنها را دارد)، تا بتوانید container را بر اساس وضعیت اسکرول استایل دهید؛ مثلاً fade لبهها روی قاب. در استفادهٔ headless، Viewport را تا زمان وجود data-pending-scroll با visibility: hidden مخفی کنید تا layout برای اسکرول اولیه حفظ شود.
Viewport اسکرولشونده.
عنصر محتوای Transcript. هر فرزند مستقیم باید یک MessageScroller.Item باشد.
یک ردیف Transcript: پیام، marker، ردیف تایپ، جداکننده یا ردیف بارگذاری بیشتر.
دکمهای که به ابتدا یا انتهای Transcript اسکرول میکند. وقتی مقصدی برای اسکرول نباشد، inert میشود و از ترتیب تب خارج میگردد.
کنترلهای دستوری Transcript.
همهٔ دستورها در صورت عدم امکان اجرا false برمیگردانند.
scrollToStart و scrollToEnd فقط وقتی Viewport هنوز mount نشده false میدهند.
scrollToMessage وقتی هدف mount نشده و قابل صفبندی نیست false برمیگرداند.
گزینههای دستور:
لبههایی که Viewport میتواند به سمت آنها اسکرول کند؛ برای UIهای همجوار که به این مقادیر در JavaScript نیاز دارند. برای استایل خود scroller، attribute data-scrollable را ترجیح دهید.
وضعیت visibility برای outline، جستجو و UI مربوط به turn فعال. جدا از useMessageScrollerScrollable مشترک میشود تا هزینهٔ visibility فقط وقتی پرداخت شود که مصرفکننده به آن نیاز دارد.
در صورت نیاز به outline محدودتر (مثلاً فقط پیامهای کاربر، turnهای Anchorشده یا نتایج جستجو)، visibleMessageIds را در اپلیکیشن فیلتر کنید.