Spaces:
Sleeping
Sleeping
| """κ°μ±λΆμ μ€λ²λ·° ν. | |
| μ 체 κ°μ± λΆμ + λΈλλ λ©μ λΆμ + LLM 2μ°¨ κ²μ¦ κ²°κ³Ό. | |
| """ | |
| import streamlit as st | |
| from core.charts import create_brand_sentiment_chart | |
| from core.supabase_client import get_polarity_stats, get_answers_by_polarity | |
| from core.utils import truncate_text | |
| def render(data: dict): | |
| """μ€λ²λ·° ν λ λλ§.""" | |
| # --- μ 체 κ°μ± λΆμ --- | |
| st.markdown("##### π μ 체 κ°μ± λΆμ") | |
| st.caption("AI λ΅λ³μ μ 체 κ°μ± λΆν¬λ₯Ό νμΈν©λλ€ (κΈμ /μ€λ¦½/λΆμ )") | |
| polarity_in_house_only = st.checkbox( | |
| "π μμ¬ λΈλλ μΈκΈ λ΅λ³λ§ 보기", | |
| value=False, | |
| key="sentiment:polarity_in_house_filter", | |
| help="μ²΄ν¬ μ μμ¬ λΈλλκ° μΈκΈλ λ΅λ³λ§ νμν©λλ€.", | |
| ) | |
| try: | |
| polarity_stats = get_polarity_stats(data["campaign_id"], in_house_only=polarity_in_house_only) | |
| positive_count = polarity_stats.get("positive", 0) | |
| neutral_count = polarity_stats.get("neutral", 0) | |
| negative_count = polarity_stats.get("negative", 0) | |
| total_answers_pol = positive_count + neutral_count + negative_count | |
| pol_col1, pol_col2, pol_col3, pol_col4 = st.columns(4) | |
| with pol_col1: | |
| st.metric("μ 체 λΆμ", f"{total_answers_pol:,}건") | |
| with pol_col2: | |
| pos_rate = (positive_count / total_answers_pol * 100) if total_answers_pol > 0 else 0 | |
| st.metric("π κΈμ ", f"{positive_count:,}건", f"{pos_rate:.1f}%") | |
| with pol_col3: | |
| neu_rate = (neutral_count / total_answers_pol * 100) if total_answers_pol > 0 else 0 | |
| st.metric("π μ€λ¦½", f"{neutral_count:,}건", f"{neu_rate:.1f}%") | |
| with pol_col4: | |
| neg_rate = (negative_count / total_answers_pol * 100) if total_answers_pol > 0 else 0 | |
| st.metric("π λΆμ ", f"{negative_count:,}건", f"{neg_rate:.1f}%") | |
| st.markdown("---") | |
| polarity_filter = st.selectbox( | |
| "κ°μ± λΆλ₯ μ ν", | |
| options=["positive", "neutral", "negative"], | |
| format_func=lambda x: {"positive": "π κΈμ ", "neutral": "π μ€λ¦½", "negative": "π λΆμ "}[x], | |
| key="sentiment:polarity_filter_tab6", | |
| ) | |
| polarity_page = st.number_input("νμ΄μ§", min_value=1, value=1, key="sentiment:polarity_page") | |
| polarity_items, polarity_total = get_answers_by_polarity( | |
| data["campaign_id"], polarity_filter, page=polarity_page, page_size=20, | |
| in_house_only=polarity_in_house_only, | |
| ) | |
| st.markdown(f"**{polarity_total:,}건** μ€ {len(polarity_items)}건 νμ") | |
| for item in polarity_items: | |
| _render_polarity_item(item) | |
| except Exception as e: | |
| st.error(f"κ°μ± λ°μ΄ν° λ‘λ μ€ν¨: {e}") | |
| st.info("Supabase μ°κ²° μ€μ μ νμΈνμΈμ") | |
| # --- LLM 2μ°¨ κ²μ¦ κ²°κ³Ό --- | |
| _render_llm_verification_summary(data) | |
| # --- λΈλλ λΆμ --- | |
| st.markdown("---") | |
| st.markdown("##### π·οΈ λΈλλ λ©μ λΆμ") | |
| st.caption("μμ¬ λΈλλμ κ²½μμ¬ λΈλλκ° AI λ΅λ³μμ μ΄λ»κ² μΈκΈλλμ§ λΆμν©λλ€") | |
| brand_data = data["brand_data"] or {} | |
| in_house_summary = brand_data.get("in_house_summary", []) | |
| competitor_summary = brand_data.get("competitor_summary", []) | |
| total_answers = brand_data.get("total_answers", 0) | |
| st.markdown(f"**λΆμλ AI λ΅λ³**: {total_answers}건") | |
| st.markdown("---") | |
| brand_col1, brand_col2 = st.columns(2) | |
| with brand_col1: | |
| st.markdown("##### π μμ¬ λΈλλ") | |
| if in_house_summary: | |
| for brand in in_house_summary[:5]: | |
| _render_brand_card(brand, "in_house") | |
| else: | |
| st.info("μμ¬ λΈλλ λ°μ΄ν°κ° μμ΅λλ€") | |
| with brand_col2: | |
| st.markdown("##### π’ κ²½μμ¬ λΈλλ") | |
| if competitor_summary: | |
| for brand in competitor_summary[:5]: | |
| _render_brand_card(brand, "competitor") | |
| else: | |
| st.info("κ²½μμ¬ λΈλλ λ°μ΄ν°κ° μμ΅λλ€") | |
| if in_house_summary or competitor_summary: | |
| st.markdown("---") | |
| st.markdown("##### π λΈλλλ³ κ°μ± λΉκ΅") | |
| all_brands = in_house_summary + competitor_summary | |
| if all_brands: | |
| fig = create_brand_sentiment_chart(all_brands) | |
| st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False}) | |
| def _render_llm_verification_summary(data: dict): | |
| """LLM 2μ°¨ κ²μ¦ κ²°κ³Ό μμ½ λ λλ§.""" | |
| st.markdown("---") | |
| st.markdown("##### π€ LLM 2μ°¨ κ²μ¦ κ²°κ³Ό") | |
| st.caption( | |
| "DeBERTa(1μ°¨ AI)κ° λΆμ κ°μ§ν λ΅λ³μ LLM(2μ°¨ AI)μ΄ μ¬κ²μ¦ν κ²°κ³Όμ λλ€. " | |
| "π΄ μ ν = μ€μ λΆμ νμΈ (리μ€ν¬) | π’ μ€ν = λΆμ μλ νμΈ (μμ )" | |
| ) | |
| llm_stats = data.get("llm_verification_stats") or {} | |
| total_nudge = data.get("total_nudge", 0) | |
| total_verified = llm_stats.get("total_verified", 0) | |
| true_negatives = llm_stats.get("true_negatives", 0) | |
| false_positives = llm_stats.get("false_positives", 0) | |
| pending = total_nudge - total_verified | |
| if total_nudge == 0: | |
| st.info("λΆμ κ°μ§λ λμ§ νλ³΄κ° μμ΅λλ€.") | |
| return | |
| # --- Metrics --- | |
| llm_col1, llm_col2, llm_col3, llm_col4 = st.columns(4) | |
| with llm_col1: | |
| st.metric("π λμ§ ν보", f"{total_nudge:,}건") | |
| with llm_col2: | |
| verify_rate = (total_verified / total_nudge * 100) if total_nudge > 0 else 0 | |
| st.metric("β κ²μ¦ μλ£", f"{total_verified:,}건", f"{verify_rate:.0f}%") | |
| with llm_col3: | |
| tp_rate = (true_negatives / total_verified * 100) if total_verified > 0 else 0 | |
| st.metric("π― μ ν", f"{true_negatives:,}건", f"{tp_rate:.1f}%") | |
| with llm_col4: | |
| fp_rate = (false_positives / total_verified * 100) if total_verified > 0 else 0 | |
| st.metric("π« μ€ν", f"{false_positives:,}건", f"{fp_rate:.1f}%") | |
| # --- Visual bar --- | |
| if total_verified > 0: | |
| tp_pct = true_negatives / total_nudge * 100 | |
| fp_pct = false_positives / total_nudge * 100 | |
| pending_pct = pending / total_nudge * 100 | |
| st.markdown(f""" | |
| <div style="display: flex; height: 28px; border-radius: 6px; overflow: hidden; margin: 8px 0;"> | |
| <div style="width: {tp_pct}%; background: #EF4444; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: bold;"> | |
| {'μ ν' if tp_pct > 8 else ''} | |
| </div> | |
| <div style="width: {fp_pct}%; background: #10B981; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: bold;"> | |
| {'μ€ν' if fp_pct > 8 else ''} | |
| </div> | |
| <div style="width: {pending_pct}%; background: #D1D5DB; display: flex; align-items: center; justify-content: center; color: #6B7280; font-size: 12px;"> | |
| {'λ―Έκ²μ¦' if pending_pct > 8 else ''} | |
| </div> | |
| </div> | |
| <div style="display: flex; gap: 16px; font-size: 12px; color: #6B7280; margin-bottom: 4px;"> | |
| <span>π΄ μ ν {tp_pct:.1f}%</span> | |
| <span>π’ μ€ν {fp_pct:.1f}%</span> | |
| <span>βͺ λ―Έκ²μ¦ {pending_pct:.1f}%</span> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # --- Confirmed negative tier distribution --- | |
| candidates = data.get("candidates", []) | |
| confirmed = [c for c in candidates if c.get("llm_verified") and c.get("llm_is_negative")] | |
| if confirmed: | |
| tier_dist: dict[str, int] = {} | |
| for c in confirmed: | |
| tier = c.get("llm_adjusted_tier") or "UNKNOWN" | |
| tier_dist[tier] = tier_dist.get(tier, 0) + 1 | |
| tier_colors = {"HIGH": "#EF4444", "MEDIUM": "#F59E0B", "LOW": "#3B82F6", "NONE": "#10B981", "UNKNOWN": "#9CA3AF"} | |
| st.markdown("**μ ν λ΅λ³μ LLM λ±κΈ λΆν¬**") | |
| tier_cols = st.columns(len(tier_dist)) | |
| for i, (tier, count) in enumerate(sorted(tier_dist.items(), key=lambda x: -x[1])): | |
| color = tier_colors.get(tier, "#9CA3AF") | |
| pct = count / len(confirmed) * 100 | |
| with tier_cols[i]: | |
| st.markdown(f""" | |
| <div style="text-align: center; padding: 8px; background: {color}15; border-radius: 8px; border: 1px solid {color}40;"> | |
| <div style="font-size: 20px; font-weight: bold; color: {color};">{count}</div> | |
| <div style="font-size: 12px; color: #6B7280;">{tier} ({pct:.0f}%)</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| def _render_polarity_item(item: dict): | |
| """κ°μ± νλͺ© λ λλ§.""" | |
| polarity_emoji = {"positive": "π", "neutral": "π", "negative": "π"}.get(item.get('overall_polarity'), "β") | |
| confidence = item.get('overall_confidence', 0) or 0 | |
| with st.expander(f"{polarity_emoji} {truncate_text(item.get('question_content', 'N/A'), 80)}", expanded=False): | |
| 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: | |
| st.markdown(f"**Tier**: {item.get('routing_tier', 'N/A')}") | |
| st.markdown(f"**κ°μ **: {item.get('dominant_emotion', 'N/A')}") | |
| 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_brand_card(brand: dict, brand_type: str): | |
| """λΈλλ μΉ΄λ λ λλ§.""" | |
| brand_name = brand.get("brand_name", "Unknown") | |
| total_mentions = brand.get("total_mentions", 0) | |
| positive_rate = brand.get("positive_rate", 0) | |
| negative_rate = brand.get("negative_rate", 0) | |
| bg_color = "#F0F9FF" if brand_type == "in_house" else "#FEF3C7" | |
| brand_html = f'<div style="background: {bg_color}; border-radius: 8px; padding: 12px; margin: 8px 0;">' | |
| brand_html += f'<div style="font-weight: bold; font-size: 16px; margin-bottom: 8px;">{brand_name}</div>' | |
| brand_html += f'<div style="display: flex; gap: 16px; font-size: 13px;">' | |
| brand_html += f'<span>π μΈκΈ: <strong>{total_mentions}</strong></span>' | |
| brand_html += f'<span style="color: #10B981;">β κΈμ : <strong>{positive_rate:.1f}%</strong></span>' | |
| brand_html += f'<span style="color: #EF4444;">β λΆμ : <strong>{negative_rate:.1f}%</strong></span>' | |
| brand_html += '</div></div>' | |
| st.markdown(brand_html, unsafe_allow_html=True) | |