Spaces:
Sleeping
Sleeping
| """๋ฆฌํฌํธ ํญ ๊ณตํต ์ ํธ๋ฆฌํฐ. | |
| Feature๋ณ ๋ฏธ๋ฆฌ๋ณด๊ธฐ, HTML ์์ฑ, CSV ๋ณํ ๋ฑ ๊ณตํต ํจ์. | |
| """ | |
| import io | |
| import csv | |
| import pandas as pd | |
| import requests | |
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| from core.api_client import ChainShiftClient | |
| def render_feature_section( | |
| client: ChainShiftClient, | |
| campaign_id: int, | |
| feature_key: str, | |
| title: str, | |
| description: str, | |
| start_date: str, | |
| end_date: str, | |
| api_key: str = "", | |
| access_token: str = "", | |
| ): | |
| """๋จ์ผ Feature ์น์ ๋ ๋๋ง.""" | |
| html_state_key = f"html_content_{feature_key}_{campaign_id}" | |
| insights_key = f"insights_enabled_{feature_key}_{campaign_id}" | |
| with st.container(border=True): | |
| # Header | |
| c1, c2 = st.columns([4, 1]) | |
| with c1: | |
| st.markdown(f"**{title}**") | |
| st.caption(description) | |
| # Preview Section (Lazy loaded) | |
| with st.expander(f"๐๏ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ", expanded=False): | |
| try: | |
| result = get_report_feature_data(api_key, campaign_id, feature_key, start_date, end_date, access_token=access_token) | |
| if result.get("success"): | |
| data = result.get("data", {}) | |
| render_feature_preview(feature_key, data) | |
| else: | |
| st.warning(f"๋ฐ์ดํฐ ๋ก๋ ์คํจ: {result.get('error', 'Unknown')}") | |
| except Exception as e: | |
| st.error(f"๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ค๋ฅ: {e}") | |
| # LLM Insights checkbox | |
| enable_insights = st.checkbox( | |
| "๐ค LLM ์ธ์ฌ์ดํธ ํฌํจ", | |
| value=st.session_state.get(insights_key, True), | |
| key=f"insights_cb_{feature_key}", | |
| help="์ปจ์คํดํธ ํค์ ๋ถ์ ์ฝ๋ฉํธ๋ฅผ ์ถ๊ฐํฉ๋๋ค", | |
| ) | |
| st.session_state[insights_key] = enable_insights | |
| # Generated HTML display section | |
| if html_state_key in st.session_state: | |
| html_data = st.session_state[html_state_key] | |
| html_content = html_data.get("content", "") | |
| html_url = html_data.get("url", "") | |
| st.success(f"โ HTML ๋ฆฌํฌํธ ์์ฑ ์๋ฃ" + (" (LLM ์ธ์ฌ์ดํธ ํฌํจ)" if html_data.get("insights") else "")) | |
| if html_content: | |
| # Action buttons | |
| col_open, col_dl, col_csv, col_reset = st.columns(4) | |
| 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=f"html_open_{feature_key}_disabled") | |
| with col_dl: | |
| st.download_button( | |
| label="๐ฅ HTML ๋ค์ด๋ก๋", | |
| data=b'\xef\xbb\xbf' + html_content.lstrip('\ufeff').encode("utf-8"), | |
| file_name=f"{feature_key}_{campaign_id}_{start_date}_{end_date}.html", | |
| mime="text/html; charset=utf-8", | |
| use_container_width=True, | |
| key=f"html_dl_{feature_key}", | |
| ) | |
| with col_csv: | |
| try: | |
| result = get_report_feature_data(api_key, campaign_id, feature_key, start_date, end_date, access_token=access_token) | |
| if result.get("success"): | |
| csv_data = convert_report_data_to_csv(feature_key, result.get("data", {})) | |
| st.download_button( | |
| label="๐ CSV ๋ค์ด๋ก๋", | |
| data=csv_data.encode("utf-8-sig"), | |
| file_name=f"{feature_key}_{campaign_id}_{start_date}_{end_date}.csv", | |
| mime="text/csv", | |
| use_container_width=True, | |
| key=f"csv_{feature_key}_post", | |
| ) | |
| else: | |
| st.button("๐ CSV ๋ค์ด๋ก๋", disabled=True, use_container_width=True, key=f"csv_{feature_key}_post_disabled") | |
| except Exception: | |
| st.button("๐ CSV ๋ค์ด๋ก๋", disabled=True, use_container_width=True, key=f"csv_{feature_key}_post_error") | |
| with col_reset: | |
| if st.button("๐ ๋ค์ ์์ฑ", key=f"html_reset_{feature_key}", use_container_width=True): | |
| del st.session_state[html_state_key] | |
| st.rerun() | |
| # Inline preview | |
| with st.expander("๐๏ธ HTML ๋ฏธ๋ฆฌ๋ณด๊ธฐ", expanded=False): | |
| components.html(html_content, height=500, scrolling=True) | |
| elif html_url: | |
| # Fallback: HTML download failed, show direct link | |
| col_link, col_csv, col_reset = st.columns(3) | |
| with col_link: | |
| st.link_button("๐ ๋ฆฌํฌํธ ์ด๊ธฐ (์ธ๋ถ ๋งํฌ)", html_url, use_container_width=True) | |
| with col_csv: | |
| try: | |
| result = get_report_feature_data(api_key, campaign_id, feature_key, start_date, end_date, access_token=access_token) | |
| if result.get("success"): | |
| csv_data = convert_report_data_to_csv(feature_key, result.get("data", {})) | |
| st.download_button( | |
| label="๐ CSV ๋ค์ด๋ก๋", | |
| data=csv_data.encode("utf-8-sig"), | |
| file_name=f"{feature_key}_{campaign_id}_{start_date}_{end_date}.csv", | |
| mime="text/csv", | |
| use_container_width=True, | |
| key=f"csv_{feature_key}_fallback", | |
| ) | |
| else: | |
| st.button("๐ CSV ๋ค์ด๋ก๋", disabled=True, use_container_width=True, key=f"csv_{feature_key}_fallback_disabled") | |
| except Exception: | |
| st.button("๐ CSV ๋ค์ด๋ก๋", disabled=True, use_container_width=True, key=f"csv_{feature_key}_fallback_error") | |
| with col_reset: | |
| if st.button("๐ ๋ค์ ์์ฑ", key=f"html_reset_{feature_key}", use_container_width=True): | |
| del st.session_state[html_state_key] | |
| st.rerun() | |
| else: | |
| # Generate button | |
| col_html, col_csv = st.columns(2) | |
| with col_html: | |
| if st.button(f"๐ HTML ์์ฑ", key=f"html_{feature_key}", use_container_width=True): | |
| spinner_text = "HTML ์์ฑ ์ค..." + (" (LLM ์ธ์ฌ์ดํธ ํฌํจ)" if enable_insights else "") | |
| with st.spinner(spinner_text): | |
| try: | |
| # Use url mode to avoid Vercel 4.5MB response limit. | |
| # Download HTML from Supabase Storage directly. | |
| result_url = client.generate_html_report( | |
| campaign_id=campaign_id, | |
| start_date=start_date, | |
| end_date=end_date, | |
| features=[feature_key], | |
| enable_insights=enable_insights, | |
| output_mode="url", | |
| ) | |
| if result_url.get("success"): | |
| data = result_url.get("data") or {} | |
| html_url = data.get("html_url", "") if isinstance(data, dict) else "" | |
| html_content = "" | |
| 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: | |
| st.error("HTML ์์ฑ ์คํจ: ์คํ ๋ฆฌ์ง URL์ด ๋ฐํ๋์ง ์์์ต๋๋ค.") | |
| else: | |
| st.session_state[html_state_key] = { | |
| "content": html_content, | |
| "url": html_url, | |
| "insights": enable_insights, | |
| } | |
| st.rerun() | |
| else: | |
| st.error("HTML ์์ฑ ์คํจ: " + str(result_url.get("error", "Unknown"))) | |
| except Exception as e: | |
| st.error(f"์ค๋ฅ: {e}") | |
| with col_csv: | |
| try: | |
| result = get_report_feature_data(api_key, campaign_id, feature_key, start_date, end_date, access_token=access_token) | |
| if result.get("success"): | |
| csv_data = convert_report_data_to_csv(feature_key, result.get("data", {})) | |
| st.download_button( | |
| label="๐ CSV ๋ค์ด๋ก๋", | |
| data=csv_data.encode("utf-8-sig"), | |
| file_name=f"{feature_key}_{campaign_id}_{start_date}_{end_date}.csv", | |
| mime="text/csv", | |
| use_container_width=True, | |
| key=f"csv_{feature_key}", | |
| ) | |
| else: | |
| st.button("๐ CSV ๋ค์ด๋ก๋", disabled=True, use_container_width=True, key=f"csv_{feature_key}_disabled") | |
| except Exception: | |
| st.button("๐ CSV ๋ค์ด๋ก๋", disabled=True, use_container_width=True, key=f"csv_{feature_key}_error") | |
| def render_feature_preview(feature_key: str, data: dict): | |
| """Feature๋ณ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์๊ฐํ.""" | |
| if feature_key == "overview": | |
| cols = st.columns(4) | |
| with cols[0]: | |
| st.metric("์ด ์ง๋ฌธ ์", data.get("total_tasks", 0)) | |
| with cols[1]: | |
| st.metric("์ด ๋ต๋ณ ์", data.get("total_answers", 0)) | |
| with cols[2]: | |
| st.metric("๊ฐ์์ฑ", f"{data.get('overall_visibility_pct', 0):.1f}%") | |
| with cols[3]: | |
| dr = data.get("date_range", {}) | |
| period = f"{dr.get('start', '?')} ~ {dr.get('end', '?')}" | |
| st.metric("๋ถ์ ๊ธฐ๊ฐ", period[:20]) | |
| elif feature_key == "visibility": | |
| platforms = data.get("platforms", []) | |
| if platforms: | |
| rows = [] | |
| for p in platforms: | |
| for b in p.get("brands", []): | |
| rows.append({ | |
| "ํ๋ซํผ": p.get("platform", ""), | |
| "๋ธ๋๋": b.get("brand_name", ""), | |
| "๊ฐ์์ฑ (%)": b.get("visibility_pct", 0), | |
| }) | |
| if rows: | |
| df = pd.DataFrame(rows) | |
| st.dataframe(df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("ํ๋ซํผ ๋ฐ์ดํฐ ์์") | |
| elif feature_key == "citations": | |
| sources = data.get("sources", [])[:10] | |
| if sources: | |
| df = pd.DataFrame(sources) | |
| cols = [c for c in ["source_host_url", "total_citations", "pct_of_total"] if c in df.columns] | |
| if cols: | |
| st.dataframe(df[cols], use_container_width=True, hide_index=True) | |
| else: | |
| st.info("์ธ์ฉ ๋ฐ์ดํฐ ์์") | |
| elif feature_key == "citation-trends": | |
| sources = data.get("sources", []) | |
| if sources: | |
| rows = [] | |
| for s in sources: | |
| for pt in s.get("trend", []): | |
| rows.append({ | |
| "date": pt.get("task_date", ""), | |
| "source": s.get("source_host_url", ""), | |
| "citations": pt.get("citation_count", 0), | |
| }) | |
| if rows: | |
| df = pd.DataFrame(rows) | |
| pivot = df.pivot_table(index="date", columns="source", values="citations", aggfunc="sum").fillna(0) | |
| st.line_chart(pivot) | |
| else: | |
| st.info("์๊ณ์ด ๋ฐ์ดํฐ ์์") | |
| elif feature_key == "content-types": | |
| types = data.get("content_types", []) | |
| if types: | |
| df = pd.DataFrame(types) | |
| if "content_type" in df.columns and "total_citations" in df.columns: | |
| st.bar_chart(df.set_index("content_type")["total_citations"]) | |
| else: | |
| st.info("์ฝํ ์ธ ์ ํ ๋ฐ์ดํฐ ์์") | |
| elif feature_key == "sentiment": | |
| in_house = data.get("in_house_brands", []) | |
| competitor = data.get("competitor_brands", []) | |
| if in_house: | |
| st.markdown("**๐ข ์์ฌ ๋ธ๋๋**") | |
| df_ih = pd.DataFrame(in_house) | |
| cols_ih = ["brand_name", "total_mentions", "positive_rate", "negative_rate"] | |
| cols_ih = [c for c in cols_ih if c in df_ih.columns] | |
| if cols_ih: | |
| st.dataframe(df_ih[cols_ih], use_container_width=True, hide_index=True) | |
| if competitor: | |
| st.markdown("**๐ฏ ๊ฒฝ์์ฌ ๋ธ๋๋**") | |
| df_comp = pd.DataFrame(competitor) | |
| cols_comp = ["brand_name", "total_mentions", "positive_rate", "negative_rate"] | |
| cols_comp = [c for c in cols_comp if c in df_comp.columns] | |
| if cols_comp: | |
| st.dataframe(df_comp[cols_comp], use_container_width=True, hide_index=True) | |
| if not in_house and not competitor: | |
| brands = data.get("brands", []) | |
| if brands: | |
| df = pd.DataFrame(brands) | |
| cols = [c for c in ["brand_name", "brand_type", "positive_rate", "negative_rate"] if c in df.columns] | |
| if cols: | |
| st.dataframe(df[cols], use_container_width=True, hide_index=True) | |
| else: | |
| st.info("๊ฐ์ ๋ถ์ ๋ฐ์ดํฐ ์์") | |
| elif feature_key == "homepage-citations": | |
| daily_data = data.get("daily_data", [])[:10] | |
| if daily_data: | |
| rows = [] | |
| for day in daily_data: | |
| for entry in day.get("entries", []): | |
| rows.append({ | |
| "๋ ์ง": day.get("task_date", ""), | |
| "ํ๋ซํผ": entry.get("platform", ""), | |
| "์ธ์ฉ ํ์": entry.get("citation_count", 0), | |
| }) | |
| if rows: | |
| df = pd.DataFrame(rows) | |
| st.dataframe(df, use_container_width=True, hide_index=True) | |
| else: | |
| st.info("ํํ์ด์ง ์ธ์ฉ ๋ฐ์ดํฐ ์์") | |
| def get_report_feature_data( | |
| api_key: str, | |
| campaign_id: int, | |
| feature: str, | |
| start_date: str | None = None, | |
| end_date: str | None = None, | |
| access_token: str = "", | |
| ): | |
| """Fetch report feature data with caching.""" | |
| client = ChainShiftClient(api_key=api_key or None, access_token=access_token or None) | |
| if feature == "overview": | |
| return client.get_report_overview(campaign_id, start_date, end_date) | |
| elif feature == "visibility": | |
| return client.get_report_visibility(campaign_id, start_date, end_date) | |
| elif feature == "citations": | |
| return client.get_report_citations(campaign_id, start_date, end_date, limit=50) | |
| elif feature == "citation-trends": | |
| return client.get_report_citation_trends(campaign_id, start_date, end_date) | |
| elif feature == "content-types": | |
| return client.get_report_content_types(campaign_id, start_date, end_date) | |
| elif feature == "sentiment": | |
| return client.get_report_sentiment(campaign_id) | |
| elif feature == "homepage-citations": | |
| return client.get_report_homepage_citations(campaign_id, start_date, end_date) | |
| else: | |
| return {"success": False, "error": f"Unknown feature: {feature}"} | |
| def convert_report_data_to_csv(feature: str, data: dict) -> str: | |
| """Convert report feature data to CSV format.""" | |
| output = io.StringIO() | |
| writer = csv.writer(output) | |
| if feature == "overview": | |
| dr = data.get("date_range", {}) | |
| writer.writerow(["ํญ๋ชฉ", "๊ฐ"]) | |
| writer.writerow(["์บ ํ์ธ ID", data.get("campaign_id", "")]) | |
| writer.writerow(["๋ถ์ ๊ธฐ๊ฐ", f"{dr.get('start', '')} ~ {dr.get('end', '')}"]) | |
| writer.writerow(["์ด ์ง๋ฌธ ์", data.get("total_tasks", 0)]) | |
| writer.writerow(["์ด ๋ต๋ณ ์", data.get("total_answers", 0)]) | |
| writer.writerow(["๊ฐ์์ฑ ๋น์จ (%)", data.get("overall_visibility_pct", 0)]) | |
| elif feature == "visibility": | |
| writer.writerow(["ํ๋ซํผ", "๋ธ๋๋", "์ ํ", "๊ฐ์์ฑ (%)", "๋ธ๋๋ ์ธ๊ธ ์", "์ด ๋ต๋ณ ์"]) | |
| for platform in data.get("platforms", []): | |
| for brand in platform.get("brands", []): | |
| writer.writerow([ | |
| platform.get("platform", ""), | |
| brand.get("brand_name", ""), | |
| brand.get("brand_type", ""), | |
| brand.get("visibility_pct", 0), | |
| brand.get("brand_mentions", 0), | |
| platform.get("total_answers", 0), | |
| ]) | |
| elif feature == "citations": | |
| writer.writerow(["๋๋ฉ์ธ", "์ ํ", "์ธ์ฉ ํ์", "๋ต๋ณ ์ธ๊ธ ์", "๋น์จ (%)"]) | |
| for item in data.get("sources", []): | |
| writer.writerow([ | |
| item.get("source_host_url", ""), | |
| item.get("source_host_type", ""), | |
| item.get("total_citations", 0), | |
| item.get("total_answer_mentions", 0), | |
| item.get("pct_of_total", 0), | |
| ]) | |
| elif feature == "citation-trends": | |
| writer.writerow(["์ธ์ฉ ์ถ์ฒ", "์ ํ", "๋ ์ง", "์ธ์ฉ ํ์", "๋ต๋ณ ์ธ๊ธ ์", "๋น์จ (%)"]) | |
| for source in data.get("sources", []): | |
| host = source.get("source_host_url", "") | |
| host_type = source.get("source_host_type", "") | |
| for point in source.get("trend", []): | |
| writer.writerow([ | |
| host, | |
| host_type, | |
| point.get("task_date", ""), | |
| point.get("citation_count", 0), | |
| point.get("answer_mention_count", 0), | |
| point.get("citation_pct", 0), | |
| ]) | |
| elif feature == "content-types": | |
| writer.writerow(["์ฝํ ์ธ ์ ํ", "์ธ์ฉ ํ์", "๋ต๋ณ ์ธ๊ธ ์", "๋น์จ (%)"]) | |
| for item in data.get("content_types", []): | |
| writer.writerow([ | |
| item.get("content_type", ""), | |
| item.get("total_citations", 0), | |
| item.get("total_answer_mentions", 0), | |
| item.get("pct_of_total", 0), | |
| ]) | |
| elif feature == "sentiment": | |
| writer.writerow(["๋ธ๋๋", "์ ํ", "์ด ๋ฉ์ ", "๊ธ์ %", "๋ถ์ %", "์ค๋ฆฝ %"]) | |
| for item in data.get("in_house_brands", []): | |
| t = item.get("total_mentions", 0) | |
| neutral = round(item.get("neutral_count", 0) / t * 100, 1) if t > 0 else 0.0 | |
| writer.writerow([ | |
| item.get("brand_name", ""), | |
| "์์ฌ", | |
| t, | |
| f"{item.get('positive_rate', 0):.1f}", | |
| f"{item.get('negative_rate', 0):.1f}", | |
| f"{neutral:.1f}", | |
| ]) | |
| for item in data.get("competitor_brands", []): | |
| t = item.get("total_mentions", 0) | |
| neutral = round(item.get("neutral_count", 0) / t * 100, 1) if t > 0 else 0.0 | |
| writer.writerow([ | |
| item.get("brand_name", ""), | |
| "๊ฒฝ์์ฌ", | |
| item.get("total_mentions", 0), | |
| f"{item.get('positive_rate', 0):.1f}", | |
| f"{item.get('negative_rate', 0):.1f}", | |
| f"{neutral:.1f}", | |
| ]) | |
| if not data.get("in_house_brands") and not data.get("competitor_brands"): | |
| for item in data.get("brands", []): | |
| pos = item.get("positive_rate", item.get("positive", 0)) | |
| neg = item.get("negative_rate", item.get("negative", 0)) | |
| neutral = 100 - pos - neg | |
| writer.writerow([ | |
| item.get("brand_name", item.get("name", "")), | |
| item.get("brand_type", item.get("type", "")), | |
| item.get("total_mentions", 0), | |
| f"{pos:.1f}", | |
| f"{neg:.1f}", | |
| f"{neutral:.1f}", | |
| ]) | |
| elif feature == "homepage-citations": | |
| writer.writerow(["๋ ์ง", "ํ๋ซํผ", "์ธ์ฉ ์ถ์ฒ", "์ธ์ฉ ํ์", "๋ต๋ณ ์ธ๊ธ ์"]) | |
| for day in data.get("daily_data", []): | |
| task_date = day.get("task_date", "") | |
| for entry in day.get("entries", []): | |
| writer.writerow([ | |
| task_date, | |
| entry.get("platform", ""), | |
| entry.get("source_host_url", ""), | |
| entry.get("citation_count", 0), | |
| entry.get("answer_mention_count", 0), | |
| ]) | |
| return output.getvalue() | |