Spaces:
Sleeping
Sleeping
""" | |
Verification result formatting components. | |
Similar to MultiClaimVerificationResult.tsx in the React frontend. | |
""" | |
import html | |
from typing import Dict, Any, List | |
from utils.markdown_utils import convert_markdown_to_html | |
def format_verification_results(data: Dict[str, Any]) -> str: | |
"""Format verification results in HTML to match the React frontend structure.""" | |
verification_data = data.get("data", {}) | |
verified_claims = verification_data.get("verified_claims", []) | |
if not verified_claims: | |
return """ | |
<div style="text-align: center; padding: 2rem; background: white; border-radius: 8px; border: 1px solid #dee2e6;"> | |
<h3>No Claims Found</h3> | |
<p>No specific claims found to verify in the provided text.</p> | |
</div> | |
""" | |
# Get summary data | |
summary = verification_data.get("verification_summary", {}) | |
html_parts = [] | |
# Header Section | |
html_parts.append(_format_header(summary)) | |
# Summary Stats Grid | |
html_parts.append(_format_stats_grid(summary)) | |
# Truthfulness Distribution | |
html_parts.append(_format_truthfulness_distribution(summary)) | |
# Individual Claims Section | |
html_parts.append(_format_claims_header()) | |
# Individual Claim Cards | |
for i, claim in enumerate(verified_claims, 1): | |
html_parts.append(_format_claim_card(claim, i)) | |
# Processing Time Breakdown | |
html_parts.append(_format_processing_time(summary)) | |
return "".join(html_parts) | |
def _format_header(summary: Dict[str, Any]) -> str: | |
"""Format the header section.""" | |
return f""" | |
<div style="text-align: center; margin-bottom: 2rem;"> | |
<h1 style="font-size: 2rem; font-weight: bold; color: #212529; margin-bottom: 0.5rem;">Verification Results</h1> | |
<p style="color: #495057;"> | |
Analyzed {summary.get('total_claims_found', 'N/A')} claims in {summary.get('processing_time', {}).get('total_seconds', 0):.1f} seconds | |
</p> | |
</div> | |
""" | |
def _format_stats_grid(summary: Dict[str, Any]) -> str: | |
"""Format the statistics grid.""" | |
avg_confidence = summary.get('average_confidence', 0) * 100 | |
verification_rate = summary.get('verification_rate', '0%') | |
if isinstance(verification_rate, (int, float)): | |
verification_rate = f"{verification_rate:.1f}%" | |
return f""" | |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;"> | |
<div style="background: white; padding: 1.5rem; border-radius: 8px; border: 1px solid #e9ecef; text-align: center;"> | |
<div style="font-size: 2rem; font-weight: bold; color: #4263eb; margin-bottom: 0.5rem;">{summary.get('total_claims_found', 'N/A')}</div> | |
<div style="font-size: 0.9rem; color: #495057; font-weight: 500;">Total Claims</div> | |
</div> | |
<div style="background: white; padding: 1.5rem; border-radius: 8px; border: 1px solid #e9ecef; text-align: center;"> | |
<div style="font-size: 2rem; font-weight: bold; color: #15aabf; margin-bottom: 0.5rem;">{summary.get('successful_verifications', 'N/A')}</div> | |
<div style="font-size: 0.9rem; color: #495057; font-weight: 500;">Verified</div> | |
</div> | |
<div style="background: white; padding: 1.5rem; border-radius: 8px; border: 1px solid #e9ecef; text-align: center;"> | |
<div style="font-size: 2rem; font-weight: bold; color: #4263eb; margin-bottom: 0.5rem;">{avg_confidence:.0f}%</div> | |
<div style="font-size: 0.9rem; color: #495057; font-weight: 500;">Avg Confidence</div> | |
</div> | |
<div style="background: white; padding: 1.5rem; border-radius: 8px; border: 1px solid #e9ecef; text-align: center;"> | |
<div style="font-size: 2rem; font-weight: bold; color: #fab005; margin-bottom: 0.5rem;">{verification_rate}</div> | |
<div style="font-size: 0.9rem; color: #495057; font-weight: 500;">Success Rate</div> | |
</div> | |
</div> | |
""" | |
def _format_truthfulness_distribution(summary: Dict[str, Any]) -> str: | |
"""Format the truthfulness distribution section.""" | |
truthfulness_dist = summary.get('truthfulness_distribution', {}) | |
if not truthfulness_dist: | |
return "" | |
dist_items = [] | |
color_map = { | |
'TRUE': '#e8f5e9', | |
'MOSTLY TRUE': '#e3f2fd', | |
'NEUTRAL': '#fff8e1', | |
'MOSTLY FALSE': '#ffebee', | |
'FALSE': '#ffebee' | |
} | |
for category, count in truthfulness_dist.items(): | |
bg_color = color_map.get(category.upper(), '#f5f5f5') | |
dist_items.append(f""" | |
<div style="text-align: center; padding: 1rem; border-radius: 8px; background: {bg_color};"> | |
<div style="font-size: 0.9rem; font-weight: 500; color: #212529; margin-bottom: 0.5rem;">{category}</div> | |
<div style="font-size: 1.2rem; font-weight: bold; color: #495057;">{count}</div> | |
</div> | |
""") | |
return f""" | |
<div style="background: white; padding: 1.5rem; border-radius: 8px; border: 1px solid #e9ecef; margin-bottom: 2rem;"> | |
<h3 style="font-size: 1.2rem; font-weight: 600; color: #212529; margin-bottom: 1rem;">Truthfulness Distribution</h3> | |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;"> | |
{"".join(dist_items)} | |
</div> | |
</div> | |
""" | |
def _format_claims_header() -> str: | |
"""Format the claims section header.""" | |
return """ | |
<h2 style="font-size: 1.5rem; font-weight: bold; color: #212529; margin: 2rem 0 1rem 0;">Verified Claims</h2> | |
""" | |
def _format_claim_card(claim: Dict[str, Any], index: int) -> str: | |
"""Format an individual claim card.""" | |
truthfulness = claim.get("truthfulness", "UNKNOWN").upper() | |
confidence = claim.get("confidence", 0) | |
claim_text = claim.get("claim", "").strip() | |
evidence = claim.get("evidence", "").strip() | |
explanation = claim.get("explanation", "").strip() | |
sources = claim.get("sources", []) | |
# Status color | |
status_colors = { | |
'TRUE': 'background: #e8f5e9; color: #2e7d32; border: 1px solid #c8e6c9;', | |
'MOSTLY TRUE': 'background: #e3f2fd; color: #1565c0; border: 1px solid #bbdefb;', | |
'NEUTRAL': 'background: #fff8e1; color: #ff8f00; border: 1px solid #ffecb3;', | |
'MOSTLY FALSE': 'background: #ffebee; color: #c62828; border: 1px solid #ffcdd2;', | |
'FALSE': 'background: #ffebee; color: #c62828; border: 1px solid #ffcdd2;' | |
} | |
status_style = status_colors.get(truthfulness, 'background: #f5f5f5; color: #424242; border: 1px solid #e0e0e0;') | |
# Confidence color | |
conf_color = '#1565c0' if confidence >= 0.8 else '#ff8f00' if confidence >= 0.6 else '#c62828' | |
# Check if we have valid evidence separate from explanation | |
has_valid_evidence = (evidence and | |
evidence.strip() and | |
evidence.lower() != 'evidence' and | |
evidence != '[object Object]' and | |
'[object Object]' not in evidence and | |
evidence.strip() not in ['# EVIDENCE', 'EVIDENCE', 'Evidence'] and | |
len(evidence.strip()) > 20 and | |
'Evidence analysis is included in the explanation section' not in evidence) | |
# Format sources | |
sources_html = _format_sources(sources) | |
# Convert markdown to HTML for explanation and evidence | |
explanation_html = convert_markdown_to_html(str(explanation)) if explanation else "No analysis available for this claim." | |
evidence_html = convert_markdown_to_html(str(evidence)) if has_valid_evidence else "" | |
# Only first claim is expanded by default | |
is_expanded = (index == 1) | |
return f""" | |
<details {"open" if is_expanded else ""} style="background: white; border: 1px solid #dee2e6; border-radius: 8px; margin-bottom: 1.5rem; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);"> | |
<summary style="padding: 1.5rem; cursor: pointer; background: white; color: #212529; border-bottom: 1px solid #e9ecef; list-style: none;"> | |
<div style="display: flex; align-items: center; margin-bottom: 1rem;"> | |
<span style="display: inline-flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; border-radius: 50%; background: #4263eb; color: white !important; font-size: 0.9rem; font-weight: 500; margin-right: 0.75rem; text-align: center; line-height: 1;"> | |
<span style="color: white !important;">{index}</span> | |
</span> | |
<h3 style="font-size: 1.1rem; font-weight: 600; color: #212529; margin: 0;"> | |
Claim {index} | |
</h3> | |
</div> | |
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;"> | |
<span style="padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.875rem; font-weight: 500; {status_style}"> | |
{truthfulness} | |
</span> | |
<div style="display: flex; align-items: center; gap: 0.25rem;"> | |
<span style="font-weight: 600; font-size: 0.875rem; color: {conf_color};"> | |
{confidence * 100:.0f}% | |
</span> | |
<span style="font-size: 0.875rem; color: #495057;">confidence</span> | |
</div> | |
</div> | |
<p style="color: #212529; font-size: 1rem; line-height: 1.6; margin: 0 0 1rem 0;">{claim_text}</p> | |
<div style="display: flex; align-items: center; color: #4263eb; font-weight: 500; font-size: 0.9rem;"> | |
<span style="margin-right: 0.5rem;">{'Hide Evidence' if is_expanded else 'Show Evidence'}</span> | |
<svg style="width: 1rem; height: 1rem; fill: none; stroke: #4263eb; transition: transform 0.2s; transform: {'rotate(180deg)' if is_expanded else 'rotate(0deg)'};" viewBox="0 0 24 24" stroke-width="2"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path> | |
</svg> | |
</div> | |
</summary> | |
<!-- Evidence and Analysis Section --> | |
<div class="claim-content" style="padding: 1.5rem; background: #f8f9fa;"> | |
<!-- Add CSS to ensure all text is visible --> | |
<style> | |
.claim-content * {{ | |
color: #212529 !important; | |
}} | |
.claim-content strong {{ | |
color: #212529 !important; | |
font-weight: 600 !important; | |
}} | |
.claim-content em {{ | |
color: #495057 !important; | |
}} | |
.claim-content h1, .claim-content h2, .claim-content h3, .claim-content h4, .claim-content h5, .claim-content h6 {{ | |
color: #212529 !important; | |
}} | |
.claim-content p {{ | |
color: #495057 !important; | |
}} | |
/* Fix Confidence Notes: text color specifically */ | |
.claim-content p:contains("Confidence Notes:") {{ | |
color: #212529 !important; | |
}} | |
.claim-content p strong:contains("Confidence Notes:") {{ | |
color: #212529 !important; | |
}} | |
</style> | |
<!-- Analysis Section --> | |
{f''' | |
<div style="margin-bottom: 1.5rem;"> | |
<h4 style="font-size: 1rem; font-weight: 600; color: #212529; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;"> | |
<svg style="width: 1.25rem; height: 1.25rem; fill: none; stroke: #4263eb;" viewBox="0 0 24 24" stroke-width="2"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |
</svg> | |
Analysis | |
</h4> | |
<div style="background: white; padding: 1rem; border-radius: 6px; border: 1px solid #dee2e6;"> | |
<div style="color: #495057; line-height: 1.6;">{explanation_html}</div> | |
</div> | |
</div> | |
''' if explanation else ''} | |
<!-- Evidence Section --> | |
<div style="margin-bottom: 1.5rem;"> | |
<h4 style="font-size: 1rem; font-weight: 600; color: #212529; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;"> | |
<svg style="width: 1.25rem; height: 1.25rem; fill: none; stroke: #15aabf;" viewBox="0 0 24 24" stroke-width="2"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path> | |
</svg> | |
Evidence | |
</h4> | |
<div style="background: white; padding: 1rem; border-radius: 6px; border: 1px solid #dee2e6;"> | |
{f'<div style="color: #495057; line-height: 1.6;">{evidence_html}</div>' if has_valid_evidence else ''' | |
<div style="text-align: center; padding: 1rem; color: #868e96; font-style: italic;"> | |
<svg style="width: 2rem; height: 2rem; margin: 0 auto 0.5rem; fill: none; stroke: #adb5bd;" viewBox="0 0 24 24" stroke-width="2"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path> | |
</svg> | |
<p style="color: #868e96; margin: 0.5rem 0;">Detailed evidence analysis is included in the explanation above.</p> | |
<p style="font-size: 0.75rem; margin-top: 0.25rem; color: #868e96;">This claim's verification is based on the comprehensive analysis provided.</p> | |
</div> | |
'''} | |
</div> | |
</div> | |
{sources_html} | |
</div> | |
</details> | |
""" | |
def _format_sources(sources: List) -> str: | |
"""Format the sources section.""" | |
if not sources: | |
return "" | |
source_links = [] | |
for j, source in enumerate(sources[:3], 1): | |
if source: | |
# Handle both string URLs and source objects | |
if isinstance(source, str): | |
safe_url = html.escape(source) | |
# Extract domain name for title | |
try: | |
domain = source.split('/')[2] if '/' in source and len(source.split('/')) > 2 else source[:30] | |
safe_title = html.escape(domain) | |
except: | |
safe_title = html.escape(source[:30]) | |
else: | |
safe_url = html.escape(source.get('url', '')) | |
safe_title = html.escape(source.get('title', 'Source')) | |
source_links.append(f""" | |
<a href="{safe_url}" target="_blank" style="display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; color: #4263eb; font-size: 0.875rem; font-weight: 500; transition: all 0.2s; margin-bottom: 0.5rem;" onmouseover="this.style.background='#e7f1ff'; this.style.borderColor='#4263eb';" onmouseout="this.style.background='#f8f9fa'; this.style.borderColor='#dee2e6';"> | |
<svg style="width: 1rem; height: 1rem; fill: none; stroke: #4263eb;" viewBox="0 0 24 24" stroke-width="2"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path> | |
</svg> | |
{safe_title} | |
</a> | |
""") | |
return f""" | |
<div style="margin-top: 1.5rem;"> | |
<h4 style="font-size: 1rem; font-weight: 600; color: #212529; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;"> | |
<svg style="width: 1.25rem; height: 1.25rem; fill: none; stroke: #4263eb;" viewBox="0 0 24 24" stroke-width="2"> | |
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path> | |
</svg> | |
Sources ({len(sources)}) | |
</h4> | |
<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">{"".join(source_links)}</div> | |
</div> | |
""" | |
def _format_processing_time(summary: Dict[str, Any]) -> str: | |
"""Format the processing time breakdown section.""" | |
step_breakdown = summary.get('processing_time', {}).get('step_breakdown', {}) | |
if not step_breakdown: | |
return "" | |
breakdown_items = [] | |
for step, time in step_breakdown.items(): | |
step_name = step.replace('_', ' ').title() | |
breakdown_items.append(f""" | |
<div style="text-align: center; padding: 1rem; border-radius: 8px; background: #f8f9fa;"> | |
<div style="font-size: 1.1rem; font-weight: bold; color: #4263eb; margin-bottom: 0.25rem;">{time:.1f}s</div> | |
<div style="font-size: 0.875rem; color: #495057;">{step_name}</div> | |
</div> | |
""") | |
return f""" | |
<div style="background: white; padding: 1.5rem; border-radius: 8px; border: 1px solid #e9ecef; margin-top: 2rem;"> | |
<h3 style="font-size: 1.2rem; font-weight: 600; color: #212529; margin-bottom: 1rem;">Processing Time Breakdown</h3> | |
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;"> | |
{"".join(breakdown_items)} | |
</div> | |
</div> | |
""" |