'use client' import TextareaAutosize from 'react-textarea-autosize' import { cn } from '@/lib/utils' import { usePrompt } from '@/hooks/usePrompt' import { useThreads } from '@/hooks/useThreads' import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { ArrowRight } from 'lucide-react' import { IconPaperclip, IconWorld, IconAtom, IconEye, IconTool, IconCodeCircle2, IconPlayerStopFilled, IconBrandSpeedtest, IconX, } from '@tabler/icons-react' import { useTranslation } from 'react-i18next' import { useGeneralSetting } from '@/hooks/useGeneralSetting' import { useModelProvider } from '@/hooks/useModelProvider' import { useAppState } from '@/hooks/useAppState' import { MovingBorder } from './MovingBorder' import { MCPTool } from '@/types/completion' import { listen } from '@tauri-apps/api/event' import { SystemEvent } from '@/types/events' import { getTools } from '@/services/mcp' import { useChat } from '@/hooks/useChat' import DropdownModelProvider from '@/containers/DropdownModelProvider' import { ModelLoader } from '@/containers/loaders/ModelLoader' type ChatInputProps = { className?: string showSpeedToken?: boolean model?: ThreadModel } const ChatInput = ({ model, className, showSpeedToken = true, }: ChatInputProps) => { const textareaRef = useRef(null) const [isFocused, setIsFocused] = useState(false) const [rows, setRows] = useState(1) const { streamingContent, updateTools, abortControllers, loadingModel } = useAppState() const { prompt, setPrompt } = usePrompt() const { currentThreadId } = useThreads() const { t } = useTranslation() const { spellCheckChatInput } = useGeneralSetting() const { tokenSpeed } = useAppState() const maxRows = 10 const { selectedModel } = useModelProvider() const { sendMessage } = useChat() const [message, setMessage] = useState('') const handleSendMesage = (prompt: string) => { if (!selectedModel) { setMessage('Please select a model to start chatting.') return } setMessage('') sendMessage(prompt) } useEffect(() => { const handleFocusIn = () => { if (document.activeElement === textareaRef.current) { setIsFocused(true) } } const handleFocusOut = () => { if (document.activeElement !== textareaRef.current) { setIsFocused(false) } } document.addEventListener('focusin', handleFocusIn) document.addEventListener('focusout', handleFocusOut) return () => { document.removeEventListener('focusin', handleFocusIn) document.removeEventListener('focusout', handleFocusOut) } }, []) useEffect(() => { function setTools() { getTools().then((data: MCPTool[]) => { updateTools(data) }) } setTools() let unsubscribe = () => {} listen(SystemEvent.MCP_UPDATE, setTools).then((unsub) => { // Unsubscribe from the event when the component unmounts unsubscribe = unsub }) return unsubscribe }, [updateTools]) // Focus when component mounts useEffect(() => { if (textareaRef.current) { textareaRef.current.focus() } }, []) // Focus when thread changes useEffect(() => { if (textareaRef.current) { textareaRef.current.focus() } }, [currentThreadId]) // Focus when streaming content finishes useEffect(() => { if (!streamingContent && textareaRef.current) { // Small delay to ensure UI has updated setTimeout(() => { textareaRef.current?.focus() }, 10) } }, [streamingContent]) const stopStreaming = useCallback( (threadId: string) => { abortControllers[threadId]?.abort() }, [abortControllers] ) return (
{streamingContent && (
)}
{ setPrompt(e.target.value) // Count the number of newlines to estimate rows const newRows = (e.target.value.match(/\n/g) || []).length + 1 setRows(Math.min(newRows, maxRows)) }} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey && prompt) { e.preventDefault() // Submit the message when Enter is pressed without Shift handleSendMesage(prompt) // When Shift+Enter is pressed, a new line is added (default behavior) } }} placeholder={t('common.placeholder.chatInput')} autoFocus spellCheck={spellCheckChatInput} data-gramm={spellCheckChatInput} data-gramm_editor={spellCheckChatInput} data-gramm_grammarly={spellCheckChatInput} className={cn( 'bg-transparent pt-4 w-full flex-shrink-0 border-none resize-none outline-0 px-4', rows < maxRows && 'scrollbar-hide', className )} />
{model?.provider === 'llama.cpp' && loadingModel ? ( ) : ( )} {/* File attachment - always available */}
{/* Microphone - always available - Temp Hide */} {/*
*/} {selectedModel?.capabilities?.includes('vision') && (
)} {selectedModel?.capabilities?.includes('embeddings') && (
)} {selectedModel?.capabilities?.includes('tools') && (
)} {selectedModel?.capabilities?.includes('web_search') && (
)} {selectedModel?.capabilities?.includes('reasoning') && (
)}
{showSpeedToken && (
{Math.round(tokenSpeed?.tokenSpeed ?? 0)} tokens/sec
)}
{streamingContent ? ( ) : ( )}
{message && !selectedModel && (
{message} { setMessage('') }} />
)}
) } export default ChatInput