MOAI / css.py
code-slicer's picture
Update css.py
3e0c8a2 verified
raw
history blame
6.73 kB
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'<div style="font-size:11px;color:#888;margin-top:4px;">{datetime.now().strftime("%H:%M")}</div>'
if show_time else ""
)
# 곡톡 풍선 래퍼
def _wrap(html_inner: str) -> str:
return (
f'''<div style="text-align:{align}; margin:6px 0;">'''
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}{ts_html}</span></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,
)