import streamlit as st import streamlit.components.v1 as components import re import uuid import pandas as pd import time from zoneinfo import ZoneInfo from datetime import datetime # 타임스탬프용 # ────────────────── 말풍선 생성 함수 # 색상 정의 PRIMARY_USER = "#e2f6e8" PRIMARY_BOT = "#f6f6f6" #f5f5f5 # ③ 말풍선 테마 팔레트 & 헬퍼 THEMES = { "피스타치오": {"user": "#C6E0D6", "bot": "#FFFFFF", "accent": "#0B8A5A"}, "스카이블루": {"user": "#C8D9E6", "bot": "#FFFFFF", "accent": "#5D768B"}, "크리미오트": {"user": "#E6DAC8", "bot": "#FFFFFF", "accent": "#A48D78"}, } def _get_colors(): theme = st.session_state.get("bubble_theme", "피스타치오") return THEMES.get(theme, THEMES["피스타치오"]) def render_message( message: str, sender: str = "bot", chips: list[str] | None = None, key: str | None = None, *, animated: bool = False, # 타자 효과 ON/OFF speed_cps: int = 40, # 초당 글자 수 by_word: bool = False, # 단어 단위 출력 ) -> str | None: import re, time # ③ 테마 값 읽기 palette = _get_colors() # show_time = st.session_state.get("show_time", False) and sender == "bot" show_time = bool(st.session_state.get("show_time", False)) color = palette["user"] if sender == "user" else palette["bot"] align = "right" if sender == "user" else "left" pad = "10px 14px" fsz = "13px" message = str(message).rstrip() if show_time: try: tz = st.session_state.get("tz", "Asia/Seoul") # 기본 KST ts_text = datetime.now(ZoneInfo(tz)).strftime("%H:%M") except Exception: ts_text = datetime.now().strftime("%H:%M") else: ts_text = "" # ⬅️ 토글 off면 시간 문자열 비우기 # 공통 풍선 래퍼 # ✅ 카톡 스타일: 시간은 말풍선 '밖' (왼쪽: 봇=시각+버블, 오른쪽: 유저=버블+시각) def _wrap(html_inner: str, ts_text_local: str = ts_text): bubble = ( f'''{html_inner}''' ) if ts_text_local: if sender == "user": # 사용자: 시간(좌) + 버블 ts = ( f'''{ts_text_local}''' ) inner = ts + bubble else: # 봇: 버블 + 시간(우) ts = ( f'''{ts_text_local}''' ) inner = bubble + ts else: inner = bubble row_align = "flex-end" if sender == "user" else "flex-start" return ( f'''
{inner}
''' ) if not animated: st.markdown(_wrap(message), unsafe_allow_html=True) else: ph = st.empty() buf = "" segments = re.split(r'(<[^>]+>)', message) delay = max(0.005, 1.0 / max(1, speed_cps)) for seg in segments: if not seg: continue if seg.startswith("<") and seg.endswith(">"): buf += seg ph.markdown(_wrap(buf), unsafe_allow_html=True) else: if by_word or st.session_state.get("type_by_word", False): for w in seg.split(" "): buf = (buf + " " + w).strip() ph.markdown(_wrap(buf), unsafe_allow_html=True) time.sleep(delay * 5) else: for ch in seg: buf += ch ph.markdown(_wrap(buf), unsafe_allow_html=True) time.sleep(delay) if chips: prefix = f"{key or 'chips'}_{abs(hash(message))}" clicked = render_chip_buttons(chips, key_prefix=prefix) return clicked return None # ────────────────── 칩버튼 생성 함수 def render_chip_buttons(options, key_prefix="chip", selected_value=None): def slugify(text): return re.sub(r"[^a-zA-Z0-9]+", "-", str(text)).strip("-").lower() or "empty" session_key = f"{key_prefix}_selected" selected_value = st.session_state.get(session_key) # 스타일 적용 st.markdown(f""" """, unsafe_allow_html=True) clicked_val = None #cols = st.columns(len(options)) for idx, opt in enumerate(options): if opt is None or (isinstance(opt, float) and pd.isna(opt)) or str(opt).strip()=="": continue is_selected = (opt == selected_value) is_refresh_btn = "다른 여행지 보기" in str(opt) disabled = (opt == selected_value) and not is_refresh_btn label = f"{opt}" if is_selected else opt # stable key safe_opt = slugify(opt) stable_key = f"{key_prefix}_{idx}_{safe_opt}" if st.button(label, key=stable_key, disabled=disabled): clicked_val = opt return clicked_val # ────────────────── 메시지 리플레이 함수 def replay_log(chat_container=None): with chat_container: for sender, msg in st.session_state.chat_log: render_message(msg, sender=sender) # ────────────────── 메시지 로깅&생성 함수 def log_and_render( msg, sender, chat_container=None, key=None, chips=None, *, animated: bool | None = None, speed_cps: int = 45, by_word: bool = False, ): # 중복 방지 sent_once = st.session_state.setdefault("sent_once", {}) if key and sent_once.get(key): return if key: sent_once[key] = True if st.session_state.chat_log and st.session_state.chat_log[-1] == (sender, msg): return # 로그 저장(리플레이는 정적표시) st.session_state.chat_log.append((sender, msg)) # 기본 정책: 봇 메시지는 타자 효과, 유저 메시지는 즉시 표시 if animated is None: animated = (sender == "bot") and st.session_state.get("typewriter_on", True) with chat_container: return render_message( msg, sender=sender, chips=chips, key=key, animated=animated, speed_cps=speed_cps, by_word=by_word, )