import { Paperclip } from 'lucide-react' import { forwardRef, useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type KeyboardEvent, } from 'react' import styled from 'styled-components' import Block from '../../Primitives/Block/Block' import PrimitiveTextarea from '../../Primitives/PrimitiveTextArea' import PrimitiveButton from '../../Primitives/PrimitiveButton/PrimitiveButton' import ChatInputV2AttachmentRow from './ChatInputV2AttachmentRow' import { assignForwardedRef, removePxFromValue, truncatePlaceholder, } from './utils' import { useComponentToken } from '../../../context/useComponentToken' import { ChatInputV2MobileTokensType } from './ChatInputV2Mobile.tokens' import { InputStateV2 } from '../inputV2.types' import { MobileChatInputV2Props } from './ChatInputV2.types' const HiddenScrollbarTextarea = styled(PrimitiveTextarea)` /* Hide scrollbar for Firefox */ scrollbar-width: none; /* Hide scrollbar for Chrome, Safari, and Edge */ &::-webkit-scrollbar { display: none; } /* Hide scrollbar for IE and Edge */ -ms-overflow-style: none; ` const MobileChatInputV2 = forwardRef( function MobileChatInputV2( { webTokens, value, onChange, topContent, secondaryAction, placeholder, attachedFiles, handleAttachClick, onFileRemove, onFileClick = () => {}, disabled = false, onSecondaryActionClick, id, onEnter = () => {}, onKeyDown: onKeyDownFromProps, style: styleFromProps, ...textareaRest }, ref ) { const mobileTokens = useComponentToken( 'CHAT_INPUTV2_MOBILE' ) as ChatInputV2MobileTokensType const containerRef = useRef(null) const setContainerNode = useCallback( (node: HTMLDivElement | null) => { containerRef.current = node assignForwardedRef(node, ref) }, [ref] ) const textareaRef = useRef(null) const mergedTextareaStyle = useMemo((): CSSProperties => { const base: CSSProperties = { lineHeight: mobileTokens.inputContainer.lineHeight, } if ( styleFromProps && typeof styleFromProps === 'object' && !Array.isArray(styleFromProps) ) { return { ...styleFromProps, ...base } } return base }, [styleFromProps, mobileTokens.inputContainer.lineHeight]) const handleTextareaKeyDown = useCallback( (e: KeyboardEvent) => { onKeyDownFromProps?.(e) if (e.defaultPrevented) return if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() onEnter() } }, [onKeyDownFromProps, onEnter] ) const [truncatedPlaceholder, setTruncatedPlaceholder] = useState< string | undefined >(placeholder) const handleTextareaChange = (e: ChangeEvent) => { onChange?.(e.target.value) } // Update truncated placeholder when textarea resizes useEffect(() => { const el = textareaRef.current if (!el) return const updatePlaceholder = () => { setTruncatedPlaceholder(truncatePlaceholder(el, placeholder)) } updatePlaceholder() const resizeObserver = new ResizeObserver(updatePlaceholder) resizeObserver.observe(el) return () => resizeObserver.disconnect() }, [placeholder]) useEffect(() => { const el = textareaRef.current if (!el) return el.style.height = 'auto' el.style.height = `${Math.min(el.scrollHeight, removePxFromValue(mobileTokens.inputContainer.maxHeight ?? 0))}px` if ( el.scrollHeight > removePxFromValue(mobileTokens.inputContainer.minHeight ?? 0) ) { el.style.borderRadius = mobileTokens.inputContainer .borderRadius[InputStateV2.FOCUS] as string } else { el.style.borderRadius = mobileTokens.inputContainer .borderRadius[InputStateV2.DEFAULT] as string } }, [value, mobileTokens.inputContainer]) const attachmentButtonDimensions = useMemo(() => { const buttonWidth = `${mobileTokens.attachmentButtonDimensions.width}` const gap = mobileTokens.gap const total = `calc(${buttonWidth} + ${gap})` return { buttonWidth, gap, total, } }, [mobileTokens.attachmentButtonDimensions.width, mobileTokens.gap]) /** Reserve space for the absolutely positioned secondary action (right + width from tokens). */ const textareaPaddingRight = useMemo(() => { if (!secondaryAction) return 0 const { right, width } = mobileTokens.secondaryAction const rightCss = typeof right === 'number' ? `${right}px` : String(right) const widthCss = typeof width === 'number' ? `${width}px` : String(width) return `calc(${rightCss} + ${widthCss})` }, [secondaryAction, mobileTokens.secondaryAction]) return ( {attachedFiles && attachedFiles.length > 0 && ( {})} onFileClick={onFileClick} outerContainerRef={containerRef} gap={`${mobileTokens.gap}`} tokens={webTokens} /> )} {topContent} {secondaryAction && ( {secondaryAction} )} ) } ) MobileChatInputV2.displayName = 'MobileChatInputV2' export default MobileChatInputV2