Spaces:
Sleeping
Sleeping
| 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); | |
| // Processing Config State | |
| const[modelProvider, setModelProvider] = useState('nrp'); | |
| const[apiKey, setApiKey] = useState(''); | |
| const[baseUrl, setBaseUrl] = useState('https://ellm.nrp-nautilus.io/v1'); // NRP Default | |
| const[modelName, setModelName] = useState('qwen3'); // Default | |
| 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); | |
| // Data States | |
| 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); | |
| // Tags | |
| const[configuredTags, setConfiguredTags] = useState<any>({}); | |
| // Manual Labeling State | |
| 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(''); | |
| // Agent Chat State | |
| 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 }); | |
| // Resampling configuration | |
| const[resampleCount, setResampleCount] = useState<number>(1); | |
| // Drag Selection references | |
| const isDraggingQueueRef = useRef(false); | |
| const isDraggingDatasetRef = useRef(false); | |
| // Quick Demo State | |
| 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); | |
| // Provide generic NRP configs | |
| 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 { | |
| // 1. Ensure the link is added and forcefully requeued so it is 'Pending' | |
| 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] }) | |
| }); | |
| // 2. Setup FormData exactly like normal queue | |
| 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'); | |
| // 3. Read Stream | |
| 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'); | |
| } | |
| // 4. Look up result from dataset | |
| 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); | |
| // Sort descending to ensure we get the latest processed run | |
| 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> | |
| )} | |
| {/* AGENT NEXUS TAB */} | |
| {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> | |
| )} | |
| {/* QUEUE TAB */} | |
| {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> | |
| {/* Process Controls */} | |
| {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> | |
| )} | |
| {/* PROFILES TAB */} | |
| {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> | |
| )} | |
| {/* DATASET TAB (Data Manager) */} | |
| {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> | |
| )} | |
| {/* GROUND TRUTH TAB */} | |
| {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> | |
| )} | |
| {/* MANUAL LABELING STUDIO */} | |
| {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 |