code-slicer commited on
Commit
f4e1c38
·
verified ·
1 Parent(s): bee0046

Upload css(타이핑).py

Browse files
Files changed (1) hide show
  1. css(타이핑).py +186 -0
css(타이핑).py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import streamlit.components.v1 as components
3
+ import re
4
+ import uuid
5
+ import pandas as pd
6
+ import time
7
+
8
+ # ────────────────── 말풍선 생성 함수
9
+ # 색상 정의
10
+ PRIMARY_USER = "#e2f6e8"
11
+ PRIMARY_BOT = "#f6f6f6"
12
+
13
+ def render_message(
14
+ message: str,
15
+ sender: str = "bot",
16
+ chips: list[str] | None = None,
17
+ key: str | None = None,
18
+ *,
19
+ animated: bool = False, # ← 추가: 타자 효과 ON/OFF
20
+ speed_cps: int = 40, # ← 추가: 초당 글자 수
21
+ by_word: bool = False, # ← 추가: 단어 단위 출력
22
+ ) -> str | None:
23
+ """
24
+ - `message` : 표시할 텍스트 (HTML 허용)
25
+ - `sender` : "user" | "bot"
26
+ - `chips` : 버튼 형태로 보여 줄 문자열 리스트
27
+ - return : 사용자가 클릭한 칩(문자열) 또는 None
28
+ """
29
+ color = PRIMARY_USER if sender == "user" else PRIMARY_BOT
30
+ align = "right" if sender == "user" else "left"
31
+ message = str(message).rstrip()
32
+
33
+ # 공통 풍선 래퍼
34
+ def _wrap(html_inner: str) -> str:
35
+ return (
36
+ f'''<div style="text-align:{align}; margin:6px 0;">'''
37
+ f'''<p style="font-size:13px;"></p>'''
38
+ f'''<span style="background:{color}; padding:10px 14px; border-radius:12px;'''
39
+ f'''display:inline-block; max-width:80%; font-size:13px; line-height:1.45;'''
40
+ f'''word-break:break-word;">{html_inner}</span></div>'''
41
+ )
42
+
43
+ if not animated:
44
+ st.markdown(_wrap(message), unsafe_allow_html=True)
45
+ else:
46
+ ph = st.empty()
47
+ buf = ""
48
+
49
+ # 태그는 즉시 추가, 텍스트만 점진 출력(태그 반쯤 잘려 보이는 현상 최소화)
50
+ segments = re.split(r'(<[^>]+>)', message)
51
+ delay = max(0.005, 1.0 / max(1, speed_cps))
52
+
53
+ for seg in segments:
54
+ if not seg:
55
+ continue
56
+ if seg.startswith("<") and seg.endswith(">"):
57
+ # 태그는 구조 유지 위해 한 번에 추가
58
+ buf += seg
59
+ ph.markdown(_wrap(buf), unsafe_allow_html=True)
60
+ else:
61
+ if by_word:
62
+ for w in seg.split(" "):
63
+ buf = (buf + " " + w).strip()
64
+ ph.markdown(_wrap(buf), unsafe_allow_html=True)
65
+ time.sleep(delay * 5) # 단어 단위는 조금 더 여유
66
+ else:
67
+ for ch in seg:
68
+ buf += ch
69
+ ph.markdown(_wrap(buf), unsafe_allow_html=True)
70
+ time.sleep(delay)
71
+
72
+ # 칩 버튼
73
+ if chips:
74
+ prefix = f"{key or 'chips'}_{abs(hash(message))}"
75
+ clicked = render_chip_buttons(chips, key_prefix=prefix)
76
+ return clicked
77
+ return None
78
+
79
+ # ────────────────── 칩버튼 생성 함수
80
+ def render_chip_buttons(options, key_prefix="chip", selected_value=None):
81
+ def slugify(text):
82
+ return re.sub(r"[^a-zA-Z0-9]+", "-", str(text)).strip("-").lower() or "empty"
83
+ session_key = f"{key_prefix}_selected"
84
+ selected_value = st.session_state.get(session_key)
85
+
86
+ # 스타일 적용
87
+ st.markdown(f"""
88
+ <style>
89
+ div[data-testid="stHorizontalBlock"]{{
90
+ display:block !important;
91
+ }}
92
+ button[data-testid="stBaseButton-secondary"] {{
93
+ background-color: white;
94
+ border: 1px solid #e3e8e7;
95
+ border-radius: 20px;
96
+ padding: 6px 14px;
97
+ font-size: 14px;
98
+ cursor: pointer;
99
+ transition: 0.2s ease-in-out;
100
+ margin-bottom: -2px;
101
+ width: 230px;
102
+ text-align:center;
103
+ }}
104
+
105
+ button[data-testid="stBaseButton-secondary"]:hover {{
106
+ background-color: #e8f0ef;
107
+ border-color: #009c75;
108
+ color: #009c75;
109
+ }}
110
+ button[data-testid="baseButton-secondary"][disabled]{{
111
+ background-color: white;
112
+ border-color: #009c75; !important;
113
+ color: #009c75; !important;
114
+ }}
115
+ </style>
116
+ """, unsafe_allow_html=True)
117
+
118
+
119
+ clicked_val = None
120
+
121
+ #cols = st.columns(len(options))
122
+ for idx, opt in enumerate(options):
123
+ if opt is None or (isinstance(opt, float) and pd.isna(opt)) or str(opt).strip()=="":
124
+ continue
125
+
126
+ is_selected = (opt == selected_value)
127
+ is_refresh_btn = "다른 여행지 보기" in str(opt)
128
+ disabled = (opt == selected_value) and not is_refresh_btn
129
+
130
+ label = f"{opt}" if is_selected else opt
131
+
132
+ # stable key
133
+ safe_opt = slugify(opt)
134
+ stable_key = f"{key_prefix}_{idx}_{safe_opt}"
135
+
136
+ if st.button(label, key=stable_key, disabled=disabled):
137
+ clicked_val = opt
138
+
139
+ return clicked_val
140
+
141
+
142
+ # ────────────────── 메시지 리플레이 함수
143
+ def replay_log(chat_container=None):
144
+ with chat_container:
145
+ for sender, msg in st.session_state.chat_log:
146
+ render_message(msg, sender=sender)
147
+
148
+
149
+ # ────────────────── 메시지 로깅&생성 함수
150
+ def log_and_render(
151
+ msg,
152
+ sender,
153
+ chat_container=None,
154
+ key=None,
155
+ chips=None,
156
+ *,
157
+ animated: bool | None = None,
158
+ speed_cps: int = 45,
159
+ by_word: bool = False,
160
+ ):
161
+ # 중복 방지
162
+ sent_once = st.session_state.setdefault("sent_once", {})
163
+ if key and sent_once.get(key):
164
+ return
165
+ if key:
166
+ sent_once[key] = True
167
+ if st.session_state.chat_log and st.session_state.chat_log[-1] == (sender, msg):
168
+ return
169
+
170
+ # 로그 저장(리플레이는 정적표시)
171
+ st.session_state.chat_log.append((sender, msg))
172
+
173
+ # 기본 정책: 봇 메시지는 타자 효과, 유저 메시지는 즉시 표시
174
+ if animated is None:
175
+ animated = (sender == "bot") and st.session_state.get("typewriter_on", True)
176
+
177
+ with chat_container:
178
+ return render_message(
179
+ msg,
180
+ sender=sender,
181
+ chips=chips,
182
+ key=key,
183
+ animated=animated,
184
+ speed_cps=speed_cps,
185
+ by_word=by_word,
186
+ )