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.

365 lines (361 loc) 11.4 kB
'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