@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.
365 lines (361 loc) • 11.4 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var core = require('@liveblocks/core');
var react$1 = require('@liveblocks/react');
var _private = require('@liveblocks/react/_private');
var reactSlot = require('@radix-ui/react-slot');
var react = require('react');
var slate = require('slate');
var slateHistory = require('slate-history');
var slateReact = require('slate-react');
var requestSubmit = require('../../utils/request-submit.cjs');
var useInitial = require('../../utils/use-initial.cjs');
var normalize = require('../slate/plugins/normalize.cjs');
var isEmpty = require('../slate/utils/is-empty.cjs');
var contexts = require('./contexts.cjs');
const AI_COMPOSER_SUBMIT_NAME = "AiComposerSubmit";
const AI_COMPOSER_ABORT_NAME = "AiComposerAbort";
const AI_COMPOSER_EDITOR_NAME = "AiComposerEditor";
const AI_COMPOSER_FORM_NAME = "AiComposerForm";
const emptyMessages\u03A3 = new core.Signal([]);
function getLastMessageId(messages) {
const lastMessage = messages[messages.length - 1];
if (lastMessage === void 0) {
return null;
}
return lastMessage.id;
}
function getAbortableMessageId(messages) {
return messages.find(
(message) => message.role === "assistant" && (message.status === "generating" || message.status === "awaiting-tool")
)?.id;
}
const AiComposerForm = react.forwardRef(
({
onComposerSubmit,
onSubmit,
disabled,
chatId,
branchId,
asChild,
...props
}, forwardedRef) => {
const Component = asChild ? reactSlot.Slot : "form";
const client = react$1.useClient();
const formRef = react.useRef(null);
const editor = useInitial.useInitial(
() => normalize.withNormalize(slateHistory.withHistory(slateReact.withReact(slate.createEditor())))
);
const [isEditorEmpty, setEditorEmpty] = react.useState(true);
const [isSubmitting, setSubmitting] = react.useState(false);
const [isFocused, setFocused] = react.useState(false);
const messages\u03A3 = chatId ? client[core.kInternal].ai.signals.getChatMessagesForBranch\u03A3(chatId, branchId) : emptyMessages\u03A3;
const lastMessageId = _private.useSignal(messages\u03A3, getLastMessageId);
const abortableMessageId = _private.useSignal(messages\u03A3, getAbortableMessageId);
const isAvailable = _private.useSignal(
// Subscribe to connection status signal
client[core.kInternal].ai.signals.status\u03A3,
// "Disconnected" means the AI service is not available
// as it represents a final error status.
(status) => status !== "disconnected"
);
const isDisabled = isSubmitting || disabled === true;
const canAbort = isAvailable && abortableMessageId !== void 0;
const canSubmit = isAvailable && !isEditorEmpty && !canAbort;
const clear = react.useCallback(() => {
slate.Transforms.delete(editor, {
at: {
anchor: slate.Editor.start(editor, []),
focus: slate.Editor.end(editor, [])
}
});
}, [editor]);
const select = react.useCallback(() => {
slate.Transforms.select(editor, slate.Editor.end(editor, []));
}, [editor]);
const focus = react.useCallback(
(resetSelection = true) => {
try {
if (!slateReact.ReactEditor.isFocused(editor)) {
slate.Transforms.select(
editor,
resetSelection || !editor.selection ? slate.Editor.end(editor, []) : editor.selection
);
slateReact.ReactEditor.focus(editor);
}
} catch {
}
},
[editor]
);
const blur = react.useCallback(() => {
try {
slateReact.ReactEditor.blur(editor);
} catch {
}
}, [editor]);
const onSubmitEnd = react.useCallback(() => {
clear();
setSubmitting(false);
}, [clear]);
const handleSubmit = react.useCallback(
(event) => {
if (disabled) {
return;
}
const isEditorEmpty2 = isEmpty.isEmpty(editor, editor.children);
if (isEditorEmpty2) {
event.preventDefault();
return;
}
onSubmit?.(event);
if (onComposerSubmit === void 0 || event.isDefaultPrevented()) {
event.preventDefault();
return;
}
const content = editor.children.map((block) => {
if ("type" in block && block.type === "paragraph") {
return block.children.map((child) => {
if ("text" in child) {
return child.text;
}
return "";
}).join("");
}
return "";
}).join("\n");
const promise = onComposerSubmit(
{ text: content, lastMessageId },
event
);
event.preventDefault();
if (promise) {
setSubmitting(true);
promise.then(onSubmitEnd);
} else {
onSubmitEnd();
}
},
[disabled, editor, onSubmit, onComposerSubmit, onSubmitEnd, lastMessageId]
);
_private.useLayoutEffect(() => {
setEditorEmpty(isEmpty.isEmpty(editor, editor.children));
}, [editor]);
const handleEditorValueChange = react.useCallback(() => {
setEditorEmpty(isEmpty.isEmpty(editor, editor.children));
}, [editor]);
const submit = react.useCallback(() => {
if (!canSubmit) {
return;
}
requestAnimationFrame(() => {
if (formRef.current) {
requestSubmit.requestSubmit(formRef.current);
}
});
}, [canSubmit]);
const abort = react.useCallback(() => {
if (!canAbort || !abortableMessageId) {
return;
}
client[core.kInternal].ai.abort(abortableMessageId);
}, [canAbort, abortableMessageId, client]);
react.useImperativeHandle(
forwardedRef,
() => formRef.current,
[]
);
return /* @__PURE__ */ jsxRuntime.jsx(
contexts.AiComposerEditorContext.Provider,
{
value: {
editor,
onEditorValueChange: handleEditorValueChange,
abortableMessageId,
setFocused
},
children: /* @__PURE__ */ jsxRuntime.jsx(
contexts.AiComposerContext.Provider,
{
value: {
isDisabled,
isEmpty: isEditorEmpty,
isFocused,
canSubmit,
canAbort,
submit,
abort,
clear,
focus,
blur,
select
},
children: /* @__PURE__ */ jsxRuntime.jsx(Component, { onSubmit: handleSubmit, ...props, ref: formRef })
}
)
}
);
}
);
function AiComposerEditorPlaceholder({
attributes,
children
}) {
const { opacity: _opacity, ...style } = attributes.style;
return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, style, "data-placeholder": "", children });
}
const AiComposerEditor = react.forwardRef(
({
defaultValue = "",
onKeyDown,
onFocus,
onBlur,
disabled,
autoFocus,
dir,
...props
}, forwardedRef) => {
const { editor, onEditorValueChange, setFocused } = contexts.useAiComposerEditorContext();
const {
submit,
isDisabled: isComposerDisabled,
isFocused,
focus,
blur,
select
} = contexts.useAiComposer();
const isDisabled = disabled || isComposerDisabled;
const handleKeyDown = react.useCallback(
(event) => {
onKeyDown?.(event);
if (event.isDefaultPrevented())
return;
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
submit();
} else if (event.key === "Enter" && event.shiftKey) {
event.preventDefault();
editor.insertBreak();
} else if (event.key === "Escape") {
blur();
}
},
[editor, onKeyDown, submit, blur]
);
const handleFocus = react.useCallback(
(event) => {
onFocus?.(event);
if (!event.isDefaultPrevented()) {
setFocused(true);
}
},
[onFocus, setFocused]
);
const handleBlur = react.useCallback(
(event) => {
onBlur?.(event);
if (!event.isDefaultPrevented()) {
setFocused(false);
}
},
[onBlur, setFocused]
);
react.useImperativeHandle(
forwardedRef,
() => slateReact.ReactEditor.toDOMNode(editor, editor),
[editor]
);
_private.useLayoutEffect(() => {
if (autoFocus) {
focus();
}
}, [autoFocus, editor, focus]);
_private.useLayoutEffect(() => {
if (isFocused && editor.selection === null) {
select();
}
}, [editor, select, isFocused]);
const initialValue = react.useMemo(() => {
return defaultValue.split("\n").map((text) => ({ type: "paragraph", children: [{ text }] }));
}, [defaultValue]);
return /* @__PURE__ */ jsxRuntime.jsx(
slateReact.Slate,
{
editor,
initialValue,
onValueChange: onEditorValueChange,
children: /* @__PURE__ */ jsxRuntime.jsx(
slateReact.Editable,
{
dir,
enterKeyHint: "send",
autoCapitalize: "sentences",
"aria-label": "Composer editor",
onKeyDown: handleKeyDown,
onFocus: handleFocus,
onBlur: handleBlur,
"data-focused": isFocused || void 0,
"data-disabled": isDisabled || void 0,
...props,
readOnly: isDisabled,
disabled: isDisabled,
renderPlaceholder: AiComposerEditorPlaceholder
}
)
}
);
}
);
const AiComposerSubmit = react.forwardRef(({ disabled, asChild, ...props }, forwardedRef) => {
const Component = asChild ? reactSlot.Slot : "button";
const { isDisabled: isComposerDisabled, canSubmit } = contexts.useAiComposer();
const isDisabled = isComposerDisabled || disabled || !canSubmit;
return /* @__PURE__ */ jsxRuntime.jsx(
Component,
{
type: "submit",
...props,
ref: forwardedRef,
disabled: isDisabled
}
);
});
const AiComposerAbort = react.forwardRef(({ disabled, onClick, asChild, ...props }, forwardedRef) => {
const Component = asChild ? reactSlot.Slot : "button";
const { isDisabled: isComposerDisabled, canAbort, abort } = contexts.useAiComposer();
const isDisabled = isComposerDisabled || disabled || !canAbort;
const handleClick = react.useCallback(
(event) => {
onClick?.(event);
if (event.isDefaultPrevented()) {
return;
}
abort();
},
[abort, onClick]
);
return /* @__PURE__ */ jsxRuntime.jsx(
Component,
{
type: "button",
...props,
ref: forwardedRef,
disabled: isDisabled,
onClick: handleClick
}
);
});
if (process.env.NODE_ENV !== "production") {
AiComposerEditor.displayName = AI_COMPOSER_EDITOR_NAME;
AiComposerForm.displayName = AI_COMPOSER_FORM_NAME;
AiComposerSubmit.displayName = AI_COMPOSER_SUBMIT_NAME;
AiComposerAbort.displayName = AI_COMPOSER_ABORT_NAME;
}
exports.Abort = AiComposerAbort;
exports.AiComposerAbort = AiComposerAbort;
exports.AiComposerForm = AiComposerForm;
exports.AiComposerSubmit = AiComposerSubmit;
exports.Editor = AiComposerEditor;
exports.Form = AiComposerForm;
exports.Submit = AiComposerSubmit;
//# sourceMappingURL=index.cjs.map