@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.
357 lines (354 loc) • 11 kB
JavaScript
import { jsx } from 'react/jsx-runtime';
import { Signal, kInternal } from '@liveblocks/core';
import { useClient } from '@liveblocks/react';
import { useSignal, useLayoutEffect } from '@liveblocks/react/_private';
import { Slot } from '@radix-ui/react-slot';
import { forwardRef, useRef, useState, useCallback, useImperativeHandle, useMemo } from 'react';
import { createEditor, Transforms, Editor } from 'slate';
import { withHistory } from 'slate-history';
import { withReact, ReactEditor, Slate, Editable } from 'slate-react';
import { requestSubmit } from '../../utils/request-submit.js';
import { useInitial } from '../../utils/use-initial.js';
import { withNormalize } from '../slate/plugins/normalize.js';
import { isEmpty } from '../slate/utils/is-empty.js';
import { AiComposerEditorContext, AiComposerContext, useAiComposerEditorContext, useAiComposer } from './contexts.js';
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 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 = forwardRef(
({
onComposerSubmit,
onSubmit,
disabled,
chatId,
branchId,
asChild,
...props
}, forwardedRef) => {
const Component = asChild ? Slot : "form";
const client = useClient();
const formRef = useRef(null);
const editor = useInitial(
() => withNormalize(withHistory(withReact(createEditor())))
);
const [isEditorEmpty, setEditorEmpty] = useState(true);
const [isSubmitting, setSubmitting] = useState(false);
const [isFocused, setFocused] = useState(false);
const messages\u03A3 = chatId ? client[kInternal].ai.signals.getChatMessagesForBranch\u03A3(chatId, branchId) : emptyMessages\u03A3;
const lastMessageId = useSignal(messages\u03A3, getLastMessageId);
const abortableMessageId = useSignal(messages\u03A3, getAbortableMessageId);
const isAvailable = useSignal(
// Subscribe to connection status signal
client[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 = useCallback(() => {
Transforms.delete(editor, {
at: {
anchor: Editor.start(editor, []),
focus: Editor.end(editor, [])
}
});
}, [editor]);
const select = useCallback(() => {
Transforms.select(editor, Editor.end(editor, []));
}, [editor]);
const focus = useCallback(
(resetSelection = true) => {
try {
if (!ReactEditor.isFocused(editor)) {
Transforms.select(
editor,
resetSelection || !editor.selection ? Editor.end(editor, []) : editor.selection
);
ReactEditor.focus(editor);
}
} catch {
}
},
[editor]
);
const blur = useCallback(() => {
try {
ReactEditor.blur(editor);
} catch {
}
}, [editor]);
const onSubmitEnd = useCallback(() => {
clear();
setSubmitting(false);
}, [clear]);
const handleSubmit = useCallback(
(event) => {
if (disabled) {
return;
}
const isEditorEmpty2 = 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]
);
useLayoutEffect(() => {
setEditorEmpty(isEmpty(editor, editor.children));
}, [editor]);
const handleEditorValueChange = useCallback(() => {
setEditorEmpty(isEmpty(editor, editor.children));
}, [editor]);
const submit = useCallback(() => {
if (!canSubmit) {
return;
}
requestAnimationFrame(() => {
if (formRef.current) {
requestSubmit(formRef.current);
}
});
}, [canSubmit]);
const abort = useCallback(() => {
if (!canAbort || !abortableMessageId) {
return;
}
client[kInternal].ai.abort(abortableMessageId);
}, [canAbort, abortableMessageId, client]);
useImperativeHandle(
forwardedRef,
() => formRef.current,
[]
);
return /* @__PURE__ */ jsx(
AiComposerEditorContext.Provider,
{
value: {
editor,
onEditorValueChange: handleEditorValueChange,
abortableMessageId,
setFocused
},
children: /* @__PURE__ */ jsx(
AiComposerContext.Provider,
{
value: {
isDisabled,
isEmpty: isEditorEmpty,
isFocused,
canSubmit,
canAbort,
submit,
abort,
clear,
focus,
blur,
select
},
children: /* @__PURE__ */ jsx(Component, { onSubmit: handleSubmit, ...props, ref: formRef })
}
)
}
);
}
);
function AiComposerEditorPlaceholder({
attributes,
children
}) {
const { opacity: _opacity, ...style } = attributes.style;
return /* @__PURE__ */ jsx("span", { ...attributes, style, "data-placeholder": "", children });
}
const AiComposerEditor = forwardRef(
({
defaultValue = "",
onKeyDown,
onFocus,
onBlur,
disabled,
autoFocus,
dir,
...props
}, forwardedRef) => {
const { editor, onEditorValueChange, setFocused } = useAiComposerEditorContext();
const {
submit,
isDisabled: isComposerDisabled,
isFocused,
focus,
blur,
select
} = useAiComposer();
const isDisabled = disabled || isComposerDisabled;
const handleKeyDown = 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 = useCallback(
(event) => {
onFocus?.(event);
if (!event.isDefaultPrevented()) {
setFocused(true);
}
},
[onFocus, setFocused]
);
const handleBlur = useCallback(
(event) => {
onBlur?.(event);
if (!event.isDefaultPrevented()) {
setFocused(false);
}
},
[onBlur, setFocused]
);
useImperativeHandle(
forwardedRef,
() => ReactEditor.toDOMNode(editor, editor),
[editor]
);
useLayoutEffect(() => {
if (autoFocus) {
focus();
}
}, [autoFocus, editor, focus]);
useLayoutEffect(() => {
if (isFocused && editor.selection === null) {
select();
}
}, [editor, select, isFocused]);
const initialValue = useMemo(() => {
return defaultValue.split("\n").map((text) => ({ type: "paragraph", children: [{ text }] }));
}, [defaultValue]);
return /* @__PURE__ */ jsx(
Slate,
{
editor,
initialValue,
onValueChange: onEditorValueChange,
children: /* @__PURE__ */ jsx(
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 = forwardRef(({ disabled, asChild, ...props }, forwardedRef) => {
const Component = asChild ? Slot : "button";
const { isDisabled: isComposerDisabled, canSubmit } = useAiComposer();
const isDisabled = isComposerDisabled || disabled || !canSubmit;
return /* @__PURE__ */ jsx(
Component,
{
type: "submit",
...props,
ref: forwardedRef,
disabled: isDisabled
}
);
});
const AiComposerAbort = forwardRef(({ disabled, onClick, asChild, ...props }, forwardedRef) => {
const Component = asChild ? Slot : "button";
const { isDisabled: isComposerDisabled, canAbort, abort } = useAiComposer();
const isDisabled = isComposerDisabled || disabled || !canAbort;
const handleClick = useCallback(
(event) => {
onClick?.(event);
if (event.isDefaultPrevented()) {
return;
}
abort();
},
[abort, onClick]
);
return /* @__PURE__ */ 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;
}
export { AiComposerAbort as Abort, AiComposerAbort, AiComposerForm, AiComposerSubmit, AiComposerEditor as Editor, AiComposerForm as Form, AiComposerSubmit as Submit };
//# sourceMappingURL=index.js.map