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,
)