Spaces:
Sleeping
Sleeping
| """๊ธฐํ ์์ญ ๋ญํน ํ ์ด๋ธ + ์์ธ Expander. | |
| Clusters sorted by opportunity_score DESC. | |
| Top-10 with expanders showing sample fanouts and top sources. | |
| v9.0: ๋น์ฆ๋์ค ํด์, percentile, ์ธ์ฉ ์ ๋ฌด ๋ฉ์์ง ์ถ๊ฐ. | |
| """ | |
| import statistics | |
| import streamlit as st | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| def _get_percentile_rank(value: float, all_values: list[float]) -> int: | |
| """Return the percentile rank (0-100) of a value within a list.""" | |
| if not all_values: | |
| return 0 | |
| count_below = sum(1 for v in all_values if v < value) | |
| return int(count_below / len(all_values) * 100) | |
| def _interpret_density(density: float, median_density: float) -> str: | |
| """Interpret density relative to median.""" | |
| if density < median_density * 0.5: | |
| return "๊ฒฝ์ ๋ฎ์" | |
| elif density < median_density * 1.5: | |
| return "๊ฒฝ์ ๋ณดํต" | |
| return "๊ฒฝ์ ๋์" | |
| def _interpret_opportunity(opp: float, median_opp: float) -> str: | |
| """Interpret opportunity score.""" | |
| if opp >= median_opp * 1.5: | |
| return "๊ธฐํ ํผ" | |
| elif opp >= median_opp * 0.5: | |
| return "๊ธฐํ ๋ณดํต" | |
| return "๊ธฐํ ๋ฎ์" | |
| def render_opportunities(clusters: list[dict], frame: str = "all"): | |
| """Render opportunity ranking table with detail expanders.""" | |
| scored = [ | |
| c for c in clusters | |
| if c.get("opportunity_score") is not None | |
| and c.get("attention_score") is not None | |
| ] | |
| if not scored: | |
| st.info("์ค์ฝ์ด๊ฐ ๊ณ์ฐ๋ ํด๋ฌ์คํฐ๊ฐ ์์ต๋๋ค.") | |
| return | |
| # Sort by opportunity_score DESC | |
| scored.sort(key=lambda c: float(c.get("opportunity_score", 0) or 0), reverse=True) | |
| # Pre-compute statistics for percentile/interpretation | |
| all_attns = [float(c.get("attention_score", 0) or 0) for c in scored] | |
| all_densities = [float(c.get("citation_density", 0) or 0) for c in scored] | |
| all_opps = [float(c.get("opportunity_score", 0) or 0) for c in scored] | |
| median_density = statistics.median(all_densities) if all_densities else 0 | |
| median_opp = statistics.median(all_opps) if all_opps else 0 | |
| # Top metrics | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("์ค์ฝ์ด๋ง ์๋ฃ", f"{len(scored)}๊ฐ ํด๋ฌ์คํฐ") | |
| with col2: | |
| avg_opp = sum(all_opps) / len(all_opps) | |
| st.metric("ํ๊ท Opportunity", f"{avg_opp:.4f}") | |
| with col3: | |
| top = scored[0] | |
| st.metric("Top ๊ธฐํ ํ ํฝ", (top.get("cluster_label") or "N/A")[:25]) | |
| st.markdown("---") | |
| # Explanation before ranking table (frame-specific) | |
| if frame == "demand": | |
| st.info( | |
| "ChatGPT๊ฐ ์์ฃผ ๊ฒ์ํ์ง๋ง ๊ฒฝ์์ด ๋ฎ์ Demand ํ ํฝ์ ๋๋ค.\n\n" | |
| "๊ธฐํ ์ ์๊ฐ ๋์์๋ก, ์ด ์ฃผ์ ์ ์ฝํ ์ธ ๋ฅผ ๋ง๋ค๋ฉด ChatGPT ๊ฒ์์ ๋ ธ์ถ๋ ๊ฐ๋ฅ์ฑ์ด ๋์ต๋๋ค." | |
| ) | |
| elif frame == "supply": | |
| st.info( | |
| "Gemini๊ฐ ์์ฃผ ์ธ์ฉํ์ง๋ง ์ถ์ฒ ๊ฒฝ์์ด ๋ฎ์ Supply ํ ํฝ์ ๋๋ค.\n\n" | |
| "๊ธฐํ ์ ์๊ฐ ๋์์๋ก, ์ด ์ฃผ์ ์ ์ฝํ ์ธ ๋ฅผ ๋ง๋ค๋ฉด Gemini ๋ต๋ณ์ ์ธ์ฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋์ต๋๋ค." | |
| ) | |
| else: | |
| st.info( | |
| "๊ธฐํ ์ ์ Top 10 ํ ํฝ์ ์์ธ ๋ถ์ํฉ๋๋ค. " | |
| "๊ธฐํ ์ ์๋ **'AI ๊ด์ฌ๋๊ฐ ๋์ง๋ง ๊ฒฝ์(์ธ์ฉ ์ถ์ฒ)์ด ์ ์ ํ ํฝ'**์ ์ฐพ์์ฃผ๋ ์งํ์ ๋๋ค.\n\n" | |
| "์ ์๊ฐ ๋์์๋ก ์ฝํ ์ธ ๋ฅผ ๋ง๋ค๋ฉด AI์ ์ธ์ฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋์ต๋๋ค." | |
| ) | |
| # Ranking table | |
| rows = [] | |
| for i, c in enumerate(scored[:50], 1): | |
| rows.append({ | |
| "์์": i, | |
| "ํ ํฝ": (c.get("cluster_label") or f"Cluster-{c['id'][:8]}")[:40], | |
| "Attention": f"{float(c.get('attention_score', 0) or 0):.4f}", | |
| "Density": f"{float(c.get('citation_density', 0) or 0):.4f}", | |
| "Opportunity": f"{float(c.get('opportunity_score', 0) or 0):.4f}", | |
| "Fanouts": c.get("fanout_count", 0), | |
| }) | |
| df = pd.DataFrame(rows) | |
| st.dataframe(df, use_container_width=True, hide_index=True) | |
| # Detail expanders for top 10 | |
| st.markdown("#### Top 10 ์์ธ") | |
| for i, c in enumerate(scored[:10], 1): | |
| label = c.get("cluster_label") or f"Cluster-{c['id'][:8]}" | |
| opp = float(c.get("opportunity_score", 0) or 0) | |
| with st.expander(f"#{i} {label} (Opportunity: {opp:.4f})", key=f"research:opp_detail:{c['id']}"): | |
| detail_col1, detail_col2 = st.columns(2) | |
| with detail_col1: | |
| attn = float(c.get("attention_score", 0) or 0) | |
| density = float(c.get("citation_density", 0) or 0) | |
| attn_pct = _get_percentile_rank(attn, all_attns) | |
| density_label = _interpret_density(density, median_density) | |
| opp_label = _interpret_opportunity(opp, median_opp) | |
| st.markdown("**Score ์์ธ**") | |
| st.write(f"- Attention: {attn:.4f} (์์ {100 - attn_pct}%)") | |
| st.write(f"- Density: {density:.4f} ({density_label})") | |
| st.write(f"- Opportunity: {opp:.4f} ({opp_label})") | |
| st.write(f"- Fanout ์: {c.get('fanout_count', 0)}") | |
| if c.get("unique_questions"): | |
| st.write(f"- ๊ด๋ จ ์ง๋ฌธ ์: {c['unique_questions']}") | |
| with detail_col2: | |
| # Sample fanouts/citations with context | |
| samples = c.get("sample_fanouts") or [] | |
| if samples: | |
| if frame == "supply": | |
| st.markdown("**๋ํ ์ธ์ฉ ๋ฌธ๊ตฌ** (Gemini๊ฐ ์ค์ ๋ก ์ธ์ฉํ ํ ์คํธ):") | |
| else: | |
| st.markdown("**๋ํ AI ์ถ๊ฐ ์ง๋ฌธ** (์ด ํ ํฝ์์ AI๊ฐ ์ค์ ๋ก ์์ฑํ ์ง๋ฌธ๋ค):") | |
| for s in samples[:5]: | |
| st.write(f"- {s}") | |
| if frame == "supply": | |
| st.caption("์ด๋ฐ ํํ์ ์ฝํ ์ธ ๋ฅผ ๋ง๋ค๋ฉด Gemini ๋ต๋ณ์ ์ธ์ฉ๋ ์ ์์ต๋๋ค.") | |
| else: | |
| st.caption("์ด๋ฐ ์ง๋ฌธ์ ๋ํ ์ฝํ ์ธ ๋ฅผ ๋ง๋ค๋ฉด AI ๋ต๋ณ์ ์ธ์ฉ๋ ์ ์์ต๋๋ค.") | |
| else: | |
| st.write("์ํ ์์") | |
| # Top sources bar chart | |
| top_sources = c.get("top_sources") | |
| if top_sources and isinstance(top_sources, list) and len(top_sources) > 0: | |
| st.caption("์ด ํ ํฝ์์ AI๊ฐ ์ธ์ฉํ ์ฃผ์ ์ถ์ฒ์ ๋๋ค. ์ฌ๊ธฐ์ ์์ฌ ์ฝํ ์ธ ๊ฐ ์๋ค๋ฉด ์ง์ถ ๊ธฐํ์ ๋๋ค.") | |
| domains = [s.get("host_url", "unknown") for s in top_sources[:10]] | |
| counts = [s.get("count", 0) for s in top_sources[:10]] | |
| fig = go.Figure(go.Bar( | |
| x=counts, | |
| y=domains, | |
| orientation="h", | |
| marker_color="#059669", | |
| )) | |
| fig.update_layout( | |
| title="Top ์ธ์ฉ ์ถ์ฒ", | |
| xaxis_title="์ธ์ฉ ์", | |
| yaxis=dict(autorange="reversed"), | |
| height=300, | |
| margin=dict(l=0, r=0, t=30, b=0), | |
| template="plotly_white", | |
| ) | |
| st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False}) | |
| else: | |
| st.info("์ด ํ ํฝ์ ์์ง AI๊ฐ ํน์ ์ถ์ฒ๋ฅผ ์ธ์ฉํ์ง ์๊ณ ์์ด, ์ฝํ ์ธ ์ ์ ๊ธฐํ๊ฐ ๋์ฑ ํฝ๋๋ค.") | |