"""Utility functions for Gen3 Nudge Detection Dashboard."""
# Confidence tier thresholds
CONFIDENCE_HIGH_THRESHOLD = 0.85
CONFIDENCE_MEDIUM_THRESHOLD = 0.70
def get_confidence_tier(confidence: float | None) -> tuple[str, str, str]:
"""Get confidence tier info for nudge candidates.
Returns:
Tuple of (tier, emoji, description)
"""
if confidence is None:
return "LOW", "π’", "μ€ν κ°λ₯μ±"
if confidence >= CONFIDENCE_HIGH_THRESHOLD:
return "HIGH", "π΄", "νμ€ν λμ§ λμ"
elif confidence >= CONFIDENCE_MEDIUM_THRESHOLD:
return "MEDIUM", "π‘", "κ²ν νμ"
return "LOW", "π’", "μ€ν κ°λ₯μ±"
def truncate_text(text: str | None, max_length: int, suffix: str = "...") -> str:
"""Truncate text to max_length with suffix."""
if not text:
return "N/A"
if len(text) <= max_length:
return text
return text[:max_length] + suffix
def format_brands_list(brands: list[str] | None) -> str:
"""Format list of brands for display."""
if not brands:
return "N/A"
return ", ".join(brands)
def highlight_evidence_spans(text: str, evidence_spans: list[dict] | None) -> str:
"""Highlight evidence spans in text using HTML.
Args:
text: Full answer text
evidence_spans: List of evidence span dicts with text, start, end, type
Returns:
HTML string with highlighted spans
"""
if not evidence_spans or not text:
return text or ""
# Sort spans by start position (descending) to avoid index shifting
sorted_spans = sorted(
[s for s in evidence_spans if s.get("text")],
key=lambda s: s.get("start", 0) if s.get("start") is not None else -1,
reverse=True,
)
result = text
for span in sorted_spans:
span_text = span.get("text", "")
span_type = span.get("type", "negative")
start = span.get("start")
end = span.get("end")
# Color by type
color_map = {
"negative": "#FF6B6B",
"positive": "#51CF66",
"neutral": "#748FFC",
"comparison": "#FAB005",
"hallucination": "#ADB5BD",
"category_general": "#9775FA",
}
color = color_map.get(span_type, "#ADB5BD")
mark_style = f'background-color: {color}; padding: 2px 4px; border-radius: 3px;'
mark_style = f'background-color: {color}; padding: 2px 4px; border-radius: 3px;'
if start is not None and end is not None and 0 <= start < end <= len(result):
# Use exact positions
before = result[:start]
highlighted = f'{result[start:end]}'
after = result[end:]
result = before + highlighted + after
elif span_text and span_text in result:
# Fallback: find text in result
highlighted = f'{span_text}'
result = result.replace(span_text, highlighted, 1)
elif span_text and "..." in span_text:
# Ellipsis fallback: LLM truncated the evidence with "..."
# Split into fragments and highlight each one found in the text
fragments = [f.strip() for f in span_text.split("...") if f.strip()]
for frag in fragments:
if frag in result:
highlighted = f'{frag}'
result = result.replace(frag, highlighted, 1)
return result
def get_llm_tier_badge(adjusted_tier: str | None, is_negative: bool | None) -> tuple[str, str]:
"""Get badge info for LLM verification result.
Returns:
Tuple of (badge_text, badge_color)
"""
if adjusted_tier is None:
return "λ―Έκ²μ¦", "gray"
if adjusted_tier == "NONE" or is_negative is False:
return "β
μ€ν (False Positive)", "green"
tier_map = {
"HIGH": ("π΄ λΆμ νμΈ (HIGH)", "red"),
"MEDIUM": ("π‘ λΆμ νμΈ (MEDIUM)", "orange"),
"LOW": ("π’ λΆμ νμΈ (LOW)", "blue"),
}
return tier_map.get(adjusted_tier, ("νμΈλ¨", "gray"))
def get_feedback_reason_label(reason: str | None) -> str:
"""Get human-readable label for feedback wrong_reason."""
reason_labels = {
"actually_positive": "μ€μ λ‘λ κΈμ μ μΈ λ΄μ©μ
λλ€",
"actually_neutral": "μ€μ λ‘λ μ€λ¦½μ μΈ λ΄μ©μ
λλ€",
"wrong_evidence": "κ·Όκ±° λ¬Έμ₯μ΄ μλͺ» μΆμΆλμμ΅λλ€",
"context_missing": "λ§₯λ½μ΄ λΉ μ Έμ μ€ν΄κ° μμ΅λλ€",
"wrong_brand": "λΈλλκ° μλͺ» μΈμλμμ΅λλ€",
"other": "κΈ°ν",
}
return reason_labels.get(reason, reason or "")
def get_feedback_type_emoji(feedback_type: str | None) -> str:
"""Get emoji for feedback type."""
emoji_map = {
"correct": "π",
"wrong": "π",
"ambiguous": "π€",
}
return emoji_map.get(feedback_type, "")