GitHub Action
Sync from GitHub
ef78361
Raw
History Blame Contribute Delete
6.52 kB
"""ํ† ํ”ฝ ์ธํ…”๋ฆฌ์ „์Šค 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()