GitHub Action
Sync from GitHub
ef78361
Raw
History Blame Contribute Delete
7.24 kB
"""๊ณ„์ธต ๋ถ„์„ ์ง„ํ–‰ ํ˜„ํ™ฉ + ๋ถ„์„ ๊ฒฐ๊ณผ ํƒญ.
์ง„ํ–‰ ํ˜„ํ™ฉ: 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} &middot; {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("์งˆ๋ฌธ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„์ง ์—†์Šต๋‹ˆ๋‹ค.")