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