GitHub Action
Sync from GitHub
ef78361
Raw
History Blame Contribute Delete
7.8 kB
"""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'}")