@assistant-ui/react
Version:
TypeScript/React library for AI Chat
116 lines • 5 kB
JavaScript
"use client";
import { jsx as _jsx } from "react/jsx-runtime";
import { composeEventHandlers } from "@radix-ui/primitive";
import { useComposedRefs } from "@radix-ui/react-compose-refs";
import { Slot } from "@radix-ui/react-slot";
import { forwardRef, useCallback, useEffect, useRef, } from "react";
import TextareaAutosize from "react-textarea-autosize";
import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom.js";
import { useAssistantState, useAssistantApi } from "../../context/index.js";
import { flushSync } from "@assistant-ui/tap";
/**
* A text input component for composing messages.
*
* This component provides a rich text input experience with automatic resizing,
* keyboard shortcuts, file paste support, and intelligent focus management.
* It integrates with the composer context to manage message state and submission.
*
* @example
* ```tsx
* <ComposerPrimitive.Input
* placeholder="Type your message..."
* submitOnEnter={true}
* addAttachmentOnPaste={true}
* />
* ```
*/
export const ComposerPrimitiveInput = forwardRef(({ autoFocus = false, asChild, disabled: disabledProp, onChange, onKeyDown, onPaste, submitOnEnter = true, cancelOnEscape = true, unstable_focusOnRunStart = true, unstable_focusOnScrollToBottom = true, unstable_focusOnThreadSwitched = true, addAttachmentOnPaste = true, ...rest }, forwardedRef) => {
const api = useAssistantApi();
const value = useAssistantState(({ composer }) => {
if (!composer.isEditing)
return "";
return composer.text;
});
const Component = asChild ? Slot : TextareaAutosize;
const isDisabled = useAssistantState(({ thread }) => thread.isDisabled) || disabledProp;
const textareaRef = useRef(null);
const ref = useComposedRefs(forwardedRef, textareaRef);
useEscapeKeydown((e) => {
if (!cancelOnEscape)
return;
// Only handle ESC if it originated from within this input
if (!textareaRef.current?.contains(e.target))
return;
const composer = api.composer();
if (composer.getState().canCancel) {
composer.cancel();
e.preventDefault();
}
});
const handleKeyPress = (e) => {
if (isDisabled || !submitOnEnter)
return;
// ignore IME composition events
if (e.nativeEvent.isComposing)
return;
if (e.key === "Enter" && e.shiftKey === false) {
const isRunning = api.thread().getState().isRunning;
if (!isRunning) {
e.preventDefault();
textareaRef.current?.closest("form")?.requestSubmit();
}
}
};
const handlePaste = async (e) => {
if (!addAttachmentOnPaste)
return;
const threadCapabilities = api.thread().getState().capabilities;
const files = Array.from(e.clipboardData?.files || []);
if (threadCapabilities.attachments && files.length > 0) {
try {
e.preventDefault();
await Promise.all(files.map((file) => api.composer().addAttachment(file)));
}
catch (error) {
console.error("Error adding attachment:", error);
}
}
};
const autoFocusEnabled = autoFocus && !isDisabled;
const focus = useCallback(() => {
const textarea = textareaRef.current;
if (!textarea || !autoFocusEnabled)
return;
textarea.focus({ preventScroll: true });
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
}, [autoFocusEnabled]);
useEffect(() => focus(), [focus]);
useOnScrollToBottom(() => {
if (api.composer().getState().type === "thread" &&
unstable_focusOnScrollToBottom) {
focus();
}
});
useEffect(() => {
if (api.composer().getState().type !== "thread" ||
!unstable_focusOnRunStart)
return undefined;
return api.on("thread.run-start", focus);
}, [unstable_focusOnRunStart, focus, api]);
useEffect(() => {
if (api.composer().getState().type !== "thread" ||
!unstable_focusOnThreadSwitched)
return undefined;
return api.on("thread-list-item.switched-to", focus);
}, [unstable_focusOnThreadSwitched, focus, api]);
return (_jsx(Component, { name: "input", value: value, ...rest, ref: ref, disabled: isDisabled, onChange: composeEventHandlers(onChange, (e) => {
if (!api.composer().getState().isEditing)
return;
flushSync(() => {
api.composer().setText(e.target.value);
});
}), onKeyDown: composeEventHandlers(onKeyDown, handleKeyPress), onPaste: composeEventHandlers(onPaste, handlePaste) }));
});
ComposerPrimitiveInput.displayName = "ComposerPrimitive.Input";
//# sourceMappingURL=ComposerInput.js.map