"""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, "")