| import React, { useState, useRef, useEffect } from 'react'; |
| import { |
| AlertCircle, Play, Upload, List, Database, BarChart2, ExternalLink, |
| Users, MessageSquare, TrendingUp, ShieldCheck, UserCheck, Search, PlusCircle, |
| StopCircle, RefreshCw, CheckCircle2, PenTool, ClipboardCheck, Info, Clock, FileText, |
| Tag, Home, Cpu, FlaskConical, Target, Trash2, ArrowUpRight, CheckSquare, Square, |
| Layers, Activity, Zap, BrainCircuit, Network, Archive, Plus, Edit3, RotateCcw, |
| Bot, Trophy, HelpCircle, Settings, Calculator, ChevronDown, ChevronUp |
| } from 'lucide-react'; |
|
|
| function App() { |
| const[activeTab, setActiveTab] = useState('home'); |
| const[logs, setLogs] = useState<string>('System Ready.\n'); |
| const [isProcessing, setIsProcessing] = useState(false); |
| const logContainerRef = useRef<HTMLDivElement>(null); |
| |
| |
| const[modelProvider, setModelProvider] = useState('nrp'); |
| const[apiKey, setApiKey] = useState(''); |
| const[baseUrl, setBaseUrl] = useState('https://ellm.nrp-nautilus.io/v1'); |
| const[modelName, setModelName] = useState('qwen3'); |
| const[projectId, setProjectId] = useState(''); |
| const [location, setLocation] = useState('us-central1'); |
| const[includeComments, setIncludeComments] = useState(false); |
| const[reasoningMethod, setReasoningMethod] = useState('cot'); |
| const[promptTemplate, setPromptTemplate] = useState('standard'); |
| const[customQuery, setCustomQuery] = useState(''); |
| const [maxRetries, setMaxRetries] = useState(1); |
| const[availablePrompts, setAvailablePrompts] = useState<any[]>([]); |
| |
| const[useSearch, setUseSearch] = useState(false); |
| const[useCode, setUseCode] = useState(false); |
|
|
| |
| const[queueList, setQueueList] = useState<any[]>([]); |
| const [selectedQueueItems, setSelectedQueueItems] = useState<Set<string>>(new Set()); |
| const[expandedQueueItems, setExpandedQueueItems] = useState<Set<string>>(new Set()); |
| const[lastQueueIndex, setLastQueueIndex] = useState<number | null>(null); |
| |
| const[singleLinkInput, setSingleLinkInput] = useState(''); |
| const [profileList, setProfileList] = useState<any[]>([]); |
| const[selectedProfile, setSelectedProfile] = useState<any>(null); |
| const [profilePosts, setProfilePosts] = useState<any[]>([]); |
| const[integrityBoard, setIntegrityBoard] = useState<any[]>([]); |
| |
| const[datasetList, setDatasetList] = useState<any[]>([]); |
| const[selectedItems, setSelectedItems] = useState<Set<string>>(new Set()); |
| const[lastDatasetIndex, setLastDatasetIndex] = useState<number | null>(null); |
|
|
| const [benchmarks, setBenchmarks] = useState<any>(null); |
| const[leaderboard, setLeaderboard] = useState<any[]>([]); |
| const[refreshTrigger, setRefreshTrigger] = useState(0); |
|
|
| |
| const[configuredTags, setConfiguredTags] = useState<any>({}); |
|
|
| |
| const[manualLink, setManualLink] = useState(''); |
| const [manualCaption, setManualCaption] = useState(''); |
| const[manualTags, setManualTags] = useState(''); |
| const[manualReasoning, setManualReasoning] = useState(''); |
| const[manualScores, setManualScores] = useState({ |
| visual: 5, audio: 5, source: 5, logic: 5, emotion: 5, |
| va: 5, vc: 5, ac: 5, final: 50 |
| }); |
| const[showRubric, setShowRubric] = useState(false); |
| const[aiReference, setAiReference] = useState<any>(null); |
| const[labelBrowserMode, setLabelBrowserMode] = useState<'queue' | 'dataset'>('queue'); |
| const[labelFilter, setLabelFilter] = useState(''); |
|
|
| |
| const [agentInput, setAgentInput] = useState(''); |
| const[agentMessages, setAgentMessages] = useState<any[]>([]); |
| const[agentThinking, setAgentThinking] = useState(false); |
| const [agentEndpoint, setAgentEndpoint] = useState('/a2a'); |
| const[agentMethod, setAgentMethod] = useState('agent.process'); |
| const [agentConfig, setAgentConfig] = useState({ use_search: true, use_code: false }); |
|
|
| |
| const[resampleCount, setResampleCount] = useState<number>(1); |
| |
| |
| const isDraggingQueueRef = useRef(false); |
| const isDraggingDatasetRef = useRef(false); |
|
|
| |
| const[demoLink, setDemoLink] = useState(''); |
| const[demoLogs, setDemoLogs] = useState(''); |
| const[demoIsProcessing, setDemoIsProcessing] = useState(false); |
| const[demoResult, setDemoResult] = useState<any>(null); |
| const[showDemoConfig, setShowDemoConfig] = useState(false); |
| const demoLogContainerRef = useRef<HTMLDivElement>(null); |
|
|
| useEffect(() => { |
| const handleMouseUp = () => { |
| isDraggingQueueRef.current = false; |
| isDraggingDatasetRef.current = false; |
| }; |
| window.addEventListener('mouseup', handleMouseUp); |
| return () => window.removeEventListener('mouseup', handleMouseUp); |
| },[]); |
|
|
| const handleTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => { |
| setManualTags(e.target.value); |
| }; |
|
|
| useEffect(() => { |
| const load = async (url: string, setter: any) => { |
| try { const res = await fetch(url); const d = await res.json(); setter(Array.isArray(d) ? d : (d.status==='no_data'?null:d)); } catch(e) {} |
| }; |
|
|
| load('/config/prompts', setAvailablePrompts); |
| load('/config/tags', setConfiguredTags); |
|
|
| if (activeTab === 'home') { |
| load('/benchmarks/stats', setBenchmarks); |
| load('/benchmarks/leaderboard', setLeaderboard); |
| } |
| if (activeTab === 'queue') { |
| load('/queue/list', setQueueList); |
| setSelectedQueueItems(new Set()); |
| setLastQueueIndex(null); |
| } |
| if (activeTab === 'profiles') load('/profiles/list', setProfileList); |
| if (activeTab === 'analytics') load('/analytics/account_integrity', setIntegrityBoard); |
| if (activeTab === 'dataset' || activeTab === 'manual' || activeTab === 'groundtruth') load('/dataset/list', setDatasetList); |
| if (activeTab === 'manual') load('/queue/list', setQueueList); |
| |
| setSelectedItems(new Set()); |
| setLastDatasetIndex(null); |
| },[activeTab, refreshTrigger]); |
|
|
| useEffect(() => { |
| if (logContainerRef.current) logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; |
| }, [logs]); |
|
|
| useEffect(() => { |
| if (demoLogContainerRef.current) demoLogContainerRef.current.scrollTop = demoLogContainerRef.current.scrollHeight; |
| },[demoLogs]); |
|
|
| useEffect(() => { |
| if (activeTab === 'agent' && agentMessages.length === 0) { |
| callAgent(agentMethod, { |
| input: "hello", |
| agent_config: { provider: modelProvider === 'gcloud' ? 'vertex' : modelProvider, api_key: apiKey, project_id: projectId } |
| }) |
| .then(res => res.json()) |
| .then(data => { |
| if (data.result && data.result.text) { |
| setAgentMessages([{role: 'agent', content: data.result.text}]); |
| } |
| }) |
| .catch(e => console.error("Initial Agent Ping Failed", e)); |
| } |
| }, [activeTab]); |
|
|
| const existingTags = React.useMemo(() => { |
| const tags = new Set<string>(); |
| Object.keys(configuredTags).forEach(t => tags.add(t)); |
| if (datasetList && Array.isArray(datasetList)) { |
| datasetList.forEach(item => { |
| if (item.tags && typeof item.tags === 'string') { |
| item.tags.split(/[,\n|;]+/).forEach((t: string) => { |
| const clean = t.trim().replace(/^['"]|['"]$/g, ''); |
| if (clean.length > 1) tags.add(clean); |
| }); |
| } |
| }); |
| } |
| return Array.from(tags).sort(); |
| },[datasetList, configuredTags]); |
|
|
| const toggleTag = (tag: string) => { |
| let current = manualTags.split(',').map(t => t.trim()).filter(Boolean); |
| if (current.includes(tag)) { |
| current = current.filter(t => t !== tag); |
| } else { |
| current.push(tag); |
| } |
| setManualTags(current.join(', ')); |
| }; |
|
|
| const loadProfilePosts = async (username: string) => { |
| const res = await fetch(`/profiles/${username}/posts`); |
| const data = await res.json(); |
| setProfilePosts(data); |
| setSelectedProfile(username); |
| }; |
|
|
| const sendToManualLabeler = (link: string, text: string) => { |
| setManualLink(link); |
| setManualCaption(text); |
| setManualScores({ |
| visual: 5, audio: 5, source: 5, logic: 5, emotion: 5, |
| va: 5, vc: 5, ac: 5, final: 50 |
| }); |
| setManualReasoning(''); |
| setManualTags(''); |
| setAiReference(null); |
| |
| const ref = datasetList.find(d => |
| d.source !== 'Manual' && |
| d.source !== 'manual_promoted' && |
| (d.link === link) |
| ); |
| setAiReference(ref || null); |
| |
| setActiveTab('manual'); |
| }; |
| |
| const loadFromBrowser = (item: any, mode: 'queue' | 'dataset') => { |
| setManualLink(item.link); |
| |
| const ref = datasetList.find(d => |
| d.source !== 'Manual' && |
| d.source !== 'manual_promoted' && |
| (d.id === item.id || d.link === item.link) |
| ); |
| setAiReference(ref || null); |
|
|
| if (mode === 'dataset') { |
| setManualCaption(item.caption || ''); |
| setManualTags(item.tags || ''); |
| setManualReasoning(item.reasoning || item.final_reasoning || ''); |
| setManualScores({ |
| visual: parseInt(item.visual_integrity_score || item.visual_score) || 5, |
| audio: parseInt(item.audio_integrity_score || item.audio_score) || 5, |
| source: parseInt(item.source_credibility_score || item.source_score) || 5, |
| logic: parseInt(item.logical_consistency_score || item.logic_score) || 5, |
| emotion: parseInt(item.emotional_manipulation_score || item.emotion_score) || 5, |
| va: parseInt(item.video_audio_score || item.align_video_audio) || 5, |
| vc: parseInt(item.video_caption_score || item.align_video_caption) || 5, |
| ac: parseInt(item.audio_caption_score || item.align_audio_caption) || 5, |
| final: parseInt(item.final_veracity_score) || 50 |
| }); |
| } else { |
| setManualCaption(''); setManualReasoning(''); setManualTags(''); |
| setManualScores({ |
| visual: 5, audio: 5, source: 5, logic: 5, emotion: 5, |
| va: 5, vc: 5, ac: 5, final: 50 |
| }); |
| } |
| }; |
|
|
| const editSelectedLabel = () => { |
| if (selectedItems.size !== 1) return alert("Please select exactly one item to edit."); |
| const id = Array.from(selectedItems)[0]; |
| const item = datasetList.find(d => d.id === id); |
| if(!item) return; |
| loadFromBrowser(item, 'dataset'); |
| setActiveTab('manual'); |
| }; |
|
|
| const toggleSelection = (e: React.MouseEvent, id: string, index: number, list: any[]) => { |
| let newSet = new Set(selectedItems); |
| if (e.shiftKey && lastDatasetIndex !== null && lastDatasetIndex !== index) { |
| const start = Math.min(lastDatasetIndex, index); |
| const end = Math.max(lastDatasetIndex, index); |
| for (let i = start; i <= end; i++) { |
| newSet.add(list[i].id); |
| } |
| } else { |
| if (newSet.has(id)) newSet.delete(id); |
| else newSet.add(id); |
| } |
| setSelectedItems(newSet); |
| setLastDatasetIndex(index); |
| }; |
| |
| const toggleQueueSelection = (e: React.MouseEvent, link: string, index: number, list: any[]) => { |
| let newSet = new Set(selectedQueueItems); |
| if (e.shiftKey && lastQueueIndex !== null && lastQueueIndex !== index) { |
| const start = Math.min(lastQueueIndex, index); |
| const end = Math.max(lastQueueIndex, index); |
| for (let i = start; i <= end; i++) { |
| newSet.add(list[i].link); |
| } |
| } else { |
| if (newSet.has(link)) newSet.delete(link); |
| else newSet.add(link); |
| } |
| setSelectedQueueItems(newSet); |
| setLastQueueIndex(index); |
| }; |
|
|
| const toggleQueueExpand = (e: React.MouseEvent, link: string) => { |
| e.stopPropagation(); |
| const newSet = new Set(expandedQueueItems); |
| if (newSet.has(link)) newSet.delete(link); |
| else newSet.add(link); |
| setExpandedQueueItems(newSet); |
| }; |
|
|
| const promoteSelected = async () => { |
| if (selectedItems.size === 0) return alert("No items selected."); |
| if (!confirm(`Promote ${selectedItems.size} items to Ground Truth?`)) return; |
| try { |
| const res = await fetch('/manual/promote', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ ids: Array.from(selectedItems) }) |
| }); |
| const d = await res.json(); |
| if(d.status === 'success') { |
| alert(`Successfully promoted ${d.promoted_count} items.`); |
| setSelectedItems(new Set()); |
| setRefreshTrigger(p => p+1); |
| } else alert("Promotion failed: " + d.message); |
| } catch(e: any) { alert("Network error: " + e.toString()); } |
| }; |
|
|
| const verifySelected = async () => { |
| if (selectedItems.size === 0) return alert("No items selected."); |
| if (!confirm(`Queue ${selectedItems.size} Ground Truth items for AI Verification Pipeline?`)) return; |
| try { |
| const res = await fetch('/manual/verify_queue', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ ids: Array.from(selectedItems), resample_count: resampleCount }) |
| }); |
| const d = await res.json(); |
| if(d.status === 'success') { |
| alert(d.message); |
| setSelectedItems(new Set()); |
| setActiveTab('queue'); |
| setRefreshTrigger(p => p+1); |
| } else alert("Error: " + d.message); |
| } catch(e) { alert("Network error."); } |
| }; |
|
|
| const deleteSelected = async () => { |
| if (selectedItems.size === 0) return alert("No items selected."); |
| if (!confirm(`Delete ${selectedItems.size} items from Ground Truth? Irreversible.`)) return; |
| try { |
| const res = await fetch('/manual/delete', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ ids: Array.from(selectedItems) }) |
| }); |
| const d = await res.json(); |
| if(d.status === 'success') { |
| alert(`Deleted ${d.deleted_count} items.`); |
| setSelectedItems(new Set()); |
| setRefreshTrigger(p => p+1); |
| } else alert("Error deleting: " + d.message); |
| } catch(e) { alert("Network error."); } |
| }; |
|
|
| const deleteDataEntries = async () => { |
| if (selectedItems.size === 0) return alert("No items selected."); |
| if (!confirm(`Delete ${selectedItems.size} items? This cannot be undone.`)) return; |
|
|
| const selectedArray = Array.from(selectedItems); |
| const manualIds = selectedArray.filter(id => datasetList.find(d => d.id === id)?.source === 'Manual'); |
| const aiIds = selectedArray.filter(id => { |
| const item = datasetList.find(d => d.id === id); |
| return item?.source === 'AI' || !item?.source; |
| }); |
|
|
| try { |
| let msg = ""; |
| if (manualIds.length > 0) { |
| const res = await fetch('/manual/delete', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ ids: manualIds }) |
| }); |
| const d = await res.json(); |
| if (d.status === 'success') msg += `Deleted ${d.deleted_count} Manual items. `; |
| else msg += `Failed Manual delete: ${d.message}. `; |
| } |
|
|
| if (aiIds.length > 0) { |
| const res = await fetch('/dataset/delete', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ ids: aiIds }) |
| }); |
| const d = await res.json(); |
| if (d.status === 'success') msg += `Deleted ${d.deleted_count} AI items. `; |
| else msg += `Failed AI delete: ${d.message}. `; |
| } |
|
|
| alert(msg || "Done."); |
| setSelectedItems(new Set()); |
| setRefreshTrigger(p => p + 1); |
| } catch (e) { |
| alert("Network error: " + e); |
| } |
| }; |
|
|
| const submitManualLabel = async () => { |
| if(!manualLink) return alert("Link is required."); |
| const payload = { |
| link: manualLink, caption: manualCaption, tags: manualTags, reasoning: manualReasoning, |
| visual_integrity_score: manualScores.visual, audio_integrity_score: manualScores.audio, |
| source_credibility_score: manualScores.source, logical_consistency_score: manualScores.logic, |
| emotional_manipulation_score: manualScores.emotion, |
| video_audio_score: manualScores.va, video_caption_score: manualScores.vc, audio_caption_score: manualScores.ac, |
| final_veracity_score: manualScores.final, |
| classification: "Manual Verified" |
| }; |
| try { |
| const res = await fetch('/manual/save', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); |
| const d = await res.json(); |
| if(d.status === 'success') { |
| alert("Label Saved! Data updated."); |
| setRefreshTrigger(p => p+1); |
| } else alert("Error saving label: " + d.message); |
| } catch(e: any) { alert("Network error: " + e.toString()); } |
| }; |
|
|
| const queueUnlabeledPosts = async () => { |
| const unlabeled = profilePosts.filter(p => !p.is_labeled).map(p => p.link); |
| if(unlabeled.length === 0) return alert("All posts already labeled!"); |
| const csvContent = "link\n" + unlabeled.join("\n"); |
| const blob = new Blob([csvContent], { type: 'text/csv' }); |
| const fd = new FormData(); fd.append("file", blob, "batch_upload.csv"); |
| try { |
| await fetch('/queue/upload_csv', { method: 'POST', body: fd }); |
| alert(`Queued ${unlabeled.length} links.`); setRefreshTrigger(p => p+1); |
| } catch (e) { alert("Error uploading."); } |
| }; |
|
|
| const addSingleLink = async () => { |
| if(!singleLinkInput) return; |
| try { |
| const res = await fetch('/queue/add', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ link: singleLinkInput }) |
| }); |
| const d = await res.json(); |
| if(d.status === 'success') { |
| setSingleLinkInput(''); |
| setRefreshTrigger(p => p+1); |
| } else { alert(d.message); } |
| } catch(e) { alert("Error adding link"); } |
| }; |
|
|
| const addSingleLinkDirect = async (link: string) => { |
| if(!link) return; |
| try { |
| const res = await fetch('/queue/add', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ link: link }) |
| }); |
| const d = await res.json(); |
| if(d.status === 'success') { |
| setRefreshTrigger(p => p+1); |
| } else { alert(d.message); } |
| } catch(e) { alert("Error adding link"); } |
| }; |
|
|
| const clearProcessed = async () => { |
| if(!confirm("Remove all 'Processed' items from the queue?")) return; |
| try { |
| const res = await fetch('/queue/clear_processed', { method: 'POST' }); |
| const d = await res.json(); |
| alert(`Removed ${d.removed_count} processed items.`); |
| setRefreshTrigger(p => p+1); |
| } catch(e) { alert("Error clearing queue."); } |
| }; |
| |
| const requeueItems = async () => { |
| if(selectedQueueItems.size === 0) return alert("No items selected."); |
| if(!confirm(`Requeue ${selectedQueueItems.size} items? Their status will reset to Pending for processing.`)) return; |
| try { |
| const res = await fetch('/queue/requeue', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ links: Array.from(selectedQueueItems) }) |
| }); |
| const d = await res.json(); |
| if(d.status === 'success') { |
| alert(`Requeued ${d.count} items.`); |
| setSelectedQueueItems(new Set()); |
| setRefreshTrigger(p => p+1); |
| } |
| } catch(e) { alert("Error requeuing items."); } |
| }; |
|
|
| const deleteQueueItems = async () => { |
| if(selectedQueueItems.size === 0) return alert("No items selected."); |
| if(!confirm(`Remove ${selectedQueueItems.size} items from queue?`)) return; |
| try { |
| const res = await fetch('/queue/delete', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ links: Array.from(selectedQueueItems) }) |
| }); |
| const d = await res.json(); |
| if(d.status === 'success') { |
| alert(`Removed ${d.count} items.`); |
| setSelectedQueueItems(new Set()); |
| setRefreshTrigger(p => p+1); |
| } |
| } catch(e) { alert("Error deleting queue items."); } |
| }; |
|
|
| const stopProcessing = async () => { |
| if(!confirm("Stop batch processing?")) return; |
| await fetch('/queue/stop', { method: 'POST' }); |
| setLogs(prev => prev + '\n[SYSTEM] Stop Signal Sent.\n'); |
| }; |
|
|
| const startProcessing = async () => { |
| if (isProcessing) return; |
| setIsProcessing(true); |
| setLogs(prev => prev + '\n[SYSTEM] Starting Queue Processing...\n'); |
| const fd = new FormData(); |
| |
| const activeProvider = modelProvider === 'gcloud' ? 'vertex' : modelProvider; |
| fd.append('model_selection', activeProvider); |
| fd.append('gemini_api_key', apiKey); |
| fd.append('gemini_model_name', modelName); |
| fd.append('vertex_project_id', projectId); |
| fd.append('vertex_location', location); |
| fd.append('vertex_model_name', modelName); |
| fd.append('vertex_api_key', apiKey); |
| |
| |
| fd.append('nrp_api_key', apiKey); |
| fd.append('nrp_model_name', modelName); |
| fd.append('nrp_base_url', baseUrl); |
|
|
| fd.append('include_comments', includeComments.toString()); |
| fd.append('reasoning_method', reasoningMethod); |
| fd.append('prompt_template', promptTemplate); |
| fd.append('custom_query', customQuery); |
| fd.append('max_reprompts', maxRetries.toString()); |
| |
| fd.append('use_search', useSearch.toString()); |
| fd.append('use_code', useCode.toString()); |
|
|
| try { |
| const res = await fetch('/queue/run', { method: 'POST', body: fd }); |
| const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader(); |
| while (true) { |
| const { value, done } = await reader.read(); |
| if (done) break; |
| if (value.includes('event: close')) { setIsProcessing(false); break; } |
| const clean = value.replace(/data: /g, '').trim(); |
| if (clean) setLogs(prev => prev + clean + '\n'); |
| } |
| } catch (e) { setIsProcessing(false); } |
| setRefreshTrigger(p => p+1); |
| }; |
|
|
| const runDemo = async () => { |
| if (!demoLink) return; |
| setDemoIsProcessing(true); |
| setDemoLogs('[SYSTEM] Preparing pipeline for single execution...\n'); |
| setDemoResult(null); |
|
|
| try { |
| |
| await fetch('/queue/add', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ link: demoLink }) |
| }); |
| await fetch('/queue/requeue', { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ links: [demoLink] }) |
| }); |
|
|
| |
| const fd = new FormData(); |
| const activeProvider = modelProvider === 'gcloud' ? 'vertex' : modelProvider; |
| fd.append('model_selection', activeProvider); |
| fd.append('gemini_api_key', apiKey); |
| fd.append('gemini_model_name', modelName); |
| fd.append('vertex_project_id', projectId); |
| fd.append('vertex_location', location); |
| fd.append('vertex_model_name', modelName); |
| fd.append('vertex_api_key', apiKey); |
| fd.append('nrp_api_key', apiKey); |
| fd.append('nrp_model_name', modelName); |
| fd.append('nrp_base_url', baseUrl); |
| fd.append('include_comments', includeComments.toString()); |
| fd.append('reasoning_method', reasoningMethod); |
| fd.append('prompt_template', promptTemplate); |
| fd.append('custom_query', customQuery); |
| fd.append('max_reprompts', maxRetries.toString()); |
| |
| fd.append('use_search', useSearch.toString()); |
| fd.append('use_code', useCode.toString()); |
|
|
| setDemoLogs(prev => prev + '[SYSTEM] Sending analysis payload to model server...\n'); |
| |
| |
| const runRes = await fetch('/queue/run', { method: 'POST', body: fd }); |
| const reader = runRes.body!.pipeThrough(new TextDecoderStream()).getReader(); |
|
|
| while (true) { |
| const { value, done } = await reader.read(); |
| if (done) break; |
| if (value.includes('event: close')) break; |
| const clean = value.replace(/data: /g, '').trim(); |
| if (clean) setDemoLogs(prev => prev + clean + '\n'); |
| } |
|
|
| |
| setDemoLogs(prev => prev + '\n[SYSTEM] Fetching structured object result...\n'); |
| const dsRes = await fetch('/dataset/list'); |
| const dsList = await dsRes.json(); |
|
|
| const normalize = (l: string) => l.split('?')[0].replace('https://', '').replace('http://', '').replace('www.', '').replace(/\/$/, ''); |
| const targetLink = normalize(demoLink); |
|
|
| |
| dsList.sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); |
|
|
| const match = dsList.find((d: any) => normalize(d.link || '') === targetLink && d.source !== 'Manual'); |
| |
| if (match) { |
| setDemoResult(match); |
| setDemoLogs(prev => prev + '[SYSTEM] Result rendered successfully.\n'); |
| } else { |
| setDemoLogs(prev => prev + '[SYSTEM] Error: Could not find parsed result. Processing might have failed.\n'); |
| } |
|
|
| } catch (err: any) { |
| setDemoLogs(prev => prev + `\n[ERROR] ${err.message}\n`); |
| } finally { |
| setDemoIsProcessing(false); |
| setRefreshTrigger(p => p+1); |
| } |
| } |
|
|
| const callAgent = async (method: string, payloadParams: any) => { |
| return fetch(agentEndpoint, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| jsonrpc: "2.0", |
| method: method, |
| params: payloadParams, |
| id: Date.now() |
| }) |
| }); |
| }; |
|
|
| const sendAgentMessage = async () => { |
| if (!agentInput.trim() || agentThinking) return; |
| setAgentMessages(prev =>[...prev, {role: 'user', content: agentInput}]); |
| const currentInput = agentInput; |
| setAgentInput(''); |
| setAgentThinking(true); |
| |
| try { |
| const fullAgentConfig = { |
| ...agentConfig, |
| provider: modelProvider === 'gcloud' ? 'vertex' : modelProvider, |
| api_key: apiKey, |
| base_url: baseUrl, |
| project_id: projectId, |
| location: location, |
| model_name: modelName, |
| reasoning_method: reasoningMethod, |
| prompt_template: promptTemplate |
| }; |
|
|
| let res = await callAgent(agentMethod, { input: currentInput, agent_config: fullAgentConfig }); |
| let data = await res.json(); |
|
|
| if (data.error && data.error.code === -32601 && agentMethod === 'agent.process') { |
| res = await callAgent('agent.generate', { input: currentInput, agent_config: fullAgentConfig }); |
| data = await res.json(); |
| if (!data.error) { |
| setAgentMethod('agent.generate'); |
| } |
| } |
|
|
| let reply = "Agent sent no text."; |
| if (data.error) { |
| reply = `Agent Error: ${data.error.message || JSON.stringify(data.error)}`; |
| } else if (data.result) { |
| if (typeof data.result === 'string') reply = data.result; |
| else if (data.result.text) reply = data.result.text; |
| else if (data.result.content) reply = data.result.content; |
| else reply = JSON.stringify(data.result); |
| |
| if (data.result.update_config) { |
| const cfg = data.result.update_config; |
| if (cfg.provider) setModelProvider(cfg.provider); |
| if (cfg.api_key) setApiKey(cfg.api_key); |
| if (cfg.project_id) setProjectId(cfg.project_id); |
| } |
| } |
|
|
| setAgentMessages(prev =>[...prev, {role: 'agent', content: reply}]); |
| if (currentInput.toLowerCase().includes("queue") || currentInput.includes("http")) { |
| setTimeout(() => setRefreshTrigger(p => p+1), 2000); |
| } |
|
|
| } catch (e: any) { |
| setAgentMessages(prev =>[...prev, {role: 'agent', content: `Connection Error: ${e.message}.`}]); |
| } finally { |
| setAgentThinking(false); |
| } |
| }; |
|
|
| return ( |
| <div className="flex h-screen w-full bg-[#09090b] text-slate-200 font-sans overflow-hidden"> |
| <datalist id="modelSuggestions"> |
| <option value="gemini-1.5-pro" /> |
| <option value="gemini-1.5-flash" /> |
| <option value="gemini-2.0-flash" /> |
| <option value="qwen3" /> |
| <option value="gpt-oss" /> |
| <option value="kimi" /> |
| <option value="glm-4.7" /> |
| <option value="minimax-m2" /> |
| <option value="glm-v" /> |
| <option value="gemma3" /> |
| </datalist> |
| |
| {/* SIDEBAR */} |
| <div className="w-[280px] flex flex-col border-r border-slate-800/60 bg-[#0c0c0e]"> |
| <div className="h-16 flex items-center px-6 border-b border-slate-800/60"> |
| <h1 className="text-sm font-bold text-white">vChat <span className="text-slate-500">Manager</span></h1> |
| </div> |
| <div className="flex-1 p-4 space-y-1"> |
| {[ |
| {id:'home', l:'Home & Benchmarks', i:Home}, |
| {id:'agent', l:'Agent Nexus', i:Bot}, |
| {id:'queue', l:'Ingest Queue', i:List}, |
| {id:'profiles', l:'User Profiles', i:Users}, |
| {id:'manual', l:'Labeling Studio', i:PenTool}, |
| {id:'dataset', l:'Data Manager', i:Archive}, |
| {id:'groundtruth', l:'Ground Truth (Verified)', i:ShieldCheck}, |
| {id:'analytics', l:'Analytics', i:BarChart2} |
| ].map(t => ( |
| <button key={t.id} onClick={() => setActiveTab(t.id)} |
| className={`w-full flex items-center gap-3 px-4 py-3 text-xs font-medium rounded-lg ${activeTab===t.id ? 'bg-indigo-600/20 text-indigo-300' : 'text-slate-500 hover:bg-white/5'}`}> |
| <t.i className="w-4 h-4" /> {t.l} |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| {/* MAIN CONTENT */} |
| <div className="flex-1 flex flex-col bg-[#09090b] overflow-hidden"> |
| <div className="h-16 border-b border-slate-800/60 flex items-center px-8 bg-[#09090b]"> |
| <span className="text-sm font-bold text-white uppercase tracking-wider">{activeTab}</span> |
| </div> |
| |
| <div className="flex-1 p-6 overflow-hidden flex flex-col"> |
| |
| {/* HOME TAB */} |
| {activeTab === 'home' && ( |
| <div className="h-full overflow-y-auto space-y-8 max-w-5xl pr-2"> |
| |
| {/* QUICK DEMO SECTION */} |
| <div className="bg-slate-900/50 border border-slate-800 rounded-xl p-6 shadow-sm"> |
| <div className="flex justify-between items-start mb-4"> |
| <div> |
| <h2 className="text-xl font-bold text-white flex items-center gap-2"> |
| <Zap className="w-5 h-5 text-amber-400" /> Try LiarMP4 - Quick Demo |
| </h2> |
| <p className="text-sm text-slate-400 mt-1"> |
| Test the Multimodal Factuality Pipeline on a single video URL. |
| </p> |
| </div> |
| <button |
| onClick={() => setShowDemoConfig(!showDemoConfig)} |
| className="text-xs font-bold text-slate-500 hover:text-slate-300 flex items-center gap-1 bg-slate-950 px-3 py-1.5 rounded border border-slate-800" |
| > |
| <Settings className="w-4 h-4" /> |
| {showDemoConfig ? "Hide Config" : "Show Config"} |
| </button> |
| </div> |
| |
| {showDemoConfig && ( |
| <div className="bg-slate-950 p-4 rounded-lg border border-slate-800 mb-4 grid grid-cols-2 gap-6 animate-in fade-in zoom-in-95 duration-200"> |
| <div className="space-y-3"> |
| <label className="text-[10px] text-slate-500 uppercase font-bold block border-b border-slate-800 pb-1">LLM Provider Config</label> |
| <select value={modelProvider} onChange={e => setModelProvider(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white"> |
| <option value="vertex">Vertex AI (Enterprise)</option> |
| <option value="gemini">Gemini API (Public)</option> |
| <option value="gcloud">Google Cloud (Project + API Key)</option> |
| <option value="nrp">NRP (Nautilus Envoy Gateway)</option> |
| </select> |
| |
| {modelProvider === 'nrp' && ( |
| <> |
| <input value={baseUrl} onChange={e => setBaseUrl(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white" placeholder="Base URL"/> |
| <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white" placeholder="API Token"/> |
| </> |
| )} |
| {modelProvider === 'gemini' && ( |
| <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white" placeholder="API Key"/> |
| )} |
| {(modelProvider === 'vertex' || modelProvider === 'gcloud') && ( |
| <> |
| <input value={projectId} onChange={e => setProjectId(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white" placeholder="Project ID"/> |
| <input value={location} onChange={e => setLocation(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white" placeholder="Location"/> |
| {modelProvider === 'gcloud' && <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white" placeholder="API Key"/>} |
| </> |
| )} |
| <input list="modelSuggestions" value={modelName} onChange={e => setModelName(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white" placeholder="Model Name (e.g. gemini-1.5-pro)"/> |
| </div> |
| <div className="space-y-3"> |
| <label className="text-[10px] text-slate-500 uppercase font-bold block border-b border-slate-800 pb-1">Inference Strategy</label> |
| <select value={reasoningMethod} onChange={e => setReasoningMethod(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white"> |
| <option value="none">Direct (No CoT)</option> |
| <option value="cot">Standard Chain of Thought</option> |
| <option value="fcot">Fractal Chain of Thought</option> |
| </select> |
| <select value={promptTemplate} onChange={e => setPromptTemplate(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white"> |
| {availablePrompts.length > 0 ? availablePrompts.map(p => ( |
| <option key={p.id} value={p.id}>{p.name}</option> |
| )) : <option value="standard">Standard</option>} |
| </select> |
| |
| <label className="text-[10px] text-slate-500 uppercase font-bold block border-b border-slate-800 pb-1 mt-3">Agentic Tools</label> |
| <label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer"> |
| <input type="checkbox" className="accent-indigo-500" checked={useSearch} onChange={e => setUseSearch(e.target.checked)} /> |
| Enable Web Search Retrieval |
| </label> |
| <label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer"> |
| <input type="checkbox" className="accent-indigo-500" checked={useCode} onChange={e => setUseCode(e.target.checked)} /> |
| Enable Code Execution |
| </label> |
| </div> |
| </div> |
| )} |
| |
| <div className="flex gap-2 mb-4"> |
| <input |
| type="text" |
| value={demoLink} |
| onChange={(e) => setDemoLink(e.target.value)} |
| placeholder="Enter X/Twitter Video URL here..." |
| className="flex-1 bg-slate-950 border border-slate-700 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-indigo-500" |
| disabled={demoIsProcessing} |
| /> |
| <button |
| onClick={runDemo} |
| disabled={demoIsProcessing || !demoLink} |
| className="bg-indigo-600 hover:bg-indigo-500 disabled:bg-slate-700 text-white px-6 py-3 rounded-lg font-bold flex items-center gap-2 transition" |
| > |
| {demoIsProcessing ? <RefreshCw className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4 fill-white" />} |
| {demoIsProcessing ? "Processing..." : "Analyze"} |
| </button> |
| </div> |
| |
| {(demoIsProcessing || demoLogs) && !demoResult && ( |
| <div className="bg-black border border-slate-800 rounded-lg p-4 font-mono text-[10px] text-emerald-500 h-64 overflow-y-auto whitespace-pre-wrap animate-in fade-in duration-300" ref={demoLogContainerRef}> |
| {demoLogs || "Initializing pipeline..."} |
| </div> |
| )} |
| |
| {demoResult && ( |
| <div className="mt-6 border-t border-slate-800 pt-6 animate-in fade-in slide-in-from-bottom-4 duration-500"> |
| <div className="flex justify-between items-start mb-6"> |
| <div> |
| <h3 className="text-xl font-bold text-white flex items-center gap-2"> |
| <ShieldCheck className="w-6 h-6 text-emerald-400" /> |
| <span className="text-indigo-400 uppercase tracking-wider text-[16px] bg-indigo-500/10 px-2 py-0.5 rounded border border-indigo-500/20">[{demoResult.config_model || 'AI'}] |
| </span> |
| Analysis Complete |
| </h3> |
| <div className="text-xs text-slate-400 mt-2 font-mono"> |
| ID: {demoResult.id} | Prompt: {demoResult.config_prompt} | Reasoning: {demoResult.config_reasoning} |
| </div> |
| </div> |
| <div className="bg-slate-950 border border-slate-800 px-6 py-2 rounded-lg text-center"> |
| <div className="text-[10px] uppercase text-slate-500 font-bold mb-1">Final Score</div> |
| <div className={`text-4xl font-bold font-mono ${demoResult.final_veracity_score < 50 ? 'text-red-400' : 'text-emerald-400'}`}> |
| {demoResult.final_veracity_score} |
| </div> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-3 gap-6 mb-6"> |
| <div className="col-span-2 space-y-4"> |
| <div className="bg-slate-950 p-4 rounded-lg border border-slate-800"> |
| <div className="text-xs font-bold text-indigo-400 uppercase mb-2">Video Context</div> |
| <div className="text-sm text-slate-300 italic leading-relaxed">"{demoResult.caption || 'No specific context found'}"</div> |
| </div> |
| |
| <div className="bg-slate-950 p-4 rounded-lg border border-slate-800"> |
| <div className="text-xs font-bold text-amber-400 uppercase mb-2">AI Reasoning</div> |
| <div className="text-sm text-slate-300 whitespace-pre-wrap leading-relaxed">{demoResult.reasoning || 'No reasoning provided'}</div> |
| </div> |
| |
| {demoResult.tags && ( |
| <div className="flex flex-wrap gap-2"> |
| {demoResult.tags.split(',').map((t: string) => ( |
| <span key={t} className="px-2 py-1 bg-slate-800 border border-slate-700 rounded text-xs text-slate-400 font-mono">{t.trim()}</span> |
| ))} |
| </div> |
| )} |
| </div> |
| |
| <div className="space-y-4"> |
| <div className="bg-slate-950 p-4 rounded-lg border border-slate-800"> |
| <div className="text-xs font-bold text-sky-400 uppercase mb-3">Veracity Vectors</div> |
| <div className="space-y-2"> |
| {[ |
| { label: 'Visual Integrity', score: parseFloat(demoResult.visual_score) || 0 }, |
| { label: 'Audio Integrity', score: parseFloat(demoResult.audio_score) || 0 }, |
| { label: 'Source Credibility', score: parseFloat(demoResult.source_score) || 0 }, |
| { label: 'Logical Consistency', score: parseFloat(demoResult.logic_score) || 0 }, |
| { label: 'Emotional Manip.', score: parseFloat(demoResult.emotion_score) || 0 }, |
| ].map((v) => ( |
| <div key={v.label} className="flex justify-between items-center text-xs"> |
| <span className="text-slate-400 w-32 truncate">{v.label}</span> |
| <div className="flex items-center gap-2 flex-1 ml-2"> |
| <div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden"> |
| <div className={`h-full ${v.score < 5 ? 'bg-red-400' : v.score < 8 ? 'bg-amber-400' : 'bg-emerald-400'}`} style={{ width: `${(v.score / 10) * 100}%` }} /> |
| </div> |
| <span className="font-mono text-slate-300 w-6 text-right">{v.score}</span> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| <div className="bg-slate-950 p-4 rounded-lg border border-slate-800"> |
| <div className="text-xs font-bold text-pink-400 uppercase mb-3">Modality Alignment</div> |
| <div className="space-y-2"> |
| {[ |
| { label: 'Video ↔ Audio', score: parseFloat(demoResult.align_video_audio) || 0 }, |
| { label: 'Video ↔ Caption', score: parseFloat(demoResult.align_video_caption) || 0 }, |
| { label: 'Audio ↔ Caption', score: parseFloat(demoResult.align_audio_caption) || 0 }, |
| ].map((v) => ( |
| <div key={v.label} className="flex justify-between items-center text-xs"> |
| <span className="text-slate-400 w-32 truncate">{v.label}</span> |
| <div className="flex items-center gap-2 flex-1 ml-2"> |
| <div className="flex-1 h-1.5 bg-slate-800 rounded-full overflow-hidden"> |
| <div className={`h-full ${v.score < 5 ? 'bg-red-400' : v.score < 8 ? 'bg-amber-400' : 'bg-emerald-400'}`} style={{ width: `${(v.score / 10) * 100}%` }} /> |
| </div> |
| <span className="font-mono text-slate-300 w-6 text-right">{v.score}</span> |
| </div> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div className="flex justify-end"> |
| <button onClick={() => {setDemoResult(null); setDemoLogs(''); setDemoLink('');}} className="text-xs text-slate-500 hover:text-white flex items-center gap-1 border border-slate-800 px-3 py-1.5 rounded bg-slate-950"> |
| <RotateCcw className="w-3 h-3"/> Reset Demo |
| </button> |
| </div> |
| </div> |
| )} |
| </div> |
| |
| {/* EXISTING HOME TAB CONTENT */} |
| <div className="grid grid-cols-3 gap-6"> |
| <div className="col-span-2 bg-slate-900/50 border border-slate-800 rounded-xl p-6"> |
| <h2 className="text-xl font-bold text-white mb-4">Philosophy & Methodology</h2> |
| <p className="text-sm text-slate-400 mb-4"> |
| The goal of this research is to test various predictive models, generative AI models, prompting techniques, and agents against a rigorous <strong>Ground Truth</strong> standard. |
| </p> |
| <div className="grid grid-cols-2 gap-4 mt-6"> |
| <div className="p-4 bg-black/50 border border-slate-800 rounded-lg"> |
| <div className="flex items-center gap-2 text-indigo-400 font-bold text-xs uppercase mb-2"> |
| <Calculator className="w-3 h-3"/> AI Generated Label Score |
| </div> |
| <div className="text-[10px] font-mono text-slate-400 leading-relaxed"> |
| Formula to calculate the overall veracity score for each post:<br/><br/> |
| <code className="text-indigo-300 bg-indigo-950/30 p-1 rounded block mb-2">S_final = ((w1*V_vis + w2*V_aud + w3*V_src) / 3) * min(1, M_vc / 5) * 10</code> |
| <span className="text-slate-500 italic mt-2 block">Applies the Recontextualization Penalty based on Alignment metrics. Stored in dataset.csv.</span> |
| </div> |
| </div> |
| <div className="p-4 bg-black/50 border border-slate-800 rounded-lg"> |
| <div className="flex items-center gap-2 text-emerald-400 font-bold text-xs uppercase mb-2"> |
| <Target className="w-3 h-3"/> AI Config Accuracy Formula |
| </div> |
| <div className="text-[10px] font-mono text-slate-400 leading-relaxed"> |
| Formula to calculate the overall score for how accurate the config combination is (based on difference against Ground Truth):<br/><br/> |
| <code className="text-emerald-300 bg-emerald-950/30 p-1 rounded block mb-1">Composite MAE = Mean Absolute Error across all 8 sub-vectors</code> |
| <code className="text-emerald-300 bg-emerald-950/30 p-1 rounded block">Binary Accuracy = Σ(Predict_bin == GT_bin) / N * 100</code> |
| <span className="text-slate-500 italic mt-2 block">Evaluates agent parameters & prompt efficacy against comprehensive factors.</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| <div className="bg-indigo-900/20 border border-indigo-500/30 rounded-xl p-6 flex flex-col justify-center items-center"> |
| <div className="text-xs uppercase text-indigo-400 font-bold mb-2">Ground Truth Accuracy</div> |
| {benchmarks ? ( |
| <> |
| <div className="text-5xl font-mono font-bold text-white mb-2">{benchmarks.accuracy_percent}%</div> |
| <div className="text-xs text-slate-500 mb-1">Comp MAE: {benchmarks.mae} points</div> |
| <div className="text-[10px] text-emerald-400 font-bold mb-2">Tag Acc: {benchmarks.tag_accuracy_percent}%</div> |
| <div className="text-xs text-slate-500 mt-2">{benchmarks.count} verified samples</div> |
| </> |
| ) : ( |
| <span className="text-slate-600">No data found</span> |
| )} |
| </div> |
| </div> |
| |
| {/* Configuration Leaderboard */} |
| <div className="bg-slate-900/50 border border-slate-800 rounded-xl p-6"> |
| <h3 className="text-sm font-bold text-white uppercase mb-4 flex items-center gap-2"> |
| <Trophy className="w-4 h-4 text-amber-400"/> Automated Hill Climbing (Leaderboard) |
| </h3> |
| <div className="overflow-x-auto"> |
| <table className="w-full text-left text-xs text-slate-400"> |
| <thead className="bg-slate-950 text-slate-500 uppercase"> |
| <tr> |
| <th className="p-3">Type</th> |
| <th className="p-3">Model</th> |
| <th className="p-3">Prompt</th> |
| <th className="p-3">Reasoning</th> |
| <th className="p-3 text-center">Tools</th> |
| <th className="p-3 text-center">FCoT Depth</th> |
| <th className="p-3 text-right text-emerald-400">Accuracy</th> |
| <th className="p-3 text-right">Comp. MAE</th> |
| <th className="p-3 text-right">Tag Acc</th> |
| <th className="p-3"></th> |
| </tr> |
| </thead> |
| <tbody className="divide-y divide-slate-800"> |
| {leaderboard && leaderboard.map((row, i) => ( |
| <tr key={i} className="hover:bg-white/5"> |
| <td className="p-3 font-mono text-xs">{row.type === 'GenAI' ? <span className="text-indigo-400">GenAI</span> : <span className="text-pink-400">Pred</span>}</td> |
| <td className="p-3 font-mono text-white">{row.model}</td> |
| <td className="p-3">{row.prompt}</td> |
| <td className="p-3 uppercase text-[10px]">{row.reasoning}</td> |
| <td className="p-3 text-center text-sky-400 font-mono text-[10px]">{row.tools || 'None'}</td> |
| <td className="p-3 text-center text-slate-400 font-mono">{row.fcot_depth ?? 0}</td> |
| <td className="p-3 text-right font-bold text-emerald-400">{row.accuracy}%</td> |
| <td className="p-3 text-right font-mono text-amber-400">{row.comp_mae}</td> |
| <td className="p-3 text-right">{row.tag_acc}%</td> |
| <td className="p-3 text-center"> |
| <div className="group relative"> |
| <HelpCircle className="w-4 h-4 text-slate-600 cursor-help"/> |
| <div className="absolute right-0 bottom-6 w-64 p-3 bg-black border border-slate-700 rounded shadow-xl hidden group-hover:block z-50 text-[10px] whitespace-pre-wrap text-left"> |
| <div className="font-bold mb-1 text-slate-400">Config Params</div> |
| <div>{row.params}</div> |
| <div className="mt-2 pt-2 border-t border-slate-800 text-slate-400 font-bold">Samples: {row.samples}</div> |
| </div> |
| </div> |
| </td> |
| </tr> |
| ))} |
| {(!leaderboard || leaderboard.length === 0) && ( |
| <tr><td colSpan={10} className="p-4 text-center text-slate-600">No benchmark data available.</td></tr> |
| )} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| |
| {/* Detailed Vector Accuracies */} |
| <div className="bg-slate-900/50 border border-slate-800 rounded-xl p-6 mt-6 mb-8"> |
| <h3 className="text-sm font-bold text-white uppercase mb-4 flex items-center gap-2"> |
| <BarChart2 className="w-4 h-4 text-sky-400"/> Detailed Vector Error Analysis (MAE) |
| </h3> |
| <div className="overflow-x-auto"> |
| <table className="w-full text-left text-xs text-slate-400"> |
| <thead className="bg-slate-950 text-slate-500 uppercase"> |
| <tr> |
| <th className="p-3">Model</th> |
| <th className="p-3">Prompt</th> |
| <th className="p-3">Reasoning</th> |
| <th className="p-3">Tools / Techniques</th> |
| <th className="p-3 text-right">Vis</th> |
| <th className="p-3 text-right">Aud</th> |
| <th className="p-3 text-right">Src</th> |
| <th className="p-3 text-right">Log</th> |
| <th className="p-3 text-right">Emo</th> |
| <th className="p-3 text-right">V-A</th> |
| <th className="p-3 text-right">V-C</th> |
| <th className="p-3 text-right">A-C</th> |
| </tr> |
| </thead> |
| <tbody className="divide-y divide-slate-800"> |
| {leaderboard && leaderboard.map((row, i) => ( |
| <tr key={i} className="hover:bg-white/5"> |
| <td className="p-3 font-mono text-white">{row.model}</td> |
| <td className="p-3">{row.prompt}</td> |
| <td className="p-3 uppercase text-[10px]">{row.reasoning}</td> |
| <td className="p-3 text-sky-400 font-mono text-[10px]">{row.tools || 'None'}</td> |
| <td className="p-3 text-right font-mono">{row.err_visual_score ?? '-'}</td> |
| <td className="p-3 text-right font-mono">{row.err_audio_score ?? '-'}</td> |
| <td className="p-3 text-right font-mono">{row.err_source_score ?? '-'}</td> |
| <td className="p-3 text-right font-mono">{row.err_logic_score ?? '-'}</td> |
| <td className="p-3 text-right font-mono">{row.err_emotion_score ?? '-'}</td> |
| <td className="p-3 text-right font-mono">{row.err_align_video_audio ?? '-'}</td> |
| <td className="p-3 text-right font-mono">{row.err_align_video_caption ?? '-'}</td> |
| <td className="p-3 text-right font-mono">{row.err_align_audio_caption ?? '-'}</td> |
| </tr> |
| ))} |
| {(!leaderboard || leaderboard.length === 0) && ( |
| <tr><td colSpan={12} className="p-4 text-center text-slate-600">No detailed benchmark data available.</td></tr> |
| )} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'agent' && ( |
| <div className="flex h-full gap-6"> |
| <div className="w-1/3 overflow-y-auto pr-2 flex flex-col gap-4"> |
| <div className="bg-slate-900/50 border border-slate-800 rounded-xl p-6 flex flex-col"> |
| <h2 className="text-lg font-bold text-white flex items-center gap-2 mb-4"> |
| <BrainCircuit className="w-5 h-5 text-indigo-400"/> Agent Configuration |
| </h2> |
| <div className="text-xs text-slate-400 mb-6"> |
| This interface interacts with the <strong>LiarMP4 Agent</strong> running on the Google Cloud Agent Development Kit (ADK) via the A2A Protocol. You can instruct the agent to change configs directly in chat. |
| </div> |
| <div className="bg-slate-950 p-4 rounded border border-slate-800"> |
| <div className="text-[10px] uppercase text-slate-500 font-bold mb-2 flex items-center gap-2"><Settings className="w-3 h-3"/> Connection</div> |
| <div className="space-y-2"> |
| <label className="text-[10px] text-slate-400">Agent Endpoint URL</label> |
| <input |
| value={agentEndpoint} |
| onChange={e => setAgentEndpoint(e.target.value)} |
| className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white font-mono placeholder-slate-600" |
| placeholder="e.g. /a2a" |
| /> |
| </div> |
| </div> |
| </div> |
| |
| <div className="bg-slate-900/50 p-4 rounded-xl border border-slate-800"> |
| <div className="text-[10px] uppercase text-slate-500 font-bold mb-2 flex items-center gap-2"><Settings className="w-3 h-3"/> Inference Config</div> |
| <div className="space-y-2"> |
| <label className="text-[10px] text-slate-500">Provider</label> |
| <select value={modelProvider} onChange={e => setModelProvider(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white"> |
| <option value="vertex">Vertex AI (Enterprise)</option> |
| <option value="gemini">Gemini API (Public)</option> |
| <option value="gcloud">Google Cloud (Project + API Key)</option> |
| <option value="nrp">NRP (Nautilus Envoy Gateway)</option> |
| </select> |
| |
| {modelProvider === 'vertex' && ( |
| <> |
| <input value={projectId} onChange={e => setProjectId(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="gcp-project-id"/> |
| <input value={location} onChange={e => setLocation(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="us-central1"/> |
| </> |
| )} |
| {modelProvider === 'gemini' && ( |
| <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="API Key"/> |
| )} |
| {modelProvider === 'gcloud' && ( |
| <> |
| <input value={projectId} onChange={e => setProjectId(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="Project Name / ID"/> |
| <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="API Key"/> |
| <input value={location} onChange={e => setLocation(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="us-central1"/> |
| </> |
| )} |
| {modelProvider === 'nrp' && ( |
| <> |
| <div className="space-y-1"> |
| <input value={baseUrl} onChange={e => setBaseUrl(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="https://ellm.nrp-nautilus.io/v1"/> |
| </div> |
| <div className="space-y-1"> |
| <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="NRP API Token"/> |
| </div> |
| </> |
| )} |
| |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Model Name</label> |
| <input list="modelSuggestions" value={modelName} onChange={e => setModelName(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="Model Name"/> |
| </div> |
| |
| <div className="space-y-1 mt-2"> |
| <label className="text-[10px] text-slate-500">Reasoning Method</label> |
| <select value={reasoningMethod} onChange={e => setReasoningMethod(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white"> |
| <option value="none">Direct (No CoT)</option> |
| <option value="cot">Standard Chain of Thought</option> |
| <option value="fcot">Fractal Chain of Thought</option> |
| </select> |
| </div> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Prompt Persona</label> |
| <select value={promptTemplate} onChange={e => setPromptTemplate(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white"> |
| {availablePrompts.length > 0 ? availablePrompts.map(p => ( |
| <option key={p.id} value={p.id}>{p.name}</option> |
| )) : <option value="standard">Standard</option>} |
| </select> |
| </div> |
| </div> |
| </div> |
|
|
| <div className="bg-slate-900/50 p-4 rounded-xl border border-slate-800"> |
| <div className="text-[10px] uppercase text-slate-500 font-bold mb-2 flex items-center gap-2"><Zap className="w-3 h-3"/> Agent Capabilities</div> |
| <ul className="text-xs text-slate-400 space-y-1 pl-4 list-disc"> |
| <li>Process raw video & audio modalities via A2A</li> |
| <li>Fetch & analyze comment sentiment and community context</li> |
| <li>Run full Factuality pipeline (FCoT) & Generate Veracity Vectors</li> |
| <li>Automatically save raw AI Labeled JSON files & sync to Data Manager</li> |
| <li>Verify and compare AI outputs against Ground Truth</li> |
| <li>Reprompt dynamically for missing scores or incomplete data</li> |
| </ul> |
| </div> |
| |
| <div className="bg-slate-900/50 p-4 rounded-xl border border-slate-800"> |
| <div className="text-[10px] uppercase text-slate-500 font-bold mb-2 flex items-center gap-2"><Cpu className="w-3 h-3"/> Active Tools</div> |
| <div className="space-y-2"> |
| <label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer"> |
| <input type="checkbox" className="accent-indigo-500" checked={agentConfig.use_search} onChange={e => setAgentConfig({...agentConfig, use_search: e.target.checked})} /> |
| Enable Google Search Retrieval |
| </label> |
| <label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer"> |
| <input type="checkbox" className="accent-indigo-500" checked={agentConfig.use_code} onChange={e => setAgentConfig({...agentConfig, use_code: e.target.checked})} /> |
| Enable Code Execution Environment |
| </label> |
| </div> |
| </div> |
| |
| <div className="bg-slate-900/50 p-4 rounded-xl border border-slate-800 mb-6"> |
| <div className="text-[10px] uppercase text-slate-500 font-bold mb-2 flex items-center gap-2"><MessageSquare className="w-3 h-3"/> Quick Commands</div> |
| <div className="flex flex-col gap-2"> |
| <button onClick={() => setAgentInput("Set provider to gemini")} className="text-left text-[10px] text-indigo-400 hover:text-indigo-300 bg-indigo-900/20 p-2 rounded border border-indigo-500/30">"Set provider to Gemini"</button> |
| <button onClick={() => setAgentInput("Run full pipeline on ")} className="text-left text-[10px] text-indigo-400 hover:text-indigo-300 bg-indigo-900/20 p-2 rounded border border-indigo-500/30">"Run full pipeline on [URL]"</button> |
| </div> |
| </div> |
| </div> |
|
|
| <div className="flex-1 bg-slate-900/50 border border-slate-800 rounded-xl flex flex-col overflow-hidden"> |
| <div className="p-4 border-b border-slate-800 bg-slate-950/50"> |
| <div className="text-xs font-bold text-white">Agent Interaction (A2A)</div> |
| </div> |
| <div className="flex-1 p-4 overflow-y-auto space-y-4"> |
| {agentMessages.map((m, i) => ( |
| <div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}> |
| <div className={`max-w-[80%] p-3 rounded-lg text-xs ${m.role === 'user' ? 'bg-indigo-600 text-white' : 'bg-slate-800 text-slate-300'} whitespace-pre-wrap`}> |
| {m.content} |
| </div> |
| </div> |
| ))} |
| {agentThinking && ( |
| <div className="flex justify-start"> |
| <div className="max-w-[80%] p-3 rounded-lg text-xs bg-slate-800 text-slate-300 animate-pulse"> |
| Processing request through pipeline... |
| </div> |
| </div> |
| )} |
| </div> |
| <div className="p-4 bg-slate-950 border-t border-slate-800 flex gap-2"> |
| <input |
| value={agentInput} |
| onChange={e => setAgentInput(e.target.value)} |
| onKeyDown={e => e.key === 'Enter' && sendAgentMessage()} |
| className="flex-1 bg-slate-900 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-500" |
| placeholder="Message the agent (e.g., 'Analyze this video: https://...')" |
| disabled={agentThinking} |
| /> |
| <button onClick={sendAgentMessage} disabled={agentThinking} className="bg-indigo-600 hover:bg-indigo-500 disabled:bg-slate-700 text-white p-2 rounded"> |
| <ArrowUpRight className="w-4 h-4"/> |
| </button> |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'queue' && ( |
| <div className="flex h-full gap-6"> |
| <div className="w-[300px] bg-slate-900/50 border border-slate-800 rounded-xl p-4 flex flex-col gap-4 overflow-y-auto"> |
| <div className="bg-slate-950 border border-slate-800 rounded p-3"> |
| <label className="text-[10px] text-slate-500 uppercase font-bold mb-2 block">Quick Ingest</label> |
| <div className="flex gap-2"> |
| <input |
| value={singleLinkInput} |
| onChange={e => setSingleLinkInput(e.target.value)} |
| className="flex-1 bg-slate-900 border border-slate-700 rounded p-1.5 text-xs text-white placeholder-slate-600" |
| placeholder="https://x.com/..." |
| /> |
| <button onClick={addSingleLink} className="bg-indigo-600 hover:bg-indigo-500 text-white rounded p-1.5"> |
| <Plus className="w-4 h-4" /> |
| </button> |
| </div> |
| </div> |
| |
| <div className="text-xs font-bold text-indigo-400 uppercase">Config</div> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Provider</label> |
| <select value={modelProvider} onChange={e => setModelProvider(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white"> |
| <option value="vertex">Vertex AI (Enterprise)</option> |
| <option value="gemini">Gemini API (Public)</option> |
| <option value="gcloud">Google Cloud (Project + API Key)</option> |
| <option value="nrp">NRP (Nautilus Envoy Gateway)</option> |
| </select> |
| </div> |
| {modelProvider === 'vertex' && ( |
| <> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Project ID</label> |
| <input value={projectId} onChange={e => setProjectId(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white" placeholder="gcp-project-id"/> |
| </div> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Location</label> |
| <input value={location} onChange={e => setLocation(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white" placeholder="us-central1"/> |
| </div> |
| </> |
| )} |
| {modelProvider === 'gemini' && ( |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">API Key</label> |
| <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white" placeholder="AIzaSy..."/> |
| </div> |
| )} |
| {modelProvider === 'gcloud' && ( |
| <> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Project Name / ID</label> |
| <input value={projectId} onChange={e => setProjectId(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white" placeholder="gcp-project-id"/> |
| </div> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">API Key</label> |
| <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white" placeholder="AIzaSy..."/> |
| </div> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Location</label> |
| <input value={location} onChange={e => setLocation(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white" placeholder="us-central1"/> |
| </div> |
| </> |
| )} |
| {modelProvider === 'nrp' && ( |
| <> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">API Base URL</label> |
| <input value={baseUrl} onChange={e => setBaseUrl(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="https://ellm.nrp-nautilus.io/v1"/> |
| </div> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">API Key</label> |
| <input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white placeholder-slate-600" placeholder="NRP API Token"/> |
| </div> |
| </> |
| )} |
|
|
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Model Name</label> |
| <input list="modelSuggestions" value={modelName} onChange={e => setModelName(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white" placeholder="Model Name"/> |
| </div> |
|
|
| <div className="space-y-1 mt-2"> |
| <label className="text-[10px] text-slate-500">Reasoning Method</label> |
| <select value={reasoningMethod} onChange={e => setReasoningMethod(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white"> |
| <option value="none">Direct (No CoT)</option> |
| <option value="cot">Standard Chain of Thought</option> |
| <option value="fcot">Fractal Chain of Thought</option> |
| </select> |
| </div> |
| <div className="space-y-1"> |
| <label className="text-[10px] text-slate-500">Prompt Persona</label> |
| <select value={promptTemplate} onChange={e => setPromptTemplate(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded p-2 text-xs text-white"> |
| {availablePrompts.length > 0 ? availablePrompts.map(p => ( |
| <option key={p.id} value={p.id}>{p.name}</option> |
| )) : <option value="standard">Standard</option>} |
| </select> |
| </div> |
| |
| <div className="space-y-2 mt-2"> |
| <label className="text-[10px] text-slate-500 uppercase font-bold block border-b border-slate-800 pb-1">Agentic Tools</label> |
| <label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer"> |
| <input type="checkbox" className="accent-indigo-500" checked={useSearch} onChange={e => setUseSearch(e.target.checked)} /> |
| Enable Web Search Retrieval |
| </label> |
| <label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer"> |
| <input type="checkbox" className="accent-indigo-500" checked={useCode} onChange={e => setUseCode(e.target.checked)} /> |
| Enable Code Execution |
| </label> |
| </div> |
| |
| {} |
| {isProcessing ? ( |
| <button onClick={stopProcessing} className="w-full py-2 bg-red-600 hover:bg-red-500 text-white rounded font-bold text-xs flex items-center justify-center gap-2 animate-pulse"> |
| <StopCircle className="w-3 h-3"/> STOP PROCESSING |
| </button> |
| ) : ( |
| <button onClick={startProcessing} className="w-full py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded font-bold text-xs flex items-center justify-center gap-2"> |
| <Play className="w-3 h-3"/> Start Batch |
| </button> |
| )} |
| |
| <div className="flex gap-2"> |
| <button onClick={clearProcessed} className="flex-1 py-2 bg-slate-700 hover:bg-slate-600 text-slate-300 rounded font-bold text-[10px] flex items-center justify-center gap-1"> |
| <Trash2 className="w-3 h-3"/> Clear Done |
| </button> |
| <button onClick={requeueItems} className="flex-1 py-2 bg-sky-900/50 hover:bg-sky-900 text-sky-300 border border-sky-900 rounded font-bold text-[10px] flex items-center justify-center gap-1"> |
| <RotateCcw className="w-3 h-3"/> Requeue Sel. |
| </button> |
| <button onClick={deleteQueueItems} className="flex-1 py-2 bg-red-900/50 hover:bg-red-900 text-red-300 border border-red-900 rounded font-bold text-[10px] flex items-center justify-center gap-1"> |
| <Trash2 className="w-3 h-3"/> Delete Sel. |
| </button> |
| </div> |
| </div> |
| <div className="flex-1 flex flex-col gap-4 overflow-hidden"> |
| <div className="flex-1 bg-slate-900/30 border border-slate-800 rounded-xl overflow-auto"> |
| <table className="w-full text-left text-xs text-slate-400 select-none"> |
| <thead className="bg-slate-950 sticky top-0"> |
| <tr> |
| <th className="p-3 w-8"><Square className="w-4 h-4 text-slate-600"/></th> |
| <th className="p-3">Type</th> |
| <th className="p-3">Link</th> |
| <th className="p-3">Status</th> |
| </tr> |
| </thead> |
| <tbody> |
| {queueList.map((q, i, arr) => ( |
| <React.Fragment key={i}> |
| <tr |
| className={`border-t border-slate-800/50 hover:bg-white/5 ${selectedQueueItems.has(q.link) ? 'bg-indigo-900/20' : ''}`} |
| onMouseDown={(e) => { |
| isDraggingQueueRef.current = true; |
| toggleQueueSelection(e, q.link, i, arr); |
| }} |
| onMouseEnter={() => { |
| if (isDraggingQueueRef.current && !selectedQueueItems.has(q.link)) { |
| setSelectedQueueItems(prev => new Set(prev).add(q.link)); |
| setLastQueueIndex(i); |
| } |
| }} |
| > |
| <td className="p-3 cursor-pointer"> |
| {selectedQueueItems.has(q.link) ? <CheckSquare className="w-4 h-4 text-indigo-400"/> : <Square className="w-4 h-4 text-slate-600"/>} |
| </td> |
| <td className="p-3 font-bold text-[10px]">{q.task_type === 'Verify' ? <span className="text-amber-400">VERIFY</span> : <span className="text-slate-500">INGEST</span>}</td> |
| <td className="p-3"> |
| <div className="flex items-center gap-2"> |
| <span className="text-sky-500 font-mono break-all">{q.link}</span> |
| {q.comments && q.comments.length > 0 && ( |
| <button |
| onClick={(e) => toggleQueueExpand(e, q.link)} |
| className="px-2 py-1 bg-slate-800 rounded text-[10px] text-slate-300 hover:bg-slate-700 flex items-center gap-1 z-10" |
| > |
| {expandedQueueItems.has(q.link) ? <ChevronUp className="w-3 h-3"/> : <ChevronDown className="w-3 h-3"/>} |
| {q.comments.length} Comments |
| </button> |
| )} |
| </div> |
| </td> |
| <td className="p-3"> |
| {q.status === 'Processed' ? |
| <span className="text-emerald-500 flex items-center gap-1"><CheckCircle2 className="w-3 h-3"/> Done</span> : |
| q.status === 'Error' ? |
| <span className="text-red-500 flex items-center gap-1"><AlertCircle className="w-3 h-3"/> Error</span> : |
| <span className="text-amber-500">Pending</span> |
| } |
| </td> |
| </tr> |
| {expandedQueueItems.has(q.link) && q.comments && q.comments.length > 0 && ( |
| <tr className="bg-slate-900/40"> |
| <td colSpan={4} className="p-0"> |
| <div className="px-12 py-3 border-l-2 border-indigo-500 ml-4 space-y-2"> |
| {q.comments.map((c: any, ci: number) => ( |
| <div key={ci} className="flex flex-col gap-1 p-2 bg-slate-900 border border-slate-800 rounded"> |
| <div className="flex justify-between items-center"> |
| <div className="text-[10px] font-bold text-indigo-400">{c.author}</div> |
| <div className="flex gap-2"> |
| {c.link && <a href={c.link} target="_blank" rel="noreferrer" className="text-[10px] text-sky-400 hover:underline">View Post</a>} |
| {c.link && <button onClick={() => addSingleLinkDirect(c.link)} className="text-[10px] bg-slate-800 hover:bg-slate-700 text-slate-300 px-2 py-0.5 rounded flex items-center gap-1"><Plus className="w-3 h-3"/> Queue Comment</button>} |
| </div> |
| </div> |
| <div className="text-xs text-slate-400 line-clamp-2">{c.text}</div> |
| </div> |
| ))} |
| </div> |
| </td> |
| </tr> |
| )} |
| </React.Fragment> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| <div ref={logContainerRef} className="h-40 bg-black border border-slate-800 rounded-xl p-3 font-mono text-[10px] text-emerald-500 overflow-y-auto whitespace-pre-wrap">{logs}</div> |
| </div> |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'profiles' && ( |
| <div className="flex h-full gap-6"> |
| <div className="w-1/3 bg-slate-900/50 border border-slate-800 rounded-xl overflow-hidden flex flex-col"> |
| <div className="p-3 bg-slate-950 border-b border-slate-800 text-xs font-bold text-slate-400">Scraped Accounts</div> |
| <div className="flex-1 overflow-auto"> |
| {profileList.map((p, i) => ( |
| <div key={i} onClick={() => loadProfilePosts(p.username)} |
| className={`p-3 border-b border-slate-800/50 cursor-pointer hover:bg-white/5 ${selectedProfile===p.username ? 'bg-indigo-900/20 border-l-2 border-indigo-500': ''}`}> |
| <div className="text-sm font-bold text-white">@{p.username}</div> |
| <div className="text-[10px] text-slate-500">{p.posts_count} posts stored</div> |
| </div> |
| ))} |
| </div> |
| </div> |
| <div className="flex-1 bg-slate-900/50 border border-slate-800 rounded-xl overflow-hidden flex flex-col"> |
| <div className="p-3 bg-slate-950 border-b border-slate-800 flex justify-between items-center"> |
| <span className="text-xs font-bold text-slate-400">Post History {selectedProfile ? `(@${selectedProfile})` : ''}</span> |
| {selectedProfile && ( |
| <button onClick={queueUnlabeledPosts} className="px-3 py-1 bg-indigo-600 hover:bg-indigo-500 text-white rounded text-[10px] flex items-center gap-1 transition hover:scale-105"> |
| <PlusCircle className="w-3 h-3"/> Auto-Label |
| </button> |
| )} |
| </div> |
| <div className="flex-1 overflow-auto"> |
| <table className="w-full text-left text-xs text-slate-400"> |
| <thead className="bg-slate-900/80 sticky top-0"><tr><th className="p-3">Date</th><th className="p-3">Text</th><th className="p-3">Status</th><th className="p-3">Action</th></tr></thead> |
| <tbody className="divide-y divide-slate-800"> |
| {profilePosts.map((row, i) => ( |
| <tr key={i} className="hover:bg-white/5"> |
| <td className="p-3 whitespace-nowrap text-slate-500">{row.timestamp?.split('T')[0]}</td> |
| <td className="p-3 truncate max-w-[300px]">{row.text}</td> |
| <td className="p-3">{row.is_labeled ? <span className="text-emerald-500 flex items-center gap-1"><ShieldCheck className="w-3 h-3"/> Labeled</span> : <span className="text-slate-600">Unlabeled</span>}</td> |
| <td className="p-3 flex gap-2"> |
| <button onClick={() => sendToManualLabeler(row.link, row.text)} className="text-indigo-400 hover:text-indigo-300 flex items-center gap-1 bg-indigo-900/30 px-2 py-1 rounded hover:bg-indigo-900/50"><PenTool className="w-3 h-3"/> Manual</button> |
| </td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'dataset' && ( |
| <div className="h-full overflow-auto bg-slate-900/50 border border-slate-800 rounded-xl flex flex-col"> |
| <div className="p-4 border-b border-slate-800 flex justify-between items-center bg-slate-950"> |
| <span className="font-bold text-slate-400 text-sm flex items-center gap-2"> |
| <Archive className="w-4 h-4"/> Data Manager (Unified) |
| </span> |
| <div className="flex gap-2 items-center"> |
| <div className="text-[10px] text-slate-500 bg-slate-900 px-2 py-1 rounded border border-slate-800 flex gap-2"> |
| <span>Total: {datasetList.length}</span> |
| <span>AI: {datasetList.filter(d => d.source === 'AI').length}</span> |
| <span>Manual: {datasetList.filter(d => d.source === 'Manual').length}</span> |
| </div> |
| {selectedItems.size === 1 && ( |
| <button onClick={editSelectedLabel} className="bg-sky-600 text-white text-xs px-3 py-1 rounded font-bold hover:bg-sky-500 flex items-center gap-2"> |
| <Edit3 className="w-3 h-3"/> Edit Label |
| </button> |
| )} |
| <button onClick={deleteDataEntries} className="bg-red-600 text-white text-xs px-3 py-1 rounded font-bold hover:bg-red-500 flex items-center gap-2"> |
| <Trash2 className="w-3 h-3"/> Delete Selected |
| </button> |
| <button onClick={promoteSelected} className="bg-emerald-600 text-white text-xs px-3 py-1 rounded font-bold hover:bg-emerald-500 flex items-center gap-2"> |
| <ShieldCheck className="w-3 h-3"/> Add Selected to Ground Truth |
| </button> |
| </div> |
| </div> |
| <div className="flex-1 overflow-auto"> |
| <table className="w-full text-left text-xs text-slate-400 select-none"> |
| <thead className="bg-slate-950 sticky top-0"> |
| <tr> |
| <th className="p-3 w-8"><Square className="w-4 h-4 text-slate-600"/></th> |
| <th className="p-3">Source</th> |
| <th className="p-3">ID</th> |
| <th className="p-3">Config</th> |
| <th className="p-3">Caption</th> |
| <th className="p-3">Score</th> |
| </tr> |
| </thead> |
| <tbody className="divide-y divide-slate-800"> |
| {datasetList.map((row, i, arr) => ( |
| <tr |
| key={i} |
| className={`hover:bg-white/5 ${selectedItems.has(row.id) ? 'bg-indigo-900/20' : ''} ${row.source==='Manual'?'bg-emerald-900/5':''}`} |
| onMouseDown={(e) => { |
| isDraggingDatasetRef.current = true; |
| toggleSelection(e, row.id, i, arr); |
| }} |
| onMouseEnter={() => { |
| if (isDraggingDatasetRef.current && !selectedItems.has(row.id)) { |
| setSelectedItems(prev => new Set(prev).add(row.id)); |
| setLastDatasetIndex(i); |
| } |
| }} |
| > |
| <td className="p-3 cursor-pointer"> |
| {selectedItems.has(row.id) ? <CheckSquare className="w-4 h-4 text-indigo-400"/> : <Square className="w-4 h-4 text-slate-600"/>} |
| </td> |
| <td className="p-3"> |
| {row.source === 'Manual' ? ( |
| <span className="text-emerald-400 text-[10px] font-bold border border-emerald-900 bg-emerald-900/20 px-1 rounded">MANUAL</span> |
| ) : ( |
| <div className="flex flex-col gap-1"> |
| <span className="text-indigo-400 text-[10px] font-bold border border-indigo-900 bg-indigo-900/20 px-1 rounded w-fit">AI</span> |
| </div> |
| )} |
| </td> |
| <td className="p-3 font-mono text-slate-500">{row.id}</td> |
| <td className="p-3 text-[10px] text-slate-400"> |
| {row.source !== 'Manual' ? ( |
| <> |
| <div className="font-bold text-slate-300">{row.config_model || 'N/A'}</div> |
| <div>{row.config_prompt} | {row.config_reasoning}</div> |
| </> |
| ) : ( |
| <span className="text-slate-600">N/A</span> |
| )} |
| </td> |
| <td className="p-3 truncate max-w-[300px]" title={row.caption}>{row.caption}</td> |
| <td className="p-3 font-bold text-white">{row.final_veracity_score}</td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'groundtruth' && ( |
| <div className="h-full overflow-auto bg-slate-900/50 border border-slate-800 rounded-xl flex flex-col"> |
| <div className="p-4 border-b border-slate-800 flex justify-between items-center bg-slate-950"> |
| <span className="font-bold text-emerald-400 text-sm flex items-center gap-2"><ShieldCheck className="w-4 h-4"/> Verified Ground Truth CSV</span> |
| <div className="flex gap-2"> |
| <span className="text-xs text-slate-500 py-1 mr-4">{datasetList.filter(d => d.source === 'Manual').length} Verified Items</span> |
| |
| <div className="flex items-center gap-2 mr-2"> |
| <label className="text-[10px] uppercase text-slate-500 font-bold">Resamples:</label> |
| <input |
| type="number" min="1" max="50" |
| value={resampleCount} |
| onChange={e => setResampleCount(parseInt(e.target.value) || 1)} |
| className="w-16 bg-slate-900 border border-slate-700 rounded p-1 text-xs text-white" |
| /> |
| </div> |
| |
| <button onClick={verifySelected} className="bg-indigo-600 text-white text-xs px-3 py-1 rounded font-bold hover:bg-indigo-500 flex items-center gap-2"> |
| <RotateCcw className="w-3 h-3"/> Verify Scores (Re-Queue AI Run) |
| </button> |
| <button onClick={deleteSelected} className="bg-red-600 text-white text-xs px-3 py-1 rounded font-bold hover:bg-red-500 flex items-center gap-2"> |
| <Trash2 className="w-3 h-3"/> Delete Selected |
| </button> |
| </div> |
| </div> |
| <div className="flex-1 overflow-auto"> |
| <table className="w-full text-left text-xs text-slate-400 select-none"> |
| <thead className="bg-slate-950 sticky top-0"> |
| <tr> |
| <th className="p-3 w-8"><Square className="w-4 h-4 text-slate-600"/></th> |
| <th className="p-3">ID</th> |
| <th className="p-3">Caption</th> |
| <th className="p-3">Score</th> |
| <th className="p-3">Source</th> |
| </tr> |
| </thead> |
| <tbody className="divide-y divide-slate-800"> |
| {datasetList.filter(d => d.source === 'Manual').map((row, i, arr) => ( |
| <tr |
| key={i} |
| className={`hover:bg-white/5 ${selectedItems.has(row.id) ? 'bg-red-900/10' : ''}`} |
| onMouseDown={(e) => { |
| isDraggingDatasetRef.current = true; |
| toggleSelection(e, row.id, i, arr); |
| }} |
| onMouseEnter={() => { |
| if (isDraggingDatasetRef.current && !selectedItems.has(row.id)) { |
| setSelectedItems(prev => new Set(prev).add(row.id)); |
| setLastDatasetIndex(i); |
| } |
| }} |
| > |
| <td className="p-3 cursor-pointer"> |
| {selectedItems.has(row.id) ? <CheckSquare className="w-4 h-4 text-red-400"/> : <Square className="w-4 h-4 text-slate-600"/>} |
| </td> |
| <td className="p-3 font-mono text-emerald-400">{row.id}</td> |
| <td className="p-3 truncate max-w-[300px]" title={row.caption}>{row.caption}</td> |
| <td className="p-3 font-bold text-white">{row.final_veracity_score}</td> |
| <td className="p-3 text-[10px] uppercase text-slate-500">{row.source}</td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| )} |
|
|
| {} |
| {activeTab === 'manual' && ( |
| <div className="flex h-full gap-6 relative"> |
| {/* Rubric Overlay */} |
| {showRubric && ( |
| <div className="absolute inset-0 z-50 bg-[#09090b]/95 backdrop-blur-sm p-8 flex justify-center items-center"> |
| <div className="bg-slate-900 border border-slate-800 w-full max-w-4xl h-[90vh] rounded-xl flex flex-col shadow-2xl"> |
| <div className="p-6 border-b border-slate-800 flex justify-between items-center"> |
| <h2 className="text-xl font-bold text-white">Labeling Guide & Rubric</h2> |
| <button onClick={() => setShowRubric(false)} className="text-slate-400 hover:text-white"><Trash2 className="w-6 h-6 rotate-45"/></button> |
| </div> |
| <div className="flex-1 overflow-y-auto p-8 prose prose-invert max-w-none"> |
| <h3>Core Scoring Philosophy</h3> |
| <p><strong>1</strong> = Malicious/Fabricated. <strong>5</strong> = Unknown/Generic. <strong>10</strong> = Authentic/Verified.</p> |
| <h4>A. Visual Integrity</h4> |
| <ul> |
| <li><strong>1-2 (Deepfake):</strong> AI-generated or spatially altered.</li> |
| <li><strong>3-4 (Deceptive):</strong> Real footage, misleading edit (speed/crop).</li> |
| <li><strong>5-6 (Context):</strong> Real footage, false context or stock B-roll.</li> |
| <li><strong>9-10 (Raw):</strong> Verified raw footage/metadata.</li> |
| </ul> |
| </div> |
| </div> |
| </div> |
| )} |
| |
| <div className="w-[280px] bg-slate-900/50 border border-slate-800 rounded-xl flex flex-col overflow-hidden"> |
| <div className="flex border-b border-slate-800"> |
| <button onClick={() => setLabelBrowserMode('queue')} className={`flex-1 py-3 text-xs font-bold ${labelBrowserMode==='queue'?'text-indigo-400 border-b-2 border-indigo-500':''}`}>Queue</button> |
| <button onClick={() => setLabelBrowserMode('dataset')} className={`flex-1 py-3 text-xs font-bold ${labelBrowserMode==='dataset'?'text-indigo-400 border-b-2 border-indigo-500':''}`}>Reviewed</button> |
| </div> |
| <div className="p-2 border-b border-slate-800"> |
| <input value={labelFilter} onChange={e => setLabelFilter(e.target.value)} placeholder="Filter..." className="w-full bg-slate-950 border border-slate-700 rounded px-2 py-1 text-xs text-white"/> |
| </div> |
| <div className="flex-1 overflow-auto"> |
| {(labelBrowserMode==='queue' ? queueList : datasetList) |
| .filter(i => (i.link || '').includes(labelFilter) || (i.id || '').includes(labelFilter)) |
| .map((item, i) => ( |
| <div key={i} onClick={() => loadFromBrowser(item, labelBrowserMode)} |
| className={`p-3 border-b border-slate-800/50 cursor-pointer hover:bg-white/5 ${manualLink===item.link?'bg-indigo-900/20 border-l-2 border-indigo-500':''}`}> |
| <div className="text-[10px] text-indigo-400 font-mono mb-1 truncate">{item.id || 'Pending'}</div> |
| <div className="text-xs text-slate-300 truncate">{item.link}</div> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| {/* Main Workspace with Split View Support */} |
| <div className="flex-1 bg-slate-900/50 border border-slate-800 rounded-xl overflow-hidden flex"> |
| |
| {/* THE FORM */} |
| <div className="flex-1 p-6 overflow-y-auto"> |
| <div className="flex justify-between items-center mb-6 pb-4 border-b border-slate-800"> |
| <h2 className="text-lg font-bold text-white flex items-center gap-2"><PenTool className="w-5 h-5"/> Studio</h2> |
| <div className="flex gap-2"> |
| <button onClick={() => setShowRubric(true)} className="bg-slate-800 hover:bg-slate-700 text-indigo-400 px-3 py-2 rounded-lg font-bold text-xs flex gap-2 items-center"><Info className="w-4 h-4"/> Reference Guide</button> |
| <a href={manualLink} target="_blank" rel="noreferrer" className={`bg-slate-800 text-white px-3 py-2 rounded-lg font-bold flex gap-2 ${!manualLink && 'opacity-50 pointer-events-none'}`}><ExternalLink className="w-4 h-4"/> Open</a> |
| <button onClick={submitManualLabel} className="bg-emerald-600 text-white px-6 py-2 rounded-lg font-bold flex gap-2"><ClipboardCheck className="w-4 h-4"/> Save & Add to GT</button> |
| </div> |
| </div> |
| <div className="space-y-6"> |
| <div className="bg-slate-950 p-4 rounded-lg border border-slate-800"> |
| <div className="mb-4"> |
| <label className="text-xs uppercase text-slate-500 font-bold">Link</label> |
| <input value={manualLink} onChange={e => setManualLink(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-sm text-indigo-400 font-mono mt-1"/> |
| </div> |
| <div> |
| <label className="text-xs uppercase text-slate-500 font-bold">Caption</label> |
| <textarea value={manualCaption} onChange={e => setManualCaption(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-sm text-slate-300 mt-1 h-20"/> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-2 gap-8"> |
| <div> |
| <h3 className="text-sm font-bold text-indigo-400 uppercase mb-4 border-b border-slate-800 pb-2">Veracity Vectors</h3> |
| {['visual', 'audio', 'source', 'logic', 'emotion'].map(k => ( |
| <div key={k} className="mb-4"> |
| <div className="flex justify-between text-xs mb-1"> |
| <span className="capitalize text-slate-300 font-bold">{k}</span> |
| <span className="text-indigo-400 font-mono font-bold">{(manualScores as any)[k]}/10</span> |
| </div> |
| <input type="range" min="1" max="10" value={(manualScores as any)[k]} onChange={e => setManualScores({...manualScores,[k]: parseInt(e.target.value)})} className="w-full accent-indigo-500"/> |
| </div> |
| ))} |
| </div> |
| <div> |
| <h3 className="text-sm font-bold text-emerald-400 uppercase mb-4 border-b border-slate-800 pb-2">Modality Alignment</h3> |
| {['va', 'vc', 'ac'].map(k => ( |
| <div key={k} className="mb-4"> |
| <div className="flex justify-between text-xs mb-1"> |
| <span className="capitalize text-slate-300 font-bold">{k.replace('v', 'Video-').replace('a', 'Audio-').replace('c', 'Caption')}</span> |
| <span className="text-emerald-400 font-mono font-bold">{(manualScores as any)[k]}/10</span> |
| </div> |
| <input type="range" min="1" max="10" value={(manualScores as any)[k]} onChange={e => setManualScores({...manualScores,[k]: parseInt(e.target.value)})} className="w-full accent-emerald-500"/> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| <div className="bg-slate-950 p-4 rounded-lg border border-slate-800"> |
| <div className="flex justify-between items-center mb-4"> |
| <h3 className="text-sm font-bold text-white uppercase">Final Veracity Score</h3> |
| <span className="text-2xl font-bold font-mono text-emerald-400">{manualScores.final}</span> |
| </div> |
| <input type="range" min="0" max="100" value={manualScores.final} onChange={e => setManualScores({...manualScores, final: parseInt(e.target.value)})} className="w-full accent-white mb-6"/> |
| |
| <div className="mb-4"> |
| <label className="text-xs uppercase text-slate-500 font-bold">Reasoning</label> |
| <textarea value={manualReasoning} onChange={e => setManualReasoning(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-sm text-slate-300 mt-1 h-24" placeholder="Justification..."/> |
| </div> |
| <div> |
| <label className="text-xs uppercase text-slate-500 font-bold">Tags</label> |
| <input value={manualTags} onChange={handleTagsChange} className="w-full bg-slate-900 border border-slate-700 rounded p-2 text-sm text-slate-300 mt-1" placeholder="politics, satire..."/> |
| {existingTags.length > 0 && ( |
| <div className="flex flex-wrap gap-2 mt-2"> |
| {existingTags.map(t => ( |
| <button key={t} onClick={() => toggleTag(t)} className="px-2 py-1 bg-slate-800 hover:bg-slate-700 text-[10px] rounded text-slate-400 border border-slate-700">{t}</button> |
| ))} |
| </div> |
| )} |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* AI REFERENCE PANEL */} |
| {aiReference && ( |
| <div className="w-[300px] bg-slate-950 border-l border-slate-800 p-4 overflow-y-auto"> |
| <h3 className="text-xs font-bold text-indigo-400 uppercase mb-4 flex items-center gap-2"> |
| <BrainCircuit className="w-4 h-4"/> AI Reference |
| </h3> |
| <div className="text-[10px] text-slate-500 mb-2 font-mono break-all">ID: {aiReference.id}</div> |
| |
| <div className="mb-6 bg-slate-900 p-3 rounded border border-slate-800"> |
| <div className="text-xs text-slate-400 font-bold uppercase mb-1">AI Score</div> |
| <div className={`text-2xl font-mono font-bold ${aiReference.final_veracity_score < 50 ? 'text-red-400' : 'text-emerald-400'}`}> |
| {aiReference.final_veracity_score}/100 |
| </div> |
| </div> |
| |
| <div className="mb-4"> |
| <div className="text-xs text-slate-400 font-bold uppercase mb-1">Reasoning</div> |
| <div className="text-xs text-slate-400 whitespace-pre-wrap leading-relaxed p-2 bg-slate-900 rounded border border-slate-800/50"> |
| {aiReference.reasoning || "No reasoning provided."} |
| </div> |
| </div> |
| |
| <div className="mb-4"> |
| <div className="text-xs text-slate-400 font-bold uppercase mb-1">Configuration</div> |
| <div className="text-[10px] space-y-1"> |
| <div className="flex justify-between"><span className="text-slate-500">Model:</span> <span className="text-slate-300">{aiReference.config_model || 'Unknown'}</span></div> |
| <div className="flex justify-between"><span className="text-slate-500">Prompt:</span> <span className="text-slate-300">{aiReference.config_prompt || 'Unknown'}</span></div> |
| <div className="flex justify-between"><span className="text-slate-500">Reasoning:</span> <span className="text-slate-300">{aiReference.config_reasoning || 'Unknown'}</span></div> |
| </div> |
| </div> |
| |
| {aiReference.raw_toon && ( |
| <div className="mt-4 pt-4 border-t border-slate-800"> |
| <details> |
| <summary className="text-[10px] cursor-pointer text-indigo-400 hover:text-indigo-300 font-bold">Show Raw AI-Labeled Data (JSON/TOON)</summary> |
| <pre className="text-[9px] text-slate-500 whitespace-pre-wrap mt-2 bg-black p-2 rounded border border-slate-800 overflow-x-auto"> |
| {aiReference.raw_toon} |
| </pre> |
| </details> |
| </div> |
| )} |
| </div> |
| )} |
| |
| </div> |
| </div> |
| )} |
|
|
| {activeTab === 'analytics' && ( |
| <div className="h-full overflow-auto"> |
| <div className="flex items-center justify-between mb-4"> |
| <h3 className="text-sm font-bold text-indigo-400 uppercase flex items-center gap-2"> |
| <UserCheck className="w-4 h-4"/> Account Integrity Leaderboard |
| </h3> |
| <button onClick={() => setRefreshTrigger(p => p+1)} className="text-xs text-slate-500 hover:text-white flex gap-1 items-center"><RefreshCw className="w-3 h-3"/> Refresh</button> |
| </div> |
| <div className="bg-slate-900/50 border border-slate-800 rounded-xl overflow-hidden"> |
| <table className="w-full text-left text-xs text-slate-400"> |
| <thead className="bg-slate-950"><tr><th className="p-4">User</th><th className="p-4">Avg Veracity</th><th className="p-4">Samples</th><th className="p-4">Rating</th></tr></thead> |
| <tbody className="divide-y divide-slate-800"> |
| {integrityBoard.map((row, i) => ( |
| <tr key={i} className="hover:bg-white/5"> |
| <td className="p-4 font-bold text-white">@{row.username}</td> |
| <td className="p-4 text-indigo-300 font-mono text-lg">{row.avg_veracity}</td> |
| <td className="p-4 text-slate-500">{row.posts_labeled} posts</td> |
| <td className="p-4"> |
| {row.avg_veracity > 70 ? <span className="text-emerald-500 bg-emerald-500/10 px-2 py-1 rounded font-bold">High Trust</span> : |
| <span className="text-amber-500 bg-amber-500/10 px-2 py-1 rounded font-bold">Mixed</span>} |
| </td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| )} |
|
|
| </div> |
| </div> |
| </div> |
| ) |
| } |
|
|
| export default App |