UNPKG

@liveblocks/react-ui

Version:

A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.

394 lines (390 loc) 14.3 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var react$1 = require('@liveblocks/react'); var _private = require('@liveblocks/react/_private'); var react = require('react'); var ArrowDown = require('../icons/ArrowDown.cjs'); var Spinner = require('../icons/Spinner.cjs'); var overrides = require('../overrides.cjs'); var cn = require('../utils/cn.cjs'); var useVisible = require('../utils/use-visible.cjs'); var AiChatAssistantMessage = require('./internal/AiChatAssistantMessage.cjs'); var AiChatUserMessage = require('./internal/AiChatUserMessage.cjs'); var AiComposer = require('./internal/AiComposer.cjs'); const MIN_DISTANCE_BOTTOM_SCROLL_INDICATOR = 60; const defaultComponents = { Empty: () => null, Loading: () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-loading lb-ai-chat-loading", children: /* @__PURE__ */ jsxRuntime.jsx(Spinner.SpinnerIcon, {}) }) }; const AiChatMessages = react.forwardRef( ({ messages, overrides, components, showReasoning, showRetrievals, showSources, lastSentMessageId, scrollToBottom, onScrollAtBottomChange, containerRef, footerRef, messagesRef, bottomTrailingMarkerRef, trailingSpacerRef, className, ...props }, forwardedRef) => { const hasLastSentMessage = lastSentMessageId !== null; react.useEffect( () => { if (!hasLastSentMessage) { return; } const container = containerRef.current; const footer = footerRef.current; const messages2 = messagesRef.current; if (!container || !footer || !messages2) { return; } const trailingSpacer = trailingSpacerRef.current; const bottomTrailingMarker = bottomTrailingMarkerRef.current; let containerHeight = void 0; let footerHeight = void 0; let messagesHeight = void 0; const resetTrailingSpace = () => { trailingSpacer?.style.removeProperty("height"); bottomTrailingMarker?.style.removeProperty("top"); }; const updateTrailingSpace = (updatedContainerHeight, updatedFooterHeight, updatedMessagesHeight) => { if (!trailingSpacer || !bottomTrailingMarker) { return; } const lastMessage = messages2.lastElementChild; const penultimateMessage = lastMessage?.previousElementSibling; if (updatedContainerHeight === void 0) { updatedContainerHeight = container.getBoundingClientRect().height; } if (updatedFooterHeight === void 0) { updatedFooterHeight = footer.getBoundingClientRect().height; } if (updatedMessagesHeight === void 0) { updatedMessagesHeight = messages2.getBoundingClientRect().height; } if (updatedContainerHeight === containerHeight && updatedFooterHeight === footerHeight && updatedMessagesHeight === messagesHeight) { if (!lastMessage || !penultimateMessage || container.scrollHeight === container.clientHeight) { resetTrailingSpace(); } return; } containerHeight = updatedContainerHeight; footerHeight = updatedFooterHeight; messagesHeight = updatedMessagesHeight; if (!lastMessage || !penultimateMessage) { resetTrailingSpace(); return; } if (container.scrollHeight === container.clientHeight) { resetTrailingSpace(); return; } const penultimateMessageScrollMarginTop = Number.parseFloat( getComputedStyle(penultimateMessage).scrollMarginTop ); const messagesRect = messages2.getBoundingClientRect(); const penultimateMessageRect = penultimateMessage.getBoundingClientRect(); const heightFromPenultimateMessageTopToMessagesListBottom = messagesRect.bottom - penultimateMessageRect.top; const differenceHeight = penultimateMessageScrollMarginTop + heightFromPenultimateMessageTopToMessagesListBottom + (footerHeight ?? 0); const trailingSpace = Math.max(containerHeight - differenceHeight, 0); trailingSpacer.style.height = `${trailingSpace}px`; bottomTrailingMarker.style.top = `${-trailingSpace}px`; }; const resizeObserver = new ResizeObserver((entries) => { let updatedContainerHeight = containerHeight; let updatedFooterHeight = footerHeight; let updatedMessagesHeight = messagesHeight; for (const entry of entries) { const entryHeight = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height; if (entry.target === container) { updatedContainerHeight = entryHeight; } else if (entry.target === footer) { updatedFooterHeight = entryHeight; } else if (entry.target === messages2) { updatedMessagesHeight = entryHeight; } } updateTrailingSpace( updatedContainerHeight, updatedFooterHeight, updatedMessagesHeight ); }); resizeObserver.observe(container); resizeObserver.observe(footer); resizeObserver.observe(messages2); requestAnimationFrame(() => updateTrailingSpace()); return () => { resizeObserver.disconnect(); resetTrailingSpace(); }; }, // This effect only uses stable refs. [hasLastSentMessage] // eslint-disable-line react-hooks/exhaustive-deps ); useVisible.useIntersectionCallback( bottomTrailingMarkerRef, (isIntersecting) => { onScrollAtBottomChange.current(isIntersecting); }, { root: containerRef, rootMargin: MIN_DISTANCE_BOTTOM_SCROLL_INDICATOR } ); react.useEffect( () => { scrollToBottom.current("instant"); }, // `scrollToBottom` is a stable ref containing the callback. [] // eslint-disable-line react-hooks/exhaustive-deps ); react.useEffect( () => { if (lastSentMessageId) { scrollToBottom.current("smooth", true); } }, // `scrollToBottom` is a stable ref containing the callback. [lastSentMessageId] // eslint-disable-line react-hooks/exhaustive-deps ); react.useEffect( () => { const onScrollAtBottomChangeCallback = onScrollAtBottomChange.current; return () => { onScrollAtBottomChangeCallback(null); }; }, // `onScrollAtBottomChange` is a stable ref containing the callback. [] // eslint-disable-line react-hooks/exhaustive-deps ); return /* @__PURE__ */ jsxRuntime.jsx( "div", { className: cn.cn("lb-ai-chat-messages", className), ref: forwardedRef, ...props, children: messages.map((message) => { if (message.role === "user") { return /* @__PURE__ */ jsxRuntime.jsx( AiChatUserMessage.AiChatUserMessage, { message, overrides, components }, message.id ); } else if (message.role === "assistant") { return /* @__PURE__ */ jsxRuntime.jsx( AiChatAssistantMessage.AiChatAssistantMessage, { message, overrides, components, showReasoning, showRetrievals, showSources }, message.id ); } else { return null; } }) } ); } ); function scrollIntoView(element, options) { if (!element) { return; } if (element.getClientRects().length === 0) { return; } element.scrollIntoView(options); } const AiChat = react.forwardRef( ({ chatId, copilotId, autoFocus, overrides: overrides$1, knowledge: localKnowledge, tools = {}, onComposerSubmit, layout = "inset", showReasoning, showRetrievals, showSources, components, className, responseTimeout, ...props }, forwardedRef) => { const { messages, isLoading, error } = react$1.useAiChatMessages(chatId); const [lastSentMessageId, setLastSentMessageId] = react.useState(null); const $ = overrides.useOverrides(overrides$1); const Empty = components?.Empty ?? defaultComponents.Empty; const Loading = components?.Loading ?? defaultComponents.Loading; const containerRef = react.useRef(null); const messagesRef = react.useRef(null); const footerRef = react.useRef(null); const bottomMarkerRef = react.useRef(null); const bottomTrailingMarkerRef = react.useRef(null); const trailingSpacerRef = react.useRef(null); const [isScrollAtBottom, setScrollAtBottom] = react.useState( null ); const onScrollAtBottomChange = _private.useLatest(setScrollAtBottom); const isScrollIndicatorVisible = messages && isScrollAtBottom !== null ? !isScrollAtBottom : false; react.useImperativeHandle( forwardedRef, () => containerRef.current, [] ); const scrollToBottom = _private.useLatest( (behavior, includeTrailingSpace = false) => { if (includeTrailingSpace) { setTimeout(() => { scrollIntoView(bottomMarkerRef.current, { behavior, block: "end" }); }, 0); } else { scrollIntoView(bottomTrailingMarkerRef.current, { behavior, block: "end" }); } } ); return /* @__PURE__ */ jsxRuntime.jsxs( "div", { ref: containerRef, ...props, className: cn.cn( "lb-root lb-ai-chat", `lb-ai-chat:layout-${layout}`, className ), children: [ Object.entries(tools).map(([name, tool]) => /* @__PURE__ */ jsxRuntime.jsx(react$1.RegisterAiTool, { chatId, name, tool }, name)), localKnowledge ? localKnowledge.map((knowledge, index) => /* @__PURE__ */ jsxRuntime.jsx( react$1.RegisterAiKnowledge, { chatId, ...knowledge }, `${index}:${knowledge.description}` )) : null, /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-ai-chat-content", children: isLoading ? /* @__PURE__ */ jsxRuntime.jsx(Loading, {}) : error !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-error lb-ai-chat-error", children: $.AI_CHAT_MESSAGES_ERROR(error) }) : messages.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(Empty, { chatId, copilotId }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [ /* @__PURE__ */ jsxRuntime.jsx( AiChatMessages, { messages, overrides: overrides$1, components, showReasoning, showRetrievals, showSources, lastSentMessageId, scrollToBottom, onScrollAtBottomChange, containerRef, footerRef, messagesRef, bottomTrailingMarkerRef, trailingSpacerRef, ref: forwardedRef } ), /* @__PURE__ */ jsxRuntime.jsx( "div", { ref: trailingSpacerRef, "data-trailing-spacer": "", style: { pointerEvents: "none", height: 0 }, "aria-hidden": true } ) ] }) }), /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lb-ai-chat-footer", ref: footerRef, children: [ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lb-ai-chat-footer-actions", children: /* @__PURE__ */ jsxRuntime.jsx( "div", { className: "lb-root lb-elevation lb-elevation-moderate lb-ai-chat-scroll-indicator", "data-visible": isScrollIndicatorVisible ? "" : void 0, children: /* @__PURE__ */ jsxRuntime.jsx( "button", { className: "lb-ai-chat-scroll-indicator-button", tabIndex: isScrollIndicatorVisible ? 0 : -1, "aria-hidden": !isScrollIndicatorVisible, onClick: () => scrollToBottom.current("smooth"), children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lb-icon-container", children: /* @__PURE__ */ jsxRuntime.jsx(ArrowDown.ArrowDownIcon, {}) }) } ) } ) }), /* @__PURE__ */ jsxRuntime.jsx( AiComposer.AiComposer, { chatId, copilotId, overrides: overrides$1, autoFocus, responseTimeout, onComposerSubmit, onComposerSubmitted: ({ id }) => setLastSentMessageId(id), className: cn.cn( "lb-ai-chat-composer", layout === "inset" ? "lb-elevation lb-elevation-moderate" : void 0 ) }, chatId ) ] }), messages && messages.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx( "div", { ref: bottomMarkerRef, style: { position: "sticky", height: 0 }, "aria-hidden": true, "data-bottom-marker": "", children: /* @__PURE__ */ jsxRuntime.jsx( "div", { ref: bottomTrailingMarkerRef, style: { position: "absolute", height: 0 }, "data-bottom-trailing-marker": "" } ) } ) : null ] } ); } ); exports.AiChat = AiChat; //# sourceMappingURL=AiChat.cjs.map