Spaces:
Sleeping
Sleeping
| """ํ ํฝ ์ธํ ๋ฆฌ์ ์ค Feature Plugin. | |
| API: /api/v1/research | |
| ADR-013 Phase 4 + ADR-014 Phase 3 (Multi-Model UX). | |
| """ | |
| import streamlit as st | |
| from core.supabase_client import ( | |
| get_topic_clusters, get_topic_map_snapshot, find_cross_model_pair, | |
| ) | |
| from . import topic_map, opportunities, distribution, guide, summary | |
| from .cross_model import render_cross_model | |
| from .content_actions import render_content_actions | |
| from .keyword_suggest import render_keyword_suggestions | |
| from .unified_scoring import render_unified_scoring | |
| FEATURE_CONFIG = { | |
| "key": "research", | |
| "name": "ํ ํฝ ์ธํ ๋ฆฌ์ ์ค", | |
| "icon": "๐ฌ", | |
| "description": "AI๊ฐ ์ด๋ค ํ ํฝ์ ๊ด์ฌ์ ๊ฐ๊ณ ์๋์ง, ์ด๋์ ์ฝํ ์ธ ๊ธฐํ๊ฐ ์๋์ง ๋ถ์", | |
| "api_base": "/api/v1/research", | |
| "order": 3, | |
| } | |
| # Source configs: label, description, help text | |
| SOURCE_OPTIONS = { | |
| "์ ์ฒด": { | |
| "source": None, | |
| "desc": "ChatGPT + Gemini ์ ์ฒด ํ ํฝ์ ํตํฉํ์ฌ ๋ถ์ํฉ๋๋ค.", | |
| "fanout_label": "Fanout/Citation", | |
| "frame": "all", | |
| }, | |
| "ChatGPT (Demand)": { | |
| "source": "chatgpt", | |
| "desc": "์๋น์๊ฐ AI์๊ฒ **๋ฌด์์ ๋ฌผ์ด๋ณด๋์ง** ๋ถ์ํฉ๋๋ค. ChatGPT์ sub-query ๋ถํด ๋ฐ์ดํฐ ๊ธฐ๋ฐ.", | |
| "fanout_label": "Fanout", | |
| "frame": "demand", | |
| }, | |
| "Gemini (Supply)": { | |
| "source": "gemini", | |
| "desc": "AI๊ฐ **๋ฌด์์ ๊ทผ๊ฑฐ๋ก ๋ต๋ณํ๋์ง** ๋ถ์ํฉ๋๋ค. Gemini์ citation quote ๋ฐ์ดํฐ ๊ธฐ๋ฐ.", | |
| "fanout_label": "Citation", | |
| "frame": "supply", | |
| }, | |
| } | |
| def render(base_ctx): | |
| """ํ ํฝ ์ธํ ๋ฆฌ์ ์ค feature ๋ ๋๋ง.""" | |
| campaign_id = base_ctx["campaign_id"] | |
| st.caption("AI๊ฐ ์ด๋ค ํ ํฝ์ ๊ด์ฌ์ ๊ฐ๊ณ ์๋์ง, ์ด๋์ ์ฝํ ์ธ ๊ธฐํ๊ฐ ์๋์ง ๋ถ์ํฉ๋๋ค.") | |
| # --- Model Selector --- | |
| selected = st.radio( | |
| "๋ถ์ ๋ชจ๋ธ", | |
| list(SOURCE_OPTIONS.keys()), | |
| horizontal=True, | |
| key="research:model_selector", | |
| help="ChatGPT๋ ์๋น์ ๊ฒ์ ์๋(Demand), Gemini๋ AI ์ธ์ฉ ๊ทผ๊ฑฐ(Supply)๋ฅผ ๋ํ๋ ๋๋ค.", | |
| ) | |
| source_cfg = SOURCE_OPTIONS[selected] | |
| source = source_cfg["source"] | |
| st.caption(source_cfg["desc"]) | |
| # Education expander | |
| with st.expander("์ด๋ป๊ฒ ์๋ํ๋์?", expanded=False): | |
| if source_cfg["frame"] == "demand": | |
| st.markdown(""" | |
| **ChatGPT Demand ๋ถ์** | |
| ChatGPT๋ ์ฌ์ฉ์ ์ง๋ฌธ์ 8-15๊ฐ์ ์ธ๋ถ ์ง๋ฌธ(fanout)์ผ๋ก ๋ถํดํ์ฌ Bing์์ ๊ฒ์ํฉ๋๋ค. | |
| ์ด fanout ํจํด์ ๋ถ์ํ๋ฉด **์๋น์๊ฐ AI์๊ฒ ๋ฌด์์ ๋ฌผ์ด๋ณด๋์ง** ํ์ ํ ์ ์์ต๋๋ค. | |
| ``` | |
| ์ฌ์ฉ์ ์ง๋ฌธ โ ChatGPT๊ฐ 8-15๊ฐ sub-query ์์ฑ โ Bing ๊ฒ์ โ ๋ต๋ณ ํฉ์ฑ | |
| โ | |
| sub-query ํด๋ฌ์คํฐ๋ง โ Demand ํ ํฝ | |
| ``` | |
| """) | |
| elif source_cfg["frame"] == "supply": | |
| st.markdown(""" | |
| **Gemini Supply ๋ถ์** | |
| Gemini๋ ๋ต๋ณ ์ ์น ์ฝํ ์ธ ์์ ์ง์ ๋ฌธ์ฅ์ ์ถ์ถ(extractive summarization)ํ์ฌ ์ธ์ฉํฉ๋๋ค. | |
| ์ธ์ฉ ํจํด์ ๋ถ์ํ๋ฉด **AI๊ฐ ์ด๋ค ์ฝํ ์ธ ๋ฅผ ๊ทผ๊ฑฐ๋ก ์ ํํ๋์ง** ํ์ ํ ์ ์์ต๋๋ค. | |
| ``` | |
| ์ฌ์ฉ์ ์ง๋ฌธ โ Gemini ๊ฒ์ ํ๋จ โ Google Search โ 2000๋จ์ด ์์ฐ ๋ด ์ธ์ฉ ์ถ์ถ | |
| โ | |
| citation quote ํด๋ฌ์คํฐ๋ง โ Supply ํ ํฝ | |
| ``` | |
| """) | |
| else: | |
| st.markdown(""" | |
| AI ๋ชจ๋ธ์ด ์ฌ์ฉ์ ์ง๋ฌธ์ ๋ต๋ณํ ๋, ๋ด๋ถ์ ์ผ๋ก ์ฌ๋ฌ ๊ฐ์ ์ธ๋ถ ์ง๋ฌธ(์ถ๊ฐ ์ง๋ฌธ)์ | |
| ๋ง๋ค์ด ์กฐ์ฌํฉ๋๋ค. ์ด ์ถ๊ฐ ์ง๋ฌธ๋ค์ ๋ถ์ํ๋ฉด **AI๊ฐ ์ด๋ค ์ฃผ์ ์ ๊ด์ฌ์ ๊ฐ๊ณ ์๋์ง**, | |
| ์ด๋ค ๋ถ์ผ์์ **๊ฒฝ์์ด ์น์ดํ์ง**๋ฅผ ํ์ ํ ์ ์์ต๋๋ค. | |
| **๋ฐ์ดํฐ ํ๋ฆ:** | |
| ``` | |
| ์ฌ์ฉ์ ์ง๋ฌธ โ AI๊ฐ ์ถ๊ฐ ์ง๋ฌธ ์์ฑ โ ๋ต๋ณ ์์ฑ โ ์ถ์ฒ ์ธ์ฉ | |
| โ | |
| ์ ์ฌํ ์ถ๊ฐ ์ง๋ฌธ๋ผ๋ฆฌ ๋ฌถ๊ธฐ (ํด๋ฌ์คํฐ๋ง) | |
| โ | |
| ๊ฐ ํ ํฝ์ ์ ์ ๊ณ์ฐ | |
| ยท AI ๊ด์ฌ๋: AI๊ฐ ์ด ์ฃผ์ ๋ฅผ ์ผ๋ง๋ ์์ฃผ ๋ฌผ์ด๋ณด๋๊ฐ | |
| ยท ๊ฒฝ์ ๋ฐ๋: ์ด ์ฃผ์ ์ ์ผ๋ง๋ ๋ง์ ์ถ์ฒ๊ฐ ์ธ์ฉ๋๋๊ฐ | |
| ยท ๊ธฐํ ์ ์: ๊ด์ฌ์ ๋์ง๋ง ๊ฒฝ์์ด ๋ฎ์ ์์ญ | |
| ``` | |
| ์์ธํ ๋ด์ฉ์ **"๋ถ์ ๊ฐ์ด๋"** ํญ์ ์ฐธ์กฐํ์ธ์. | |
| """) | |
| clusters = get_topic_clusters(campaign_id, source=source) | |
| if not clusters: | |
| if source: | |
| st.warning(f"{selected} ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค. ํด๋น ๋ชจ๋ธ์ ํด๋ฌ์คํฐ๋ง์ ๋จผ์ ์คํํ์ธ์.") | |
| else: | |
| st.warning("ํ ํฝ ํด๋ฌ์คํฐ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค. ํด๋ฌ์คํฐ๋ง์ ๋จผ์ ์คํํ์ธ์.") | |
| return | |
| snapshot = get_topic_map_snapshot(campaign_id, source=source) | |
| # Summary metrics | |
| summary.render_summary(clusters, frame=source_cfg["frame"]) | |
| st.markdown("---") | |
| # Cross-model pair (one DB call, reused across tabs) | |
| pair = find_cross_model_pair(campaign_id) | |
| # Build tab list โ Unified Scoring only in "์ ์ฒด" mode with pair | |
| tab_names = [ | |
| "๐บ๏ธ ํ ํฝ ๋งต", | |
| "๐ฏ ๊ธฐํ ์์ญ", | |
| "๐ ํ ํฝ ๋ถํฌ", | |
| "๐ Cross-Model", | |
| "๐ก ์ฝํ ์ธ ๊ฐ์ด๋", | |
| "๐ ํค์๋ ์ถ์ฒ", | |
| ] | |
| show_unified = source_cfg["frame"] == "all" and pair is not None | |
| if show_unified: | |
| tab_names.append("โ๏ธ Unified Score") | |
| tab_names.append("๐ ๋ถ์ ๊ฐ์ด๋") | |
| tabs = st.tabs(tab_names) | |
| idx = 0 | |
| with tabs[idx]: | |
| topic_map.render_topic_map(clusters, snapshot, frame=source_cfg["frame"]) | |
| idx += 1 | |
| with tabs[idx]: | |
| opportunities.render_opportunities(clusters, frame=source_cfg["frame"]) | |
| idx += 1 | |
| with tabs[idx]: | |
| distribution.render_distribution(clusters, frame=source_cfg["frame"]) | |
| idx += 1 | |
| with tabs[idx]: | |
| if pair: | |
| render_cross_model(base_ctx, pair) | |
| else: | |
| st.info("์ด ์บ ํ์ธ์ ๋ํ Cross-Model ๋ถ์์ด ์์ง ์์ต๋๋ค.") | |
| idx += 1 | |
| with tabs[idx]: | |
| if pair: | |
| render_content_actions(base_ctx, pair) | |
| else: | |
| st.info("Cross-Model ๋ถ์ ์๋ฃ ํ ์ฝํ ์ธ ๊ฐ์ด๋๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.") | |
| idx += 1 | |
| with tabs[idx]: | |
| render_keyword_suggestions(clusters, frame=source_cfg["frame"]) | |
| idx += 1 | |
| if show_unified: | |
| with tabs[idx]: | |
| render_unified_scoring(base_ctx, pair) | |
| idx += 1 | |
| with tabs[idx]: | |
| guide.render_data_flow() | |