import { useMemo, useRef, useState, useEffect } from "react"; import { toast } from "react-toastify"; import SendOutlinedIcon from "@mui/icons-material/SendOutlined"; import StopCircleOutlinedIcon from "@mui/icons-material/StopCircleOutlined"; import { CircularProgress, IconButton, TextField } from "@mui/material"; import WarningIcon from "@mui/icons-material/Warning"; import { useAppContext } from "../../contexts/AppContext"; import { useChatContext } from "../../contexts/ChatContext"; type ChatInputProps = {}; const detectTextDirection = (text: string) => { const firstStrongChar = text.match(/[A-Za-z\u0590-\u05FF]/); if (firstStrongChar) { return /[\u0590-\u05FF]/.test(firstStrongChar[0]) ? "rtl" : "ltr"; } return "ltr"; }; const WarningMessage = ({ show, inputMaxLength }: { show: boolean, inputMaxLength: number }) => { const [isVisible, setIsVisible] = useState(false); const [position, setPosition] = useState('translate-y-0'); useEffect(() => { if (show) { setIsVisible(true); setPosition('-top-7'); const timer = setTimeout(() => { setPosition('top-0'); setTimeout(() => { setIsVisible(false); }, 300); }, 2000); return () => clearTimeout(timer); } }, [show]); if (!isVisible) return null; return (
The message should be max {inputMaxLength} characters
); }; export default function ChatInput({ }: ChatInputProps) { const { configs } = useAppContext(); const { selectedChat, canReplyToBot, sendMessage, abortMessage } = useChatContext(); const inputMaxLength = configs?.input_max_length || 1000; const inputRef = useRef(null); const warningTimeoutRef = useRef(null); const [message, setMessage] = useState(""); const [loading, setLoading] = useState(false); const [showWarning, setShowWarning] = useState(false); const textDirection = useMemo(() => detectTextDirection(message), [message]); const handleSubmit = async (e?: React.FormEvent) => { e?.preventDefault(); if (!selectedChat) { toast.error("No chat selected"); return; } setLoading(true); sendMessage(message, selectedChat.id, { rtl: textDirection === "rtl", onSuccess: () => { setLoading(false); if (inputRef.current) { setTimeout(() => inputRef.current?.focus(), 0); } }, onError: (err) => { console.error(err); toast.error("Failed to send message"); setLoading(false); if (inputRef.current) { setTimeout(() => inputRef.current?.focus(), 0); } }, }); // immediately clear setMessage(""); }; const handleAbort = () => { abortMessage(); setLoading(false); }; const [currentIndex, setCurrentIndex] = useState(0); const messageCount = configs?.loading_messages?.length || 1; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); useEffect(() => { const loopMessages = async () => { for (let i = 0; i < messageCount; i++) { setCurrentIndex(i); await delay(3000); } }; if (loading) { loopMessages(); } }, [loading]); useEffect(() => { return () => { if (warningTimeoutRef.current) { clearTimeout(warningTimeoutRef.current); } }; }, []); const handleMessageChange = (e: React.ChangeEvent) => { const newValue = e.target.value; if (newValue.length > inputMaxLength) { if (warningTimeoutRef.current) { clearTimeout(warningTimeoutRef.current); warningTimeoutRef.current = null; } setShowWarning(false); setTimeout(() => setShowWarning(true), 0); setMessage(newValue.slice(0, inputMaxLength)); } else { setMessage(newValue); } }; if (!selectedChat) return null; return (
{loading && (
{configs?.loading_messages?.[currentIndex] || "Compiling analysis and sources"}
)} {selectedChat && !canReplyToBot && (
Follow up questions are not supported
)}
{ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (message.trim()) { handleSubmit(); } } }} disabled={loading} />
{loading ? ( ) : ( )}
); }