Skip to content

Conversation

گزارش رویدادِ فقط‌افزودنی را به‌شکل گفت‌وگو رندر می‌کند. این عنصر تنها سه کاری را برعهده می‌گیرد که خسته‌کننده‌اند و به‌آسانی اشتباه می‌شوند و بس: نگاشت رویدادها به گره‌ها، نگه‌داشتن نما چسبیده به پایین بدون زیرپا گذاشتن پیمایش خودِ خواننده، و تطبیق سطرها با فهرست گره‌ها.

کجا به کارش ببرید: وقتی رونوشتی جریان‌دار (گفت‌وگو، نشست یک عامل، یک گزارش) را رندر می‌کنید و می‌خواهید هر گونه محتوا (پیام، فراخوانی ابزار، سطر وضعیت) یک ثبت مستقل باشد، نه شاخه‌ای تازه در رندرکننده‌ای که مدام بزرگ‌تر می‌شود.

اینکه یک پیام یا فراخوانی ابزار چه شکلی است، کار نمای ثبت‌شده است نه این عنصر. نگاشتش ranuts/conversation است و پیمایشش createBottomFollower از ranuts/utils.

شروع سریع

html
<r-conversation empty="هنوز پیامی نیست" style="height: 400px"></r-conversation>
ts
const chat = document.createElement('r-conversation');

chat.register({
  kind: 'message',
  // کدام رویدادها از آنِ من‌اند و به کدام گره تعلق دارند.
  match: (e) =>
    e.type === 'message/start'
      ? { id: e.id, role: 'start' }
      : e.type === 'message/delta'
        ? { id: e.id, role: 'update' }
        : null,
  // چگونه در وضعیت خودم تا می‌شوند.
  start: () => ({ text: '' }),
  update: (state, e) => ({ text: state.text + e.text }),
  // دلتاهای توکنی در هر فریم به یک بازترسیم جمع می‌شوند؛ واقعیت‌های گسسته منتظر نمی‌مانند.
  publication: (e) => (e.type === 'message/delta' ? 'animation-frame' : 'immediate'),
  // آن وضعیت چگونه به صفحه می‌رسد.
  mount: () => document.createElement('r-markdown'),
  patch: (el, node) => {
    el.content = node.state.text;
  },
});

chat.push({ type: 'message/start', id: 'm1' });
chat.push({ type: 'message/delta', id: 'm1', text: 'Hello' });

container.append(chat);

برای متن روان، سطر مورد نظر <r-markdown> است: در حالت پیش‌فرض mode="streaming" از پیش **bold، بک‌تیک، پیوند و فرمول $$ِ نیمه‌رسیده را می‌بندد، پس هیچ نمایی لازم نیست این کار را بکند.

قاعده‌هایی که شکستنشان گاز می‌گیرد

  • پیش از نخستین push همهٔ نماها را ثبت کنید. نگاشت یک بار از مجموعهٔ ثبت‌شده ساخته می‌شود، پس ثبت دیرهنگام هر رویدادی را که پیش‌تر تا خورده بی‌صدا از دست می‌دهد. عنصر به‌جای این کار استثنا می‌اندازد.
  • update وضعیت را تا می‌کند و patch آن را در DOM می‌نویسد. نامشان جداست چون کارشان جداست: patch چیزی تا نمی‌کند و روی سطری که جریان دارد در هر فریم یک بار اجرا می‌شود، پس ارزانش نگه دارید.
  • mount اختیاری است. نمایی که آن را ندارد فقط وضعیتی فراهم می‌کند که نماهای دیگر از راه reader.previous می‌خوانند و خودش چیزی رندر نمی‌کند.
  • سطرها همان جایی می‌مانند که باز شده‌اند. پیامی که جریان دارد با هر دلتا به انتهای فهرست نمی‌پرد.

چسبیدن به پایین

به‌طور پیش‌فرض روشن است. تا وقتی محتوا می‌رسد نما به پایین چسبیده می‌ماند، به‌محض اینکه خواننده به بالا پیمایش کند می‌ایستد، و وقتی دوباره پایین بیاید دوباره می‌چسبد — و در هیچ لحظه‌ای پیمایش دستی را زیرپا نمی‌گذارد، چون دنبال‌کننده به‌جای گوش‌دادن به دستگاه‌های ورودی، نوشتن‌های پیمایشی خودش را از آنِ خواننده تشخیص می‌دهد.

ts
chat.addEventListener('pinnedchange', (e) => {
  jumpButton.hidden = e.detail.pinned;
});

follow="false" از همان آغاز مهار را به خواننده می‌سپارد؛ scrollToBottom() آن را پس می‌گیرد. برای صفحه‌بندی محتوای قدیمی‌تر، پیش از افزودن به ابتدا captureAnchor() و پس از آن restoreAnchor() را صدا بزنید تا خواننده همان چیزی را که می‌دید همچنان ببیند.

مرجع API

خصیصه‌ها

خصیصهنوعپیش‌فرضتوضیح
followbooleantrueتا وقتی خواننده از کف دور نشده، محتوای تازه را دنبال می‌کند.
emptystring''متنی که تا وقتی نگاشت هیچ سطری نساخته نمایش می‌یابد. خالی = پنهان.
pinnedbooleantrueفقط‌خواندنی. اینکه نما هم‌اکنون محتوای تازه را دنبال می‌کند یا نه.
sheetstring''CSS تزریق‌شده به shadow DOM عنصر.

متدها

متدتوضیح
register(view)یک گونه محتوا را ثبت می‌کند. پس از نخستین push استثنا می‌اندازد.
push(event)یک رویداد را نگاشت می‌کند و هرچه تغییر کرده را می‌کشد.
reset()همهٔ گره‌ها و سطرها را دور می‌ریزد و نماهای ثبت‌شده را نگه می‌دارد.
scrollToBottom()تا کف پیمایش می‌کند و دنبال‌کردن را از سر می‌گیرد.
captureAnchor(key?)پیش از افزودن محتوای قدیمی به ابتدا، جای یک سطر را به یاد می‌سپارد.
restoreAnchor()سطر به‌یادسپرده را به جای پیشینش برمی‌گرداند.

رویدادها

رویدادDetailچه زمانی
pinnedchange{ pinned: boolean }چسبیدن به پایین به دست می‌آید یا از دست می‌رود.

اسلات‌ها

اسلاتتوضیح
footerناحیهٔ چسبان زیر سطرها: کادر نوشتن اینجا می‌نشیند و ارتفاعش پایش می‌شود.

Part‌ها

conversation (ناحیهٔ پیمایش)، list، row، footer، empty.

هر سطر data-kind و data-key هم دارد، پس مصرف‌کننده می‌تواند بدون دست‌بردن در درخت shadow به آن استایل بدهد یا پیدایش کند.

استایل

<r-conversation> ۱۴ ویژگی سفارشی CSS از آنِ خود و افزون بر آن توکن‌های معنایی‌ای که از پوسته می‌خواند در اختیار می‌گذارد. آن را هرجا که ارث می‌رسد تعیین کنید: :root، یک نگه‌دارنده، یا خود عنصر.

css
r-conversation {
  --ran-conversation-background: var(--ran-color-bg-subtle);
}

Part‌ها: conversation · empty · footer · list · older

فهرست کامل در توکن‌های استایل است و اینکه کدام توکن را برگزینید در سیستم طراحی آمده.

همچنین ببینید

  • ranuts/stream: تبدیل SSE یک ارائه‌دهنده به رویدادهایی که اینجا push می‌شوند
  • ranuts/conversation: خودِ نگاشت، به‌همراه آهنگ به‌روزرسانی
  • Markdown: سطرِ آگاه از جریان، برای متن روان

منتشرشده تحت مجوز MIT.