Message Scroller
beui/message-scrollerFreeComponent
A reader-aware conversation viewport that follows streamed output at the live edge and releases control when the reader moves away.
Install it
npx shadcn@latest add https://beui.dev/r/message-scroller.jsonSource
1"use client";2// beui.dev/components/agents/message-scroller34import { useReducedMotion } from "motion/react";5import {6 type ComponentPropsWithRef,7 type Ref,8 useCallback,9 useEffect,10 useLayoutEffect,11 useRef,12 useState,13} from "react";14import {15 PreviewRail,16 type PreviewRailItem,17} from "@/components/motion/preview-rail";18import { cn } from "@/lib/utils";1920const PREVIEW_TITLE_LENGTH = 56;21const PREVIEW_DESCRIPTION_LENGTH = 88;2223function truncateMessageText(text: string, limit: number) {24 if (text.length <= limit) return text;25 const excerpt = text.slice(0, limit);26 const boundary = excerpt.lastIndexOf(" ");27 return `${excerpt.slice(0, boundary > limit * 0.65 ? boundary : limit).trim()}…`;28}2930function getMessageText(message: HTMLElement) {31 const surface =32 message.querySelector<HTMLElement>('[data-slot="message-bubble-content"]') ??33 message.querySelector<HTMLElement>('[data-slot="message-content"]') ??34 message;35 return (surface.textContent ?? "").replace(/\s+/g, " ").trim();36}3738function getMessagePreview(39 message: HTMLElement,40 assistantResponse?: HTMLElement,41) {42 const text = getMessageText(message);43 if (!text) {44 return { label: "Message", description: undefined };45 }4647 if (text.length <= PREVIEW_TITLE_LENGTH) {48 const responseText = assistantResponse49 ? getMessageText(assistantResponse)50 : "";51 return {52 label: text,53 description: responseText54 ? truncateMessageText(responseText, PREVIEW_DESCRIPTION_LENGTH)55 : undefined,56 };57 }5859 const titleExcerpt = text.slice(0, PREVIEW_TITLE_LENGTH);60 const titleBoundary = titleExcerpt.lastIndexOf(" ");61 const titleEnd =62 titleBoundary > PREVIEW_TITLE_LENGTH * 0.6563 ? titleBoundary64 : PREVIEW_TITLE_LENGTH;65 const label = `${text.slice(0, titleEnd).trim()}…`;66 const responseText = assistantResponse67 ? getMessageText(assistantResponse)68 : text.slice(titleEnd).trim();69 return {70 label,71 description: responseText72 ? truncateMessageText(responseText, PREVIEW_DESCRIPTION_LENGTH)73 : undefined,74 };75}7677export interface MessageScrollerProps extends ComponentPropsWithRef<"div"> {78 /** Keep streamed output pinned while the reader remains near the end. */79 followOutput?: boolean;80 /** Distance from the end that still counts as following the output. */81 followThreshold?: number;82 /** Smoothly follow growing content. */83 smooth?: boolean;84// … truncated
What it pulls in
npm packages
Files it writes
More from beui
All 104 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Action Swap Bluraction-swap-blur | beui | Components | Free | 3 deps · 4 files | |
| Action Swap Cascadeaction-swap-cascade | beui | Components | Free | 3 deps · 4 files | |
| Action Swap Rollaction-swap-roll | beui | Components | Free | 3 deps · 4 files | |
| Agent Activityagent-activity | beui | Components | Free | 4 deps · 9 files | |
| Agent Loading States Agent Progressagent-progress | beui | Components | Free | 3 deps · 3 files | |
| AI Sidebarai-sidebar | beui | Components | Free | 4 deps · 5 files | |
| Animated Badgeanimated-badge | beui | Components | Free | 4 deps · 3 files | |
| Number Animation Animated Numberanimated-number | beui | Components | Free | 3 deps · 3 files |
