GitHub Action
Sync from GitHub
ef78361
Raw
History Blame Contribute Delete
21.7 kB
"""๋ฆฌํฌํŠธ ํƒญ ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ.
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("ํ™ˆํŽ˜์ด์ง€ ์ธ์šฉ ๋ฐ์ดํ„ฐ ์—†์Œ")
@st.cache_data(ttl=300)
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()