@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
JavaScript
'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