File size: 10,670 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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
"""Dashboard expander and section components."""

import html
import streamlit as st

from core.charts import EMOTION_KO
from core.utils import get_confidence_tier, truncate_text


# Content type labels for citations
CONTENT_TYPE_LABELS = {
    "EDITORIAL": "๐Ÿ“ฐ ์—๋””ํ† ๋ฆฌ์–ผ",
    "TUTORIAL_REVIEW": "๐Ÿ“ ๋ฆฌ๋ทฐ/ํŠœํ† ๋ฆฌ์–ผ",
    "COMPARISON": "โš–๏ธ ๋น„๊ต ๋ถ„์„",
    "RANKED_LIST": "๐Ÿ“Š ์ˆœ์œ„ ๋ชฉ๋ก",
    "FORUM_THREAD": "๐Ÿ’ฌ ํฌ๋Ÿผ/์ปค๋ฎค๋‹ˆํ‹ฐ",
    "HOMEPAGE": "๐Ÿ  ํ™ˆํŽ˜์ด์ง€",
    "CATALOG": "๐Ÿ“ฆ ์นดํƒˆ๋กœ๊ทธ",
    "DOCUMENTATION": "๐Ÿ“š ๋ฌธ์„œ",
    "FAQ": "โ“ FAQ",
    "WHITEPAPER": "๐Ÿ“„ ๋ฐฑ์„œ",
    "PRESS_RELEASE": "๐Ÿ“ข ๋ณด๋„์ž๋ฃŒ",
    "CASE_STUDY": "๐Ÿ’ผ ์‚ฌ๋ก€์—ฐ๊ตฌ",
    "PRICING": "๐Ÿ’ฐ ๊ฐ€๊ฒฉ์ •๋ณด",
    "DETAIL": "๐Ÿ” ์ƒ์„ธํŽ˜์ด์ง€",
    "DIRECTORY_ENTRY": "๐Ÿ“‹ ๋””๋ ‰ํ† ๋ฆฌ",
    "SUBSTITUTE": "๐Ÿ”„ ๋Œ€์ฒด์ œ",
    "OTHERS": "๐Ÿ“Ž ๊ธฐํƒ€",
}


def render_citation(cit: dict) -> None:
    """Render a single citation item.

    Args:
        cit: Citation dict with source_url, content_type, page_title
    """
    url = cit.get("source_url", "")
    ctype = cit.get("content_type") or "OTHERS"
    title = cit.get("page_title") or ""
    type_label = CONTENT_TYPE_LABELS.get(ctype, f"๐Ÿ“Ž {ctype}")
    display_url = url[:50] + "..." if len(url) > 50 else url
    display_title = f' "{title[:30]}..."' if title and len(title) > 30 else f' "{title}"' if title else ""
    st.markdown(
        f'<span style="background: #E0E7FF; color: #3730A3; padding: 2px 6px; '
        f'border-radius: 4px; font-size: 11px; margin-right: 4px;">{type_label}</span> '
        f'<a href="{url}" target="_blank">{display_url}</a>{display_title}',
        unsafe_allow_html=True
    )


def render_nudge_expander(
    item: dict,
    answer_id: int | None,
    index: int,
    fetch_full_answer_fn,
    fetch_citations_fn,
) -> None:
    """Render nudge candidate expander with full details.

    Args:
        item: Nudge candidate data dict
        answer_id: Answer ID for Athena fetch
        index: Item index for display
        fetch_full_answer_fn: Function to fetch full answer from Athena
        fetch_citations_fn: Function to fetch citations (Supabase fallback)
    """
    confidence = item.get("overall_confidence", 0) or 0
    tier, _, _ = get_confidence_tier(confidence)
    emotion = item.get("dominant_emotion", "N/A")
    emotion_ko = EMOTION_KO.get(emotion, emotion) if emotion else "N/A"
    answer = item.get("answer_preview", "")
    brand_detail = item.get("brand_sentiment_detail", {})

    with st.expander(f"๐Ÿ“– ์ƒ์„ธ ๋ณด๊ธฐ (๋‹ต๋ณ€ #{answer_id or index+1})"):
        # Analysis explanation box
        st.markdown(f"""
<div style="background: #FFF7ED; border-left: 4px solid #F59E0B; padding: 12px; margin-bottom: 12px; border-radius: 0 8px 8px 0; font-size: 13px;">
<strong>๐Ÿ“Š ๋ถ„์„ ๊ฒฐ๊ณผ ํ•ด์„</strong><br><br>
<strong>๐Ÿ“„ ๋‹ต๋ณ€ ์ „์ฒด ๋ถ€์ • ํ™•์‹ ๋„: {confidence:.0%} ({tier})</strong><br>
๋‹ต๋ณ€ ์ „์ฒด๊ฐ€ ๋ถ€์ •์ ์ธ ํ†ค์ธ์ง€ ํŒ๋‹จํ•œ ์ ์ˆ˜์ž…๋‹ˆ๋‹ค. (์—ฌ๋Ÿฌ ๋ธŒ๋žœ๋“œ๊ฐ€ ์–ธ๊ธ‰๋˜๋ฉด ํ˜ผํ•ฉ๋จ)<br><br>
<strong>๐Ÿ” ๋ธŒ๋žœ๋“œ๋ณ„ ๋ถ€์ • ํ™•์‹ ๋„</strong> (์•„๋ž˜ ABSA ์ฐธ์กฐ)<br>
ํŠน์ • ๋ธŒ๋žœ๋“œ์— ๋Œ€ํ•œ ์–ธ๊ธ‰๋งŒ ์ถ”์ถœํ•˜์—ฌ ๊ทธ ์–ธ๊ธ‰์ด ๋ถ€์ •์ ์ธ์ง€ ํŒ๋‹จํ•œ ์ ์ˆ˜์ž…๋‹ˆ๋‹ค.<br>
<em style="color: #9CA3AF;">์˜ˆ: ๋‹ต๋ณ€ ์ „์ฒด๋Š” 64%(LOW)์—ฌ๋„, ํŠน์ • ๋ธŒ๋žœ๋“œ ์–ธ๊ธ‰์€ 91%(HIGH)์ผ ์ˆ˜ ์žˆ์Œ</em><br><br>
<strong>๋‹ต๋ณ€ ํ†ค: {emotion_ko}</strong><br>
๋‹ต๋ณ€ ์ „์ฒด์˜ ๊ฐ์ •์  ๋ถ„์œ„๊ธฐ์ž…๋‹ˆ๋‹ค.
</div>
""", unsafe_allow_html=True)

        # Full answer from Athena
        st.markdown("**๐Ÿค– AI ๋‹ต๋ณ€ ์ „๋ฌธ**")

        if answer_id:
            full_answer_key = f"full_answer_{answer_id}"
            load_full_key = f"load_full_{answer_id}"
            if full_answer_key not in st.session_state:
                st.session_state[full_answer_key] = None

            load_full = st.checkbox(
                "๐Ÿ“ฅ ์ „์ฒด ๋‹ต๋ณ€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ",
                key=load_full_key,
                value=st.session_state.get(full_answer_key) is not None
            )

            if load_full and st.session_state.get(full_answer_key) is None:
                with st.spinner("์ „์ฒด ๋‹ต๋ณ€์„ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘..."):
                    full_content = fetch_full_answer_fn(answer_id)
                    if isinstance(full_content, str) and len(full_content) > 0:
                        st.session_state[full_answer_key] = full_content
                        st.rerun()
                    else:
                        # Store empty string to prevent infinite re-fetch loop
                        st.session_state[full_answer_key] = ""

            cached = st.session_state.get(full_answer_key)
            display_answer = cached if (isinstance(cached, str) and len(cached) > 0) else answer or "N/A"
            is_full = isinstance(cached, str) and len(cached) > 0
            label = "โœ… ์ „์ฒด ๋‹ต๋ณ€ ๋กœ๋“œ๋จ" if is_full else f"๐Ÿ“„ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ({len(answer or '')}์ž)"
            st.caption(label)
        else:
            display_answer = answer or "N/A"

        st.markdown(
            f'<div style="background: #FEF2F2; padding: 12px; border-radius: 8px; '
            f'font-size: 14px; white-space: pre-wrap; word-break: break-word; '
            f'max-height: 400px; overflow-y: auto;">{html.escape(display_answer)}</div>',
            unsafe_allow_html=True
        )

        # Brand sentiment detail
        if brand_detail and isinstance(brand_detail, dict):
            st.markdown("**๐Ÿ” ๋ธŒ๋žœ๋“œ๋ณ„ ๊ฐ์„ฑ ๋ถ„์„ (ABSA) - ๋ธŒ๋žœ๋“œ๋ณ„ ๋ถ€์ • ํ™•์‹ ๋„**")
            _render_brand_absa(brand_detail)

        # Citations
        st.markdown("**๐Ÿ”— ์ธ์šฉ ์ถœ์ฒ˜ (Citation Sources)**")
        _render_citations_section(answer_id, item.get("citation_urls", []), fetch_citations_fn)


def _render_brand_absa(brand_detail: dict) -> None:
    """Render brand ABSA results."""
    in_house_data = brand_detail.get("in_house", {})
    in_house_absa = in_house_data.get("absa_results", [])
    for absa in in_house_absa:
        if isinstance(absa, dict):
            brand_name = absa.get("brand", "Unknown")
            sentiment = absa.get("sentiment", "N/A")
            conf = absa.get("confidence", 0)
            absa_tier, absa_emoji, _ = get_confidence_tier(conf)
            sent_color = "#10B981" if sentiment == "positive" else "#EF4444" if sentiment == "negative" else "#6B7280"
            st.markdown(
                f'<span style="background: {sent_color}; color: white; padding: 2px 8px; '
                f'border-radius: 4px; font-size: 12px; margin-right: 8px;">{sentiment}</span> '
                f'<strong>{brand_name}</strong> (๐Ÿ  ์ž์‚ฌ) - {absa_emoji} ๋ธŒ๋žœ๋“œ ํ™•์‹ ๋„ {conf:.0%} ({absa_tier})',
                unsafe_allow_html=True
            )

    competitor_data = brand_detail.get("competitor", {})
    competitor_brands = competitor_data.get("brands", [])
    competitor_absa = competitor_data.get("absa_results", [])

    if competitor_absa:
        for absa in competitor_absa:
            if isinstance(absa, dict):
                brand_name = absa.get("brand", "Unknown")
                sentiment = absa.get("sentiment", "N/A")
                conf = absa.get("confidence", 0)
                absa_tier, absa_emoji, _ = get_confidence_tier(conf)
                sent_color = "#10B981" if sentiment == "positive" else "#EF4444" if sentiment == "negative" else "#6B7280"
                st.markdown(
                    f'<span style="background: {sent_color}; color: white; padding: 2px 8px; '
                    f'border-radius: 4px; font-size: 12px; margin-right: 8px;">{sentiment}</span> '
                    f'<strong>{brand_name}</strong> (๐Ÿข ๊ฒฝ์Ÿ์‚ฌ) - {absa_emoji} ๋ธŒ๋žœ๋“œ ํ™•์‹ ๋„ {conf:.0%} ({absa_tier})',
                    unsafe_allow_html=True
                )
    elif competitor_brands:
        st.markdown(
            f'<span style="background: #6B7280; color: white; padding: 2px 8px; '
            f'border-radius: 4px; font-size: 12px;">์–ธ๊ธ‰๋จ</span> '
            f'<strong>{", ".join(competitor_brands)}</strong> (๐Ÿข ๊ฒฝ์Ÿ์‚ฌ)',
            unsafe_allow_html=True
        )


def _render_citations_section(answer_id: int | None, citation_urls: list, fetch_citations_fn) -> None:
    """Render citations section."""
    citations_key = f"citations_{answer_id}"
    if citations_key not in st.session_state:
        st.session_state[citations_key] = None

    if st.session_state.get(citations_key) is None and answer_id:
        citations = fetch_citations_fn(answer_id)
        st.session_state[citations_key] = citations if citations else []

    citations = st.session_state.get(citations_key, [])
    if citations:
        if len(citations) <= 5:
            for cit in citations:
                render_citation(cit)
        else:
            for cit in citations[:5]:
                render_citation(cit)
            with st.expander(f"๐Ÿ“‚ ๋‚˜๋จธ์ง€ {len(citations) - 5}๊ฐœ ๋” ๋ณด๊ธฐ"):
                for cit in citations[5:]:
                    render_citation(cit)
    elif citation_urls:
        if len(citation_urls) <= 5:
            for url in citation_urls:
                st.markdown(f"โ€ข [{url[:60]}...]({url})" if len(url) > 60 else f"โ€ข [{url}]({url})")
        else:
            for url in citation_urls[:5]:
                st.markdown(f"โ€ข [{url[:60]}...]({url})" if len(url) > 60 else f"โ€ข [{url}]({url})")
            with st.expander(f"๐Ÿ“‚ ๋‚˜๋จธ์ง€ {len(citation_urls) - 5}๊ฐœ ๋” ๋ณด๊ธฐ"):
                for url in citation_urls[5:]:
                    st.markdown(f"โ€ข [{url[:60]}...]({url})" if len(url) > 60 else f"โ€ข [{url}]({url})")
    else:
        st.caption("์ธ์šฉ ์†Œ์Šค ์—†์Œ")


def render_feedback_section(feedback_stats: dict) -> None:
    """Render feedback statistics expander section.

    Args:
        feedback_stats: Dict with feedback counts and accuracy
    """
    from .metrics import render_feedback_stats

    fb_total = feedback_stats.get("total_feedback", 0)
    if fb_total > 0:
        with st.expander("๐Ÿ“ **ํ”ผ๋“œ๋ฐฑ ๋ถ„์„** - ์‚ฌ์šฉ์ž ๊ฒ€์ฆ ํ˜„ํ™ฉ", expanded=False):
            render_feedback_stats(feedback_stats)


def render_llm_verification_section(item: dict, is_false_positive: bool = True) -> None:
    """Render LLM verification item section (used inside expander).

    This is a wrapper that calls render_verification_item from cards module.

    Args:
        item: Verification result dict
        is_false_positive: True for FP, False for TN
    """
    from .cards import render_verification_item
    render_verification_item(item, is_false_positive)