Spaces:
Sleeping
Sleeping
| """์ ์ฒด ๋ฆฌํฌํธ ์์ฑ ํญ.""" | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| import pandas as pd | |
| import requests | |
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| from core.api_client import ChainShiftClient | |
| def _build_one( | |
| api_key: str, | |
| campaign_id: int, | |
| feat_key: str, | |
| start_date: str, | |
| end_date: str, | |
| enable_insights: bool, | |
| homepage_urls: list[str] | None, | |
| ) -> tuple[str, dict | None, str | None]: | |
| """Worker thread โ st.* ํธ์ถ ๊ธ์ง. ๋ ๋ฆฝ HTTP ํด๋ผ์ด์ธํธ๋ก feature ๋น๋.""" | |
| try: | |
| thread_client = ChainShiftClient(api_key=api_key) | |
| result = thread_client.build_html_feature( | |
| campaign_id=campaign_id, | |
| feature=feat_key, | |
| start_date=start_date, | |
| end_date=end_date, | |
| enable_insights=enable_insights, | |
| enable_action_items=True, | |
| homepage_urls=homepage_urls if feat_key == "homepage-citations" else None, | |
| ) | |
| return feat_key, result, None | |
| except Exception as e: | |
| return feat_key, None, str(e) | |
| def _fetch_report_history( | |
| campaign_id: int, page: int = 1, page_size: int = 20, | |
| _api_key: str = "", _access_token: str = "", | |
| ) -> dict: | |
| """Cached fetch for HTML report history.""" | |
| client = ChainShiftClient(api_key=_api_key or None, access_token=_access_token or None) | |
| return client.get_html_report_history(campaign_id, page=page, page_size=page_size) | |
| AVAILABLE_FEATURES = [ | |
| ("overview", "1. ๊ฐ์์ฑ ๋ถ์ ๊ฐ์"), | |
| ("visibility", "2. AI ๊ฒ์ ๊ฐ์์ฑ"), | |
| ("citations", "3. ์ธ์ฉ ์ถ์ฒ ๋ถ์"), | |
| ("citation-trends", "4. ์ธ์ฉ ์ถ์ฒ ์๊ณ์ด"), | |
| ("content-types", "5. ์ฝํ ์ธ ์ ํ"), | |
| ("sentiment", "6. ๋ธ๋๋ ๊ฐ์ "), | |
| ("homepage-citations", "7. ํํ์ด์ง ์ธ์ฉ๋ฅ "), | |
| ] | |
| def render(client: ChainShiftClient, base_ctx: dict, start_date: str, end_date: str): | |
| """์ ์ฒด ๋ฆฌํฌํธ ์์ฑ ์น์ .""" | |
| st.markdown("#### ๐ ์ ์ฒด HTML ๋ฆฌํฌํธ ์์ฑ") | |
| st.caption("7๊ฐ Feature๋ฅผ ํฌํจํ ํตํฉ HTML ๋ฆฌํฌํธ๋ฅผ ์์ฑํฉ๋๋ค. LLM ์ธ์ฌ์ดํธ๋ก ์ปจ์คํดํธ ํค์ ๋ถ์์ ์ถ๊ฐํ ์ ์์ต๋๋ค.") | |
| with st.expander("โ๏ธ ๋ฆฌํฌํธ ์ต์ ", expanded=True): | |
| st.markdown("**ํฌํจํ Feature ์ ํ**") | |
| selected_features = [] | |
| col1, col2 = st.columns(2) | |
| for i, (feat_key, feat_label) in enumerate(AVAILABLE_FEATURES): | |
| with col1 if i < 4 else col2: | |
| if st.checkbox(feat_label, value=True, key=f"reports:full_feat_{feat_key}"): | |
| selected_features.append(feat_key) | |
| st.markdown("---") | |
| enable_insights = st.checkbox( | |
| "๐ค LLM ์ธ์ฌ์ดํธ ์์ฑ", | |
| value=True, | |
| help="Gemini API๋ฅผ ์ฌ์ฉํ์ฌ ์ปจ์คํดํธ ํค์ ๋ถ์ ์ธ์ฌ์ดํธ๋ฅผ ์ถ๊ฐํฉ๋๋ค (์์ฑ ์๊ฐ ์ฆ๊ฐ)", | |
| ) | |
| st.text_area( | |
| "Homepage URLs (์ค๋ฐ๊ฟ ๊ตฌ๋ถ, ์ ํ)", | |
| help="ํํ์ด์ง ์ธ์ฉ๋ฅ ๋ถ์์ ์ฌ์ฉํ URL ๋ชฉ๋ก", | |
| key="reports:homepage_urls_input", | |
| height=80, | |
| ) | |
| # Parse homepage URLs from text area | |
| homepage_urls_raw = st.session_state.get("reports:homepage_urls_input", "") | |
| homepage_urls = [u.strip() for u in homepage_urls_raw.splitlines() if u.strip()] or None | |
| # Feature display name lookup | |
| _feat_display = dict(AVAILABLE_FEATURES) | |
| if st.button("๐ ์ ์ฒด ๋ฆฌํฌํธ ์์ฑ", key="reports:generate_html_btn", type="primary", disabled=not selected_features): | |
| campaign_id = base_ctx["campaign_id"] | |
| total = len(selected_features) | |
| progress_bar = st.progress(0, text="๋ฆฌํฌํธ ์์ฑ ์ค๋น ์ค...") | |
| status_container = st.container() | |
| built_features: list[dict] = [] | |
| skipped_features: list[str] = [] | |
| total_build_ms = 0 | |
| # Phase 1: Build features in parallel (I/O-bound HTTP calls) | |
| with ThreadPoolExecutor(max_workers=total) as executor: | |
| futures = { | |
| executor.submit( | |
| _build_one, client.api_key, campaign_id, feat_key, | |
| start_date, end_date, enable_insights, homepage_urls, | |
| ): feat_key | |
| for feat_key in selected_features | |
| } | |
| completed = 0 | |
| for future in as_completed(futures): | |
| feat_key = futures[future] | |
| feat_label = _feat_display.get(feat_key, feat_key) | |
| completed += 1 | |
| fk, result, error = future.result() | |
| if error: | |
| skipped_features.append(feat_key) | |
| with status_container: | |
| st.caption(f" {feat_label} ์คํจ: {error}") | |
| elif result and result.get("success"): | |
| feat_resp = result["data"] | |
| built_features.append(feat_resp["feature_data"]) | |
| build_ms = feat_resp.get("build_time_ms", 0) | |
| total_build_ms += build_ms | |
| insight_tag = " +์ธ์ฌ์ดํธ" if feat_resp.get("insights_generated") else "" | |
| with status_container: | |
| st.caption(f" {feat_label} ({build_ms/1000:.1f}s{insight_tag})") | |
| else: | |
| skipped_features.append(feat_key) | |
| with status_container: | |
| st.caption(f" {feat_label} ๊ฑด๋๋") | |
| progress_bar.progress( | |
| completed / (total + 1), | |
| text=f"({completed}/{total}) ๋น๋ ์๋ฃ...", | |
| ) | |
| # Restore original feature order for rendering | |
| feat_order = {k: i for i, k in enumerate(selected_features)} | |
| built_features.sort(key=lambda f: feat_order.get(f.get("feature_id", ""), 99)) | |
| if not built_features: | |
| progress_bar.empty() | |
| st.error("๋ชจ๋ Feature ์์ฑ์ ์คํจํ์ต๋๋ค.") | |
| else: | |
| # Phase 2: Render final report | |
| progress_bar.progress( | |
| total / (total + 1), | |
| text="HTML ๋ฆฌํฌํธ ์กฐ๋ฆฝ ์ค...", | |
| ) | |
| try: | |
| render_result = client.render_html_report( | |
| campaign_id=campaign_id, | |
| features_data=built_features, | |
| start_date=start_date, | |
| end_date=end_date, | |
| enable_insights=enable_insights, | |
| enable_action_items=True, | |
| output_mode="url", | |
| ) | |
| if render_result.get("success"): | |
| data = render_result.get("data") or {} | |
| if not isinstance(data, dict): | |
| progress_bar.empty() | |
| st.error("๋ฆฌํฌํธ ๋ ๋๋ง ์คํจ: ์๋ฒ ์๋ต์ด ๋น์ ์์ ๋๋ค.") | |
| else: | |
| render_ms = data.get("generation_time_ms", 0) | |
| # Download HTML from Supabase Storage URL directly. | |
| # url mode avoids Vercel 4.5MB response body limit. | |
| html_content = "" | |
| html_url = data.get("html_url") or "" | |
| if html_url: | |
| try: | |
| dl_resp = requests.get(html_url, timeout=30) | |
| dl_resp.raise_for_status() | |
| dl_resp.encoding = "utf-8" | |
| html_content = dl_resp.text | |
| except Exception as dl_err: | |
| st.warning(f"HTML ๋ค์ด๋ก๋ ์คํจ, URL ๋งํฌ๋ก ๋์ฒด: {dl_err}") | |
| if not html_url and not html_content: | |
| progress_bar.empty() | |
| st.error("๋ฆฌํฌํธ ๋ ๋๋ง ์คํจ: ์คํ ๋ฆฌ์ง URL์ด ๋ฐํ๋์ง ์์์ต๋๋ค.") | |
| else: | |
| # Persist results in session_state for rerun survival | |
| st.session_state["full_report_result"] = { | |
| "html_content": html_content, | |
| "html_url": html_url, | |
| "report_id": data.get("report_id", "")[:8], | |
| "features_generated": data.get("features_generated", []), | |
| "file_size_kb": round(data.get("file_size_bytes", 0) / 1024, 1), | |
| "total_sec": round((total_build_ms + render_ms) / 1000, 1), | |
| "insights_generated": data.get("insights_generated", False), | |
| "skipped_features": skipped_features, | |
| "campaign_id": campaign_id, | |
| "start_date": start_date, | |
| "end_date": end_date, | |
| } | |
| progress_bar.empty() | |
| st.rerun() | |
| else: | |
| progress_bar.empty() | |
| st.error("๋ฆฌํฌํธ ๋ ๋๋ง ์คํจ: " + str(render_result.get("error", "Unknown error"))) | |
| except Exception as e: | |
| progress_bar.empty() | |
| st.error(f"๋ฆฌํฌํธ ๋ ๋๋ง ์ค๋ฅ: {e}") | |
| elif not selected_features: | |
| st.warning("์ต์ 1๊ฐ ์ด์์ Feature๋ฅผ ์ ํํ์ธ์.") | |
| # โโ Results display (persists across reruns via session_state) โโ | |
| report_state = st.session_state.get("full_report_result") | |
| if report_state: | |
| html_content = report_state["html_content"] | |
| report_id = report_state["report_id"] | |
| features_generated = report_state["features_generated"] | |
| file_size_kb = report_state["file_size_kb"] | |
| total_sec = report_state["total_sec"] | |
| skipped = report_state["skipped_features"] | |
| r_campaign_id = report_state["campaign_id"] | |
| r_start = report_state["start_date"] | |
| r_end = report_state["end_date"] | |
| if skipped: | |
| st.warning(f"์ผ๋ถ Feature๋ฅผ ๊ฑด๋๋ฐ๊ณ ๋ฆฌํฌํธ๋ฅผ ์์ฑํ์ต๋๋ค: {', '.join(skipped)}") | |
| st.success("๋ฆฌํฌํธ๊ฐ ์์ฑ๋์์ต๋๋ค!") | |
| st.markdown(f""" | |
| <div style="background: linear-gradient(135deg, #E5E2FF 0%, #F0FDFA 100%); | |
| padding: 20px; border-radius: 12px; margin: 16px 0;"> | |
| <h4 style="margin: 0 0 12px 0; color: #2B239B;">๋ฆฌํฌํธ ์์ฑ ์๋ฃ</h4> | |
| <p style="margin: 8px 0;"><strong>Report ID:</strong> {report_id}...</p> | |
| <p style="margin: 8px 0;"><strong>Features:</strong> {len(features_generated)}๊ฐ ({len(skipped)}๊ฐ ๊ฑด๋๋)</p> | |
| <p style="margin: 8px 0;"><strong>ํ์ผ ํฌ๊ธฐ:</strong> {file_size_kb} KB</p> | |
| <p style="margin: 8px 0;"><strong>์์ฑ ์๊ฐ:</strong> {total_sec}์ด</p> | |
| <p style="margin: 8px 0;"><strong>LLM ์ธ์ฌ์ดํธ:</strong> {'ํฌํจ' if report_state.get('insights_generated') else '๋ฏธํฌํจ'}</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| html_url = report_state.get("html_url", "") | |
| if html_content: | |
| col_open, col_download, col_clear = st.columns(3) | |
| with col_open: | |
| if html_url: | |
| st.link_button("์ ์ฐฝ์์ ๋ณด๊ธฐ", html_url, use_container_width=True) | |
| else: | |
| st.button("์ ์ฐฝ์์ ๋ณด๊ธฐ", disabled=True, use_container_width=True, key="reports:open_disabled") | |
| with col_download: | |
| file_name = f"AI_๊ฐ์์ฑ_๋ฆฌํฌํธ_{r_campaign_id}_{r_start}_{r_end}.html" | |
| st.download_button( | |
| label="HTML ๋ค์ด๋ก๋", | |
| data=b'\xef\xbb\xbf' + html_content.lstrip('\ufeff').encode("utf-8"), | |
| file_name=file_name, | |
| mime="text/html; charset=utf-8", | |
| use_container_width=True, | |
| key="reports:full_report_download", | |
| ) | |
| with col_clear: | |
| if st.button("์ด๊ธฐํ", key="reports:clear_result", use_container_width=True): | |
| del st.session_state["full_report_result"] | |
| st.rerun() | |
| with st.expander("๋ฆฌํฌํธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", expanded=False): | |
| components.html(html_content, height=800, scrolling=True) | |
| elif html_url: | |
| # Fallback: HTML download failed, show direct link | |
| col_link, col_clear = st.columns(2) | |
| with col_link: | |
| st.link_button("๋ฆฌํฌํธ ์ด๊ธฐ (์ธ๋ถ ๋งํฌ)", html_url, use_container_width=True) | |
| with col_clear: | |
| if st.button("์ด๊ธฐํ", key="reports:clear_result", use_container_width=True): | |
| del st.session_state["full_report_result"] | |
| st.rerun() | |
| st.markdown("---") | |
| # Report History | |
| st.markdown("##### ๐ ์์ฑ ์ด๋ ฅ") | |
| try: | |
| history_result = _fetch_report_history( | |
| base_ctx["campaign_id"], | |
| _api_key=base_ctx.get("api_key", ""), | |
| _access_token=base_ctx.get("access_token", ""), | |
| ) | |
| if history_result.get("success"): | |
| history_data = history_result["data"] | |
| reports = history_data.get("items", []) | |
| total = history_data.get("total", 0) | |
| if reports: | |
| st.markdown(f"์ด **{total}**๊ฑด์ ๋ฆฌํฌํธ๊ฐ ์์ฑ๋์์ต๋๋ค.") | |
| history_rows = [] | |
| for r in reports: | |
| status_emoji = "โ " if r.get("status") == "completed" else "โ" | |
| features = r.get("features_included", []) | |
| file_size_kb = round(r.get("file_size_bytes", 0) / 1024, 1) | |
| history_rows.append({ | |
| "์์ฑ์ผ": r.get("created_at", "")[:16].replace("T", " "), | |
| "์ํ": f"{status_emoji}", | |
| "Features": f"{len(features)}/{len(AVAILABLE_FEATURES)}", | |
| "๊ธฐ๊ฐ": f"{r.get('start_date', '?')} ~ {r.get('end_date', '?')}", | |
| "ํฌ๊ธฐ": f"{file_size_kb} KB", | |
| "๋งํฌ": r.get("html_url") or "-", | |
| }) | |
| df = pd.DataFrame(history_rows) | |
| st.dataframe( | |
| df, | |
| use_container_width=True, | |
| hide_index=True, | |
| column_config={ | |
| "๋งํฌ": st.column_config.LinkColumn("๋งํฌ", display_text="์ด๊ธฐ"), | |
| }, | |
| ) | |
| else: | |
| st.info("์์ง ์์ฑ๋ ๋ฆฌํฌํธ๊ฐ ์์ต๋๋ค.") | |
| except Exception as e: | |
| st.warning(f"์ด๋ ฅ ๋ก๋ ์คํจ: {e}") | |