diff --git a/app/globals.css b/app/globals.css index 70285a3526..24a4c1efe0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -142,6 +142,22 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + + --animate-floating-action: floatingAction 2s ease-in-out infinite; + + @keyframes floatingAction { + 0% { + transform: translateY(calc(-4px*1)); + } + + 50% { + transform: translateY(calc(4px*1)); + } + + to { + transform: translateY(calc(-4px*1)); + } + } } /* diff --git a/components/chat.tsx b/components/chat.tsx index 4380db16a5..79d8dac3ff 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -32,6 +32,7 @@ import { MultimodalInput } from "./multimodal-input"; import { getChatHistoryPaginationKey } from "./sidebar-history"; import { toast } from "./toast"; import type { VisibilityType } from "./visibility-selector"; +import { ScrollToBottomProvider } from "@/hooks/use-scroll-to-bottom"; export function Chat({ id, @@ -156,6 +157,7 @@ export function Chat({ return ( <> +
- + ; @@ -38,60 +39,111 @@ export type PromptInputTextareaProps = ComponentProps & { 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 = (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(null); + const textareaRef = + (forwardedRef as React.RefObject) || internalRef; + const prevLineCountRef = useRef(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) => { + onChange?.(e); + }; - // Submit on Enter (without Shift) - e.preventDefault(); - const form = e.currentTarget.form; - if (form) { - form.requestSubmit(); + const handleKeyDown: KeyboardEventHandler = (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 ( -