Spaces:
Sleeping
Sleeping
| """๊ณ์ธต ๋ถ์ ์งํ ํํฉ + ๋ถ์ ๊ฒฐ๊ณผ ํญ. | |
| ์งํ ํํฉ: Supabase ์ง์ ์ฟผ๋ฆฌ (Vercel timeout ํํผ) | |
| ๋ถ์ ๊ฒฐ๊ณผ: API ํธ์ถ (์ง๋ฌธ ๋ฐ์ดํฐ) | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| from core.api_client import ChainShiftClient | |
| from core.job_realtime import ( | |
| get_active_hierarchy_jobs, | |
| get_recent_hierarchy_jobs, | |
| get_hierarchy_step_label, | |
| format_job_duration, | |
| get_status_emoji, | |
| get_status_label, | |
| ) | |
| # ============================================================================ | |
| # ์งํ ํํฉ ํญ | |
| # ============================================================================ | |
| def render_active(base_ctx: dict): | |
| """์งํ ํํฉ ํญ ๋ ๋๋ง.""" | |
| st.markdown("##### โณ ์งํ ํํฉ") | |
| st.caption("๋๊ธฐ ์ค์ด๊ฑฐ๋ ์ฒ๋ฆฌ ์ค์ธ ๊ณ์ธต ๋ถ์ Job์ ํ์ธํฉ๋๋ค") | |
| if st.button("๐ ์๋ก๊ณ ์นจ", key="hier:refresh_active"): | |
| st.rerun() | |
| try: | |
| active_jobs = get_active_hierarchy_jobs(limit=5) | |
| except Exception as e: | |
| st.warning(f"Job ๋ชฉ๋ก ๋ก๋ ์คํจ: {e}") | |
| active_jobs = [] | |
| if not active_jobs: | |
| st.info("ํ์ฌ ์งํ ์ค์ธ ๊ณ์ธต ๋ถ์ Job์ด ์์ต๋๋ค.") | |
| return | |
| for job in active_jobs: | |
| _render_active_card(job) | |
| def _render_active_card(job: dict): | |
| """ํ์ฑ Job ์นด๋ ๋ ๋๋ง.""" | |
| progress = job.get("progress", 0) | |
| status = job.get("status", "") | |
| step = job.get("current_step") | |
| step_label = get_hierarchy_step_label(step) | |
| status_emoji = get_status_emoji(status) | |
| status_label = get_status_label(status) | |
| duration = format_job_duration(job) | |
| prompt = job.get("prompt", "") | |
| title = job.get("title") or prompt[:30] | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(135deg, #EDE9FE 0%, #F5F3FF 100%); | |
| border-radius: 12px; padding: 20px; margin-bottom: 16px; | |
| border: 1px solid #C4B5FD;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <div> | |
| <span style="font-size: 28px;">{status_emoji}</span> | |
| <span style="font-size: 18px; font-weight: bold; margin-left: 8px;">{title}</span> | |
| </div> | |
| <div style="text-align: right;"> | |
| <div style="font-size: 32px; font-weight: bold; color: #6D28D9;">{progress}%</div> | |
| <div style="font-size: 12px; color: #6B7280;">์์์๊ฐ: {duration}</div> | |
| </div> | |
| </div> | |
| <div style="margin-top: 12px; font-size: 14px; color: #374151;"> | |
| {status_label} · {step_label} | |
| </div> | |
| <div style="margin-top: 4px; font-size: 12px; color: #6B7280;"> | |
| ํค์๋: {prompt} | |
| </div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.progress(progress / 100) | |
| # ============================================================================ | |
| # ๋ถ์ ๊ฒฐ๊ณผ ํญ | |
| # ============================================================================ | |
| def render_history(base_ctx: dict): | |
| """๋ถ์ ๊ฒฐ๊ณผ ํญ ๋ ๋๋ง.""" | |
| st.markdown("##### ๐ ๋ถ์ ๊ฒฐ๊ณผ") | |
| st.caption("์๋ฃ๋ ๊ณ์ธต ๋ถ์ Job ์ด๋ ฅ๊ณผ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํฉ๋๋ค") | |
| if st.button("๐ ์๋ก๊ณ ์นจ", key="hier:refresh_history"): | |
| st.rerun() | |
| try: | |
| jobs = get_recent_hierarchy_jobs(limit=20) | |
| except Exception as e: | |
| st.warning(f"Job ์ด๋ ฅ ๋ก๋ ์คํจ: {e}") | |
| jobs = [] | |
| if not jobs: | |
| st.info("์์ง ๊ณ์ธต ๋ถ์ ์ด๋ ฅ์ด ์์ต๋๋ค.") | |
| return | |
| # Job ์ด๋ ฅ ํ ์ด๋ธ | |
| rows = [] | |
| for j in jobs: | |
| status = j.get("status", "") | |
| emoji = get_status_emoji(status) | |
| label = get_status_label(status) | |
| duration = format_job_duration(j) | |
| rows.append({ | |
| "์ํ": f"{emoji} {label}", | |
| "์ ๋ชฉ": j.get("title") or "-", | |
| "ํค์๋": j.get("prompt", "")[:30], | |
| "์งํ๋ฅ ": f"{j.get('progress', 0)}%", | |
| "์์์๊ฐ": 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) | |
| # ์๋ฃ๋ Job expander: ์ง๋ฌธ ํต๊ณ + ์ํ | |
| completed_jobs = [j for j in jobs if j.get("status") == "completed"] | |
| if not completed_jobs: | |
| return | |
| st.markdown("---") | |
| st.markdown("###### ์๋ฃ๋ ๋ถ์ ์์ธ") | |
| if not base_ctx.get("api_key") and not base_ctx.get("access_token"): | |
| st.caption("์ง๋ฌธ ์์ธ ๋ณด๊ธฐ๋ ์ธ์ฆ์ด ํ์ํฉ๋๋ค.") | |
| return | |
| client = ChainShiftClient( | |
| api_key=base_ctx.get("api_key"), | |
| access_token=base_ctx.get("access_token"), | |
| ) | |
| for job in completed_jobs[:5]: | |
| job_id = job["id"] | |
| title = job.get("title") or job.get("prompt", "")[:30] | |
| created = (job.get("created_at") or "")[:10] | |
| with st.expander(f"{title} ({created})", expanded=False): | |
| _render_job_detail(client, job_id) | |
| def _render_job_detail(client: ChainShiftClient, job_id: str): | |
| """์๋ฃ๋ Job ์์ธ: ์ง๋ฌธ ํต๊ณ + ์ํ ์ง๋ฌธ.""" | |
| # ์ง๋ฌธ ํต๊ณ | |
| try: | |
| stats_resp = client.get_hierarchy_question_stats(job_id) | |
| stats = (stats_resp or {}).get("data") or {} | |
| except Exception as e: | |
| st.warning(f"ํต๊ณ ๋ก๋ ์คํจ: {e}") | |
| stats = {} | |
| if stats: | |
| total = stats.get("total_questions", 0) | |
| brand_count = stats.get("brand_mention_count", 0) | |
| persona_count = stats.get("persona_included_count", 0) | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("์ด ์ง๋ฌธ", f"{total:,}๊ฐ") | |
| with col2: | |
| st.metric("๋ธ๋๋ ํฌํจ", f"{brand_count:,}๊ฐ") | |
| with col3: | |
| st.metric("ํ๋ฅด์๋ ์ ์ฉ", f"{persona_count:,}๊ฐ") | |
| # ์ฌ์ ๋ณ ๋ถํฌ | |
| by_depth1 = stats.get("by_journey_depth1") or {} | |
| if by_depth1: | |
| st.markdown("**์ฌ์ ์ ํ๋ณ ๋ถํฌ**") | |
| depth1_labels = { | |
| "awareness_comparison": "์ธ์ง/๋น๊ต", | |
| "purchase": "๊ตฌ๋งค", | |
| "post_purchase": "๊ตฌ๋งค ํ", | |
| } | |
| depth1_rows = [ | |
| {"์ฌ์ ": depth1_labels.get(k, k), "์ง๋ฌธ ์": v} | |
| for k, v in by_depth1.items() | |
| ] | |
| st.dataframe( | |
| pd.DataFrame(depth1_rows), | |
| use_container_width=True, | |
| hide_index=True, | |
| ) | |
| # ์ํ ์ง๋ฌธ 10๊ฐ | |
| try: | |
| q_resp = client.get_hierarchy_questions(job_id, page=1, page_size=10) | |
| questions = ((q_resp or {}).get("data") or {}).get("items") or [] | |
| except Exception: | |
| questions = [] | |
| if questions: | |
| st.markdown("**์ํ ์ง๋ฌธ (์ต๋ 10๊ฐ)**") | |
| for i, q in enumerate(questions, 1): | |
| journey = q.get("journey_depth2", "") | |
| question_text = q.get("question", "") | |
| brand = " ๐ท๏ธ" if q.get("brand_mention") else "" | |
| st.markdown(f"{i}. [{journey}] {question_text}{brand}") | |
| elif stats: | |
| st.caption("์ง๋ฌธ ๋ฐ์ดํฐ๊ฐ ์์ง ์์ต๋๋ค.") | |