import { forwardRef, useImperativeHandle, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useDebouncedCallback } from "use-debounce"; import useUploadImage from "@/app/hooks/useUploadImage"; import Locale from "@/app/locales"; import useSubmitHandler from "@/app/hooks/useSubmitHandler"; import { CHAT_PAGE_SIZE, LAST_INPUT_KEY, Path } from "@/app/constant"; import { ChatCommandPrefix, useChatCommand } from "@/app/command"; import { useChatStore } from "@/app/store/chat"; import { usePromptStore } from "@/app/store/prompt"; import { useAppConfig } from "@/app/store/config"; import usePaste from "@/app/hooks/usePaste"; import { ChatActions } from "./ChatActions"; import PromptHints, { RenderPompt } from "./PromptHint"; // import CEIcon from "@/app/icons/command&enterIcon.svg"; // import EnterIcon from "@/app/icons/enterIcon.svg"; import SendIcon from "@/app/icons/sendIcon.svg"; import Btn from "@/app/components/Btn"; import Thumbnail from "@/app/components/ThumbnailImg"; export interface ChatInputPanelProps { inputRef: React.RefObject; isMobileScreen: boolean; renderMessages: any[]; attachImages: string[]; userInput: string; hitBottom: boolean; inputRows: number; setAttachImages: (imgs: string[]) => void; setUserInput: (v: string) => void; setIsLoading: (value: boolean) => void; showChatSetting: (value: boolean) => void; _setMsgRenderIndex: (value: number) => void; showModelSelector: (value: boolean) => void; setAutoScroll: (value: boolean) => void; scrollDomToBottom: () => void; } export interface ChatInputPanelInstance { setUploading: (v: boolean) => void; doSubmit: (userInput: string) => void; setMsgRenderIndex: (v: number) => void; } // only search prompts when user input is short const SEARCH_TEXT_LIMIT = 30; export default forwardRef( function ChatInputPanel(props, ref) { const { attachImages, inputRef, setAttachImages, userInput, isMobileScreen, setUserInput, setIsLoading, showChatSetting, renderMessages, _setMsgRenderIndex, hitBottom, inputRows, showModelSelector, setAutoScroll, scrollDomToBottom, } = props; const [uploading, setUploading] = useState(false); const [promptHints, setPromptHints] = useState([]); const chatStore = useChatStore(); const navigate = useNavigate(); const config = useAppConfig(); const { uploadImage } = useUploadImage(attachImages, { emitImages: setAttachImages, setUploading, }); const { submitKey, shouldSubmit } = useSubmitHandler(); const autoFocus = !isMobileScreen; // wont auto focus on mobile screen // chat commands shortcuts const chatCommands = useChatCommand({ new: () => chatStore.newSession(), newm: () => navigate(Path.NewChat), prev: () => chatStore.nextSession(-1), next: () => chatStore.nextSession(1), clear: () => chatStore.updateCurrentSession( (session) => (session.clearContextIndex = session.messages.length), ), del: () => chatStore.deleteSession(chatStore.currentSessionIndex), }); // prompt hints const promptStore = usePromptStore(); const onSearch = useDebouncedCallback( (text: string) => { const matchedPrompts = promptStore.search(text); setPromptHints(matchedPrompts); }, 100, { leading: true, trailing: true }, ); // check if should send message const onInputKeyDown = (e: React.KeyboardEvent) => { // if ArrowUp and no userInput, fill with last input if ( e.key === "ArrowUp" && userInput.length <= 0 && !(e.metaKey || e.altKey || e.ctrlKey) ) { setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? ""); e.preventDefault(); return; } if (shouldSubmit(e) && promptHints.length === 0) { doSubmit(userInput); e.preventDefault(); } }; const onPromptSelect = (prompt: RenderPompt) => { setTimeout(() => { setPromptHints([]); const matchedChatCommand = chatCommands.match(prompt.content); if (matchedChatCommand.matched) { // if user is selecting a chat command, just trigger it matchedChatCommand.invoke(); setUserInput(""); } else { // or fill the prompt setUserInput(prompt.content); } inputRef.current?.focus(); }, 30); }; const doSubmit = (userInput: string) => { if (userInput.trim() === "") return; const matchCommand = chatCommands.match(userInput); if (matchCommand.matched) { setUserInput(""); setPromptHints([]); matchCommand.invoke(); return; } setIsLoading(true); chatStore .onUserInput(userInput, attachImages) .then(() => setIsLoading(false)); setAttachImages([]); localStorage.setItem(LAST_INPUT_KEY, userInput); setUserInput(""); setPromptHints([]); if (!isMobileScreen) inputRef.current?.focus(); setAutoScroll(true); }; useImperativeHandle(ref, () => ({ setUploading, doSubmit, setMsgRenderIndex, })); function scrollToBottom() { setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE); scrollDomToBottom(); } const onInput = (text: string) => { setUserInput(text); const n = text.trim().length; // clear search results if (n === 0) { setPromptHints([]); } else if (text.startsWith(ChatCommandPrefix)) { setPromptHints(chatCommands.search(text)); } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { // check if need to trigger auto completion if (text.startsWith("/")) { let searchText = text.slice(1); onSearch(searchText); } } }; function setMsgRenderIndex(newIndex: number) { newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex); newIndex = Math.max(0, newIndex); _setMsgRenderIndex(newIndex); } const { handlePaste } = usePaste(attachImages, { emitImages: setAttachImages, setUploading, }); let containerClassName = "border-t border-chat-input-top"; let inputClassName = " flex flex-col px-5 pb-5"; let actionsClassName = "py-2.5"; let labelClassName = "rounded-md p-4 gap-4"; let textarea = "min-h-chat-input"; if (isMobileScreen) { containerClassName = "rounded-tl-md rounded-tr-md"; inputClassName = "flex flex-row-reverse items-center gap-2 p-3"; actionsClassName = ""; labelClassName = " rounded-chat-input p-3 gap-3 flex-1"; textarea = "h-chat-input-mobile"; } return (
showChatSetting(true)} scrollToBottom={scrollToBottom} hitBottom={hitBottom} uploading={uploading} showPromptHints={() => { // Click again to close if (promptHints.length > 0) { setPromptHints([]); return; } inputRef.current?.focus(); setUserInput("/"); onSearch(""); }} className={actionsClassName} isMobileScreen={isMobileScreen} />