import React, { useEffect, useContext, useRef, useState } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil'; import SubmitButton from './SubmitButton'; import OptionsBar from './OptionsBar'; import { EndpointMenu } from './EndpointMenu'; import Footer from './Footer'; import { useMessageHandler, ThemeContext } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; export default function TextChat({ isSearchView = false }) { const inputRef = useRef(null); const isComposing = useRef(false); const [text, setText] = useRecoilState(store.text); const { theme } = useContext(ThemeContext); const conversation = useRecoilValue(store.conversation); const latestMessage = useRecoilValue(store.latestMessage); const endpointsConfig = useRecoilValue(store.endpointsConfig); const isSubmitting = useRecoilValue(store.isSubmitting); const setShowBingToneSetting = useSetRecoilState(store.showBingToneSetting); // TODO: do we need this? const disabled = false; const { ask, stopGenerating } = useMessageHandler(); const isNotAppendable = latestMessage?.unfinished & !isSubmitting || latestMessage?.error; const { conversationId, jailbreak } = conversation || {}; const [isSpeechSupported, setIsSpeechSupported] = useState(false); const [isListening, setIsListening] = useState(false); useEffect(() => { if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) { setIsSpeechSupported(true); } else { console.log("Browser does not support SpeechRecognition"); setIsSpeechSupported(false); return; } if (!('SpeechRecognition' in window) && !('webkitSpeechRecognition' in window)) { console.log("Browser does not support SpeechRecognition"); return; } const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SpeechRecognition(); recognition.onstart = () => { console.log("Speech recognition started"); }; recognition.interimResults = true; recognition.onresult = (event) => { let transcript = ''; for (let i = 0; i < event.results.length; i++) { const result = event.results[i]; transcript += result[0].transcript; if (result.isFinal) { setText(transcript); ask({ text: transcript }); } } // Set the text with both interim and final results setText(transcript); }; recognition.onend = () => { setIsListening(false); setText(''); }; if (isListening) { recognition.start(); } else { recognition.stop(); } return () => { recognition.stop(); }; }, [isListening]); const toggleListening = (e) => { e.preventDefault(); setIsListening((prevState) => !prevState); }; // auto focus to input, when enter a conversation. useEffect(() => { if (!conversationId) { return; } // Prevents Settings from not showing on new conversation, also prevents showing toneStyle change without jailbreak if (conversationId === 'new' || !jailbreak) { setShowBingToneSetting(false); } if (conversationId !== 'search') { inputRef.current?.focus(); } // setShowBingToneSetting is a recoil setter, so it doesn't need to be in the dependency array // eslint-disable-next-line react-hooks/exhaustive-deps }, [conversationId, jailbreak]); useEffect(() => { const timeoutId = setTimeout(() => { inputRef.current?.focus(); }, 100); return () => clearTimeout(timeoutId); }, [isSubmitting]); const submitMessage = () => { ask({ text }); setText(''); }; const handleStopGenerating = (e) => { e.preventDefault(); stopGenerating(); }; const handleKeyDown = (e) => { if (e.key === 'Enter' && isSubmitting) { return; } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); } if (e.key === 'Enter' && !e.shiftKey && !isComposing?.current) { submitMessage(); } }; const handleKeyUp = (e) => { if (e.keyCode === 8 && e.target.value.trim() === '') { setText(e.target.value); } if (e.key === 'Enter' && e.shiftKey) { return console.log('Enter + Shift'); } if (isSubmitting) { return; } }; const handleCompositionStart = () => { isComposing.current = true; }; const handleCompositionEnd = () => { isComposing.current = false; }; const changeHandler = (e) => { const { value } = e.target; setText(value); }; const getPlaceholderText = () => { if (isSearchView) { return 'Click a message title to open its conversation.'; } if (disabled) { return 'Choose another model or customize GPT again'; } if (isNotAppendable) { return 'Edit your message or Regenerate.'; } return ''; }; if (isSearchView) { return <>; } let isDark = theme === 'dark'; if (theme === 'system') { isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; } return ( <>
{isSpeechSupported && ( )}
); }