Spaces:
Paused
Paused
Nawaz-khan-droid
build: finalize stratos refactor, textarea hardening, and payload limit alignment
acf82bb | import React, { useEffect, useState, useRef } from 'react'; | |
| import { Search, Link, FileText, Youtube, Image as ImageIcon, Upload, Loader2, X } from 'lucide-react'; | |
| import api from '../services/api'; | |
| import toast from 'react-hot-toast'; | |
| export default function InputBox({ onSubmit, isLoading }) { | |
| const RESEARCH_LIMIT = 25000; | |
| const [input, setInput] = useState(''); | |
| const [inputType, setInputType] = useState('text'); | |
| const [previewImage, setPreviewImage] = useState(null); | |
| const [imageBase64, setImageBase64] = useState(null); | |
| const fileInputRef = useRef(null); | |
| const textareaRef = useRef(null); | |
| const detectType = (value) => { | |
| if (value.match(/youtube\.com|youtu\.be/i)) setInputType('youtube'); | |
| else if (value.match(/^https?:\/\//i)) setInputType('url'); | |
| else setInputType('text'); | |
| }; | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| if (isLoading) return; | |
| if (inputType === 'image') { | |
| if (imageBase64) onSubmit({ image_base64: imageBase64, input_type: 'image' }); | |
| } else { | |
| if (input.length > RESEARCH_LIMIT) { | |
| toast.error('Input exceeds research limit (25k chars).'); | |
| return; | |
| } | |
| if (input.trim()) onSubmit({ input_text: input.trim(), input_type: inputType, max_sources: 10 }); | |
| } | |
| }; | |
| const adjustTextareaHeight = () => { | |
| if (!textareaRef.current) return; | |
| textareaRef.current.style.height = 'auto'; | |
| textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 160)}px`; | |
| }; | |
| useEffect(() => { | |
| adjustTextareaHeight(); | |
| }, [input, inputType]); | |
| const handleImageUpload = async (e) => { | |
| const file = e.target.files?.[0]; | |
| if (!file) return; | |
| if (file.size > 10 * 1024 * 1024) { alert('Image must be under 10MB'); return; } | |
| setPreviewImage(URL.createObjectURL(file)); | |
| const base64 = await api.fileToBase64(file); | |
| setImageBase64(base64.split(',')[1]); | |
| }; | |
| const clearImage = () => { | |
| setPreviewImage(null); | |
| setImageBase64(null); | |
| if (fileInputRef.current) fileInputRef.current.value = ''; | |
| }; | |
| const typeButtons = [ | |
| { id: 'text', icon: FileText, label: 'Text/Claim' }, | |
| { id: 'url', icon: Link, label: 'URL' }, | |
| { id: 'youtube', icon: Youtube, label: 'YouTube' }, | |
| { id: 'image', icon: ImageIcon, label: 'Visual Lens' }, | |
| ]; | |
| const placeholders = { text: 'Enter a claim, headline, or topic to verify...', url: 'Paste a news URL to verify...', youtube: 'Paste a YouTube video URL...', image: 'Upload an image for deepfake/AI detection...' }; | |
| const isNearLimit = input.length >= RESEARCH_LIMIT * 0.8; | |
| const isOverLimit = input.length > RESEARCH_LIMIT; | |
| const counterColor = isOverLimit ? 'text-red-400' : isNearLimit ? 'text-amber-400' : 'text-slate-500'; | |
| return ( | |
| <div className="fixed inset-x-0 bottom-16 z-50 px-3 pb-[max(env(safe-area-inset-bottom),0.75rem)] md:static md:px-0 md:pb-0"> | |
| <div className="mx-auto w-full max-w-4xl rounded-[1.75rem] border border-white/10 bg-slate-900/88 px-3 py-3 shadow-[0_-18px_50px_rgba(15,23,42,0.5)] backdrop-blur-md md:rounded-none md:border-0 md:bg-transparent md:px-0 md:py-0 md:shadow-none"> | |
| <div className="mb-3 flex items-center gap-1 overflow-x-auto rounded-full border border-white/10 bg-white/5 p-1 md:mx-auto md:mb-4 md:w-fit md:justify-center"> | |
| {typeButtons.map(({ id, icon: Icon, label }) => ( | |
| <button key={id} type="button" onClick={() => setInputType(id)} | |
| className={`flex items-center justify-center min-h-[44px] gap-1.5 px-3 py-2 rounded-full transition-all duration-200 text-sm font-medium snap-center shrink-0 ${inputType === id ? 'bg-sky-500/15 text-sky-300 border border-sky-400/30 shadow-[0_0_0_1px_rgba(56,189,248,0.08)]' : 'text-slate-400 hover:text-slate-200 border border-transparent hover:border-white/10 hover:bg-white/5'}`} | |
| title={label}> | |
| <Icon className="w-4 h-4" /> | |
| <span className="inline">{label}</span> | |
| </button> | |
| ))} | |
| </div> | |
| <form onSubmit={handleSubmit} className="relative z-10"> | |
| <div className="glass-card flex items-end gap-2 p-2 md:p-3"> | |
| <div className="pl-2 pb-3 text-slate-500"> | |
| {inputType === 'image' ? <ImageIcon className="w-5 h-5" /> : <Search className="w-5 h-5" />} | |
| </div> | |
| {inputType === 'image' ? ( | |
| <div className="flex-1 flex items-center gap-3"> | |
| {previewImage ? ( | |
| <div className="relative flex items-center gap-3 flex-1"> | |
| <img src={previewImage} alt="Preview" className="h-12 w-12 object-cover rounded-lg" /> | |
| <span className="text-sm text-slate-200 truncate">Image ready for analysis</span> | |
| <button type="button" onClick={clearImage} className="text-slate-500 hover:text-rose-400 transition-colors"><X className="w-4 h-4" /></button> | |
| </div> | |
| ) : ( | |
| <label className="flex-1 flex items-center gap-3 rounded-xl px-3 py-3 cursor-pointer hover:bg-white/5 transition-colors"> | |
| <Upload className="w-5 h-5 text-sky-400" /> | |
| <span className="text-slate-400 text-sm">Click to upload an image (JPG, PNG, WebP)</span> | |
| <input ref={fileInputRef} type="file" accept="image/jpeg,image/png,image/webp" onChange={handleImageUpload} className="hidden" /> | |
| </label> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="flex-1"> | |
| <textarea | |
| ref={textareaRef} | |
| rows={1} | |
| value={input} | |
| onInput={adjustTextareaHeight} | |
| onChange={(e) => { setInput(e.target.value); detectType(e.target.value); }} | |
| placeholder={placeholders[inputType]} | |
| className="max-h-40 min-h-[44px] w-full resize-none overflow-y-auto bg-transparent px-3 py-3 text-base text-slate-100 placeholder:text-slate-500 focus:outline-none" | |
| disabled={isLoading} | |
| /> | |
| <div className={`pr-3 text-right text-xs ${counterColor}`}> | |
| {input.length}/{RESEARCH_LIMIT} | |
| </div> | |
| </div> | |
| )} | |
| <button type="submit" disabled={isLoading || (inputType === 'image' ? !imageBase64 : !input.trim() || isOverLimit)} | |
| className="btn-primary flex items-center justify-center gap-2 px-5 py-3 min-h-[44px] disabled:opacity-50 disabled:cursor-not-allowed shrink-0"> | |
| {isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : <><Search className="w-5 h-5" /><span className="hidden sm:inline">Analyze</span></>} | |
| </button> | |
| </div> | |
| </form> | |
| {inputType !== 'image' && ( | |
| <div className="hidden flex-wrap gap-2 justify-center pt-4 md:flex"> | |
| {[ | |
| { text: 'Did India win the 2024 T20 World Cup?', type: 'text' }, | |
| { text: 'Is WHO recommending ban on artificial sweeteners?', type: 'text' }, | |
| { text: 'https://www.bbc.com/news', type: 'url' }, | |
| { text: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', type: 'youtube' }, | |
| ].map(({ text, type }) => ( | |
| <button key={text} type="button" onClick={() => { setInput(text); setInputType(type); }} | |
| className="rounded-full border border-white/10 bg-white/5 px-3 py-1.5 min-h-[32px] text-xs text-slate-400 transition-all hover:border-sky-400/25 hover:bg-sky-500/10 hover:text-slate-100"> | |
| {text.length > 45 ? text.substring(0, 45) + '...' : text} | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |