Dgaze / components /verification_result.py
lightmate's picture
Upload 12 files
dd8c8ac verified
"""
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>
"""