@assistant-ui/react
Version:
TypeScript/React library for AI Chat
126 lines (125 loc) • 4.38 kB
JavaScript
"use client";
// src/primitives/composer/ComposerInput.tsx
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";
import { jsx } from "react/jsx-runtime";
var 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;
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;
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 void 0;
return api.on("thread.run-start", focus);
}, [unstable_focusOnRunStart, focus, api]);
useEffect(() => {
if (api.composer().getState().type !== "thread" || !unstable_focusOnThreadSwitched)
return void 0;
return api.on("thread-list-item.switched-to", focus);
}, [unstable_focusOnThreadSwitched, focus, api]);
return /* @__PURE__ */ jsx(
Component,
{
name: "input",
value,
...rest,
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";
export {
ComposerPrimitiveInput
};
//# sourceMappingURL=ComposerInput.js.map