File size: 5,033 Bytes
ef78361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
"""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'<mark style="{mark_style}">{result[start:end]}</mark>'
            after = result[end:]
            result = before + highlighted + after
        elif span_text and span_text in result:
            # Fallback: find text in result
            highlighted = f'<mark style="{mark_style}">{span_text}</mark>'
            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'<mark style="{mark_style}">{frag}</mark>'
                    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, "")