Download frontend/src/components/chat/ChatInput.jsx from Rixhot/ZARAI: direct link, hf CLI and curl.
- Browser
- Download file 5.83 kB
-
https://huggingface.co/spaces/Rixhot/ZARAI/resolve/main/frontend/src/components/chat/ChatInput.jsx
- Command line
-
hf download hf://spaces/Rixhot/ZARAI/frontend/src/components/chat/ChatInput.jsx
-
curl -L -o ChatInput.jsx https://huggingface.co/spaces/Rixhot/ZARAI/resolve/main/frontend/src/components/chat/ChatInput.jsx
5.83 kB
| /** | |
| * JARVIS — Chat Input Bar | |
| * Multi-mode input: text / image / voice / tools | |
| * Shows pending image preview, sends on Enter. | |
| */ | |
| import React, { useState, useRef, useCallback } from 'react' | |
| import { motion, AnimatePresence } from 'framer-motion' | |
| import { | |
| Send, Image, Mic, Wrench, X, Activity, | |
| MessageSquare, Paperclip, | |
| } from 'lucide-react' | |
| import clsx from 'clsx' | |
| import useAppStore from '../../stores/appStore' | |
| import toast from 'react-hot-toast' | |
| export default function ChatInput({ onToggleTrace, showTrace }) { | |
| const [text, setText] = useState('') | |
| const textareaRef = useRef(null) | |
| const { sendMessage, isLoading, inputMode, setInputMode, | |
| pendingImage, clearPendingImage } = useAppStore() | |
| const handleSend = useCallback(async () => { | |
| if (!text.trim() || isLoading) return | |
| const msg = text.trim() | |
| setText('') | |
| textareaRef.current?.focus() | |
| await sendMessage(msg) | |
| }, [text, isLoading, sendMessage]) | |
| const handleKeyDown = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault() | |
| handleSend() | |
| } | |
| } | |
| // Auto-resize textarea | |
| const handleInput = (e) => { | |
| setText(e.target.value) | |
| const ta = e.target | |
| ta.style.height = 'auto' | |
| ta.style.height = Math.min(ta.scrollHeight, 160) + 'px' | |
| } | |
| const modes = [ | |
| { id: 'text', icon: MessageSquare, label: 'Chat' }, | |
| { id: 'image', icon: Image, label: 'Vision' }, | |
| { id: 'voice', icon: Mic, label: 'Voice' }, | |
| { id: 'tools', icon: Wrench, label: 'Tools' }, | |
| ] | |
| return ( | |
| <div className="px-4 py-3"> | |
| {/* Pending image preview */} | |
| <AnimatePresence> | |
| {pendingImage && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 6 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: 6 }} | |
| className="flex items-center gap-3 mb-3 p-2 rounded-lg | |
| bg-jarvis-panel border border-jarvis-border" | |
| > | |
| <img | |
| src={pendingImage.preview} | |
| alt="Pending" | |
| className="w-12 h-12 object-cover rounded-lg border border-jarvis-border" | |
| /> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-xs font-medium text-jarvis-text truncate"> | |
| {pendingImage.file?.name || 'Image attached'} | |
| </p> | |
| {pendingImage.caption && ( | |
| <p className="text-[10px] text-jarvis-muted truncate"> | |
| {pendingImage.caption} | |
| </p> | |
| )} | |
| </div> | |
| <button | |
| onClick={clearPendingImage} | |
| className="p-1 rounded text-jarvis-dim hover:text-jarvis-error transition-colors" | |
| > | |
| <X size={14} /> | |
| </button> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| <div className="flex items-end gap-2"> | |
| {/* Mode switcher */} | |
| <div className="flex flex-col gap-1"> | |
| {modes.map(({ id, icon: Icon, label }) => ( | |
| <button | |
| key={id} | |
| title={label} | |
| onClick={() => setInputMode(inputMode === id && id !== 'text' ? 'text' : id)} | |
| className={clsx( | |
| 'w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-200', | |
| inputMode === id | |
| ? 'bg-jarvis-accent/20 border border-jarvis-accent/40 text-jarvis-accent' | |
| : 'text-jarvis-dim hover:text-jarvis-muted border border-transparent hover:border-jarvis-border', | |
| )} | |
| > | |
| <Icon size={14} /> | |
| </button> | |
| ))} | |
| </div> | |
| {/* Text input */} | |
| <div className="flex-1 relative"> | |
| <textarea | |
| ref={textareaRef} | |
| value={text} | |
| onChange={handleInput} | |
| onKeyDown={handleKeyDown} | |
| placeholder={ | |
| inputMode === 'text' ? 'Ask JARVIS anything… (Shift+Enter for newline)' : | |
| inputMode === 'image' ? 'Ask about the image above…' : | |
| inputMode === 'voice' ? 'Or type instead…' : | |
| 'Describe what you need…' | |
| } | |
| rows={1} | |
| disabled={isLoading} | |
| className="w-full resize-none input-field pr-10 py-3 leading-relaxed | |
| text-sm font-sans min-h-[44px] max-h-[160px] overflow-y-auto" | |
| /> | |
| {/* Send button */} | |
| <button | |
| onClick={handleSend} | |
| disabled={!text.trim() || isLoading} | |
| className={clsx( | |
| 'absolute right-2 bottom-2.5 w-7 h-7 rounded-md flex items-center justify-center', | |
| 'transition-all duration-200 active:scale-90', | |
| text.trim() && !isLoading | |
| ? 'bg-jarvis-accent text-jarvis-bg hover:brightness-110' | |
| : 'text-jarvis-dim cursor-not-allowed', | |
| )} | |
| > | |
| <Send size={13} /> | |
| </button> | |
| </div> | |
| {/* Agent trace toggle */} | |
| <button | |
| onClick={onToggleTrace} | |
| title="Toggle agent reasoning trace" | |
| className={clsx( | |
| 'w-8 h-8 rounded-lg flex items-center justify-center transition-all duration-200', | |
| 'border', | |
| showTrace | |
| ? 'bg-jarvis-accent2/20 border-jarvis-accent2/40 text-jarvis-accent2' | |
| : 'border-jarvis-border text-jarvis-dim hover:text-jarvis-muted hover:border-jarvis-muted', | |
| )} | |
| > | |
| <Activity size={14} /> | |
| </button> | |
| </div> | |
| {/* Footer hint */} | |
| <p className="text-[10px] text-jarvis-dim text-center mt-2 font-mono"> | |
| JARVIS · Multi-Modal AI · Powered by Mistral + BLIP + Whisper | |
| </p> | |
| </div> | |
| ) | |
| } | |