Spaces:
Sleeping
Sleeping
| """κ°μ±λΆμ μ€ν ν. | |
| Job κ΄λ¦¬, Pre-flight 체ν¬, νμ΄νλΌμΈ μκ°ν. | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| from core.api_client import ChainShiftClient | |
| from core.job_realtime import ( | |
| get_active_jobs, | |
| get_recent_jobs, | |
| format_job_duration, | |
| get_status_emoji, | |
| get_status_label, | |
| ) | |
| def render(data: dict): | |
| """μ€ν ν λ λλ§.""" | |
| st.markdown("##### π κ°μ±λΆμ μ€ν") | |
| st.caption("μΊ νμΈμ κ°μ±λΆμ Jobμ μμνκ³ μ§ν μν©μ νμΈν©λλ€") | |
| if not data or (not data.get("api_key") and not data.get("access_token")): | |
| st.warning("μΈμ¦ μ λ³΄κ° μ€μ λμ§ μμμ΅λλ€.") | |
| return | |
| analysis_client = ChainShiftClient(api_key=data.get("api_key"), access_token=data.get("access_token")) | |
| # --- Job Status --- | |
| if st.button("π μλ‘κ³ μΉ¨", key="sentiment:manual_refresh_jobs"): | |
| st.rerun() | |
| try: | |
| active_jobs = get_active_jobs(campaign_id=data["campaign_id"], limit=5) | |
| recent_jobs = get_recent_jobs(campaign_id=data["campaign_id"], limit=20) | |
| active_ids = {j["id"] for j in active_jobs} | |
| jobs_list = active_jobs + [j for j in recent_jobs if j["id"] not in active_ids] | |
| except Exception as e: | |
| active_jobs = [] | |
| jobs_list = [] | |
| st.warning(f"Job λͺ©λ‘ λ‘λ μ€ν¨: {e}") | |
| has_active = len(active_jobs) > 0 | |
| # --- Pipeline Visualization --- | |
| st.markdown("---") | |
| st.markdown("###### π λ°μ΄ν° νμ΄νλΌμΈ") | |
| _render_pipeline(data) | |
| # --- Brand Status --- | |
| _render_brand_status(analysis_client, data.get("campaign_id")) | |
| # --- Current status + Start button --- | |
| st.markdown("---") | |
| st.markdown("###### βΆοΈ λΆμ μ€ν") | |
| if has_active: | |
| _render_active_job(active_jobs[0], analysis_client) | |
| else: | |
| _render_inactive_state(jobs_list, analysis_client, data.get("campaign_id")) | |
| # --- Job history --- | |
| st.markdown("---") | |
| st.markdown("###### π Job μ΄λ ₯") | |
| if jobs_list: | |
| _render_job_history(jobs_list) | |
| else: | |
| st.caption("Job μ΄λ ₯μ΄ μμ΅λλ€.") | |
| def _fetch_brands(client: ChainShiftClient, campaign_id: int) -> list[dict]: | |
| """μΊ νμΈ λΈλλ λͺ©λ‘ μ‘°ν (session_state μΊμ).""" | |
| cache_key = f"campaign_brands_{campaign_id}" | |
| if cache_key in st.session_state: | |
| return st.session_state[cache_key] | |
| try: | |
| brands = client.get_campaign_brands(campaign_id) | |
| st.session_state[cache_key] = brands | |
| return brands | |
| except Exception: | |
| return [] | |
| def _render_brand_status(client: ChainShiftClient, campaign_id: int): | |
| """μΊ νμΈ λΈλλ νν© νμ.""" | |
| brands = _fetch_brands(client, campaign_id) | |
| if not brands: | |
| return | |
| in_house = [b for b in brands if b.get("brand_type") in ("PRIMARY", "USER")] | |
| competitor = [b for b in brands if b.get("brand_type") == "SECONDARY"] | |
| with st.expander(f"π·οΈ μΊ νμΈ λΈλλ νν© (μμ¬ {len(in_house)}κ° / κ²½μμ¬ {len(competitor)}κ°)", expanded=False): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown(f"**μμ¬ λΈλλ** ({len(in_house)}κ°)") | |
| if in_house: | |
| for b in in_house: | |
| synonyms = b.get("synonyms") or [] | |
| syn_text = f" \nμ μ¬μ΄: {', '.join(synonyms)}" if synonyms else "" | |
| st.markdown(f"- **{b['name']}**{syn_text}") | |
| else: | |
| st.caption("λ±λ‘λ μμ¬ λΈλλ μμ") | |
| with col2: | |
| st.markdown(f"**κ²½μμ¬ λΈλλ** ({len(competitor)}κ°)") | |
| if competitor: | |
| for b in competitor: | |
| synonyms = b.get("synonyms") or [] | |
| syn_text = f" \nμ μ¬μ΄: {', '.join(synonyms)}" if synonyms else "" | |
| st.markdown(f"- **{b['name']}**{syn_text}") | |
| else: | |
| st.caption("λ±λ‘λ κ²½μμ¬ λΈλλ μμ") | |
| def _render_pipeline(data: dict): | |
| """νμ΄νλΌμΈ μκ°ν.""" | |
| pipe_col1, pipe_col2, pipe_col3, pipe_col4 = st.columns(4) | |
| overview_total = data.get("overview_total_answers") or 0 | |
| overview_ih_neg = data.get("overview_nudge_candidates") or 0 | |
| overview_llm_done = data.get("overview_llm_verified") or 0 | |
| fp_rate = data.get("overview_false_positive_rate") or 0 | |
| overview_llm_confirmed = overview_llm_done - int(overview_llm_done * fp_rate) | |
| with pipe_col1: | |
| st.markdown(f""" | |
| <div style="background: #DBEAFE; border-radius: 8px; padding: 12px; text-align: center;"> | |
| <div style="font-size: 24px;">π₯</div> | |
| <div style="font-weight: bold;">1. λ°μ΄ν° μμ§</div> | |
| <div style="font-size: 20px; color: #1D4ED8;">{overview_total:,}</div> | |
| <div style="font-size: 12px; color: #6B7280;">AI λ΅λ³</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with pipe_col2: | |
| ih_rate = (overview_ih_neg / overview_total * 100) if overview_total > 0 else 0 | |
| st.markdown(f""" | |
| <div style="background: #FEF3C7; border-radius: 8px; padding: 12px; text-align: center;"> | |
| <div style="font-size: 24px;">π</div> | |
| <div style="font-weight: bold;">2. DeBERTa λΆμ</div> | |
| <div style="font-size: 20px; color: #D97706;">{overview_ih_neg:,}</div> | |
| <div style="font-size: 12px; color: #6B7280;">λΆμ κ°μ§ ({ih_rate:.1f}%)</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with pipe_col3: | |
| verify_rate = (overview_llm_done / overview_ih_neg * 100) if overview_ih_neg > 0 else 0 | |
| st.markdown(f""" | |
| <div style="background: #D1FAE5; border-radius: 8px; padding: 12px; text-align: center;"> | |
| <div style="font-size: 24px;">π€</div> | |
| <div style="font-weight: bold;">3. LLM κ²μ¦</div> | |
| <div style="font-size: 20px; color: #059669;">{overview_llm_done:,}</div> | |
| <div style="font-size: 12px; color: #6B7280;">μλ£ ({verify_rate:.0f}%)</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| with pipe_col4: | |
| confirm_rate = (overview_llm_confirmed / overview_llm_done * 100) if overview_llm_done > 0 else 0 | |
| st.markdown(f""" | |
| <div style="background: #FEE2E2; border-radius: 8px; padding: 12px; text-align: center;"> | |
| <div style="font-size: 24px;">π―</div> | |
| <div style="font-weight: bold;">4. μ ν</div> | |
| <div style="font-size: 20px; color: #DC2626;">{overview_llm_confirmed:,}</div> | |
| <div style="font-size: 12px; color: #6B7280;">μ νλ₯ {confirm_rate:.1f}%</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| def _render_active_job(active: dict, client: ChainShiftClient): | |
| """νμ± Job λ λλ§.""" | |
| progress = active.get("progress", 0) | |
| status = active.get("status", "") | |
| status_emoji = get_status_emoji(status) | |
| status_label = get_status_label(status) | |
| duration = format_job_duration(active) | |
| message = active.get("message", "μ²λ¦¬ μ€...") | |
| total_answers = active.get("total_answers", 0) | |
| processed = active.get("processed_answers", 0) | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(135deg, #EBF5FF 0%, #F0F9FF 100%); | |
| border-radius: 12px; padding: 20px; margin-bottom: 16px; | |
| border: 1px solid #BFDBFE;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <div> | |
| <span style="font-size: 28px;">{status_emoji}</span> | |
| <span style="font-size: 20px; font-weight: bold; margin-left: 8px;">{status_label}</span> | |
| </div> | |
| <div style="text-align: right;"> | |
| <div style="font-size: 32px; font-weight: bold; color: #1D4ED8;">{progress}%</div> | |
| <div style="font-size: 12px; color: #6B7280;">μμμκ°: {duration}</div> | |
| </div> | |
| </div> | |
| <div style="margin-top: 12px; font-size: 14px; color: #374151;"> | |
| {message} | |
| </div> | |
| <div style="margin-top: 8px; font-size: 12px; color: #6B7280;"> | |
| μ²λ¦¬: {processed:,} / {total_answers:,} λ΅λ³ | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.progress(progress / 100) | |
| if st.button("β λΆμ μ·¨μ", key="sentiment:cancel_job", type="secondary"): | |
| try: | |
| client.cancel_analysis_job(active["id"]) | |
| st.success("μ·¨μ μμ² μλ£") | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"μ·¨μ μ€ν¨: {e}") | |
| def _render_inactive_state(jobs_list: list, client: ChainShiftClient, campaign_id: int): | |
| """λΉνμ± μν λ λλ§.""" | |
| # --- μ΅κ·Ό λΆμ μν --- | |
| if jobs_list: | |
| latest = jobs_list[0] | |
| latest_status = latest.get("status", "") | |
| latest_emoji = get_status_emoji(latest_status) | |
| latest_label = get_status_label(latest_status) | |
| latest_duration = format_job_duration(latest) | |
| completed_at = latest.get("completed_at") or latest.get("created_at") or "" | |
| if completed_at: | |
| completed_at = completed_at[:19].replace("T", " ") | |
| if latest_status == "completed": | |
| st.success(f"{latest_emoji} μ΅κ·Ό λΆμ: **{latest_label}** (μμ: {latest_duration}, {completed_at})") | |
| elif latest_status == "failed": | |
| st.error(f"{latest_emoji} μ΅κ·Ό λΆμ: **{latest_label}** - {(latest.get('error_message') or 'μ μ μλ μ€λ₯')[:50]}") | |
| else: | |
| st.info(f"{latest_emoji} μ΅κ·Ό λΆμ: **{latest_label}** ({completed_at})") | |
| else: | |
| st.info("μμ§ μ€νλ λΆμμ΄ μμ΅λλ€.") | |
| # --- λΆμ μ€μ --- | |
| st.markdown("###### λΆμ μ€μ ") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| run_brand = st.checkbox("μμ¬/κ²½μμ¬ κ°μ± λΆμ", value=True, key="run:brand") | |
| with col2: | |
| run_keyword = st.checkbox("ν€μλ κ°μ± λΆμ", value=False, key="run:keyword") | |
| # ν€μλ μ λ ₯ (keyword scope μ ν μ) | |
| keywords = [] | |
| if run_keyword: | |
| keywords_input = st.text_input( | |
| "λΆμ ν€μλ (μΌν ꡬλΆ)", | |
| key="run:keywords", | |
| placeholder="μ¬λ£, μν, μλ¬μ§", | |
| ) | |
| keywords = [k.strip() for k in keywords_input.split(",") if k.strip()] | |
| # LLM κ²μ¦ μ΅μ | |
| include_llm = st.checkbox( | |
| "2μ°¨ LLM κ²μ¦ ν¬ν¨", | |
| value=False, | |
| key="run:llm", | |
| help="λΆμ κ°μ§ κ²°κ³Όλ₯Ό LLMμΌλ‘ κ΅μ°¨ κ²μ¦ν©λλ€ (μκ° μΆκ°)", | |
| ) | |
| # λΆμ μμ λ²νΌ (keyword μ ν μ ν€μλ μ λ ₯ νμ) | |
| can_start = run_brand or (run_keyword and len(keywords) > 0) | |
| if st.button( | |
| "βΆοΈ λΆμ μμ", | |
| type="primary", | |
| key="sentiment:start_analysis", | |
| disabled=not can_start, | |
| ): | |
| scope = [] | |
| if run_brand: | |
| scope.append("brand") | |
| if run_keyword: | |
| scope.append("keyword") | |
| options = { | |
| "scope": scope, | |
| "keywords": keywords if run_keyword else [], | |
| "include_llm_verification": include_llm, | |
| } | |
| try: | |
| client.start_analysis_job(campaign_id, options=options) | |
| st.success("λΆμ Jobμ΄ μμ±λμμ΅λλ€!") | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"λΆμ μμ μ€ν¨: {e}") | |
| # --- μ°κ΅¬ λΆμ μ€μΊν΄λ© --- | |
| st.markdown("---") | |
| st.markdown("###### μ°κ΅¬ λΆμ (μ€λΉ μ€)") | |
| st.info( | |
| "μ°κ΅¬ λΆμμ λ°μ΄ν°ν Athena ν μ΄λΈ μΈν μλ£ ν μ¬μ© κ°λ₯ν©λλ€.\n" | |
| "νμ ν μ΄λΈ: `fanouts` (S3 μ€λ μ· λ―Έν¬ν¨)" | |
| ) | |
| def _render_job_history(jobs_list: list): | |
| """Job μ΄λ ₯ ν μ΄λΈ.""" | |
| rows = [] | |
| for j in jobs_list: | |
| status = j.get("status", "") | |
| status_emoji = get_status_emoji(status) | |
| status_label = get_status_label(status) | |
| duration = format_job_duration(j) | |
| total = j.get("total_answers", 0) | |
| nudge = j.get("nudge_candidates", 0) | |
| rows.append({ | |
| "μν": f"{status_emoji} {status_label}", | |
| "μ§νλ₯ ": f"{j.get('progress', 0)}%", | |
| "μ²λ¦¬λ": f"{total:,}건" if total else "-", | |
| "λμ§ ν보": f"{nudge:,}건" if nudge else "-", | |
| "μμμκ°": duration, | |
| "μμ±μΌ": (j.get("created_at") or "")[:19].replace("T", " "), | |
| "ID": (j.get("id") or "")[:8], | |
| }) | |
| st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True) | |