ZARAI / frontend /src /components /chat /ChatInput.jsx
Srajal2579
Deploy JARVIS
3e646d9
Raw History Blame Contribute Delete
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>
)
}