Spaces:
Sleeping
Sleeping
| 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, | |
| ) |