"""토픽 인텔리전스 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()