Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 102 additions & 50 deletions components/elements/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
HTMLAttributes,
KeyboardEventHandler,
} from "react";
import { Children } from "react";
import { Children, useCallback, useEffect, useRef } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
Expand All @@ -18,6 +18,7 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import React from "react";

export type PromptInputProps = HTMLAttributes<HTMLFormElement>;

Expand All @@ -38,60 +39,111 @@ export type PromptInputTextareaProps = ComponentProps<typeof Textarea> & {
resizeOnNewLinesOnly?: boolean;
};

export const PromptInputTextarea = ({
onChange,
className,
placeholder = "What would you like to know?",
minHeight = 48,
maxHeight = 164,
disableAutoResize = false,
resizeOnNewLinesOnly = false,
...props
}: PromptInputTextareaProps) => {
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
if (e.key === "Enter") {
// Don't submit if IME composition is in progress
if (e.nativeEvent.isComposing) {
return;
}

if (e.shiftKey) {
// Allow newline
return;
export const PromptInputTextarea = React.forwardRef<
HTMLTextAreaElement,
PromptInputTextareaProps
>(
(
{
onChange,
className,
placeholder = "Hi, there! How can I help you today?",
minHeight = 48,
maxHeight = 164,
disableAutoResize = false,
resizeOnNewLinesOnly = false,
...props
},
forwardedRef
) => {
const internalRef = useRef<HTMLTextAreaElement>(null);
const textareaRef =
(forwardedRef as React.RefObject<HTMLTextAreaElement>) || internalRef;
const prevLineCountRef = useRef<number>(0);

const adjustHeight = useCallback(() => {
const textarea = textareaRef.current;
if (!textarea || disableAutoResize) return;

// Reset height to auto to get the correct scrollHeight
textarea.style.height = "auto";
const scrollHeight = textarea.scrollHeight;

const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
textarea.style.height = `${newHeight}px`;
}, [disableAutoResize, maxHeight, minHeight, textareaRef]);

useEffect(() => {
adjustHeight();
}, [disableAutoResize, minHeight, maxHeight]);

useEffect(() => {
if (disableAutoResize) return;

const currentValue = props.value?.toString() || "";

if (resizeOnNewLinesOnly) {
const currentLineCount = (currentValue.match(/\n/g) || []).length;
if (currentLineCount !== prevLineCountRef.current) {
adjustHeight();
prevLineCountRef.current = currentLineCount;
}
} else {
adjustHeight();
}
}, [props.value, disableAutoResize, resizeOnNewLinesOnly]);

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange?.(e);
};

// Submit on Enter (without Shift)
e.preventDefault();
const form = e.currentTarget.form;
if (form) {
form.requestSubmit();
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
if (e.key === "Enter") {
// Don't submit if IME composition is in progress
if (e.nativeEvent.isComposing) {
return;
}
if (e.shiftKey) {
// Allow newline
return;
}
e.preventDefault();
const form = e.currentTarget.form;
if (form) {
form.requestSubmit();
}
}
}
};
};

return (
<Textarea
className={cn(
"w-full resize-none rounded-none border-none p-3 shadow-none outline-hidden ring-0",
disableAutoResize
? "field-sizing-fixed"
: resizeOnNewLinesOnly
? "field-sizing-fixed"
: "field-sizing-content max-h-[6lh]",
"bg-transparent dark:bg-transparent",
"focus-visible:ring-0",
className
)}
name="message"
onChange={(e) => {
onChange?.(e);
}}
onKeyDown={handleKeyDown}
placeholder={placeholder}
{...props}
/>
);
};
return (
<Textarea
ref={textareaRef}
className={cn(
"w-full resize-none rounded-none border-none p-3 shadow-none outline-hidden ring-0",
"bg-transparent dark:bg-transparent",
"focus-visible:ring-0",
disableAutoResize && "field-sizing-content max-h-[6lh]",
className
)}
style={
disableAutoResize
? undefined
: {
minHeight: `${minHeight}px`,
maxHeight: `${maxHeight}px`,
}
}
name="message"
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
{...props}
/>
);
}
);
PromptInputTextarea.displayName = "PromptInputTextarea";

export type PromptInputToolbarProps = HTMLAttributes<HTMLDivElement>;

Expand Down
23 changes: 1 addition & 22 deletions components/multimodal-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,24 +83,6 @@ function PureMultimodalInput({
const textareaRef = useRef<HTMLTextAreaElement>(null);
const { width } = useWindowSize();

const adjustHeight = useCallback(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "44px";
}
}, []);

useEffect(() => {
if (textareaRef.current) {
adjustHeight();
}
}, [adjustHeight]);

const resetHeight = useCallback(() => {
if (textareaRef.current) {
textareaRef.current.style.height = "44px";
}
}, []);

const [localStorageInput, setLocalStorageInput] = useLocalStorage(
"input",
""
Expand All @@ -112,11 +94,10 @@ function PureMultimodalInput({
// Prefer DOM value over localStorage to handle hydration
const finalValue = domValue || localStorageInput || "";
setInput(finalValue);
adjustHeight();
}
// Only run once after hydration
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [adjustHeight, localStorageInput, setInput]);
}, [localStorageInput, setInput]);

useEffect(() => {
setLocalStorageInput(input);
Expand Down Expand Up @@ -150,7 +131,6 @@ function PureMultimodalInput({

setAttachments([]);
setLocalStorageInput("");
resetHeight();
setInput("");

if (width && width > 768) {
Expand All @@ -165,7 +145,6 @@ function PureMultimodalInput({
setLocalStorageInput,
width,
chatId,
resetHeight,
]);

const uploadFile = useCallback(async (file: File) => {
Expand Down