MOAI / css.py
gagagagagaga93's picture
css.py
2bfcf64 verified
raw
history blame
7.82 kB
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'''<span style="background:{color}; padding:{pad}; border-radius:12px;'''
f'''display:inline-block; max-width:80%; font-size:{fsz}; line-height:1.45;'''
f'''word-break:break-word;">{html_inner}</span>'''
)
if ts_text_local:
if sender == "user":
# μ‚¬μš©μž: μ‹œκ°„(쒌) + 버블
ts = (
f'''<span style="font-size:11px;color:#888;white-space:nowrap;'''
f'''align-self:flex-end;margin:0 2px 2px 0;">{ts_text_local}</span>'''
)
inner = ts + bubble
else:
# 봇: 버블 + μ‹œκ°„(우)
ts = (
f'''<span style="font-size:11px;color:#888;white-space:nowrap;'''
f'''align-self:flex-end;margin:0 0 2px 2px;">{ts_text_local}</span>'''
)
inner = bubble + ts
else:
inner = bubble
row_align = "flex-end" if sender == "user" else "flex-start"
return (
f'''<div style="display:flex;align-items:flex-end;justify-content:{row_align};'''
f'''gap:2px;margin:6px 0;">{inner}</div>'''
)
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"""
<style>
div[data-testid="stHorizontalBlock"]{{
display:block !important;
}}
button[data-testid="stBaseButton-secondary"] {{
background-color: white;
border: 1px solid #e3e8e7;
border-radius: 20px;
padding: 6px 14px;
font-size: 14px;
cursor: pointer;
transition: 0.2s ease-in-out;
margin-bottom: -2px;
width: 230px;
text-align:center;
}}
button[data-testid="stBaseButton-secondary"]:hover {{
background-color: #e8f0ef;
border-color: #009c75;
color: #009c75;
}}
button[data-testid="baseButton-secondary"][disabled]{{
background-color: white;
border-color: #009c75; !important;
color: #009c75; !important;
}}
</style>
""", 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,
)