Ai Conversation
smoothui-registry/ai-conversationFreeComponent
Thread scroll container that follows the bottom only while the reader is already there, offering a jump-to-latest pill when it stops.
Install it
npx shadcn@latest add https://www.smoothui.dev/r/ai-conversation.jsonSource
1"use client";23import { cn } from "@/lib/utils";4import { ArrowDown } from "lucide-react";5import { AnimatePresence, motion, useReducedMotion } from "motion/react";6import {7 type ReactNode,8 useCallback,9 useEffect,10 useLayoutEffect,11 useRef,12 useState,13} from "react";1415const SPRING_DEFAULT = {16 bounce: 0.1,17 duration: 0.25,18 type: "spring" as const,19};20/**21 * How close to the bottom still counts as "at the bottom".22 *23 * Zero would break on fractional scroll heights, and anything large would keep24 * yanking the view down while someone is reading a few lines up.25 */26const BOTTOM_THRESHOLD_PX = 48;2728export type AIConversationProps = {29 children: ReactNode;30 className?: string;31 /**32 * Changes whenever content grows — message count, or streamed text length.33 * Used to decide when to follow the bottom.34 */35 contentKey?: string | number;36};3738/**39 * Scroll container for a thread.40 *41 * It follows the bottom **only while the reader is already there**. Scrolling up42 * during a stream is an explicit act — the user wants to read something — and43 * dragging them back down is the single most common way chat UIs become44 * unusable. When it stops following, a pill appears to offer the trip back.45 */46const AIConversation = ({47 children,48 className,49 contentKey,50}: AIConversationProps) => {51 const shouldReduceMotion = useReducedMotion();52 const viewportRef = useRef<HTMLDivElement | null>(null);53 const [isPinned, setIsPinned] = useState(true);5455 const measure = useCallback(() => {56 const viewport = viewportRef.current;57 if (!viewport) {58 return;59 }60 const distance =61 viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;62 setIsPinned(distance <= BOTTOM_THRESHOLD_PX);63 }, []);6465 const scrollToBottom = useCallback((behavior: ScrollBehavior) => {66 const viewport = viewportRef.current;67 if (!viewport) {68 return;69 }70 // Element.scrollTo is missing in jsdom and in a few older engines, and it is71 // not worth throwing over — assigning scrollTop lands in the same place,72 // just without the smoothing.73 if (typeof viewport.scrollTo === "function") {74 viewport.scrollTo({ behavior, top: viewport.scrollHeight });75 } else {76 viewport.scrollTop = viewport.scrollHeight;77 }78 setIsPinned(true);79 }, []);8081 // Layout effect, so the jump happens in the same frame the content grew and82 // the reader never sees a flash of the pre-scroll position.83 useLayoutEffect(() => {84// … truncated
What it pulls in
npm packages
Files it writes
More from SmoothUI Registry
All 178 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Agent Avataragent-avatar | SmoothUI Registry | Components | Free | no deps | |
| Ai Approvalai-approval | SmoothUI Registry | Components | Free | 2 deps | |
| Ai Artifactai-artifact | SmoothUI Registry | Components | Free | 2 deps | |
| Ai Branchai-branch | SmoothUI Registry | Components | Free | 2 deps | |
| Ai Citationai-citation | SmoothUI Registry | Components | Free | 2 deps | |
| Ai Context Meterai-context-meter | SmoothUI Registry | Components | Free | 2 deps | |
| Ai Coreai-core | SmoothUI Registry | Components | Free | 1 dep | |
| Ai Diffai-diff | SmoothUI Registry | Components | Free | 2 deps |
