"""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'{type_label} ' f'{display_url}{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"""
๐Ÿ“Š ๋ถ„์„ ๊ฒฐ๊ณผ ํ•ด์„

๐Ÿ“„ ๋‹ต๋ณ€ ์ „์ฒด ๋ถ€์ • ํ™•์‹ ๋„: {confidence:.0%} ({tier})
๋‹ต๋ณ€ ์ „์ฒด๊ฐ€ ๋ถ€์ •์ ์ธ ํ†ค์ธ์ง€ ํŒ๋‹จํ•œ ์ ์ˆ˜์ž…๋‹ˆ๋‹ค. (์—ฌ๋Ÿฌ ๋ธŒ๋žœ๋“œ๊ฐ€ ์–ธ๊ธ‰๋˜๋ฉด ํ˜ผํ•ฉ๋จ)

๐Ÿ” ๋ธŒ๋žœ๋“œ๋ณ„ ๋ถ€์ • ํ™•์‹ ๋„ (์•„๋ž˜ ABSA ์ฐธ์กฐ)
ํŠน์ • ๋ธŒ๋žœ๋“œ์— ๋Œ€ํ•œ ์–ธ๊ธ‰๋งŒ ์ถ”์ถœํ•˜์—ฌ ๊ทธ ์–ธ๊ธ‰์ด ๋ถ€์ •์ ์ธ์ง€ ํŒ๋‹จํ•œ ์ ์ˆ˜์ž…๋‹ˆ๋‹ค.
์˜ˆ: ๋‹ต๋ณ€ ์ „์ฒด๋Š” 64%(LOW)์—ฌ๋„, ํŠน์ • ๋ธŒ๋žœ๋“œ ์–ธ๊ธ‰์€ 91%(HIGH)์ผ ์ˆ˜ ์žˆ์Œ

๋‹ต๋ณ€ ํ†ค: {emotion_ko}
๋‹ต๋ณ€ ์ „์ฒด์˜ ๊ฐ์ •์  ๋ถ„์œ„๊ธฐ์ž…๋‹ˆ๋‹ค.
""", 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'
{html.escape(display_answer)}
', 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'{sentiment} ' f'{brand_name} (๐Ÿ  ์ž์‚ฌ) - {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'{sentiment} ' f'{brand_name} (๐Ÿข ๊ฒฝ์Ÿ์‚ฌ) - {absa_emoji} ๋ธŒ๋žœ๋“œ ํ™•์‹ ๋„ {conf:.0%} ({absa_tier})', unsafe_allow_html=True ) elif competitor_brands: st.markdown( f'์–ธ๊ธ‰๋จ ' f'{", ".join(competitor_brands)} (๐Ÿข ๊ฒฝ์Ÿ์‚ฌ)', 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)