"use client"; import { MessageChronology } from './day-separator'; import { MarkdownText } from './markdown-text'; import { Reasoning, ReasoningContent, ReasoningRoot, ReasoningText, ReasoningTrigger, } from './reasoning.aui'; import { ToolFallback } from './tool-fallback.aui'; import { ToolGroupContent, ToolGroupRoot, ToolGroupTrigger, } from './tool-group.aui'; import { TooltipIconButton } from './tooltip-icon-button'; import { Button } from '../../ui/button'; import { Skeleton } from '../../ui/skeleton'; import { cn } from '../../lib/utils'; import { ActionBarMorePrimitive, ActionBarPrimitive, AuiIf, type AssistantState, ComposerPrimitive, ErrorPrimitive, groupPartByType, MessagePrimitive, SuggestionPrimitive, ThreadPrimitive, type ToolCallMessagePartComponent, useAuiState, } from "@assistant-ui/react"; import { ArrowDownIcon, ArrowUpIcon, CheckIcon, CopyIcon, DownloadIcon, LoaderCircleIcon, MoreHorizontalIcon, } from "lucide-react"; import { createContext, useContext, useEffect, useRef, useState, type ComponentType, type FC, type PropsWithChildren, } from "react"; export type ThreadGroupPart = MessagePrimitive.GroupedParts.GroupPart; export const defaultThreadPartGroupBy = groupPartByType({ reasoning: ["group-chainOfThought", "group-reasoning"], "tool-call": ["group-chainOfThought", "group-tool"], "standalone-tool-call": [], }); /** * Optional component overrides for the thread. `AssistantMessage` and * `Welcome` replace whole sections; the remaining slots override how the * assistant message renders tool calls and part groups. Tool UIs registered * by name (toolkit `render`, `useAssistantDataUI`) take precedence over * `ToolFallback`. */ export type ThreadComponents = { AssistantMessage?: ComponentType | undefined; UserMessage?: ComponentType | undefined; Welcome?: ComponentType | undefined; Indicator?: ComponentType | undefined; /** Rendered directly above the composer, inside the sticky footer. */ Banner?: ComponentType | undefined; ToolFallback?: ToolCallMessagePartComponent | undefined; ToolGroup?: | ComponentType> | undefined; ReasoningGroup?: | ComponentType> | undefined; groupBy?: typeof defaultThreadPartGroupBy; }; export type ThreadProps = { components?: ThreadComponents | undefined; autoFocus?: boolean | undefined; }; const EMPTY_COMPONENTS: ThreadComponents = {}; const ThreadComponentsContext = createContext(EMPTY_COMPONENTS); // Startup exposes a loading placeholder thread; treat it as a new chat so // the composer mounts centered. Loads after startup keep the docked layout. const isNewChatView = (s: AssistantState) => s.thread.messages.length === 0 && (!s.thread.isLoading || s.threads.isLoading); // A switched thread that is still fetching its history: skeleton, not welcome. const isHistoryLoadingView = (s: AssistantState) => s.thread.messages.length === 0 && s.thread.isLoading && !s.thread.isDisabled && !s.threads.isLoading; const ThreadHistorySkeleton: FC = () => (
Loading conversation
); export const Thread: FC = ({ components = EMPTY_COMPONENTS, autoFocus = true, }) => { const isEmpty = useAuiState(isNewChatView); return ( ); }; const ThreadRoot: FC<{ isEmpty: boolean; autoFocus: boolean }> = ({ isEmpty, autoFocus, }) => { const { Welcome = ThreadWelcome, Banner } = useContext(ThreadComponentsContext); return (
{() => }
{Banner ? : null}
); }; const ThreadMessage: FC = () => { const { AssistantMessage: AssistantMessageComponent = AssistantMessage } = useContext(ThreadComponentsContext); const { UserMessage: UserMessageComponent = UserMessage } = useContext(ThreadComponentsContext); const role = useAuiState((s) => s.message.role); return ( {role === "user" ? : } ); }; const ThreadViewportBottomStateSync: FC = () => { const markerRef = useRef(null); useEffect(() => { const viewport = markerRef.current?.closest( '[data-slot="aui_thread-viewport"]', ); if (!viewport) return; let frame: number | undefined; const syncIfAtBottom = () => { cancelAnimationFrame(frame ?? 0); frame = requestAnimationFrame(() => { const bottomDistance = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight; if (Math.abs(bottomDistance) <= 1) { viewport.dispatchEvent(new Event("scroll")); } }); }; const observer = new MutationObserver((mutations) => { const reserveChanged = mutations.some((mutation) => { const target = mutation.target; if ( target instanceof HTMLElement && target.matches("[data-aui-top-anchor-reserve]") ) { return true; } return [...mutation.addedNodes, ...mutation.removedNodes].some( (node) => node instanceof HTMLElement && (node.matches("[data-aui-top-anchor-reserve]") || node.querySelector("[data-aui-top-anchor-reserve]")), ); }); if (reserveChanged) syncIfAtBottom(); }); observer.observe(viewport, { attributes: true, attributeFilter: ["style"], childList: true, subtree: true, }); return () => { observer.disconnect(); cancelAnimationFrame(frame ?? 0); }; }, []); return