Spaces:
Build error
Build error
| import { useState, useCallback, useRef } from 'react'; | |
| import { useDropzone } from 'react-dropzone'; | |
| import axios from 'axios'; | |
| import { Search, CheckCircle, AlertTriangle, Globe, ShieldCheck, FileText, Upload, X, Brain, Loader2 } from 'lucide-react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import AskExpertPanel from './AskExpertPanel'; | |
| const models = [ | |
| { id: 'multi', label: 'Multi-Model Consensus (Safe Average)', provider: 'consensus' }, | |
| { id: 'groq/openai/gpt-oss-120b', label: 'Groq OSS-120B (Powerhouse)', provider: 'groq' }, | |
| { id: 'grok/grok-5', label: 'Grok-5 (Uncensored xAI)', provider: 'xai' }, | |
| { id: 'groq/llama-4-70b-versatile', label: 'Llama 4 70B (Fast Versatile)', provider: 'groq' }, | |
| { id: 'kimi/kimi-3.0', label: 'Kimi 3.0 (NVIDIA Thinking)', provider: 'moonshot' }, | |
| { id: 'qwen/qwen-max-2', label: 'Qwen Max 2 (Analytical Depth)', provider: 'dashscope' }, | |
| { id: 'zai/glm-5', label: 'Z.ai GLM-5 (Reasoning Pro)', provider: 'zai' }, | |
| { id: 'deepseek/r2', label: 'DeepSeek R2 (Logic & Strategy)', provider: 'deepseek' }, | |
| { id: 'openrouter/anthropic/claude-4-sonnet', label: 'Claude 4 Sonnet (via OpenRouter)', provider: 'openrouter' }, | |
| { id: 'openrouter/openai/gpt-5o-mini', label: 'GPT-5o Mini (via OpenRouter)', provider: 'openrouter' }, | |
| { id: 'openrouter/google/gemini-3-pro', label: 'Gemini 3 Pro (via OpenRouter)', provider: 'openrouter' }, | |
| { id: 'cloudflare/meta-llama-4-70b-instruct', label: 'Llama 4 70B (Cloudflare Edge)', provider: 'cloudflare' }, | |
| { id: 'hf/NousResearch/Hermes-4-Llama-4-70B', label: 'Hermes-4 Llama 4 70B (HF)', provider: 'hf' }, | |
| ]; | |
| export default function VerificationCore() { | |
| const [query, setQuery] = useState(''); | |
| const [file, setFile] = useState(null); | |
| const [filePreview, setFilePreview] = useState(null); | |
| const [fileContent, setFileContent] = useState(''); | |
| const [results, setResults] = useState([]); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState(null); | |
| const [selectedMode, setSelectedMode] = useState('multi'); | |
| const [bendMode, setBendMode] = useState(false); | |
| const [searchStatus, setSearchStatus] = useState(''); | |
| const [progress, setProgress] = useState(0); | |
| const abortControllerRef = useRef(null); | |
| const onDrop = useCallback(async (acceptedFiles) => { | |
| const uploadedFile = acceptedFiles[0]; | |
| if (!uploadedFile) return; | |
| if (uploadedFile.size > 20 * 1024 * 1024) { | |
| setError('File size exceeds 20MB limit'); | |
| return; | |
| } | |
| setFile(uploadedFile); | |
| setError(null); | |
| setFileContent(''); | |
| setFilePreview(null); | |
| try { | |
| if (uploadedFile.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = () => setFilePreview(reader.result); | |
| reader.readAsDataURL(uploadedFile); | |
| } else if (uploadedFile.type === 'application/pdf') { | |
| const formData = new FormData(); | |
| formData.append('file', uploadedFile); | |
| const response = await axios.post('/api/parse-pdf', formData); | |
| setFileContent(response.data.text); | |
| } else if (uploadedFile.type.startsWith('text/') || ['.md', '.csv', '.json'].some(ext => uploadedFile.name.toLowerCase().endsWith(ext))) { | |
| const text = await uploadedFile.text(); | |
| setFileContent(text); | |
| } | |
| } catch (err) { | |
| setError('Failed to process file: ' + err.message); | |
| } | |
| }, []); | |
| const { getRootProps, getInputProps, isDragActive } = useDropzone({ | |
| onDrop, | |
| accept: { | |
| 'image/*': ['.jpeg', '.jpg', '.png', '.webp', '.gif'], | |
| 'application/pdf': ['.pdf'], | |
| 'text/*': ['.txt', '.md', '.csv', '.json'], | |
| }, | |
| maxFiles: 1, | |
| maxSize: 20 * 1024 * 1024, | |
| }); | |
| const removeFile = () => { | |
| setFile(null); | |
| setFilePreview(null); | |
| setFileContent(''); | |
| setError(null); | |
| }; | |
| const handleVerify = async (e) => { | |
| e.preventDefault(); | |
| if (!query.trim() && !fileContent) { | |
| setError('Please enter a query or upload a file'); | |
| return; | |
| } | |
| setLoading(true); | |
| setError(null); | |
| setResults([]); | |
| setProgress(0); | |
| abortControllerRef.current = new AbortController(); | |
| try { | |
| let finalPrompt = query.trim(); | |
| let contextData = ''; | |
| if (fileContent) { | |
| contextData = `\n\n[FILE CONTENT]:\n${fileContent.slice(0, 15000)}${fileContent.length > 15000 ? '... (truncated)' : ''}`; | |
| finalPrompt += contextData; | |
| } | |
| if (bendMode && /current|latest|today|news|update|verify|fact-check/i.test(query)) { | |
| setSearchStatus('Searching web for latest context...'); | |
| try { | |
| const searchRes = await axios.post('/api/websearch', { query: query.slice(0, 100), maxResults: 5 }, { signal: abortControllerRef.current.signal }); | |
| const searchResults = searchRes.data.results || []; | |
| if (searchResults.length > 0) { | |
| const searchContext = searchResults.map((r, i) => `[${i + 1}] ${r.title}: ${r.snippet}`).join('\n'); | |
| finalPrompt += `\n\n[WEB SEARCH RESULTS]:\n${searchContext}`; | |
| } | |
| setSearchStatus('Web context integrated'); | |
| } catch (err) { | |
| setSearchStatus('Web search failed, continuing without'); | |
| } | |
| } | |
| setProgress(30); | |
| const progressInterval = setInterval(() => setProgress(p => Math.min(p + 10, 90)), 1000); | |
| const response = await axios.post('/api/inference', { | |
| prompt: finalPrompt, | |
| model: selectedMode, | |
| bendMode, | |
| systemPrompt: bendMode | |
| ? 'You are in BEND MODE (hypothetical/strategic analysis). Prefix all outputs with [HYPOTHETICAL SCENARIO]. Provide analytical, strategic insights without real-world action.' | |
| : 'You are a fact-checking and verification assistant. Provide accurate, sourced information with confidence scores.', | |
| }, { signal: abortControllerRef.current.signal }); | |
| clearInterval(progressInterval); | |
| setProgress(100); | |
| const resultData = { | |
| id: Date.now().toString(), | |
| model: selectedMode === 'multi' ? 'Consensus Engine' : models.find(m => m.id === selectedMode)?.label, | |
| content: response.data.result, | |
| confidence: response.data.confidence || 0.85, | |
| timestamp: new Date().toISOString(), | |
| sources: response.data.sources || [], | |
| isHypothetical: bendMode, | |
| query: query.trim(), | |
| }; | |
| setResults([resultData]); | |
| const history = JSON.parse(localStorage.getItem('verificationHistory') || '[]'); | |
| history.unshift(resultData); | |
| localStorage.setItem('verificationHistory', JSON.stringify(history.slice(0, 50))); | |
| } catch (err) { | |
| if (err.name === 'AbortError') { | |
| setError('Verification cancelled by user'); | |
| } else { | |
| setError('Verification failed: ' + (err.message || 'Unknown error')); | |
| } | |
| } finally { | |
| setLoading(false); | |
| setProgress(0); | |
| setTimeout(() => setSearchStatus(''), 3000); | |
| } | |
| }; | |
| const handleCancel = () => { | |
| if (abortControllerRef.current) { | |
| abortControllerRef.current.abort(); | |
| } | |
| }; | |
| const getFileIcon = () => { | |
| if (!file) return null; | |
| if (file.type.startsWith('image/')) return <FileText className="w-5 h-5" />; | |
| if (file.type === 'application/pdf') return <FileText className="w-5 h-5" />; | |
| return <FileText className="w-5 h-5" />; | |
| }; | |
| return ( | |
| <div className="min-h-screen p-4 sm:p-6 lg:p-8 pb-32"> | |
| <div className="max-w-6xl mx-auto space-y-6"> | |
| <motion.div initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="text-center mb-8"> | |
| <h1 className="text-4xl font-bold text-gradient mb-2">Verification Core</h1> | |
| <p className="text-slate-600 dark:text-slate-400">AI-powered claim verification with multi-model consensus</p> | |
| </motion.div> | |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="card space-y-6"> | |
| <div className="flex items-center justify-between p-4 bg-helios-50 dark:bg-helios-900/20 rounded-lg border border-helios-200 dark:border-helios-800"> | |
| <div className="flex items-center space-x-3"> | |
| <Brain className={`w-6 h-6 ${bendMode ? 'text-helios-600' : 'text-slate-400'}`} /> | |
| <div> | |
| <h3 className="font-semibold text-slate-900 dark:text-white">Bend Mode</h3> | |
| <p className="text-sm text-slate-600 dark:text-slate-400">Hypothetical/strategic analysis without real-world constraints</p> | |
| </div> | |
| </div> | |
| <button onClick={() => setBendMode(!bendMode)} className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors ${bendMode ? 'bg-helios-600' : 'bg-slate-300 dark:bg-slate-600'}`}> | |
| <span className={`inline-block h-5 w-5 transform rounded-full bg-white transition-transform ${bendMode ? 'translate-x-6' : 'translate-x-1'}`} /> | |
| </button> | |
| </div> | |
| <form onSubmit={handleVerify} className="space-y-4"> | |
| <div className="relative"> | |
| <textarea value={query} onChange={(e) => setQuery(e.target.value)} | |
| placeholder={bendMode ? "Enter hypothetical scenario or strategic question..." : "Enter claim to verify, question to research, or topic to analyze..."} | |
| className="input-field min-h-[120px] resize-none" disabled={loading} /> | |
| <div className="absolute bottom-3 right-3 text-xs text-slate-400">{query.length} chars</div> | |
| </div> | |
| {!file ? ( | |
| <div {...getRootProps()} className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${isDragActive ? 'border-primary-500 bg-primary-50 dark:bg-primary-900/20' : 'border-slate-300 dark:border-slate-600 hover:border-primary-400'}`}> | |
| <input {...getInputProps()} /> | |
| <Upload className="w-8 h-8 mx-auto mb-2 text-slate-400" /> | |
| <p className="text-sm text-slate-600 dark:text-slate-400">Drop files here or click to upload (PDF, Images, Text)</p> | |
| <p className="text-xs text-slate-400 mt-1">Max 20MB</p> | |
| </div> | |
| ) : ( | |
| <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-700/50 rounded-lg"> | |
| <div className="flex items-center space-x-3"> | |
| {getFileIcon()} | |
| <div> | |
| <p className="font-medium text-slate-900 dark:text-white truncate max-w-xs">{file.name}</p> | |
| <p className="text-xs text-slate-500">{(file.size / 1024).toFixed(1)} KB {fileContent && ` - ${fileContent.length.toLocaleString()} chars extracted`}</p> | |
| </div> | |
| </div> | |
| <button type="button" onClick={removeFile} className="p-2 hover:bg-slate-200 dark:hover:bg-slate-600 rounded-lg transition-colors"><X className="w-4 h-4" /></button> | |
| </motion.div> | |
| )} | |
| {filePreview && ( | |
| <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="relative rounded-lg overflow-hidden max-h-64"> | |
| <img src={filePreview} alt="Preview" className="w-full h-full object-contain" /> | |
| </motion.div> | |
| )} | |
| <div className="space-y-2"> | |
| <label className="text-sm font-medium text-slate-700 dark:text-slate-300">AI Model Configuration</label> | |
| <select value={selectedMode} onChange={(e) => setSelectedMode(e.target.value)} className="input-field" disabled={loading}> | |
| {models.map((model) => <option key={model.id} value={model.id}>{model.label}</option>)} | |
| </select> | |
| </div> | |
| <div className="flex flex-col sm:flex-row gap-3"> | |
| <button type="submit" disabled={loading || (!query.trim() && !fileContent)} className="btn-primary flex-1 flex items-center justify-center space-x-2 disabled:opacity-50"> | |
| {loading ? (<><Loader2 className="w-5 h-5 animate-spin" /><span>Verifying...</span></>) : (<><ShieldCheck className="w-5 h-5" /><span>{bendMode ? 'Strategic Analysis' : 'Verify Claim'}</span></>)} | |
| </button> | |
| {loading && <button type="button" onClick={handleCancel} className="btn-secondary">Cancel</button>} | |
| </div> | |
| </form> | |
| {loading && progress > 0 && ( | |
| <div className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2 overflow-hidden"> | |
| <motion.div className="bg-primary-600 h-full" initial={{ width: 0 }} animate={{ width: `${progress}%` }} transition={{ duration: 0.3 }} /> | |
| </div> | |
| )} | |
| {searchStatus && <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="flex items-center space-x-2 text-sm text-primary-600 dark:text-primary-400"><Globe className="w-4 h-4 animate-pulse" /><span>{searchStatus}</span></motion.div>} | |
| {error && <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="p-4 bg-danger-50 dark:bg-danger-900/20 border border-danger-200 dark:border-danger-800 rounded-lg flex items-center space-x-2 text-danger-700 dark:text-danger-400"><AlertTriangle className="w-5 h-5" /><span>{error}</span></motion.div>} | |
| </motion.div> | |
| <AnimatePresence> | |
| {results.map((result) => ( | |
| <motion.div key={result.id} initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} className="card space-y-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-3"> | |
| {result.isHypothetical ? <Brain className="w-6 h-6 text-helios-600" /> : <CheckCircle className="w-6 h-6 text-truth-600" />} | |
| <div> | |
| <h3 className="font-semibold text-slate-900 dark:text-white">{result.isHypothetical ? 'Strategic Analysis' : 'Verification Result'}</h3> | |
| <p className="text-sm text-slate-500">{result.model}</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-sm text-slate-500">Confidence: {Math.round(result.confidence * 100)}%</span> | |
| <div className={`w-3 h-3 rounded-full ${result.confidence > 0.8 ? 'bg-truth-500' : result.confidence > 0.6 ? 'bg-warning-500' : 'bg-danger-500'}`} /> | |
| </div> | |
| </div> | |
| {result.isHypothetical && ( | |
| <div className="p-3 bg-helios-100 dark:bg-helios-900/30 border-l-4 border-helios-500 rounded"> | |
| <p className="text-sm text-helios-800 dark:text-helios-200 font-medium">HYPOTHETICAL SCENARIO: This analysis is for strategic planning purposes only.</p> | |
| </div> | |
| )} | |
| <div className="prose dark:prose-invert max-w-none"> | |
| <div className="whitespace-pre-wrap text-slate-700 dark:text-slate-300 leading-relaxed">{result.content}</div> | |
| </div> | |
| {result.sources && result.sources.length > 0 && ( | |
| <div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700"> | |
| <h4 className="font-medium text-slate-900 dark:text-white mb-2">Sources</h4> | |
| <ul className="space-y-1"> | |
| {result.sources.map((source, idx) => ( | |
| <li key={idx} className="text-sm text-primary-600 dark:text-primary-400 truncate"> | |
| <a href={source} target="_blank" rel="noopener noreferrer" className="hover:underline">{source}</a> | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| <div className="flex items-center justify-between text-xs text-slate-500 pt-4 border-t border-slate-200 dark:border-slate-700"> | |
| <span>{new Date(result.timestamp).toLocaleString()}</span> | |
| <button onClick={() => navigator.clipboard.writeText(result.content)} className="flex items-center space-x-1 hover:text-primary-600 transition-colors"><span>Copy Result</span></button> | |
| </div> | |
| </motion.div> | |
| ))} | |
| </AnimatePresence> | |
| <AskExpertPanel query={query} fileContent={fileContent} bendMode={bendMode} onResult={(expertResult) => setResults(prev => [...prev, expertResult])} /> | |
| </div> | |
| </div> | |
| ); | |
| } |