MOAI / css.py
code-slicer's picture
css.py (#1)
a3b8d27 verified
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,
)