Spaces:
Sleeping
Sleeping
| # -*- coding: utf-8 -*- | |
| # ββββββββββββββββββββββββββββββββ BOOTSTRAP (must be first) ββββββββββββββββββββββββββββββββ | |
| import os, pathlib, io, json, random | |
| HOME = pathlib.Path.home() # β μ€ν μ¬μ©μ ν λλ ν°λ¦¬ (μ°κΈ° κ°λ₯) | |
| APP_DIR = pathlib.Path(__file__).parent.resolve() | |
| # Streamlit ν/μ€μ | |
| STREAMLIT_DIR = HOME / ".streamlit" | |
| STREAMLIT_DIR.mkdir(parents=True, exist_ok=True) | |
| os.environ["STREAMLIT_HOME"] = str(STREAMLIT_DIR) | |
| os.environ["STREAMLIT_SERVER_HEADLESS"] = "true" | |
| os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false" | |
| # β HF/Transformers μΊμ: ν λ°μ .cache μ¬μ© (νμ μ HF_CACHE_ROOTλ‘ μ€λ²λΌμ΄λ κ°λ₯) | |
| CACHE_ROOT = pathlib.Path(os.environ.get("HF_CACHE_ROOT", HOME / ".cache" / f"u{os.getuid()}")) | |
| HF_HOME = CACHE_ROOT / "hf-home" | |
| TRANSFORMERS_CACHE = CACHE_ROOT / "hf-cache" | |
| HUB_CACHE = CACHE_ROOT / "hf-cache" | |
| TORCH_HOME = CACHE_ROOT / "torch-cache" | |
| XDG_CACHE_HOME = CACHE_ROOT / "xdg-cache" | |
| # ν΄λ μμ± (κΆν μ€λ₯κ° λλ©΄ /tmpλ‘ μλ ν΄λ°±) | |
| try: | |
| for p in [HF_HOME, TRANSFORMERS_CACHE, HUB_CACHE, TORCH_HOME, XDG_CACHE_HOME]: | |
| p.mkdir(parents=True, exist_ok=True) | |
| except PermissionError: | |
| TMP_ROOT = pathlib.Path("/tmp") / f"hf-cache-u{os.getuid()}" | |
| HF_HOME = TMP_ROOT / "hf-home" | |
| TRANSFORMERS_CACHE = TMP_ROOT / "hf-cache" | |
| HUB_CACHE = TMP_ROOT / "hf-cache" | |
| TORCH_HOME = TMP_ROOT / "torch-cache" | |
| XDG_CACHE_HOME = TMP_ROOT / "xdg-cache" | |
| for p in [HF_HOME, TRANSFORMERS_CACHE, HUB_CACHE, TORCH_HOME, XDG_CACHE_HOME]: | |
| p.mkdir(parents=True, exist_ok=True) | |
| os.environ["HF_HOME"] = str(HF_HOME) | |
| os.environ["TRANSFORMERS_CACHE"] = str(TRANSFORMERS_CACHE) | |
| os.environ["HUGGINGFACE_HUB_CACHE"] = str(HUB_CACHE) | |
| os.environ["TORCH_HOME"] = str(TORCH_HOME) | |
| os.environ["XDG_CACHE_HOME"] = str(XDG_CACHE_HOME) | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1") | |
| from huggingface_hub import hf_hub_download | |
| import pandas as pd | |
| import streamlit as st | |
| import requests | |
| from streamlit.components.v1 import html | |
| from css import render_message, render_chip_buttons, log_and_render, replay_log, _get_colors | |
| #st.success("π μ±μ΄ μ±κ³΅μ μΌλ‘ μμλμμ΅λλ€! λΌμ΄λΈλ¬λ¦¬ μ€μΉ μ±κ³΅!") | |
| # ββββββββββββββββββββββββββββββββ Dataset Repo μ€μ ββββββββββββββββββββββββββββββββ | |
| HF_DATASET_REPO = os.getenv("HF_DATASET_REPO", "emisdfde/moai-travel-data") | |
| HF_DATASET_REV = os.getenv("HF_DATASET_REV", "main") | |
| def _is_pointer_bytes(b: bytes) -> bool: | |
| head = b[:2048].decode(errors="ignore").lower() | |
| return ( | |
| "version https://git-lfs.github.com/spec/v1" in head | |
| or "git-lfs" in head | |
| or "xet" in head # e.g. xet ν¬μΈν° | |
| or "pointer size" in head | |
| ) | |
| def _read_csv_bytes(b: bytes) -> pd.DataFrame: | |
| try: | |
| return pd.read_csv(io.BytesIO(b), encoding="utf-8") | |
| except UnicodeDecodeError: | |
| return pd.read_csv(io.BytesIO(b), encoding="cp949") | |
| def load_csv_smart(local_path: str, | |
| hub_filename: str | None = None, | |
| repo_id: str = HF_DATASET_REPO, | |
| repo_type: str = "dataset", | |
| revision: str = HF_DATASET_REV) -> pd.DataFrame: | |
| # hub_filename μλ΅ μ λ‘컬 νμΌλͺ μ¬μ© | |
| if hub_filename is None: | |
| hub_filename = os.path.basename(local_path) | |
| # 1) λ‘컬 μ°μ | |
| if os.path.exists(local_path): | |
| with open(local_path, "rb") as f: | |
| data = f.read() | |
| if not _is_pointer_bytes(data): | |
| return _read_csv_bytes(data) | |
| # 2) νλΈ λ€μ΄λ‘λ | |
| cached = hf_hub_download(repo_id=repo_id, filename=hub_filename, | |
| repo_type=repo_type, revision=revision) | |
| try: | |
| return pd.read_csv(cached, encoding="utf-8") | |
| except UnicodeDecodeError: | |
| return pd.read_csv(cached, encoding="cp949") | |
| def load_json_smart(local_path: str, | |
| hub_filename: str | None = None, | |
| repo_id: str = HF_DATASET_REPO, | |
| repo_type: str = "dataset", | |
| revision: str = HF_DATASET_REV): | |
| if hub_filename is None: | |
| hub_filename = os.path.basename(local_path) | |
| if os.path.exists(local_path): | |
| with open(local_path, "rb") as f: | |
| data = f.read() | |
| if not _is_pointer_bytes(data): | |
| return json.loads(data.decode("utf-8")) | |
| cached = hf_hub_download(repo_id=repo_id, filename=hub_filename, | |
| repo_type=repo_type, revision=revision) | |
| with open(cached, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| # ββββββββββββββββββββββββββββββββ λ°μ΄ν° λ‘λ ββββββββββββββββββββββββββββββββ | |
| travel_df = load_csv_smart("trip_emotions.csv", "trip_emotions.csv") | |
| external_score_df = load_csv_smart("external_scores.csv", "external_scores.csv") | |
| festival_df = load_csv_smart("festivals.csv", "festivals.csv") | |
| weather_df = load_csv_smart("weather.csv", "weather.csv") | |
| package_df = load_csv_smart("packages.csv", "packages.csv") | |
| master_df = load_csv_smart("countries_cities.csv", "countries_cities.csv") | |
| theme_title_phrases = load_json_smart("theme_title_phrases.json", "theme_title_phrases.json") | |
| # νμ μ»¬λΌ κ°λ | |
| for col in ("μ¬νλλΌ", "μ¬νλμ", "μ¬νμ§"): | |
| if col not in travel_df.columns: | |
| st.error(f"'travel_df'μ '{col}' 컬λΌμ΄ μμ΅λλ€. μ€μ 컬λΌ: {travel_df.columns.tolist()}") | |
| st.stop() | |
| # ββββββββββββββββββββββββββββββββ chat_a import & μ΄κΈ°ν ββββββββββββββββββββββββββββββββ | |
| from chat_a import ( | |
| init_datasets, # β¬ οΈ μλ‘ μΆκ°λ μ§μ° μ΄κΈ°ν ν¨μ | |
| analyze_emotion, | |
| detect_intent, | |
| extract_themes, | |
| recommend_places_by_theme, | |
| detect_location_filter, | |
| generate_intro_message, | |
| theme_ui_map, | |
| ui_to_theme_map, | |
| theme_opening_lines, | |
| intent_opening_lines, | |
| apply_weighted_score_filter, | |
| get_highlight_message, | |
| get_weather_message, | |
| get_intent_intro_message, | |
| recommend_packages, | |
| handle_selected_place, | |
| generate_region_intro, | |
| parse_companion_and_age, | |
| filter_packages_by_companion_age, | |
| make_top2_description_custom, | |
| format_summary_tags_custom, | |
| make_companion_age_message | |
| ) | |
| # ββββββββββββββββββββββββββββββββ LLM ββββββββββββββββββββββββββββββββ | |
| OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434") | |
| OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gemma2:9b") | |
| OLLAMA_TIMEOUT = int(os.getenv("OLLAMA_TIMEOUT", "300")) | |
| KOREAN_SYSTEM_PROMPT = """λΉμ μ νκ΅μ΄ μ΄μμ€ν΄νΈμ λλ€. νμ νκ΅μ΄λ‘ λ΅νμΈμ.""" | |
| STRUCTURED_EXTRACTION_SYSTEM = """\ | |
| You are a travel assistant that extracts structured fields from Korean user queries. | |
| Return ONLY a valid JSON object: | |
| { | |
| "emotion": "happy|sad|stressed|excited|tired|none", | |
| "intent": "beach|hiking|shopping|food|museum|relaxing|none", | |
| "country_hint": "", | |
| "city_hint": "", | |
| "themes_hint": ["<0..3 words>"], | |
| "notes": "<very short reasoning in Korean>" | |
| } | |
| If unknown, use "none" or "" and NEVER add extra text outside JSON. | |
| """ | |
| def to_llm_mode(): | |
| # κ°μ λ λ μ¬μ΄ν΄μμ μ¬λ¬ λ² νΈμΆλμ΄λ 1νλ§ λμνκ² κ°λ | |
| if not st.session_state.get("_llm_triggered"): | |
| st.session_state["_llm_triggered"] = True | |
| st.session_state["llm_mode"] = True | |
| st.session_state["llm_intro_needed"] = True | |
| st.rerun() | |
| def _ensure_llm_state(): | |
| st.session_state.setdefault("llm_mode", False) | |
| st.session_state.setdefault("llm_inline", False) | |
| st.session_state.setdefault("llm_intro_needed", False) | |
| st.session_state.setdefault("llm_msgs", []) | |
| def show_llm_inline(): | |
| _ensure_llm_state() | |
| st.session_state["llm_inline"] = True | |
| st.session_state["llm_intro_needed"] = True | |
| def _build_structured_user_prompt(user_text: str) -> str: | |
| # λΆνμν λν μμ΄, λͺ¨λΈμ΄ JSONλ§ λ΄λλ‘ κΉλν μ λ¬ | |
| return user_text.strip() | |
| def _ollama_healthcheck(): | |
| base = OLLAMA_HOST.rstrip("/") | |
| # 1) μλ² μ΄μμλμ§ | |
| try: | |
| r = requests.get(f"{base}/api/version", timeout=5) | |
| r.raise_for_status() | |
| except requests.RequestException as e: | |
| st.error(f"β Ollama μ°κ²° μ€ν¨: {e} (host={OLLAMA_HOST})") | |
| return False | |
| # 2) λͺ¨λΈ μ€μΉ μ¬λΆ | |
| try: | |
| tags = requests.get(f"{base}/api/tags", timeout=5).json() | |
| names = [m.get("name") for m in tags.get("models", [])] | |
| if OLLAMA_MODEL not in names: | |
| st.warning(f"β οΈ λͺ¨λΈ λ―Έμ€μΉ: `{OLLAMA_MODEL}`. μλ²μμ `ollama pull {OLLAMA_MODEL}` μ€ν νμ.") | |
| except requests.RequestException as e: | |
| st.warning(f"λͺ¨λΈ λͺ©λ‘ μ‘°ν μ€ν¨: {e}") | |
| return True | |
| def _call_ollama_chat(messages, model=OLLAMA_MODEL, temperature=0.8, top_p=0.9, top_k=40, repeat_penalty=1.1, system_prompt=None): | |
| url = f"{OLLAMA_HOST}/api/chat" | |
| _msgs = [] | |
| if system_prompt: | |
| _msgs.append({"role": "system", "content": system_prompt}) | |
| _msgs.extend(messages) | |
| payload = { | |
| "model": model, | |
| "messages": _msgs, | |
| "options": {"temperature": temperature, "top_p": top_p, "top_k": top_k, "repeat_penalty": repeat_penalty}, | |
| "stream": False, | |
| } | |
| try: | |
| r = requests.post(url, json=payload, timeout=OLLAMA_TIMEOUT) | |
| r.raise_for_status() | |
| return (r.json().get("message") or {}).get("content", "") or "" | |
| except requests.Timeout: | |
| st.error(f"β±οΈ Ollama νμμμ({OLLAMA_TIMEOUT}s). host={OLLAMA_HOST}, model={model}") | |
| except requests.ConnectionError as e: | |
| st.error(f"π μ°κ²° μ€ν¨: {e} (host={OLLAMA_HOST})") | |
| except requests.HTTPError as e: | |
| try: | |
| detail = r.text[:500] | |
| except Exception: | |
| detail = str(e) | |
| st.error(f"HTTP {r.status_code}: {detail}") | |
| except requests.RequestException as e: | |
| st.error(f"μμ² μ€λ₯: {e}") | |
| return "" | |
| def call_ollama_stream(messages, *, model: str = OLLAMA_MODEL, | |
| temperature: float = 0.8, top_p: float = 0.9, | |
| top_k: int = 40, repeat_penalty: float = 1.1, | |
| num_predict: int = 200, num_ctx: int = 2048, | |
| system_prompt: str | None = None): | |
| """ | |
| Ollama /api/chat μ€νΈλ¦¬λ° μ λλ μ΄ν°. | |
| Streamlitμμλ st.write_stream(...)μΌλ‘ λ°λ‘ μΈ μ μμ. | |
| """ | |
| url = f"{OLLAMA_HOST}/api/chat" | |
| _msgs = [] | |
| if system_prompt: | |
| _msgs.append({"role": "system", "content": system_prompt}) | |
| _msgs.extend(messages) | |
| payload = { | |
| "model": model, | |
| "messages": _msgs, | |
| "options": { | |
| "temperature": temperature, | |
| "top_p": top_p, | |
| "top_k": top_k, | |
| "repeat_penalty": repeat_penalty, | |
| "num_predict": num_predict, # CPU + 9Bλ 128~256 κΆμ₯ | |
| "num_ctx": num_ctx # 2048~4096 | |
| }, | |
| "stream": True, # β ν΅μ¬ | |
| } | |
| with requests.post(url, json=payload, stream=True, timeout=OLLAMA_TIMEOUT) as resp: | |
| resp.raise_for_status() | |
| for line in resp.iter_lines(decode_unicode=True): | |
| if not line: | |
| continue | |
| data = json.loads(line) | |
| if data.get("done"): | |
| break | |
| chunk = (data.get("message") or {}).get("content", "") | |
| if chunk: | |
| yield chunk | |
| def _llm_structured_extract(user_text: str): | |
| out = _call_ollama_chat( | |
| [ | |
| {"role": "system", "content": STRUCTURED_EXTRACTION_SYSTEM}, | |
| {"role": "user", "content": _build_structured_user_prompt(user_text)} | |
| ], | |
| system_prompt=None # μμμ systemμΌλ‘ μ΄λ―Έ λ£μμ | |
| ) | |
| try: | |
| data = json.loads(out) | |
| except Exception: | |
| data = {} | |
| data.setdefault("emotion", "none") | |
| data.setdefault("intent", "none") | |
| data.setdefault("country_hint", "") | |
| data.setdefault("city_hint", "") | |
| data.setdefault("themes_hint", []) | |
| data.setdefault("notes", "") | |
| return data | |
| # ββββββββββββββββββββββββββββββββ Streamlitμ© LLM λͺ¨λ UI ββββββββββββββββββββββββββββββββ | |
| def render_llm_followup(chat_container, inline=False): | |
| _ensure_llm_state() | |
| st.markdown("# λ λμ€ μ¬νμ§μ κ΄ν΄ κΆκΈνμ μ μ΄ μμΌμ κ°μ?") | |
| for m in st.session_state.get("llm_msgs", []): | |
| with st.chat_message(m["role"]): | |
| st.markdown(m["content"]) | |
| user_msg = st.chat_input("무μμ΄λ λ¬Όμ΄λ³΄μΈμ (μ’ λ£νλ €λ©΄ 'μ’ λ£' μ λ ₯)", key="llm_query") | |
| if not user_msg: | |
| return | |
| text = user_msg.strip() | |
| # μ’ λ£ λͺ λ Ή | |
| if text in {"μ’ λ£", "quit", "exit"}: | |
| st.session_state["llm_inline"] = False | |
| st.session_state["llm_mode"] = False | |
| st.rerun() | |
| return | |
| # λν μ μ₯ | |
| st.session_state.setdefault("llm_msgs", []) | |
| st.session_state["llm_msgs"].append({"role": "user", "content": text}) | |
| # β μ€νΈλ¦¬λ° νΈμΆλ‘ λ³κ²½ | |
| try: | |
| with st.chat_message("assistant"): | |
| # μμ€ν ν둬ννΈ + νμ€ν 리 λͺ¨λ 보λ΄κΈ° | |
| msgs = st.session_state["llm_msgs"] | |
| full_text = st.write_stream( | |
| call_ollama_stream( | |
| msgs, | |
| model=OLLAMA_MODEL, | |
| system_prompt=KOREAN_SYSTEM_PROMPT, | |
| num_predict=200, # νμμ 128~256 μ‘°μ | |
| num_ctx=2048 | |
| ) | |
| ) | |
| st.session_state["llm_msgs"].append({"role": "assistant", "content": full_text}) | |
| except requests.Timeout: | |
| st.error(f"β±οΈ Ollama νμμμ({OLLAMA_TIMEOUT}s). host={OLLAMA_HOST}, model={OLLAMA_MODEL}") | |
| st.session_state["llm_msgs"].append({"role": "assistant", "content": "β οΈ νμμμμ΄ λ°μνμ΅λλ€."}) | |
| except requests.RequestException as e: | |
| st.error(f"μμ² μ€λ₯: {e}") | |
| st.session_state["llm_msgs"].append({"role": "assistant", "content": "β οΈ LLM νΈμΆ μ€ μ€λ₯κ° λ°μνμ΅λλ€."}) | |
| st.rerun() | |
| def render_llm_inline_if_open(chat_container): | |
| """llm_inline νλκ·Έκ° μΌμ Έ μμΌλ©΄ μΈλΌμΈ LLM ν¨λμ 그립λλ€.""" | |
| _ensure_llm_state() | |
| if st.session_state.get("llm_inline", False): | |
| render_llm_followup(chat_container, inline=True) | |
| # μ§μ° μ΄κΈ°ν: import μμ μλ λ°μ΄ν° μ κ·Ό κΈμ§, μ¬κΈ°μ ν λ²λ§ μ£Όμ | |
| init_datasets( | |
| travel_df=travel_df, | |
| festival_df=festival_df, | |
| external_score_df=external_score_df, | |
| weather_df=weather_df, | |
| package_df=package_df, | |
| master_df=master_df, | |
| theme_title_phrases=theme_title_phrases, | |
| ) | |
| # βββββββββββββββββββββββββββββββββββββ streamlitμ© ν¨μ | |
| def init_session(): | |
| if "chat_log" not in st.session_state: | |
| st.session_state.chat_log = [] | |
| if "mode" not in st.session_state: | |
| st.session_state.mode = None | |
| if "user_input" not in st.session_state: | |
| st.session_state.user_input = "" | |
| if "selected_theme" not in st.session_state: | |
| st.session_state.selected_theme = None | |
| def make_key(row) -> tuple[str, str]: | |
| """prev μ λ£κ³ κΊΌλΌ λ μ°λ κ³ μ ν€(μ¬νμ§, μ¬νλμ)""" | |
| return (row["μ¬νμ§"], row["μ¬νλμ"]) | |
| # βββββββββββββββββββββββββββββββββββββ streamlit μμ μ μΈ | |
| st.set_page_config(page_title="μ¬νμ λͺ¨λν¬μ΄ : λͺ¨μ(MoAi)", layout="centered") | |
| st.markdown(""" | |
| <style> | |
| /* 1) μΌμͺ½ μ¬μ΄λλ° μ 체λ₯Ό νλ©΄μ κ³ μ */ | |
| aside[data-testid="stSidebar"], [data-testid="stSidebar"]{ | |
| position: fixed !important; | |
| top: 0; left: 0; | |
| height: 100vh !important; | |
| z-index: 1000; | |
| } | |
| /* 2) μ¬μ΄λλ° μμ μ½ν μΈ λ₯Ό 'λΆλ°μ΄'λ‘: λ΄λΆ μ€ν¬λ‘€ λ */ | |
| aside[data-testid="stSidebar"] > div{ | |
| height: 100%; | |
| overflow: hidden !important; /* β λ΄μ©μ΄ κ³ μ λ¨(μ€ν¬λ‘€ μ λ¨) */ | |
| } | |
| /* 3) λ³Έλ¬Έμ μ€λ₯Έμͺ½μΌλ‘ λ°μ΄ κ²ΉμΉ¨ λ°©μ§ (340 + 24 μ¬λ°±) */ | |
| [data-testid="stAppViewContainer"] > .main .block-container, | |
| section.main > div.block-container, | |
| div[data-testid="block-container"]{ | |
| margin-left: 364px !important; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| accent = _get_colors().get("accent", "#0B8A5A") | |
| st.markdown( | |
| f""" | |
| <h3 style="color:{accent}; font-weight:1000; margin:0.25rem 0 1rem;"> | |
| π Ό μ¬νμ λͺ¨λν¬μ΄, μΆμ²μ λͺ¨μ(MoAi) | |
| </h3> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| # κ³ μ μ΄λ―Έμ§ URL | |
| #BG_URL = "https://plus.unsplash.com/premium_photo-1679830513869-cd3648acb1db?q=80&w=2127&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" | |
| # === λ°°κ²½ μ€μ UI (μμ λ¨) === | |
| st.sidebar.subheader("π¨ λ°°κ²½ μ€μ ") | |
| st.sidebar.toggle("λ°°κ²½ μ΄λ―Έμ§ μ¬μ©", key="bg_on", value=True) | |
| # 1. 'λ°°κ²½ μ΄λ―Έμ§ μ¬μ©'μ΄ ONμΌ λλ§ μ΄λ―Έμ§ κ΄λ ¨ μ΅μ νμ | |
| if st.session_state.bg_on: | |
| with st.sidebar.expander("μ΄λ―Έμ§ λ°°κ²½ μ΅μ ", expanded=True): | |
| st.text_input("λ°°κ²½ μ΄λ―Έμ§ URL", key="bg_url", value="https://images.unsplash.com/photo-1506744038136-46273834b3fb") | |
| st.slider("λ°°κ²½ μ΄λ―Έμ§ μ€λ²λ μ΄ (%)", 0, 100, 85, key="bg_overlay_pct") | |
| # 2. 'λ°°κ²½ μ΄λ―Έμ§ μ¬μ©'μ΄ OFFμΌ λλ§ λ¨μ κ΄λ ¨ μ΅μ νμ | |
| else: | |
| with st.sidebar.expander("λ¨μ λ°°κ²½ μ΅μ ", expanded=True): | |
| # μΆμ² μμ νλ νΈλ₯Ό λ²νΌμΌλ‘ ꡬν | |
| palette = { | |
| "Light Gray": "#F1F1F1", | |
| "Mint": "#E3E8E3", | |
| "Sky Blue": "#D9E1E2", | |
| "Beige": "#F0F0EC" | |
| } | |
| selected_color_name = st.radio( | |
| "μΆμ² μμ", | |
| options=palette.keys(), | |
| key="selected_color_name", | |
| horizontal=True # λ²νΌμ κ°λ‘λ‘ λ°°μ΄ | |
| ) | |
| #μ νλ λΌλμ€ λ²νΌμ μμ μ½λλ₯Ό color_pickerμ κΈ°λ³Έκ°μΌλ‘ μ¬μ© | |
| st.color_picker( | |
| "μμ μ§μ μ ν", | |
| key="bg_color", | |
| value=palette[selected_color_name] | |
| ) | |
| def apply_background(): | |
| # 보νΈ: κΈ°μ‘΄ ::before λ°°κ²½μ΄ μμΌλ©΄ λκΈ° (κ²ΉμΉ¨/λκΉ λ°©μ§) | |
| base_reset_css = """ | |
| <style> | |
| .stApp::before, .block-container::before { content:none !important; } | |
| /* μ λ ₯λ°μ€ μλ μ¬λ°± */ | |
| div[data-testid="stTextInput"] { margin-bottom:18px !important; } | |
| </style> | |
| """ | |
| st.markdown(base_reset_css, unsafe_allow_html=True) | |
| if st.session_state.get("bg_on") and st.session_state.get("bg_url"): | |
| url = st.session_state["bg_url"] | |
| overlay_alpha = float(st.session_state.get("bg_overlay_pct", 15)) / 100.0 | |
| # β μ΄λ―Έμ§ λ°°κ²½ (λ©μΈ 컨ν μΈ μμμλ§ κ³ μ λ°°κ²½ μ μ©) | |
| st.markdown(f""" | |
| <style> | |
| /* μλ¨Β·λ°°κ²½ ν¬λͺ μ²λ¦¬ */ | |
| header[data-testid="stHeader"], | |
| main, section.main {{ background: transparent !important; }} | |
| [data-testid="stAppViewContainer"] {{ | |
| background: url('{url}') center / cover no-repeat fixed; | |
| position: relative; | |
| z-index: 0; | |
| }} | |
| /* μ€λ²λ μ΄: μ΄λ―Έμ§ μμ ν°μ λ§μ μΉμ΄ κ°λ μ± ν보 */ | |
| [data-testid="stAppViewContainer"]::after {{ | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background: rgba(255, 255, 255, {overlay_alpha}); | |
| z-index: -1; | |
| pointer-events: none; | |
| }} | |
| /* 컨ν μΈ μ μ¬μ΄λλ°κ° λ°°κ²½λ³΄λ€ μμ μ€λλ‘ */ | |
| .block-container, [data-testid="stSidebar"] {{ | |
| position: relative; | |
| z-index: 1; | |
| }} | |
| /* λͺ¨λ°μΌμ fixed μ΄μκ° μμ΄ κ³ μ ν΄μ */ | |
| @media (max-width: 768px) {{ | |
| [data-testid="stAppViewContainer"] {{ | |
| background-attachment: initial; | |
| }} | |
| }} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| else: | |
| # β λ¨μ λ°°κ²½ (λ©μΈ 컨ν μΈ μμμλ§ μ μ©) | |
| color = st.session_state.get("bg_color", "#F1F1F1") | |
| st.markdown(f""" | |
| <style> | |
| [data-testid="stAppViewContainer"] {{ | |
| background-color: {color} !important; | |
| }} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # ν¨μ νΈμΆ | |
| apply_background() | |
| # ββ P κΈκΌ΄ ν¬κΈ° 14 px βββββββββββββββββββββββββββββββββββ | |
| st.markdown(""" | |
| <style> | |
| /* κΈ°λ³Έ p νκ·Έ κΈκΌ΄ ν¬κΈ° */ | |
| html, body, p { | |
| font-size: 14px !important; /* β 14 px κ³ μ */ | |
| line-height: 1.5; /* (μ ν) κ°λ μ±μ μν μ€κ°κ²© */ | |
| } | |
| /* Streamlit κΈ°λ³Έ λ§μ§ μ κ±°λ‘ λΆνμν μ¬λ°± λ°©μ§ (μ ν) */ | |
| p { | |
| margin-top: 0; | |
| margin-bottom: 0.5rem; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # βββββββββββββββββββββββββββββββββββββ region mode | |
| def region_ui(travel_df, external_score_df, festival_df, weather_df, package_df, | |
| country_filter, city_filter, chat_container, log_and_render): | |
| """region λͺ¨λ(νΉμ λλΌ, λμλ₯Ό μ§μ μΈκΈνμ κ²½μ°) μ μ© UI & λ‘μ§""" | |
| # ββββββββββββββββββ μΈμ ν€ μ μ | |
| region_key = "region_chip_selected" | |
| prev_key = "region_prev_recommended" | |
| step_key = "region_step" | |
| sample_key = "region_sample_df" | |
| # ββββββββββββββββββ 0) μ΄κΈ°ν | |
| if step_key not in st.session_state: | |
| st.session_state[step_key] = "recommend" | |
| st.session_state[prev_key] = set() | |
| st.session_state.pop(sample_key, None) | |
| # ββββββββββββββββββ 1) restart μνλ©΄ μΈνΈλ‘λ§ μΆλ ₯νκ³ μ’ λ£ | |
| if st.session_state[step_key] == "restart": | |
| log_and_render( | |
| "λ€μ μ¬νμ§λ₯Ό μΆμ²ν΄λ릴κ²μ!<br>μμ¦ λ μ€λ₯΄λ μ¬νμ΄ μμΌμ κ°μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="region_restart_intro" | |
| ) | |
| return | |
| # ββββββββββββββββββ 2) μΆμ² λ¨κ³ | |
| if st.session_state[step_key] == "recommend": | |
| # 2.1) μΆμ² 문ꡬ μΆλ ₯ (λμ λλ κ΅κ° κΈ°μ€) | |
| city_exists = bool(city_filter) and city_filter in travel_df["μ¬νλμ"].values | |
| country_exists = bool(country_filter) and country_filter in travel_df["μ¬νλλΌ"].values | |
| # μ‘΄μ¬νμ§ μλ λμμΈ κ²½μ° | |
| if city_filter and not city_exists: | |
| intro = generate_region_intro('', country_filter) | |
| log_and_render( | |
| f"μ£μ‘ν΄μ. {city_filter}μ μ¬νμ§λ μμ§ λ―Έμ μ΄μμ.<br>νμ§λ§, {intro}", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="region_intro_invalid" | |
| ) | |
| else: | |
| # μ μμ μΈ λμ/κ΅κ°μΌ κ²½μ° | |
| intro = generate_region_intro(city_filter, country_filter) | |
| log_and_render(intro, | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="region_intro") | |
| # 2.2) μ¬νμ§ ν보 λͺ©λ‘ νν°λ§ | |
| df = travel_df.drop_duplicates(subset=["μ¬νμ§"]) | |
| if city_exists: | |
| df = df[df["μ¬νλμ"].str.contains(city_filter, na=False)] | |
| elif country_exists: | |
| df = df[df["μ¬νλλΌ"].str.contains(country_filter, na=False)] | |
| # 2.3) μ΄μ μΆμ² λͺ©λ‘κ³Ό κ²ΉμΉμ§ μλ μ¬νμ§λ§ λ¨κΉ | |
| prev = st.session_state.setdefault(prev_key, set()) | |
| remaining = df[~df.apply(lambda r: make_key(r) in prev, axis=1)] | |
| # μΆμ² κ°λ₯ν μ¬νμ§κ° μλ€λ©΄ μ’ λ£ λ¨κ³λ‘ μ ν | |
| if remaining.empty and sample_key not in st.session_state: | |
| st.session_state[step_key] = "recommend_end" | |
| st.rerun() | |
| return | |
| # 2.4) μνλ§ (μ΄μ μνμ΄ μκ±°λ λΉμ΄ μμΌλ©΄ μλ‘ μΆμΆ) | |
| if sample_key not in st.session_state or st.session_state[sample_key].empty: | |
| sampled = remaining.sample( | |
| n=min(3, len(remaining)), #μ΅λ 3κ° | |
| random_state=random.randint(1, 9999) | |
| ) | |
| st.session_state[sample_key] = sampled | |
| # tuple ννλ‘ νκΊΌλ²μ μΆκ° | |
| prev.update([make_key(r) for _, r in sampled.iterrows()]) | |
| st.session_state[prev_key] = prev | |
| else: | |
| sampled = st.session_state[sample_key] | |
| loc_df = st.session_state[sample_key] | |
| # 2.5) μΆμ² 리μ€νΈ μΆλ ₯ & μΉ© UI | |
| message = ( | |
| "π μΆμ² μ¬νμ§ λͺ©λ‘<br>κ°μ₯ κ°κ³ μΆμ κ³³μ 골λΌμ£ΌμΈμ!<br><br>" + | |
| "<br>".join([ | |
| f"{i+1}. <strong>{row.μ¬νμ§}</strong> " | |
| f"({row.μ¬νλλΌ}, {row.μ¬νλμ}) " | |
| f"{getattr(row, 'νμ€μ€λͺ ', 'μ€λͺ μ΄ μμ΅λλ€')}" | |
| for i, row in enumerate(loc_df.itertuples()) | |
| ]) | |
| ) | |
| with chat_container: | |
| log_and_render(message, | |
| sender="bot", | |
| chat_container=chat_container, | |
| key=f"region_recommendation_{random.randint(1,999999)}" | |
| ) | |
| # μΉ© λ²νΌμΌλ‘ μΆμ²μ§ μ€ μ νλ°κΈ° | |
| prev_choice = st.session_state.get(region_key, None) | |
| choice = render_chip_buttons( | |
| loc_df["μ¬νμ§"].tolist() + ["λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π"], | |
| key_prefix="region_chip", | |
| selected_value=prev_choice | |
| ) | |
| # 2.7) μ ν κ²°κ³Ό μ²λ¦¬ | |
| if not choice or choice == prev_choice: | |
| return | |
| if choice == "λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π": | |
| log_and_render("λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π", | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_place_refresh_{random.randint(1,999999)}") | |
| st.session_state.pop(sample_key, None) | |
| st.rerun() | |
| return | |
| # 2.8) μ¬νμ§ μ ν μλ£ | |
| st.session_state[region_key] = choice | |
| st.session_state[step_key] = "detail" | |
| st.session_state.chat_log.append(("user", choice)) | |
| # μ€μ λ‘ μ νλ μ¬νμ§λ§ prevμ κΈ°λ‘ | |
| match = sampled[sampled["μ¬νμ§"] == choice] | |
| if not match.empty: | |
| prev.add(make_key(match.iloc[0])) | |
| st.session_state[prev_key] = prev | |
| # μν νκΈ° | |
| st.session_state.pop(sample_key, None) | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 3) μΆμ² μ’ λ£ λ¨κ³: λ μ΄μ μΆμ²ν μ¬νμ§κ° μμ λ | |
| elif st.session_state[step_key] == "recommend_end": | |
| with chat_container: | |
| # 3.1) λ©μμ§ μΆλ ₯ | |
| log_and_render( | |
| "β οΈ λ μ΄μ μλ‘μ΄ μ¬νμ§κ° μμ΄μ.<br>λ€μ μ§λ¬Ένμκ² μ΄μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="region_empty" | |
| ) | |
| # 3.2) μ¬μμ μ¬λΆ μΉ© λ²νΌ μΆλ ₯ | |
| restart_done_key = "region_restart_done" | |
| chip_ph = st.empty() | |
| if not st.session_state.get(restart_done_key, False): | |
| with chip_ph: | |
| choice = render_chip_buttons( | |
| ["μ π", "μλμ€ β"], | |
| key_prefix="region_restart" | |
| ) | |
| else: | |
| choice = None | |
| # 3.3) μμ§ μ무κ²λ μ ννμ§ μμ κ²½μ° | |
| if choice is None: | |
| return | |
| chip_ph.empty() | |
| st.session_state[restart_done_key] = True | |
| # 3.4) μ¬μ©μ μ νκ° μΆλ ₯ | |
| log_and_render( | |
| choice, | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_restart_choice_{choice}" | |
| ) | |
| # 3.5) μ¬μ©μκ° μ¬μΆμ²μ μνλ κ²½μ° | |
| if choice == "μ π": | |
| # μ¬ν μΆμ² μν μ΄κΈ°ν | |
| for k in [region_key, prev_key, sample_key, restart_done_key]: | |
| st.session_state.pop(k, None) | |
| chip_ph.empty() | |
| # λ€μ μΆμ² λ¨κ³λ‘ μ΄κΈ°ν | |
| st.session_state["user_input_rendered"] = False | |
| st.session_state["region_step"] = "restart" | |
| log_and_render( | |
| "λ€μ μ¬νμ§λ₯Ό μΆμ²ν΄λ릴κ²μ!<br>μμ¦ λ μ€λ₯΄λ μ¬νμ΄ μμΌμ κ°μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="region_restart_intro" | |
| ) | |
| return | |
| # 3.6) μ¬μ©μκ° μ’ λ£λ₯Ό μ νν κ²½μ° | |
| else: | |
| log_and_render("μ¬ν μΆμ²μ μ’ λ£ν κ²μ. νμνμ€ λ μΈμ λ μ§ λ μ°Ύμμ£ΌμΈμ! βοΈ", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="region_exit") | |
| st.stop() | |
| return | |
| # ββββββββββββββββββ 4) μ¬νμ§ μμΈ λ¨κ³ | |
| if st.session_state[step_key] == "detail": | |
| chosen = st.session_state[region_key] | |
| # city μ΄λ¦ λ½μμ μΈμ μ μ μ₯ | |
| row = travel_df[travel_df["μ¬νμ§"] == chosen].iloc[0] | |
| st.session_state["selected_city"] = row["μ¬νλμ"] | |
| st.session_state["selected_place"] = chosen | |
| log_and_render(chosen, | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_place_{chosen}") | |
| handle_selected_place( | |
| chosen, | |
| travel_df, | |
| external_score_df, | |
| festival_df, | |
| weather_df, | |
| chat_container=chat_container | |
| ) | |
| if st.session_state.get("package_rendered", False): | |
| st.session_state[step_key] = "package_end" | |
| else: | |
| st.session_state[step_key] = "companion" | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 5) λνΒ·μ°λ Ή λ°κΈ° λ¨κ³ | |
| elif st.session_state[step_key] == "companion": | |
| with chat_container: | |
| # 5.1) μλ΄ λ©μμ§ μΆλ ₯ | |
| log_and_render( | |
| "ν¨κ» κ°λ λΆμ΄λ μ°λ Ήλλ₯Ό μλ €μ£Όμλ©΄ λ λ± λ§λ μνμ 골λΌλ릴κ²μ!<br>" | |
| "1οΈβ£ λν μ¬λΆ (νΌμ / μΉκ΅¬ / 컀ν / κ°μ‘± / λ¨μ²΄)<br>" | |
| "2οΈβ£ μ°λ Ήλ (20λ / 30λ / 40λ / 50λ / 60λ μ΄μ)", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="ask_companion_age" | |
| ) | |
| # 5.1.1) λν 체ν¬λ°μ€ | |
| st.markdown( | |
| '<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">π« λν μ ν</div>', | |
| unsafe_allow_html=True | |
| ) | |
| c_cols = st.columns(5) | |
| comp_flags = { | |
| "νΌμ": c_cols[0].checkbox("νΌμ"), | |
| "μΉκ΅¬": c_cols[1].checkbox("μΉκ΅¬"), | |
| "컀ν": c_cols[2].checkbox("컀ν"), | |
| "κ°μ‘±": c_cols[3].checkbox("κ°μ‘±"), | |
| "λ¨μ²΄": c_cols[4].checkbox("λ¨μ²΄"), | |
| } | |
| companions = [k for k, v in comp_flags.items() if v] | |
| # 5.1.2) μ°λ Ή 체ν¬λ°μ€ | |
| st.markdown( | |
| '<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">π μ°λ Ή μ ν</div>', | |
| unsafe_allow_html=True | |
| ) | |
| a_cols = st.columns(5) | |
| age_flags = { | |
| "20λ": a_cols[0].checkbox("20λ"), | |
| "30λ": a_cols[1].checkbox("30λ"), | |
| "40λ": a_cols[2].checkbox("40λ"), | |
| "50λ": a_cols[3].checkbox("50λ"), | |
| "60λ μ΄μ": a_cols[4].checkbox("60λ μ΄μ"), | |
| } | |
| age_group = [k for k, v in age_flags.items() if v] | |
| # 5.1.3) νμΈ λ²νΌ | |
| confirm = st.button( | |
| "μΆμ² λ°κΈ°", | |
| key="btn_confirm_companion", | |
| disabled=not (companions or age_group), | |
| ) | |
| # 5.2) λ©μμ§ μΆλ ₯ | |
| if confirm: | |
| # μ¬μ©μ λ²λΈ μΆλ ₯ | |
| user_msg = " / ".join(companions + age_group) | |
| log_and_render( | |
| user_msg if user_msg else "μ ν μ ν¨", | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_comp_age_{random.randint(1,999999)}" | |
| ) | |
| # μΈμ μ μ₯ | |
| st.session_state["companions"] = companions or None | |
| st.session_state["age_group"] = age_group or None | |
| # λ€μ μ€ν | |
| st.session_state[step_key] = "package" | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 6) λνΒ·μ°λ Ή νν°λ§Β· ν¨ν€μ§ μΆλ ₯ λ¨κ³ | |
| elif st.session_state[step_key] == "package": | |
| # ν¨ν€μ§ λ²λΈμ μ΄λ―Έ λ§λ€μμΌλ©΄ 건λλ | |
| if st.session_state.get("package_rendered"): | |
| st.session_state[step_key] = "package_end" | |
| st.rerun() | |
| return | |
| companions = st.session_state.get("companions") | |
| age_group = st.session_state.get("age_group") | |
| city = st.session_state.get("selected_city") | |
| place = st.session_state.get("selected_place") | |
| filtered = filter_packages_by_companion_age( | |
| package_df, companions, age_group, city=city, top_n=2 | |
| ) | |
| if filtered.empty: | |
| log_and_render( | |
| "β οΈ μμ½μ§λ§ μ§κΈ 쑰건μ λ§λ ν¨ν€μ§κ° μμ΄μ.<br>" | |
| "λ€λ₯Έ 쑰건μΌλ‘ λ€μ μ°Ύμλ³ΌκΉμ?", | |
| sender="bot", chat_container=chat_container, | |
| key="no_package" | |
| ) | |
| st.session_state[step_key] = "companion" # λ€μ μ λ ₯ λ¨κ³λ‘ | |
| st.rerun() | |
| return | |
| combo_msg = make_companion_age_message(companions, age_group) | |
| header = f"{combo_msg}" | |
| # ν¨ν€μ§ μΉ΄λ μΆλ ₯ | |
| used_phrases = set() | |
| theme_row = travel_df[travel_df["μ¬νμ§"] == place] | |
| raw_theme = theme_row["ν΅ν©ν λ§λͺ "].iloc[0] if not theme_row.empty else None | |
| selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0] | |
| title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆμ²"]) | |
| sampled_titles = random.sample(title_candidates, | |
| k=min(2, len(title_candidates))) | |
| # λ©μμ§ μμ± | |
| pkg_msgs = [header] | |
| for i, (_, row) in enumerate(filtered.iterrows(), 1): | |
| desc, used_phrases = make_top2_description_custom( | |
| row.to_dict(), used_phrases | |
| ) | |
| tags = format_summary_tags_custom(row["μμ½μ 보"]) | |
| title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles) | |
| else random.choice(title_candidates)) | |
| title = f"{city} {title_phrase} ν¨ν€μ§" | |
| url = row.URL | |
| pkg_msgs.append( | |
| f"{i}. <strong>{title}</strong><br>" | |
| f"π Ό {desc}<br>{tags}<br>" | |
| f'<a href="{url}" target="_blank" rel="noopener noreferrer" ' | |
| 'style="text-decoration:none;font-weight:600;color:#009c75;">' | |
| 'π λ°λ‘κ°κΈ° β</a>' | |
| ) | |
| # λ©μμ§ μΆλ ₯ | |
| log_and_render( | |
| "<br><br>".join(pkg_msgs), | |
| sender="bot", | |
| chat_container=chat_container, | |
| key=f"pkg_bundle_{random.randint(1,999999)}" | |
| ) | |
| # μΈμ μ 리 | |
| st.session_state["package_rendered"] = True | |
| st.session_state[step_key] = "package_end" | |
| show_llm_inline() | |
| # β rerun μμ΄ κ°μ μ¬μ΄ν΄μ μΈλΌμΈ LLM ν¨λμ λ°λ‘ νμ | |
| render_llm_inline_if_open(chat_container) | |
| return | |
| # ββββββββββββββββββ 7) μ’ λ£ λ¨κ³ | |
| elif st.session_state[step_key] == "package_end": | |
| log_and_render("νμνμ€ λ μΈμ λ μ§ λ μ°Ύμμ£ΌμΈμ! βοΈ", | |
| sender="bot", chat_container=chat_container, | |
| key="goodbye") | |
| show_llm_inline() | |
| render_llm_inline_if_open(chat_container) | |
| return | |
| # βββββββββββββββββββββββββββββββββββββ intent λͺ¨λ | |
| def intent_ui(travel_df, external_score_df, festival_df, weather_df, package_df, | |
| country_filter, city_filter, chat_container, intent, log_and_render): | |
| """intent(μλλ₯Ό μ λ ₯νμ κ²½μ°) λͺ¨λ μ μ© UI & λ‘μ§""" | |
| # ββββββββββββββββββ μΈμ ν€ μ μ | |
| sample_key = "intent_sample_df" | |
| step_key = "intent_step" | |
| prev_key = "intent_prev_places" | |
| intent_key = "intent_chip_selected" | |
| # ββββββββββββββββββ 0) μ΄κΈ°ν | |
| if step_key not in st.session_state: | |
| st.session_state[step_key] = "recommend_places" | |
| st.session_state[prev_key] = set() | |
| st.session_state.pop(sample_key, None) | |
| # ββββββββββββββββββ 1) restart μνλ©΄ μΈνΈλ‘λ§ μΆλ ₯νκ³ μ’ λ£ | |
| if st.session_state[step_key] == "restart": | |
| log_and_render( | |
| "λ€μ μ¬νμ§λ₯Ό μΆμ²ν΄λ릴κ²μ!<br>μμ¦ λ μ€λ₯΄λ μ¬νμ΄ μμΌμ κ°μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="region_restart_intro" | |
| ) | |
| return | |
| # ββββββββββββββββββ 2) μ¬νμ§ μΆμ² λ¨κ³ | |
| if st.session_state[step_key] == "recommend_places": | |
| selected_theme = intent | |
| theme_df = recommend_places_by_theme(selected_theme, country_filter, city_filter) | |
| theme_df = theme_df.drop_duplicates(subset=["μ¬νλμ"]) | |
| theme_df = theme_df.drop_duplicates(subset=["μ¬νμ§"]) | |
| # 2.1) μ΄μ μΆμ² κΈ°λ‘ μΈν | |
| prev = st.session_state.setdefault(prev_key, set()) | |
| # 2.2) μ΄λ―Έ μνμ΄ μλ€λ©΄ result_df μ¬μ¬μ© | |
| if sample_key in st.session_state and not st.session_state[sample_key].empty: | |
| result_df = st.session_state[sample_key] | |
| else: | |
| # 2.3) μλ‘μ΄ μΆμ² λμ νν°λ§ | |
| candidates = theme_df[~theme_df["μ¬νμ§"].isin(prev)] | |
| # 2.4) νλ³΄κ° μλ€λ©΄ μ’ λ£ | |
| if candidates.empty: | |
| st.session_state[step_key] = "recommend_places_end" | |
| st.rerun() | |
| return | |
| # 2.5) μλ‘μ΄ μΆμ² μΆμΆ λ° μ μ₯ | |
| result_df = apply_weighted_score_filter(candidates) | |
| st.session_state[sample_key] = result_df | |
| # prevμ λ±λ‘νμ¬ μ€λ³΅ μΆμ² λ°©μ§ | |
| prev.update(result_df["μ¬νμ§"]) | |
| st.session_state[prev_key] = prev | |
| # 2.6) μ€νλ λ¬Έμ₯ μμ± | |
| opening_line = intent_opening_lines.get(selected_theme, f"'{selected_theme}' μ¬νμ§λ₯Ό μκ°ν κ²μ.") | |
| opening_line = opening_line.format(len(result_df)) | |
| # 2.7) μΆμ² λ©μμ§ κ΅¬μ± | |
| message = "<br>".join([ | |
| f"{i+1}. <strong>{row.μ¬νμ§}</strong> " | |
| f"({row.μ¬νλλΌ}, {row.μ¬νλμ}) " | |
| f"{getattr(row, 'νμ€μ€λͺ ', 'μ€λͺ μ΄ μμ΅λλ€')}" | |
| for i, row in enumerate(result_df.itertuples()) | |
| ]) | |
| # 2.8) μ±λ΄ μΆλ ₯ + μΉ© λ²νΌ λ λλ§ | |
| with chat_container: | |
| log_and_render(f"{opening_line}<br><br>{message}", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key=f"intent_recommendation_{random.randint(1,999999)}") | |
| recommend_names = result_df["μ¬νμ§"].tolist() | |
| prev_choice = st.session_state.get(intent_key, None) | |
| choice = render_chip_buttons( | |
| recommend_names + ["λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π"], | |
| key_prefix="intent_chip", | |
| selected_value=prev_choice | |
| ) | |
| # 2.9) μ ν μκ±°λ μ€λ³΅ μ νμ΄λ©΄ λκΈ° | |
| if not choice or choice == prev_choice: | |
| return | |
| # μ ν κ²°κ³Ό μ²λ¦¬ | |
| if choice: | |
| if choice == "λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π": | |
| log_and_render("λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π", | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_place_refresh_{random.randint(1,999999)}") | |
| st.session_state.pop(sample_key, None) | |
| st.rerun() | |
| return | |
| # μ μ μ νλ κ²½μ° | |
| st.session_state[intent_key] = choice | |
| st.session_state[step_key] = "detail" | |
| st.session_state.chat_log.append(("user", choice)) | |
| # μ€μ λ‘ μ νλ μ¬νμ§λ§ prevμ κΈ°λ‘ | |
| match = result_df[result_df["μ¬νμ§"] == choice] | |
| if not match.empty: | |
| prev.add(choice) | |
| st.session_state[prev_key] = prev | |
| # μν νκΈ° | |
| st.session_state.pop(sample_key, None) | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 3) μΆμ² μ’ λ£ λ¨κ³ | |
| elif st.session_state[step_key] == "recommend_places_end": | |
| # 3.1) λ©μμ§ μΆλ ₯ | |
| with chat_container: | |
| log_and_render( | |
| "β οΈ λ μ΄μ μλ‘μ΄ μ¬νμ§κ° μμ΄μ.<br>λ€μ μ§λ¬Ένμκ² μ΄μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="intent_empty" | |
| ) | |
| # 3.2) μ¬μμ μ¬λΆ μΉ© λ²νΌ μΆλ ₯ | |
| restart_done_key = "intent_restart_done" | |
| chip_ph = st.empty() | |
| if not st.session_state.get(restart_done_key, False): | |
| with chip_ph: | |
| choice = render_chip_buttons( | |
| ["μ π", "μλμ€ β"], | |
| key_prefix="intent_restart") | |
| else: | |
| choice = None | |
| # 3.3) μμ§ μ무κ²λ μ ννμ§ μμ κ²½μ° | |
| if choice is None: | |
| return | |
| chip_ph.empty() | |
| st.session_state[restart_done_key] = True | |
| # 3.4) μ¬μ©μ μ νκ° μΆλ ₯ | |
| log_and_render(choice, | |
| sender="user", | |
| chat_container=chat_container | |
| ) | |
| # 3.5) μ¬μ©μκ° μ¬μΆμ²μ μνλ κ²½μ° | |
| if choice == "μ π": | |
| for k in [sample_key, prev_key, intent_key, restart_done_key]: | |
| st.session_state.pop(k, None) | |
| chip_ph.empty() | |
| # λ€μ μΆμ² λ¨κ³λ‘ μ΄κΈ°ν | |
| st.session_state["user_input_rendered"] = False | |
| st.session_state["intent_step"] = "restart" | |
| log_and_render( | |
| "λ€μ μ¬νμ§λ₯Ό μΆμ²ν΄λ릴κ²μ!<br>μμ¦ λ μ€λ₯΄λ μ¬νμ΄ μμΌμ κ°μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="intent_restart_intro" | |
| ) | |
| return | |
| # 3.6) μ¬μ©μκ° μ’ λ£λ₯Ό μ νν κ²½μ° | |
| else: | |
| log_and_render("μ¬ν μΆμ²μ μ’ λ£ν κ²μ. νμνμ€ λ μΈμ λ μ§ λ μ°Ύμμ£ΌμΈμ! βοΈ", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="intent_exit") | |
| st.stop() | |
| return | |
| # ββββββββββββββββββ 4) μ¬νμ§ μμΈ λ¨κ³ | |
| if st.session_state[step_key] == "detail": | |
| chosen = st.session_state[intent_key] | |
| # city μ΄λ¦ λ½μμ μΈμ μ μ μ₯ | |
| row = travel_df[travel_df["μ¬νμ§"] == chosen].iloc[0] | |
| st.session_state["selected_city"] = row["μ¬νλμ"] | |
| st.session_state["selected_place"] = chosen | |
| log_and_render(chosen, | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_place_{chosen}") | |
| handle_selected_place( | |
| chosen, | |
| travel_df, | |
| external_score_df, | |
| festival_df, | |
| weather_df, | |
| chat_container=chat_container | |
| ) | |
| if st.session_state.get("package_rendered", False): | |
| st.session_state[step_key] = "package_end" | |
| else: | |
| st.session_state[step_key] = "companion" | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 5) λνΒ·μ°λ Ή λ°κΈ° λ¨κ³ | |
| elif st.session_state[step_key] == "companion": | |
| with chat_container: | |
| # 5.1) μλ΄ λ©μμ§ μΆλ ₯ | |
| log_and_render( | |
| "ν¨κ» κ°λ λΆμ΄λ μ°λ Ήλλ₯Ό μλ €μ£Όμλ©΄ λ λ± λ§λ μνμ 골λΌλ릴κ²μ!<br>" | |
| "1οΈβ£ λν μ¬λΆ (νΌμ / μΉκ΅¬ / 컀ν / κ°μ‘± / λ¨μ²΄)<br>" | |
| "2οΈβ£ μ°λ Ήλ (20λ / 30λ / 40λ / 50λ / 60λ μ΄μ)", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="ask_companion_age" | |
| ) | |
| # 5.1.1) λν 체ν¬λ°μ€ | |
| st.markdown( | |
| '<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">π« λν μ ν</div>', | |
| unsafe_allow_html=True | |
| ) | |
| c_cols = st.columns(5) | |
| comp_flags = { | |
| "νΌμ": c_cols[0].checkbox("νΌμ"), | |
| "μΉκ΅¬": c_cols[1].checkbox("μΉκ΅¬"), | |
| "컀ν": c_cols[2].checkbox("컀ν"), | |
| "κ°μ‘±": c_cols[3].checkbox("κ°μ‘±"), | |
| "λ¨μ²΄": c_cols[4].checkbox("λ¨μ²΄"), | |
| } | |
| companions = [k for k, v in comp_flags.items() if v] | |
| # 5.1.2) μ°λ Ή 체ν¬λ°μ€ | |
| st.markdown( | |
| '<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">π μ°λ Ή μ ν</div>', | |
| unsafe_allow_html=True | |
| ) | |
| a_cols = st.columns(5) | |
| age_flags = { | |
| "20λ": a_cols[0].checkbox("20λ"), | |
| "30λ": a_cols[1].checkbox("30λ"), | |
| "40λ": a_cols[2].checkbox("40λ"), | |
| "50λ": a_cols[3].checkbox("50λ"), | |
| "60λ μ΄μ": a_cols[4].checkbox("60λ μ΄μ"), | |
| } | |
| age_group = [k for k, v in age_flags.items() if v] | |
| # 5.1.3) νμΈ λ²νΌ | |
| confirm = st.button( | |
| "μΆμ² λ°κΈ°", | |
| key="btn_confirm_companion", | |
| disabled=not (companions or age_group), | |
| ) | |
| # 5.2) λ©μμ§ μΆλ ₯ | |
| if confirm: | |
| # μ¬μ©μ λ²λΈ μΆλ ₯ | |
| user_msg = " / ".join(companions + age_group) | |
| log_and_render( | |
| user_msg if user_msg else "μ ν μ ν¨", | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_comp_age_{random.randint(1,999999)}" | |
| ) | |
| # μΈμ μ μ₯ | |
| st.session_state["companions"] = companions or None | |
| st.session_state["age_group"] = age_group or None | |
| # λ€μ μ€ν | |
| st.session_state[step_key] = "package" | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 6) λνΒ·μ°λ Ή νν°λ§Β· ν¨ν€μ§ μΆλ ₯ λ¨κ³ | |
| elif st.session_state[step_key] == "package": | |
| # ν¨ν€μ§ λ²λΈμ μ΄λ―Έ λ§λ€μμΌλ©΄ 건λλ | |
| if st.session_state.get("package_rendered"): | |
| st.session_state[step_key] = "package_end" | |
| st.rerun() | |
| return | |
| companions = st.session_state.get("companions") | |
| age_group = st.session_state.get("age_group") | |
| city = st.session_state.get("selected_city") | |
| place = st.session_state.get("selected_place") | |
| filtered = filter_packages_by_companion_age( | |
| package_df, companions, age_group, city=city, top_n=2 | |
| ) | |
| if filtered.empty: | |
| log_and_render( | |
| "β οΈ μμ½μ§λ§ μ§κΈ 쑰건μ λ§λ ν¨ν€μ§κ° μμ΄μ.<br>" | |
| "λ€λ₯Έ 쑰건μΌλ‘ λ€μ μ°Ύμλ³ΌκΉμ?", | |
| sender="bot", chat_container=chat_container, | |
| key="no_package" | |
| ) | |
| st.session_state[step_key] = "companion" # λ€μ μ λ ₯ λ¨κ³λ‘ | |
| st.rerun() | |
| return | |
| combo_msg = make_companion_age_message(companions, age_group) | |
| header = f"{combo_msg}" | |
| # ν¨ν€μ§ μΉ΄λ μΆλ ₯ | |
| used_phrases = set() | |
| theme_row = travel_df[travel_df["μ¬νμ§"] == place] | |
| raw_theme = theme_row["ν΅ν©ν λ§λͺ "].iloc[0] if not theme_row.empty else None | |
| selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0] | |
| title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆμ²"]) | |
| sampled_titles = random.sample(title_candidates, | |
| k=min(2, len(title_candidates))) | |
| # λ©μμ§ μμ± | |
| pkg_msgs = [header] | |
| for i, (_, row) in enumerate(filtered.iterrows(), 1): | |
| desc, used_phrases = make_top2_description_custom( | |
| row.to_dict(), used_phrases | |
| ) | |
| tags = format_summary_tags_custom(row["μμ½μ 보"]) | |
| title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles) | |
| else random.choice(title_candidates)) | |
| title = f"{city} {title_phrase} ν¨ν€μ§" | |
| url = row.URL | |
| pkg_msgs.append( | |
| f"{i}. <strong>{title}</strong><br>" | |
| f"π Ό {desc}<br>{tags}<br>" | |
| f'<a href="{url}" target="_blank" rel="noopener noreferrer" ' | |
| 'style="text-decoration:none;font-weight:600;color:#009c75;">' | |
| 'π λ°λ‘κ°κΈ° β</a>' | |
| ) | |
| # λ©μμ§ μΆλ ₯ | |
| log_and_render( | |
| "<br><br>".join(pkg_msgs), | |
| sender="bot", | |
| chat_container=chat_container, | |
| key=f"pkg_bundle_{random.randint(1,999999)}" | |
| ) | |
| # μΈμ μ 리 | |
| st.session_state["package_rendered"] = True | |
| st.session_state[step_key] = "package_end" | |
| show_llm_inline() # νλκ·Έλ§ ON (rerun μμ) | |
| # β rerun μμ΄ κ°μ μ¬μ΄ν΄μ μΈλΌμΈ LLM ν¨λμ λ°λ‘ νμ | |
| render_llm_inline_if_open(chat_container) | |
| return | |
| # ββββββββββββββββββ 7) μ’ λ£ λ¨κ³ | |
| elif st.session_state[step_key] == "package_end": | |
| log_and_render("νμνμ€ λ μΈμ λ μ§ λ μ°Ύμμ£ΌμΈμ! βοΈ", | |
| sender="bot", chat_container=chat_container, | |
| key="goodbye") | |
| show_llm_inline() | |
| render_llm_inline_if_open(chat_container) | |
| return | |
| # βββββββββββββββββββββββββββββββββββββ emotion λͺ¨λ | |
| def emotion_ui(travel_df, external_score_df, festival_df, weather_df, package_df, | |
| country_filter, city_filter, chat_container, candidate_themes, | |
| intent, emotion_groups, top_emotions, log_and_render): | |
| """emotion(κ°μ μ μ λ ₯νμ κ²½μ°) λͺ¨λ μ μ© UI & λ‘μ§""" | |
| # ββββββββββββββββββ μΈμ ν€ μ μ | |
| sample_key = "emotion_sample_df" | |
| step_key = "emotion_step" | |
| theme_key = "selected_theme" | |
| emotion_key = "emotion_chip_selected" | |
| prev_key = "emotion_prev_places" | |
| # ββββββββββββββββββ 0) μ΄κΈ°ν | |
| if step_key not in st.session_state: | |
| st.session_state[step_key] = "theme_selection" | |
| st.session_state[prev_key] = set() | |
| st.session_state.pop(sample_key, None) | |
| # ββββββββββββββββββ 1) restart μνλ©΄ μΈνΈλ‘λ§ μΆλ ₯νκ³ μ’ λ£ | |
| if st.session_state[step_key] == "restart": | |
| log_and_render( | |
| "λ€μ μ¬νμ§λ₯Ό μΆμ²ν΄λ릴κ²μ!<br>μμ¦ λ μ€λ₯΄λ μ¬νμ΄ μμΌμ κ°μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="region_restart_intro" | |
| ) | |
| return | |
| # ββββββββββββββββββ 2) ν λ§ μΆμ² λ¨κ³ | |
| if st.session_state[step_key] == "theme_selection": | |
| # μΆμ² ν λ§ 1κ°μΌ κ²½μ° | |
| if len(candidate_themes) == 1: | |
| selected_theme = candidate_themes[0] | |
| st.session_state[theme_key] = selected_theme | |
| log_and_render(f"μΆμ² κ°λ₯ν ν λ§κ° 1κ°μ΄λ―λ‘ '{selected_theme}'μ μ νν κ²μ.", sender="bot", chat_container=chat_container) | |
| st.session_state[step_key] = "recommend_places" | |
| st.rerun() | |
| # ν λ§κ° μ¬λ¬ κ°μΌ κ²½μ° | |
| else: | |
| # μΈνΈλ‘ λ©μμ§ | |
| intro_msg = generate_intro_message(intent=intent, emotion_groups=emotion_groups, emotion_scores=top_emotions) | |
| log_and_render(f"{intro_msg}<br>μλ μ€ λ§μμ΄ λ리λ μ¬ν μ€νμΌμ 골λΌμ£ΌμΈμ π«", sender="bot", chat_container=chat_container) | |
| # ν보 ν λ§ μ€λΉ | |
| dfs = [recommend_places_by_theme(t, country_filter, city_filter) for t in candidate_themes] | |
| dfs = [df for df in dfs if not df.empty] | |
| all_theme_df = pd.concat(dfs) if dfs else pd.DataFrame(columns=travel_df.columns) | |
| all_theme_df = all_theme_df.drop_duplicates(subset=["μ¬νμ§"]) | |
| all_theme_names = all_theme_df["ν΅ν©ν λ§λͺ "].dropna().tolist() | |
| available_themes = [] | |
| for t in candidate_themes: | |
| if t in all_theme_names and t not in available_themes: | |
| available_themes.append(t) | |
| for t in all_theme_names: | |
| if t not in available_themes: | |
| available_themes.append(t) | |
| available_themes = available_themes[:3] # μ΅λ 3κ° | |
| # μΉ© UI μΆλ ₯ | |
| with chat_container: | |
| chip = render_chip_buttons( | |
| [theme_ui_map.get(t, (t, ""))[0] for t in available_themes], | |
| key_prefix="theme_chip" | |
| ) | |
| # μ νμ΄ μλ£λλ©΄ λ€μ λ¨κ³λ‘ μ΄λ | |
| if chip: | |
| selected_theme = ui_to_theme_map.get(chip, chip) | |
| st.session_state[theme_key] = selected_theme | |
| st.session_state[step_key] = "recommend_places" | |
| st.session_state["emotion_all_theme_df"] = all_theme_df | |
| log_and_render(f"{chip}", sender="user", | |
| chat_container=chat_container) | |
| st.rerun() | |
| # ββββββββββββββββββ 3) μ¬νμ§ μΆμ² λ¨κ³ | |
| if st.session_state[step_key] == "recommend_places": | |
| all_theme_df = st.session_state.get("emotion_all_theme_df", pd.DataFrame()) | |
| selected_theme = st.session_state.get(theme_key, "") | |
| prev_key = "emotion_prev_places" | |
| prev = st.session_state.setdefault(prev_key, set()) | |
| # μμΈ μ²λ¦¬: λ°μ΄ν° μμ κ²½μ° | |
| if all_theme_df.empty or not selected_theme: | |
| log_and_render("μΆμ² λ°μ΄ν°λ₯Ό λΆλ¬μ€λ λ° λ¬Έμ κ° λ°μνμ΄μ. <br>λ€μ μ λ ₯ν΄ μ£ΌμΈμ.", sender="bot", chat_container=chat_container) | |
| return | |
| if sample_key not in st.session_state: | |
| theme_df = all_theme_df[all_theme_df["ν΅ν©ν λ§λͺ "] == selected_theme] | |
| theme_df = theme_df.drop_duplicates(subset=["μ¬νλμ"]) | |
| theme_df = theme_df.drop_duplicates(subset=["μ¬νμ§"]) | |
| remaining = theme_df[~theme_df["μ¬νμ§"].isin(prev)] | |
| if remaining.empty: | |
| st.session_state[step_key] = "recommend_places_end" | |
| st.rerun() | |
| return | |
| result_df = apply_weighted_score_filter(remaining) | |
| st.session_state[sample_key] = result_df | |
| else: | |
| result_df = st.session_state[sample_key] | |
| # μΆμ² μ λΆμ‘±ν κ²½μ° Fallback 보μ | |
| if len(result_df) < 3: | |
| fallback = travel_df[ | |
| (travel_df["ν΅ν©ν λ§λͺ "] == selected_theme) & | |
| (~travel_df["μ¬νμ§"].isin(result_df["μ¬νμ§"])) | |
| ].drop_duplicates(subset=["μ¬νμ§"]) | |
| if not fallback.empty: | |
| fill_count = min(3 - len(result_df), len(fallback)) | |
| fill = fallback.sample(n=fill_count, random_state=random.randint(1, 9999)) | |
| result_df = pd.concat([result_df, fill], ignore_index=True) | |
| # μν μ μ₯ | |
| st.session_state[sample_key] = result_df | |
| # 2.1)첫 λ¬Έμ₯ μΆλ ₯ | |
| ui_name = theme_ui_map.get(selected_theme, (selected_theme,))[0] | |
| opening_line_template = theme_opening_lines.get(ui_name) | |
| opening_line = opening_line_template.format(len(result_df)) if opening_line_template else "" | |
| message = ( | |
| "<br>".join([ | |
| f"{i+1}. <strong>{row.μ¬νμ§}</strong> " | |
| f"({row.μ¬νλλΌ}, {row.μ¬νλμ}) " | |
| f"{getattr(row, 'νμ€μ€λͺ ', 'μ€λͺ μ΄ μμ΅λλ€')}" | |
| for i, row in enumerate(result_df.itertuples()) | |
| ]) | |
| ) | |
| if opening_line_template: | |
| message_combined = f"{opening_line}<br><br>{message}" | |
| with chat_container: | |
| log_and_render(message_combined, | |
| sender="bot", | |
| chat_container=chat_container, | |
| key=f"emotion_recommendation_{random.randint(1,999999)}" | |
| ) | |
| # 2.2) μΉ© λ²νΌμΌλ‘ μΆμ²μ§ μ€ μ νλ°κΈ° | |
| recommend_names = result_df["μ¬νμ§"].tolist() | |
| prev_choice = st.session_state.get(emotion_key, None) | |
| choice = render_chip_buttons( | |
| recommend_names + ["λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π"], | |
| key_prefix="emotion_chip", | |
| selected_value=prev_choice | |
| ) | |
| # 2.3) μ ν κ²°κ³Ό μ²λ¦¬ | |
| if not choice or choice == prev_choice: | |
| return | |
| if choice == "λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π": | |
| log_and_render("λ€λ₯Έ μ¬νμ§ λ³΄κΈ° π", | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_place_refresh_{random.randint(1,999999)}") | |
| st.session_state.pop(sample_key, None) | |
| st.rerun() | |
| return | |
| # μ€μ μ νν μ¬νμ§ μ²λ¦¬ | |
| st.session_state[emotion_key] = choice | |
| st.session_state[step_key] = "detail" | |
| st.session_state.chat_log.append(("user", choice)) | |
| # μ νν μ¬νμ§λ₯Ό prev κΈ°λ‘μ μΆκ° | |
| match = result_df[result_df["μ¬νμ§"] == choice] | |
| if not match.empty: | |
| prev.add(choice) | |
| st.session_state[prev_key] = prev | |
| # μν νκΈ° | |
| st.session_state.pop(sample_key, None) | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 3) μΆμ² μ’ λ£ λ¨κ³: λ μ΄μ μΆμ²ν μ¬νμ§κ° μμ λ | |
| elif st.session_state[step_key] == "recommend_places_end": | |
| with chat_container: | |
| # 3.1) λ©μμ§ μΆλ ₯ | |
| log_and_render( | |
| "β οΈ λ μ΄μ μλ‘μ΄ μ¬νμ§κ° μμ΄μ.<br>λ€μ μ§λ¬Ένμκ² μ΄μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="emotion_empty" | |
| ) | |
| # 3.2) μ¬μμ μ¬λΆ μΉ© λ²νΌ μΆλ ₯ | |
| restart_done_key = "emotion_restart_done" | |
| chip_ph = st.empty() | |
| if not st.session_state.get(restart_done_key, False): | |
| with chip_ph: | |
| choice = render_chip_buttons( | |
| ["μ π", "μλμ€ β"], | |
| key_prefix="emotion_restart" | |
| ) | |
| else: | |
| choice = None | |
| # 3.3) μμ§ μ무κ²λ μ ννμ§ μμ κ²½μ° | |
| if choice is None: | |
| return | |
| chip_ph.empty() | |
| st.session_state[restart_done_key] = True | |
| # 3.4) μ¬μ©μ μ νκ° μΆλ ₯ | |
| log_and_render( | |
| choice, | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_restart_choice_{choice}" | |
| ) | |
| # 3.5) μ¬μ©μκ° μ¬μΆμ²μ μνλ κ²½μ° | |
| if choice == "μ π": | |
| # μ¬ν μΆμ² μν μ΄κΈ°ν | |
| for k in [emotion_key, prev_key, sample_key, restart_done_key]: | |
| st.session_state.pop(k, None) | |
| chip_ph.empty() | |
| # λ€μ μΆμ² λ¨κ³λ‘ μ΄κΈ°ν | |
| st.session_state["user_input_rendered"] = False | |
| st.session_state["emotion_step"] = "restart" | |
| log_and_render( | |
| "λ€μ μ¬νμ§λ₯Ό μΆμ²ν΄λ릴κ²μ!<br>μμ¦ λ μ€λ₯΄λ μ¬νμ΄ μμΌμ κ°μ?", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="emotion_restart_intro" | |
| ) | |
| return | |
| # 3.6) μ¬μ©μκ° μ’ λ£λ₯Ό μ νν κ²½μ° | |
| else: | |
| log_and_render("μ¬ν μΆμ²μ μ’ λ£ν κ²μ. νμνμ€ λ μΈμ λ μ§ λ μ°Ύμμ£ΌμΈμ! βοΈ", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="emotion_exit") | |
| st.stop() | |
| return | |
| # ββββββββββββββββββ 4) μ¬νμ§ μμΈ λ¨κ³ | |
| if st.session_state[step_key] == "detail": | |
| chosen = st.session_state[emotion_key] | |
| # city μ΄λ¦ λ½μμ μΈμ μ μ μ₯ | |
| row = travel_df[travel_df["μ¬νμ§"] == chosen].iloc[0] | |
| st.session_state["selected_city"] = row["μ¬νλμ"] | |
| st.session_state["selected_place"] = chosen | |
| log_and_render(chosen, | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_place_{chosen}") | |
| handle_selected_place( | |
| chosen, | |
| travel_df, | |
| external_score_df, | |
| festival_df, | |
| weather_df, | |
| chat_container=chat_container | |
| ) | |
| if st.session_state.get("package_rendered", False): | |
| st.session_state[step_key] = "package_end" | |
| else: | |
| st.session_state[step_key] = "companion" | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 5) λνΒ·μ°λ Ή λ°κΈ° λ¨κ³ | |
| elif st.session_state[step_key] == "companion": | |
| with chat_container: | |
| # 5.1) μλ΄ λ©μμ§ μΆλ ₯ | |
| log_and_render( | |
| "ν¨κ» κ°λ λΆμ΄λ μ°λ Ήλλ₯Ό μλ €μ£Όμλ©΄ λ λ± λ§λ μνμ 골λΌλ릴κ²μ!<br>" | |
| "1οΈβ£ λν μ¬λΆ (νΌμ / μΉκ΅¬ / 컀ν / κ°μ‘± / λ¨μ²΄)<br>" | |
| "2οΈβ£ μ°λ Ήλ (20λ / 30λ / 40λ / 50λ / 60λ μ΄μ)", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="ask_companion_age" | |
| ) | |
| # 5.1.1) λν 체ν¬λ°μ€ | |
| st.markdown( | |
| '<div style="font-size:14px; font-weight:600; margin:14px 0px 6px 0px;">π« λν μ ν</div>', | |
| unsafe_allow_html=True | |
| ) | |
| c_cols = st.columns(5) | |
| comp_flags = { | |
| "νΌμ": c_cols[0].checkbox("νΌμ"), | |
| "μΉκ΅¬": c_cols[1].checkbox("μΉκ΅¬"), | |
| "컀ν": c_cols[2].checkbox("컀ν"), | |
| "κ°μ‘±": c_cols[3].checkbox("κ°μ‘±"), | |
| "λ¨μ²΄": c_cols[4].checkbox("λ¨μ²΄"), | |
| } | |
| companions = [k for k, v in comp_flags.items() if v] | |
| # 5.1.2) μ°λ Ή 체ν¬λ°μ€ | |
| st.markdown( | |
| '<div style="font-size:14px; font-weight:600; margin:0px 0px 6px 0px;">π μ°λ Ή μ ν</div>', | |
| unsafe_allow_html=True | |
| ) | |
| a_cols = st.columns(5) | |
| age_flags = { | |
| "20λ": a_cols[0].checkbox("20λ"), | |
| "30λ": a_cols[1].checkbox("30λ"), | |
| "40λ": a_cols[2].checkbox("40λ"), | |
| "50λ": a_cols[3].checkbox("50λ"), | |
| "60λ μ΄μ": a_cols[4].checkbox("60λ μ΄μ"), | |
| } | |
| age_group = [k for k, v in age_flags.items() if v] | |
| # 5.1.3) νμΈ λ²νΌ | |
| confirm = st.button( | |
| "μΆμ² λ°κΈ°", | |
| key="btn_confirm_companion", | |
| disabled=not (companions or age_group), | |
| ) | |
| # 5.2) λ©μμ§ μΆλ ₯ | |
| if confirm: | |
| # μ¬μ©μ λ²λΈ μΆλ ₯ | |
| user_msg = " / ".join(companions + age_group) | |
| log_and_render( | |
| user_msg if user_msg else "μ ν μ ν¨", | |
| sender="user", | |
| chat_container=chat_container, | |
| key=f"user_comp_age_{random.randint(1,999999)}" | |
| ) | |
| # μΈμ μ μ₯ | |
| st.session_state["companions"] = companions or None | |
| st.session_state["age_group"] = age_group or None | |
| # λ€μ μ€ν | |
| st.session_state[step_key] = "package" | |
| st.rerun() | |
| return | |
| # ββββββββββββββββββ 6) λνΒ·μ°λ Ή νν°λ§Β· ν¨ν€μ§ μΆλ ₯ λ¨κ³ | |
| elif st.session_state[step_key] == "package": | |
| # ν¨ν€μ§ λ²λΈμ μ΄λ―Έ λ§λ€μμΌλ©΄ 건λλ | |
| if st.session_state.get("package_rendered"): | |
| st.session_state[step_key] = "package_end" | |
| st.rerun() | |
| return | |
| companions = st.session_state.get("companions") | |
| age_group = st.session_state.get("age_group") | |
| city = st.session_state.get("selected_city") | |
| place = st.session_state.get("selected_place") | |
| filtered = filter_packages_by_companion_age( | |
| package_df, companions, age_group, city=city, top_n=2 | |
| ) | |
| if filtered.empty: | |
| log_and_render( | |
| "β οΈ μμ½μ§λ§ μ§κΈ 쑰건μ λ§λ ν¨ν€μ§κ° μμ΄μ.<br>" | |
| "λ€λ₯Έ 쑰건μΌλ‘ λ€μ μ°Ύμλ³ΌκΉμ?", | |
| sender="bot", chat_container=chat_container, | |
| key="no_package" | |
| ) | |
| st.session_state[step_key] = "companion" | |
| st.rerun() | |
| return | |
| combo_msg = make_companion_age_message(companions, age_group) | |
| header = f"{combo_msg}" | |
| # ν¨ν€μ§ μΉ΄λ μΆλ ₯ | |
| used_phrases = set() | |
| theme_row = travel_df[travel_df["μ¬νμ§"] == place] | |
| raw_theme = theme_row["ν΅ν©ν λ§λͺ "].iloc[0] if not theme_row.empty else None | |
| selected_ui_theme = theme_ui_map.get(raw_theme, (raw_theme,))[0] | |
| title_candidates = theme_title_phrases.get(selected_ui_theme, ["μΆμ²"]) | |
| sampled_titles = random.sample(title_candidates, | |
| k=min(2, len(title_candidates))) | |
| # λ©μμ§ μμ± | |
| pkg_msgs = [header] | |
| for i, (_, row) in enumerate(filtered.iterrows(), 1): | |
| desc, used_phrases = make_top2_description_custom( | |
| row.to_dict(), used_phrases | |
| ) | |
| tags = format_summary_tags_custom(row["μμ½μ 보"]) | |
| title_phrase = (sampled_titles[i-1] if i <= len(sampled_titles) | |
| else random.choice(title_candidates)) | |
| title = f"{city} {title_phrase} ν¨ν€μ§" | |
| url = row.URL | |
| pkg_msgs.append( | |
| f"{i}. <strong>{title}</strong><br>" | |
| f"π Ό {desc}<br>{tags}<br>" | |
| f'<a href="{url}" target="_blank" rel="noopener noreferrer" ' | |
| 'style="text-decoration:none;font-weight:600;color:#009c75;">' | |
| 'π λ°λ‘κ°κΈ° β</a>' | |
| ) | |
| # λ©μμ§ μΆλ ₯ | |
| log_and_render( | |
| "<br><br>".join(pkg_msgs), | |
| sender="bot", | |
| chat_container=chat_container, | |
| key=f"pkg_bundle_{random.randint(1,999999)}" | |
| ) | |
| # μΈμ μ 리 | |
| st.session_state["package_rendered"] = True | |
| st.session_state[step_key] = "package_end" | |
| show_llm_inline() # νλκ·Έλ§ ON (rerun μμ) | |
| # β rerun μμ΄ κ°μ μ¬μ΄ν΄μ μΈλΌμΈ LLM ν¨λμ λ°λ‘ νμ | |
| render_llm_inline_if_open(chat_container) | |
| return | |
| # ββββββββββββββββββ 7) μ’ λ£ λ¨κ³ | |
| elif st.session_state[step_key] == "package_end": | |
| log_and_render("νμνμ€ λ μΈμ λ μ§ λ μ°Ύμμ£ΌμΈμ! βοΈ", | |
| sender="bot", chat_container=chat_container, | |
| key="goodbye") | |
| show_llm_inline() | |
| render_llm_inline_if_open(chat_container) | |
| return | |
| # βββββββββββββββββββββββββββββββββββββ unknown λͺ¨λ | |
| def unknown_ui(country, city, chat_container, log_and_render): | |
| """unknown λͺ¨λ(μμ§ DBμ μλ λλΌΒ·λμμΌ λ μλ΄) μ μ© UI & λ‘μ§""" | |
| # μλ΄ λ©μμ§ | |
| if city: | |
| msg = (f"π μ£μ‘ν΄μ. ν΄λΉ <strong>{city}</strong>μ μ¬νμ§λ " | |
| "μμ§ μ€λΉ μ€μ΄μμ.<br> λΉ λ₯Έ μμΌ μμ μ λ°μ΄νΈν κ²μ!") | |
| elif country: | |
| msg = (f"π μ£μ‘ν΄μ. ν΄λΉ <strong>{country}</strong>μ μ¬νμ§λ " | |
| "μμ§ μ€λΉ μ€μ΄μμ.<br> λΉ λ₯Έ μμΌ μμ μ λ°μ΄νΈν κ²μ!") | |
| else: | |
| msg = "π μ£μ‘ν΄μ. ν΄λΉ μ¬νμ§λ μμ§ μ€λΉ μ€μ΄μμ." | |
| with chat_container: | |
| log_and_render( | |
| f"{msg}", | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="unknown_dest" | |
| ) | |
| # def _get_active_step_key(): | |
| # mode = st.session_state.get("mode", "unknown") | |
| # mapping = { | |
| # "region": "region_step", | |
| # "intent": "intent_step", | |
| # "emotion": "emotion_step", | |
| # "theme_selection": "theme_step", | |
| # "place_selection": "place_step", | |
| # "user_info_input": "user_info_step", | |
| # } | |
| # # λ§€νμ μμΌλ©΄ κ³΅μ© ν€λ‘ | |
| # return mapping.get(mode, "flow_step") | |
| # βββββββββββββββββββββββββββββββββββββ μ±λ΄ νΈμΆ | |
| def main(): | |
| init_session() | |
| chat_container = st.container() | |
| if not _ollama_healthcheck(): | |
| st.stop() | |
| # β νμ€ν¬λ¦°μΌ λλ§ μ‘°κΈ° λ¦¬ν΄ | |
| if st.session_state.get("llm_mode") and not st.session_state.get("llm_inline", False): | |
| render_llm_followup(chat_container, inline=False) | |
| return | |
| # ποΈ λ§νμ /νμ μ΅μ (β’, β£) | |
| st.sidebar.subheader("βοΈ λν νμ") | |
| st.sidebar.selectbox("ν λ§", ["νΌμ€νμΉμ€", "μ€μΉ΄μ΄λΈλ£¨", "ν¬λ¦¬λ―Έμ€νΈ"], key="bubble_theme") | |
| st.sidebar.toggle("νμμ€ν¬ν νμ", value=False, key="show_time") | |
| # with st.sidebar.expander("DEBUG steps", expanded=False): | |
| # st.write("mode:", st.session_state.get("mode")) | |
| # st.write("step_key:", cur_step_key) | |
| # st.write("state:", st.session_state.get(cur_step_key)) | |
| # β νμ ν¨κ³Ό on/off ν κΈ (κΈ°λ³Έ ON) | |
| st.sidebar.toggle("νμ ν¨κ³Ό", value=False, key="typewriter_on") | |
| if "chat_log" in st.session_state and st.session_state.chat_log: | |
| replay_log(chat_container) | |
| # βββββ greeting λ©μμ§ μΆλ ₯ | |
| if not st.session_state.get("greeting_rendered", False): | |
| greeting_message = ( | |
| "μλ νμΈμ. <strong>λͺ¨μ(MoAi)</strong>μ λλ€.π€<br><br>" | |
| "μμ¦ μ΄λ€ μ¬νμ΄ λ μ€λ₯΄μΈμ?<br>""λͺ¨μκ° λ± λ§λ μ¬νμ§λ₯Ό μ°Ύμλ릴κ²μ." | |
| ) | |
| log_and_render( | |
| greeting_message, | |
| sender="bot", | |
| chat_container=chat_container, | |
| key="greeting" | |
| ) | |
| st.session_state["greeting_rendered"] = True | |
| # βββββ μ¬μ©μ μ λ ₯ & μΆμ² μμ | |
| # 1) μ¬μ©μ μ λ ₯ | |
| user_input = st.text_input( | |
| "μ λ ₯μ°½", # λΉμ΄μμ§ μμ λΌλ²¨(μ κ·Όμ± ν보) | |
| placeholder="ex)'μμ¦ νλ§μ΄ νμν΄μ', 'κ°μ‘± μ¬ν μ΄λκ° μ’μκΉμ?'", | |
| key="user_input", | |
| label_visibility="collapsed", # νλ©΄μμ μ¨κΉ | |
| disabled=st.session_state.get("llm_inline", False) | |
| ) | |
| user_input_key = "last_user_input" | |
| select_keys = ["intent_chip_selected", "region_chip_selected", "emotion_chip_selected", "theme_chip_selected"] | |
| # 1-1) βμ§μ§ μλ‘ μ λ ₯β κ°μ§ | |
| prev = st.session_state.get(user_input_key, "") | |
| if user_input and user_input != prev: | |
| for k in select_keys: | |
| st.session_state.pop(k, None) | |
| st.session_state[user_input_key] = user_input | |
| st.session_state["user_input_rendered"] = False | |
| # step μ΄κΈ°ν | |
| st.session_state["region_step"] = "recommend" | |
| st.rerun() | |
| # 1-2) μ¬μ©μ λ©μμ§ ν λ²λ§ λ λλ§ | |
| if user_input and not st.session_state.get("user_input_rendered", False): | |
| log_and_render( | |
| user_input, | |
| sender="user", | |
| chat_container = chat_container, | |
| key=f"user_input_{user_input}" | |
| ) | |
| st.session_state["user_input_rendered"] = True | |
| if user_input: | |
| # 1) μ λΉμ© λ¨κ³: μμΉ/μλ λ¨Όμ | |
| country_filter, city_filter, loc_mode = detect_location_filter(user_input) | |
| intent, intent_score = detect_intent(user_input) | |
| # μ¬μ΄λλ°μμ μκ³κ°μ μΈ μ μκ² νλ€λ©΄, μμΌλ©΄ 0.70 κΈ°λ³Έ | |
| threshold = st.session_state.get("intent_threshold", 0.70) | |
| # 2) λͺ¨λ κ²°μ : μ§μ νμ β intent νμ β unknown β (κ·Έ μΈ) emotion | |
| if loc_mode == "region": | |
| mode = "region" | |
| top_emotions, emotion_groups = [], [] | |
| elif intent_score >= threshold: | |
| mode = "intent" | |
| top_emotions, emotion_groups = [], [] | |
| elif loc_mode == "unknown": | |
| mode = "unknown" | |
| top_emotions, emotion_groups = [], [] | |
| else: | |
| mode = "emotion" | |
| # 3) κ³ λΉμ© λ¨κ³: μ λ§ νμν λλ§ κ°μ±(BERT) μ€ν | |
| # with st.spinner("κ°μ λΆμ μ€..."): # UX μνμλ©΄ μ€νΌλ μΆκ° | |
| top_emotions, emotion_groups = analyze_emotion(user_input) | |
| # 4) λͺ¨λλ³ λΆκΈ° (νμν κ³μ°λ§ μν) | |
| if mode == "region": | |
| region_ui( | |
| travel_df, | |
| external_score_df, | |
| festival_df, | |
| weather_df, | |
| package_df, | |
| country_filter, | |
| city_filter, | |
| chat_container, | |
| log_and_render | |
| ) | |
| return | |
| elif mode == "intent": | |
| intent_ui( | |
| travel_df, | |
| external_score_df, | |
| festival_df, | |
| weather_df, | |
| package_df, | |
| country_filter, | |
| city_filter, | |
| chat_container, | |
| intent, | |
| log_and_render | |
| ) | |
| return | |
| elif mode == "unknown": | |
| unknown_ui( | |
| country_filter, | |
| city_filter, | |
| chat_container, | |
| log_and_render | |
| ) | |
| return | |
| else: # emotion | |
| # emotion λͺ¨λμμλ§ ν λ§ μΆμΆ (λΆνμν κ³μ° λ°©μ§) | |
| candidate_themes = extract_themes( | |
| emotion_groups, | |
| intent, | |
| force_mode=False # intent νμ μΌμ΄μ€κ° μλλΌλ©΄ False | |
| ) | |
| emotion_ui( | |
| travel_df, | |
| external_score_df, | |
| festival_df, | |
| weather_df, | |
| package_df, | |
| country_filter, | |
| city_filter, | |
| chat_container, | |
| candidate_themes, | |
| intent, | |
| emotion_groups, | |
| top_emotions, | |
| log_and_render | |
| ) | |
| if __name__ == "__main__": | |
| main() | |
| #cmd μ λ ₯-> cd "νμΌ μμΉ κ²½λ‘ λ³΅λΆ" | |
| #ex(C:\Users\gayoung\Desktop\multi\0514 - project\06 - streamlit ν μ€νΈ\test) | |
| #cmd μ λ ₯ -> streamlit run app.py | |