import { Button, Form, FormTextArea } from '@servicetitan/design-system'; import { provide, useDependencies } from '@servicetitan/react-ioc'; import { CHAT_UI_STORE_TOKEN, useCustomizationChat } from '@servicetitan/titan-chat-ui-common'; import classNames from 'classnames'; import { observer } from 'mobx-react'; import { ChangeEvent, FC, FormEvent, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { ChatInputStore } from '../../stores/chat-input.store'; import * as Styles from './chat-input.module.less'; const TIMEOUT_COOLDOWN_MS = 3000; export const ChatInput: FC<{ className?: string }> = provide({ singletons: [ChatInputStore], })( observer(({ className }) => { const customization = useCustomizationChat(); const typingTimeoutRef = useRef | undefined>(undefined); const [isSending, setIsSending] = useState(false); const textareaRef = useRef(null); const [chatUiStore, supportChatInputStore] = useDependencies( CHAT_UI_STORE_TOKEN, ChatInputStore ); const additionalButtons = customization.input?.additionalButtons; const leftButtons = useMemo( () => additionalButtons?.filter(btn => btn.position === 'left') ?? [], [additionalButtons] ); const rightButtons = useMemo( () => additionalButtons?.filter(btn => btn.position === 'right') ?? [], [additionalButtons] ); const handleSendMessage = useCallback( async (event?: FormEvent) => { event?.preventDefault(); const validateResult = await supportChatInputStore.formState.validate(); if (validateResult.hasError) { return; } const text = supportChatInputStore.formState.$.message.value; if (!text.trim() && !chatUiStore.file) { return; } supportChatInputStore.formState.$.message.onChange(''); setIsSending(true); try { await chatUiStore.sendMessageText(text); } finally { setIsSending(false); } setTimeout(() => { textareaRef.current?.focus(); }, 0); }, [chatUiStore, supportChatInputStore] ); const clearTimer = useCallback(() => { clearTimeout(typingTimeoutRef.current ?? 0); typingTimeoutRef.current = undefined; }, []); // Send typing status = false const sendTypingStatusStop = useCallback(() => { const isTyping = Boolean(typingTimeoutRef.current); if (isTyping) { clearTimer(); chatUiStore.chasitorTyping(false); } }, [chatUiStore, clearTimer]); // Send typing status = true and start the typing countdown. After the countdown, send typing status = false const sendTypingStatusStart = useCallback(() => { if (!typingTimeoutRef.current) { chatUiStore.chasitorTyping(true); } clearTimeout(typingTimeoutRef.current); typingTimeoutRef.current = setTimeout(sendTypingStatusStop, TIMEOUT_COOLDOWN_MS); }, [sendTypingStatusStop, chatUiStore]); const setUserIsTyping = useCallback( (typing: boolean) => { if (typing) { sendTypingStatusStart(); } else { sendTypingStatusStop(); } }, [sendTypingStatusStart, sendTypingStatusStop] ); const handleTextKeyPress = useCallback( (e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); clearTimer(); handleSendMessage().then(() => {}); return; } setUserIsTyping(true); }, [setUserIsTyping, handleSendMessage, clearTimer] ); useEffect(() => { if (textareaRef.current) { textareaRef.current.focus(); } }, []); return (
{leftButtons.map((button, index) => ( // eslint-disable-next-line react/no-array-index-key ))} ) => { supportChatInputStore.formState.$.message.onChange( e.currentTarget.value ); }} onFocus={() => { setTimeout(() => { chatUiStore.triggerScroll(); }, 0); }} onBlur={() => { supportChatInputStore.formState.$.message.enableAutoValidationAndValidate(); }} className={classNames('flex-grow-1', Styles.formTextarea)} disabled={!chatUiStore.isStarted || isSending} data-cy="titan-chat-input" /> {rightButtons.map((button, index) => ( // eslint-disable-next-line react/no-array-index-key ))}
); }) );