cawncade-ai / frontend /src /components /InputBox.jsx
Nawaz-khan-droid
build: finalize stratos refactor, textarea hardening, and payload limit alignment
acf82bb
Raw
History Blame Contribute Delete
7.65 kB
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>
);
}