Spaces:
Sleeping
Sleeping
| """Dashboard card components.""" | |
| import html | |
| import streamlit as st | |
| from core.charts import CONFIDENCE_TIER_COLORS | |
| from core.styles import TIER_BORDER_COLORS | |
| from core.utils import get_confidence_tier, truncate_text, format_brands_list | |
| def render_nudge_card( | |
| item: dict, | |
| tier: str, | |
| emoji: str, | |
| tier_desc: str, | |
| confidence: float, | |
| ) -> None: | |
| """Render nudge candidate card with summary info. | |
| Args: | |
| item: Nudge candidate data dict | |
| tier: Confidence tier (HIGH/MEDIUM/LOW) | |
| emoji: Tier emoji | |
| tier_desc: Tier description | |
| confidence: Confidence score (0-1) | |
| """ | |
| cej_stage = item.get("cej_depth2") or item.get("cej_depth1") or "N/A" | |
| platform = item.get("platform", "N/A") | |
| in_house = item.get("in_house_brands", []) | |
| mentioned = item.get("mentioned_brands", []) | |
| question = item.get("question_content", "") | |
| answer = item.get("answer_preview", "") | |
| tier_color = CONFIDENCE_TIER_COLORS.get(tier, "#6B7280") | |
| border_color = TIER_BORDER_COLORS.get(tier, "#6B7280") | |
| question_display = html.escape(truncate_text(question, 200)) | |
| answer_short = html.escape(truncate_text(answer, 150)) | |
| in_house_display = html.escape(format_brands_list(in_house)) | |
| mentioned_display = html.escape(format_brands_list(mentioned)) | |
| header_html = f""" | |
| <div style="border: 2px solid {border_color}; border-radius: 12px; padding: 16px; margin: 12px 0; background: #FAFAFA;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;"> | |
| <span style="background: #E0E7FF; padding: 4px 12px; border-radius: 20px; font-size: 13px;"> | |
| 📍 CEJ: <strong>{cej_stage}</strong> | |
| </span> | |
| <span style="background: {tier_color}; color: white; padding: 4px 12px; border-radius: 20px; font-size: 13px;" title="{tier_desc}"> | |
| {emoji} 답변 전체: {tier} ({confidence:.0%}) | |
| </span> | |
| </div> | |
| <div style="background: #F0F9FF; border-left: 4px solid #3B82F6; padding: 10px; margin-bottom: 10px; border-radius: 0 8px 8px 0;"> | |
| <div style="font-size: 11px; color: #3B82F6; margin-bottom: 2px;">💬 질문</div> | |
| <div style="font-size: 14px;">{question_display}</div> | |
| </div> | |
| <div style="font-size: 13px; color: #6B7280; margin-bottom: 8px;">{answer_short}...</div> | |
| <div style="display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: #6B7280;"> | |
| <span>🏷️ <strong>{in_house_display}</strong></span> | |
| <span>📢 {mentioned_display}</span> | |
| <span>🖥️ {platform}</span> | |
| </div> | |
| </div> | |
| """ | |
| st.markdown(header_html, unsafe_allow_html=True) | |
| def render_brand_card( | |
| brand_name: str, | |
| brand_type: str, | |
| sentiment_data: dict, | |
| ) -> None: | |
| """Render brand mention card. | |
| Args: | |
| brand_name: Brand name | |
| brand_type: 'in_house' or 'competitor' | |
| sentiment_data: Dict with sentiment, confidence, count | |
| """ | |
| sentiment = sentiment_data.get("sentiment", "neutral") | |
| confidence = sentiment_data.get("confidence", 0) | |
| mention_count = sentiment_data.get("count", 0) | |
| type_badge = "🏠 자사" if brand_type == "in_house" else "🏢 경쟁사" | |
| type_bg = "#DBEAFE" if brand_type == "in_house" else "#FEE2E2" | |
| sentiment_colors = { | |
| "positive": "#10B981", | |
| "negative": "#EF4444", | |
| "neutral": "#6B7280", | |
| } | |
| sent_color = sentiment_colors.get(sentiment, "#6B7280") | |
| sentiment_ko = {"positive": "긍정", "negative": "부정", "neutral": "중립"}.get(sentiment, sentiment) | |
| card_html = f""" | |
| <div style="border: 1px solid #E5E7EB; border-radius: 8px; padding: 12px; margin: 8px 0; background: white;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> | |
| <span style="font-weight: 600; font-size: 15px;">{html.escape(brand_name)}</span> | |
| <span style="background: {type_bg}; padding: 2px 8px; border-radius: 12px; font-size: 11px;">{type_badge}</span> | |
| </div> | |
| <div style="display: flex; gap: 12px; font-size: 12px;"> | |
| <span style="background: {sent_color}; color: white; padding: 2px 8px; border-radius: 4px;">{sentiment_ko}</span> | |
| <span>신뢰도: {confidence:.0%}</span> | |
| <span>언급: {mention_count}회</span> | |
| </div> | |
| </div> | |
| """ | |
| st.markdown(card_html, unsafe_allow_html=True) | |
| def render_verification_item(item: dict, is_false_positive: bool = True) -> None: | |
| """Render LLM verification item info (used inside expander). | |
| Args: | |
| item: Verification result dict | |
| is_false_positive: True for FP, False for TN | |
| """ | |
| st.markdown(f"**질문**: {item.get('question_content', 'N/A')}") | |
| st.markdown(f"**답변 미리보기**: {item.get('answer_preview', 'N/A')}") | |
| st.markdown("---") | |
| info_col1, info_col2, info_col3 = st.columns(3) | |
| with info_col1: | |
| st.markdown(f"**플랫폼**: {item.get('platform', 'N/A')}") | |
| st.markdown(f"**CEJ**: {item.get('cej_depth1', 'N/A')} / {item.get('cej_depth2', 'N/A')}") | |
| with info_col2: | |
| st.markdown(f"**1차 판정**: {item.get('routing_tier', 'N/A')}") | |
| st.markdown(f"**1차 감성**: {item.get('overall_polarity', 'N/A')}") | |
| with info_col3: | |
| llm_conf = item.get('llm_confidence', 0) or 0 | |
| st.markdown(f"**LLM 신뢰도**: {llm_conf:.1%}") | |
| st.markdown(f"**LLM 조정 Tier**: {item.get('llm_adjusted_tier', 'N/A')}") | |
| # LLM reasoning | |
| if item.get('llm_reasoning'): | |
| st.markdown("**LLM 판단 근거**:") | |
| if is_false_positive: | |
| st.info(item.get('llm_reasoning')) | |
| else: | |
| st.warning(item.get('llm_reasoning')) | |
| # Evidence spans | |
| if item.get('llm_evidence_spans'): | |
| label = "**근거 문장**:" if is_false_positive else "**부정 근거 문장**:" | |
| st.markdown(label) | |
| for span in (item.get('llm_evidence_spans') or []): | |
| st.markdown(f"- _{span}_") | |
| # Brands | |
| in_house = item.get('in_house_brands', []) or [] | |
| mentioned = item.get('mentioned_brands', []) or [] | |
| if in_house or mentioned: | |
| st.markdown(f"**자사 브랜드**: {', '.join(in_house) if in_house else 'N/A'}") | |
| st.markdown(f"**언급 브랜드**: {', '.join(mentioned) if mentioned else 'N/A'}") | |
| def render_polarity_item(item: dict) -> None: | |
| """Render polarity (sentiment) item info (used inside expander). | |
| Args: | |
| item: Sentiment summary dict | |
| """ | |
| confidence = item.get('overall_confidence', 0) or 0 | |
| st.markdown(f"**질문**: {item.get('question_content', 'N/A')}") | |
| st.markdown(f"**답변 미리보기**: {item.get('answer_preview', 'N/A')}") | |
| st.markdown("---") | |
| info_col1, info_col2, info_col3 = st.columns(3) | |
| with info_col1: | |
| st.markdown(f"**감성**: {item.get('overall_polarity', 'N/A')}") | |
| st.markdown(f"**신뢰도**: {confidence:.1%}") | |
| with info_col2: | |
| st.markdown(f"**플랫폼**: {item.get('platform', 'N/A')}") | |
| st.markdown(f"**CEJ**: {item.get('cej_depth1', 'N/A')} / {item.get('cej_depth2', 'N/A')}") | |
| with info_col3: | |
| tier = item.get('routing_tier', 'N/A') | |
| st.markdown(f"**라우팅 Tier**: {tier}") | |
| emotion = item.get('dominant_emotion', 'N/A') | |
| st.markdown(f"**감정**: {emotion}") | |
| # Brands | |
| in_house = item.get('in_house_brands', []) or [] | |
| mentioned = item.get('mentioned_brands', []) or [] | |
| if in_house or mentioned: | |
| st.markdown(f"**자사 브랜드**: {', '.join(in_house) if in_house else 'N/A'}") | |
| st.markdown(f"**언급 브랜드**: {', '.join(mentioned) if mentioned else 'N/A'}") | |