import streamlit as st import streamlit.components.v1 as components import re import uuid import pandas as pd import time from datetime import datetime # 타임스탬프용 # ────────────────── 말풍선 생성 함수 # 색상 정의 PRIMARY_USER = "#e2f6e8" PRIMARY_BOT = "#f6f6f6" # ③ 말풍선 테마 팔레트 & 헬퍼 THEMES = { "민트": {"user": "#DCFCE7", "bot": "#E0F2FE"}, "라벤더": {"user": "#EDE9FE", "bot": "#E9D5FF"}, "모노": {"user": "#EFEFEF", "bot": "#F5F5F5"}, } 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() dense = st.session_state.get("dense_mode", False) show_time = st.session_state.get("show_time", False) and sender == "bot" # ④ 봇만 시간 표시 color = palette["user"] if sender == "user" else palette["bot"] align = "right" if sender == "user" else "left" pad = "6px 10px" if dense else "10px 14px" # ③ 콤팩트 모드 패딩 fsz = "12px" if dense else "13px" # ③ 콤팩트 모드 폰트 message = str(message).rstrip() # ④ 타임스탬프 HTML ts_html = ( f'
{datetime.now().strftime("%H:%M")}
' if show_time else "" ) # 공통 풍선 래퍼 def _wrap(html_inner: str) -> str: return ( f'''
''' f'''{html_inner}{ts_html}
''' ) 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, )