Spaces:
Sleeping
Sleeping
claude's playground
feat(agent verf): Add multi-provider LLM infrastructure with dual-mode support
0a6602d | <!-- | |
| ============================================================================ | |
| agent verf - Test UI | |
| Version: 0.1.0 | |
| Last Updated: 2026-01-13 | |
| Simple HTML interface for testing the verification API. | |
| Open this file directly in your browser. | |
| ============================================================================ | |
| --> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>agent verf - Test UI</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| verdict: { | |
| true: '#22c55e', | |
| false: '#ef4444', | |
| misleading: '#f59e0b', | |
| satire: '#8b5cf6', | |
| unverified: '#6b7280', | |
| disputed: '#f97316', | |
| }, | |
| brand: { | |
| primary: '#3b82f6', | |
| secondary: '#1e40af', | |
| }, | |
| }, | |
| }, | |
| }, | |
| } | |
| </script> | |
| <style> | |
| .spinner { | |
| animation: spin 1s linear infinite; | |
| border-radius: 50%; | |
| border: 3px solid #e5e7eb; | |
| border-top-color: #3b82f6; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <!-- Header --> | |
| <header class="border-b border-gray-200 bg-white"> | |
| <div class="max-w-3xl mx-auto px-4 py-4"> | |
| <a href="#" class="flex items-center gap-2"> | |
| <span class="text-2xl">🔍</span> | |
| <span class="text-xl font-bold text-gray-900">agent verf</span> | |
| <span class="text-xs bg-gray-100 px-2 py-1 rounded">TEST UI</span> | |
| </a> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="max-w-3xl mx-auto px-4 py-8"> | |
| <!-- Input Section --> | |
| <div id="input-section"> | |
| <div class="text-center mb-8"> | |
| <h1 class="text-3xl font-bold text-gray-900 mb-3">Verify Any Content</h1> | |
| <p class="text-gray-600">Paste a URL to get an AI-powered fact-check</p> | |
| </div> | |
| <div class="bg-white rounded-lg shadow-md border border-gray-200 p-6"> | |
| <form id="verify-form" class="space-y-4"> | |
| <div> | |
| <label for="url" class="block text-sm font-medium text-gray-700 mb-2"> | |
| Paste a URL to verify | |
| </label> | |
| <input | |
| type="url" | |
| id="url" | |
| placeholder="https://twitter.com/... or any URL" | |
| class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" | |
| required | |
| > | |
| </div> | |
| <button | |
| type="submit" | |
| id="submit-btn" | |
| class="w-full py-3 px-4 rounded-lg font-medium text-white bg-brand-primary hover:bg-brand-secondary transition-colors" | |
| > | |
| Verify Content | |
| </button> | |
| </form> | |
| <p id="error-msg" class="mt-3 text-red-500 text-sm hidden"></p> | |
| </div> | |
| </div> | |
| <!-- Progress Section --> | |
| <div id="progress-section" class="hidden text-center"> | |
| <div class="spinner w-16 h-16 mx-auto mb-6"></div> | |
| <h2 class="text-2xl font-bold text-gray-900 mb-4">Verifying Content</h2> | |
| <div class="w-full bg-gray-200 rounded-full h-3 mb-4 max-w-md mx-auto"> | |
| <div id="progress-bar" class="bg-brand-primary h-3 rounded-full transition-all duration-500" style="width: 10%"></div> | |
| </div> | |
| <p id="progress-step" class="text-gray-600">Starting...</p> | |
| <div class="mt-6 flex justify-center gap-2"> | |
| <div id="dot-1" class="w-3 h-3 rounded-full bg-gray-300"></div> | |
| <div id="dot-2" class="w-3 h-3 rounded-full bg-gray-300"></div> | |
| <div id="dot-3" class="w-3 h-3 rounded-full bg-gray-300"></div> | |
| <div id="dot-4" class="w-3 h-3 rounded-full bg-gray-300"></div> | |
| </div> | |
| </div> | |
| <!-- Result Section --> | |
| <div id="result-section" class="hidden"> | |
| <!-- Verdict Header --> | |
| <div class="text-center mb-8"> | |
| <span id="verdict-badge" class="inline-flex items-center px-4 py-2 rounded-full font-bold text-lg"></span> | |
| <p id="verdict-meta" class="mt-2 text-gray-500"></p> | |
| </div> | |
| <!-- Summary --> | |
| <div class="bg-white rounded-lg shadow-md border border-gray-200 p-6 mb-6"> | |
| <h2 class="text-lg font-semibold text-gray-900 mb-3">Summary</h2> | |
| <p id="verdict-summary" class="text-gray-700"></p> | |
| </div> | |
| <!-- Key Findings --> | |
| <div id="findings-card" class="bg-white rounded-lg shadow-md border border-gray-200 p-6 mb-6 hidden"> | |
| <h2 class="text-lg font-semibold text-gray-900 mb-3">Key Findings</h2> | |
| <ul id="findings-list" class="space-y-2"></ul> | |
| </div> | |
| <!-- Evidence --> | |
| <div id="evidence-card" class="bg-white rounded-lg shadow-md border border-gray-200 p-6 mb-6 hidden"> | |
| <h2 class="text-lg font-semibold text-gray-900 mb-3">Evidence</h2> | |
| <div id="evidence-list" class="space-y-4"></div> | |
| </div> | |
| <!-- Actions --> | |
| <div class="flex justify-center gap-4 mt-8"> | |
| <button onclick="location.reload()" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg font-medium text-gray-900"> | |
| Verify Another | |
| </button> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| const API_URL = 'http://localhost:8080'; | |
| const VERDICT_STYLES = { | |
| TRUE: { bg: 'bg-green-100', text: 'text-green-700', icon: '✓', label: 'True' }, | |
| FALSE: { bg: 'bg-red-100', text: 'text-red-700', icon: '✗', label: 'False' }, | |
| MISLEADING: { bg: 'bg-amber-100', text: 'text-amber-700', icon: '⚠', label: 'Misleading' }, | |
| SATIRE: { bg: 'bg-purple-100', text: 'text-purple-700', icon: '🎭', label: 'Satire' }, | |
| UNVERIFIED: { bg: 'bg-gray-100', text: 'text-gray-700', icon: '?', label: 'Unverified' }, | |
| DISPUTED: { bg: 'bg-orange-100', text: 'text-orange-700', icon: '⚡', label: 'Disputed' }, | |
| }; | |
| const STEP_LABELS = { | |
| extraction: 'Extracting content...', | |
| triage: 'Analyzing content...', | |
| evidence: 'Gathering evidence...', | |
| synthesizer: 'Generating verdict...', | |
| done: 'Complete!', | |
| }; | |
| // Form submission | |
| document.getElementById('verify-form').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const url = document.getElementById('url').value; | |
| try { | |
| showProgress(); | |
| // Submit verification | |
| const response = await fetch(`${API_URL}/api/v1/verify`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ url, scan_mode: 'quick_scan' }), | |
| }); | |
| if (!response.ok) throw new Error('Failed to submit'); | |
| const data = await response.json(); | |
| pollStatus(data.request_id); | |
| } catch (err) { | |
| showError(err.message); | |
| } | |
| }); | |
| function showProgress() { | |
| document.getElementById('input-section').classList.add('hidden'); | |
| document.getElementById('progress-section').classList.remove('hidden'); | |
| } | |
| function showError(msg) { | |
| document.getElementById('input-section').classList.remove('hidden'); | |
| document.getElementById('progress-section').classList.add('hidden'); | |
| const errorEl = document.getElementById('error-msg'); | |
| errorEl.textContent = msg; | |
| errorEl.classList.remove('hidden'); | |
| } | |
| async function pollStatus(requestId) { | |
| try { | |
| const response = await fetch(`${API_URL}/api/v1/status/${requestId}`); | |
| const data = await response.json(); | |
| // Update progress UI | |
| const progress = data.progress || 0; | |
| document.getElementById('progress-bar').style.width = `${Math.max(progress * 100, 10)}%`; | |
| document.getElementById('progress-step').textContent = STEP_LABELS[data.current_step] || 'Processing...'; | |
| // Update dots | |
| const steps = ['extraction', 'triage', 'evidence', 'synthesizer']; | |
| steps.forEach((step, i) => { | |
| const dot = document.getElementById(`dot-${i + 1}`); | |
| if (data.current_step === step) { | |
| dot.className = 'w-3 h-3 rounded-full bg-brand-primary animate-pulse'; | |
| } else if (progress > (i + 1) * 0.25) { | |
| dot.className = 'w-3 h-3 rounded-full bg-brand-primary'; | |
| } else { | |
| dot.className = 'w-3 h-3 rounded-full bg-gray-300'; | |
| } | |
| }); | |
| if (data.status === 'completed') { | |
| fetchReceipt(requestId); | |
| } else if (data.status === 'failed') { | |
| showError(data.error || 'Verification failed'); | |
| } else { | |
| setTimeout(() => pollStatus(requestId), 1000); | |
| } | |
| } catch (err) { | |
| showError('Failed to check status'); | |
| } | |
| } | |
| async function fetchReceipt(requestId) { | |
| try { | |
| const response = await fetch(`${API_URL}/api/v1/receipt/${requestId}`); | |
| const receipt = await response.json(); | |
| showResult(receipt); | |
| } catch (err) { | |
| showError('Failed to fetch results'); | |
| } | |
| } | |
| function showResult(receipt) { | |
| document.getElementById('progress-section').classList.add('hidden'); | |
| document.getElementById('result-section').classList.remove('hidden'); | |
| const verdict = receipt.verdict; | |
| const style = VERDICT_STYLES[verdict.status] || VERDICT_STYLES.UNVERIFIED; | |
| // Verdict badge | |
| const badge = document.getElementById('verdict-badge'); | |
| badge.className = `inline-flex items-center px-4 py-2 rounded-full font-bold text-lg ${style.bg} ${style.text}`; | |
| badge.innerHTML = `<span class="mr-2">${style.icon}</span>${style.label}`; | |
| // Meta info | |
| document.getElementById('verdict-meta').textContent = | |
| `${Math.round(verdict.confidence * 100)}% confidence · ${receipt.sources_checked} sources checked`; | |
| // Summary | |
| document.getElementById('verdict-summary').textContent = verdict.summary; | |
| // Key findings | |
| if (verdict.key_findings && verdict.key_findings.length > 0) { | |
| const findingsCard = document.getElementById('findings-card'); | |
| const findingsList = document.getElementById('findings-list'); | |
| findingsCard.classList.remove('hidden'); | |
| findingsList.innerHTML = verdict.key_findings.map(f => | |
| `<li class="flex items-start gap-2"> | |
| <span class="text-brand-primary">•</span> | |
| <span class="text-gray-700">${f}</span> | |
| </li>` | |
| ).join(''); | |
| } | |
| // Evidence | |
| if (receipt.evidence && receipt.evidence.length > 0) { | |
| const evidenceCard = document.getElementById('evidence-card'); | |
| const evidenceList = document.getElementById('evidence-list'); | |
| evidenceCard.classList.remove('hidden'); | |
| evidenceList.innerHTML = receipt.evidence.map(ev => { | |
| const source = ev.sources[0]; | |
| return `<div class="border-l-2 border-gray-200 pl-4"> | |
| <p class="text-gray-700">${ev.finding}</p> | |
| ${source ? `<p class="mt-1 text-sm text-gray-500"> | |
| Source: <a href="${source.url}" target="_blank" class="text-brand-primary hover:underline">${source.domain}</a> | |
| (authority: ${Math.round(source.authority_score * 100)}%) | |
| </p>` : ''} | |
| </div>`; | |
| }).join(''); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |