Add timepicker and mood, level

#2
Files changed (1) hide show
  1. src/streamlit_app.py +272 -216
src/streamlit_app.py CHANGED
@@ -1,216 +1,272 @@
1
- import streamlit as st
2
- from supabase import create_client
3
- from datetime import datetime
4
- from zoneinfo import ZoneInfo
5
- from supabase_auth.errors import AuthApiError
6
- import time
7
- import os
8
- # ========== CONFIG ==========
9
- st.set_page_config(
10
- page_title="Personal Diary",
11
- page_icon="📔",
12
- layout="wide"
13
- )
14
-
15
- SUPABASE_URL = os.environ.get("SUPABASE_URL")
16
- SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
17
-
18
- supabase = create_client(
19
- SUPABASE_URL,
20
- SUPABASE_KEY
21
- )
22
-
23
- # ========== STYLE ==========
24
- st.markdown("""
25
- <style>
26
- .diary-card {
27
- padding: 1rem;
28
- border-radius: 12px;
29
- background-color: #f8f9fa;
30
- margin-bottom: 1rem;
31
- }
32
- .small-text {
33
- font-size: 0.8rem;
34
- color: #6c757d;
35
- }
36
- </style>
37
- """, unsafe_allow_html=True)
38
-
39
- def signup(email, password):
40
- return supabase.auth.sign_up({
41
- "email": email,
42
- "password": password
43
- })
44
-
45
-
46
- # ========== HEADER ==========
47
- st.title("📔 Personal Diary")
48
- st.caption("Ghi chép suy nghĩ mỗi ngày – phiên bản demo")
49
-
50
- # ========== LOGIN ==========
51
- if "user" not in st.session_state:
52
- st.subheader("🔐 Tài khoản")
53
-
54
- tab_login, tab_signup = st.tabs(["Đăng nhập", "Đăng ký"])
55
-
56
- # ===== LOGIN =====
57
- with tab_login:
58
- email = st.text_input("Email", key="login_email")
59
- password = st.text_input("Mật khẩu", type="password", key="login_pw")
60
-
61
- if st.button("➡️ Đăng nhập", use_container_width=True):
62
- res = supabase.auth.sign_in_with_password({
63
- "email": email,
64
- "password": password
65
- })
66
-
67
- if res.user:
68
- st.session_state.user = res.user
69
- st.success("Đăng nhập thành công")
70
- st.rerun()
71
- else:
72
- st.error("Sai email hoặc mật khẩu")
73
-
74
- # ===== SIGNUP =====
75
- with tab_signup:
76
- email = st.text_input("Email đăng ký", key="signup_email")
77
- password = st.text_input(
78
- "Mật khẩu (tối thiểu 6 ký tự)",
79
- type="password",
80
- key="signup_pw"
81
- )
82
-
83
- # init cooldown
84
- if "signup_cooldown_until" not in st.session_state:
85
- st.session_state.signup_cooldown_until = 0
86
-
87
- now = time.time()
88
- cooldown_left = int(st.session_state.signup_cooldown_until - now)
89
-
90
- if cooldown_left > 0:
91
- st.warning(f"⏳ Vui lòng thử lại sau {cooldown_left} giây")
92
-
93
- signup_disabled = cooldown_left > 0
94
-
95
- if st.button(
96
- "🆕 Tạo tài khoản",
97
- use_container_width=True,
98
- disabled=signup_disabled
99
- ):
100
- try:
101
- res = signup(email, password)
102
-
103
- if res.user:
104
- st.session_state.user = res.user
105
- st.success("Đăng ký thành công 🎉")
106
- st.rerun()
107
- else:
108
- st.error("Không thể đăng ")
109
-
110
- except AuthApiError as e:
111
- msg = str(e)
112
-
113
- # Bắt lỗi rate limit 60s
114
- if "only request this after" in msg:
115
- # Mặc định 60s nếu parse không được
116
- wait_seconds = 60
117
-
118
- # cố gắng parse số giây từ message
119
- import re
120
-
121
- match = re.search(r"after (\d+) seconds", msg)
122
- if match:
123
- wait_seconds = int(match.group(1))
124
-
125
- st.session_state.signup_cooldown_until = time.time() + wait_seconds
126
- st.error(f"🚫 Bạn đã thử quá nhiều lần. Vui lòng đợi {wait_seconds} giây rồi thử lại.")
127
- st.rerun()
128
- else:
129
- st.error("Lỗi đăng ký: " + msg)
130
-
131
-
132
- # ========== MAIN APP ==========
133
- else:
134
- user = st.session_state.user
135
-
136
- # Top bar
137
- top_left, top_right = st.columns([4, 1])
138
- with top_left:
139
- st.markdown(f"👋 Xin chào **{user.email}**")
140
- with top_right:
141
- if st.button("🚪 Đăng xuất", use_container_width=True):
142
- st.session_state.clear()
143
- st.rerun()
144
-
145
- st.divider()
146
-
147
- # Main layout
148
- left, right = st.columns([2, 3])
149
-
150
- # ===== LEFT: WRITE =====
151
- with left:
152
- st.subheader("✍️ Viết nhật ký")
153
-
154
- content = st.text_area(
155
- "Hôm nay bạn nghĩ gì?",
156
- height=250,
157
- placeholder="Viết suy nghĩ của bạn ở đây..."
158
- )
159
-
160
- if st.button("💾 Lưu nhật ký", use_container_width=True):
161
- if content.strip():
162
- supabase.table("journals").insert({
163
- "user_id": user.id,
164
- "content": content,
165
- "created_at": datetime.utcnow().isoformat()
166
- }).execute()
167
-
168
- st.success("Đã lưu nhật ký ✨")
169
- st.rerun()
170
- else:
171
- st.warning("Nội dung đang trống")
172
-
173
- # ===== RIGHT: LIST =====
174
- with right:
175
- st.subheader("📜 Nhật ký của bạn")
176
-
177
- data = supabase.table("journals") \
178
- .select("*") \
179
- .eq("user_id", user.id) \
180
- .order("created_at", desc=True) \
181
- .execute()
182
-
183
- if not data.data:
184
- st.info("Chưa có nhật ký nào")
185
- else:
186
- for row in data.data:
187
- # Format ngày cho gọn
188
- created_dt = datetime.fromisoformat(
189
- row["created_at"].replace("Z", "+00:00")
190
- ).astimezone(ZoneInfo("Asia/Ho_Chi_Minh"))
191
-
192
- date_label = created_dt.strftime("%d/%m/%Y – %H:%M")
193
-
194
- # Dropdown theo ngày
195
- with st.expander(f"📅 {date_label}", expanded=False):
196
-
197
- new_content = st.text_area(
198
- "Nội dung",
199
- row["content"],
200
- key=row["id"],
201
- height=120
202
- )
203
-
204
- col_u, col_d = st.columns(2)
205
-
206
- if col_u.button("✏️ Cập nhật", key=f"u{row['id']}"):
207
- supabase.table("journals").update({
208
- "content": new_content,
209
- "updated_at": datetime.utcnow().isoformat()
210
- }).eq("id", row["id"]).execute()
211
- st.rerun()
212
-
213
- if col_d.button("🗑️ Xoá", key=f"d{row['id']}"):
214
- supabase.table("journals").delete() \
215
- .eq("id", row["id"]).execute()
216
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from supabase import create_client
3
+ from datetime import datetime
4
+ from zoneinfo import ZoneInfo
5
+ from supabase_auth.errors import AuthApiError
6
+ import time
7
+ import os
8
+
9
+ mood_options = ["😡 Giận dữ", "😢 Buồn bã", "😐 Bình thường", "😊 Vui vẻ", "🤩 Hào hứng"]
10
+
11
+ # ========== CONFIG ==========
12
+ st.set_page_config(
13
+ page_title="Personal Diary",
14
+ page_icon="📔",
15
+ layout="wide"
16
+ )
17
+
18
+ SUPABASE_URL = os.environ.get("SUPABASE_URL")
19
+ SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
20
+
21
+ supabase = create_client(
22
+ SUPABASE_URL,
23
+ SUPABASE_KEY
24
+ )
25
+
26
+ # ========== STYLE ==========
27
+ st.markdown("""
28
+ <style>
29
+ .diary-card {
30
+ padding: 1rem;
31
+ border-radius: 12px;
32
+ background-color: #f8f9fa;
33
+ margin-bottom: 1rem;
34
+ }
35
+ .small-text {
36
+ font-size: 0.8rem;
37
+ color: #6c757d;
38
+ }
39
+ </style>
40
+ """, unsafe_allow_html=True)
41
+
42
+ def signup(email, password):
43
+ return supabase.auth.sign_up({
44
+ "email": email,
45
+ "password": password
46
+ })
47
+
48
+
49
+ # ========== HEADER ==========
50
+ st.title("📔 Personal Diary")
51
+ st.caption("Ghi chép suy nghĩ mỗi ngày – phiên bản demo")
52
+
53
+ # ========== LOGIN ==========
54
+ if "user" not in st.session_state:
55
+ st.subheader("🔐 Tài khoản")
56
+
57
+ tab_login, tab_signup = st.tabs(["Đăng nhập", "Đăng ký"])
58
+
59
+ # ===== LOGIN =====
60
+ with tab_login:
61
+ email = st.text_input("Email", key="login_email")
62
+ password = st.text_input("Mật khẩu", type="password", key="login_pw")
63
+
64
+ if st.button("➡️ Đăng nhập", use_container_width=True):
65
+ res = supabase.auth.sign_in_with_password({
66
+ "email": email,
67
+ "password": password
68
+ })
69
+
70
+ if res.user:
71
+ st.session_state.user = res.user
72
+ st.success("Đăng nhập thành công")
73
+ st.rerun()
74
+ else:
75
+ st.error("Sai email hoặc mật khẩu")
76
+
77
+ # ===== SIGNUP =====
78
+ with tab_signup:
79
+ email = st.text_input("Email đăng ký", key="signup_email")
80
+ password = st.text_input(
81
+ "Mật khẩu (tối thiểu 6 ký tự)",
82
+ type="password",
83
+ key="signup_pw"
84
+ )
85
+
86
+ # init cooldown
87
+ if "signup_cooldown_until" not in st.session_state:
88
+ st.session_state.signup_cooldown_until = 0
89
+
90
+ now = time.time()
91
+ cooldown_left = int(st.session_state.signup_cooldown_until - now)
92
+
93
+ if cooldown_left > 0:
94
+ st.warning(f"⏳ Vui lòng thử lại sau {cooldown_left} giây")
95
+
96
+ signup_disabled = cooldown_left > 0
97
+
98
+ if st.button(
99
+ "🆕 Tạo tài khoản",
100
+ use_container_width=True,
101
+ disabled=signup_disabled
102
+ ):
103
+ try:
104
+ res = signup(email, password)
105
+
106
+ if res.user:
107
+ st.session_state.user = res.user
108
+ st.success("Đăng thành công 🎉")
109
+ st.rerun()
110
+ else:
111
+ st.error("Không thể đăng ký")
112
+
113
+ except AuthApiError as e:
114
+ msg = str(e)
115
+
116
+ # Bắt lỗi rate limit 60s
117
+ if "only request this after" in msg:
118
+ # Mặc định 60s nếu parse không được
119
+ wait_seconds = 60
120
+
121
+ # cố gắng parse số giây từ message
122
+ import re
123
+
124
+ match = re.search(r"after (\d+) seconds", msg)
125
+ if match:
126
+ wait_seconds = int(match.group(1))
127
+
128
+ st.session_state.signup_cooldown_until = time.time() + wait_seconds
129
+ st.error(f"🚫 Bạn đã thử quá nhiều lần. Vui lòng đợi {wait_seconds} giây rồi thử lại.")
130
+ st.rerun()
131
+ else:
132
+ st.error("Lỗi đăng ký: " + msg)
133
+
134
+
135
+ # ========== MAIN APP ==========
136
+ else:
137
+ user = st.session_state.user
138
+
139
+ # Top bar
140
+ top_left, top_right = st.columns([4, 1])
141
+ with top_left:
142
+ st.markdown(f"👋 Xin chào **{user.email}**")
143
+ with top_right:
144
+ if st.button("🚪 Đăng xuất", use_container_width=True):
145
+ st.session_state.clear()
146
+ st.rerun()
147
+
148
+ st.divider()
149
+
150
+ # Main layout
151
+ left, right = st.columns([2, 3])
152
+
153
+ # ===== LEFT: WRITE =====
154
+ with left:
155
+ st.subheader("✍️ Viết nhật ")
156
+
157
+ # 1. Chọn Thời gian
158
+ col_date, col_time = st.columns(2)
159
+ with col_date:
160
+ d = st.date_input("Chọn ngày", datetime.now())
161
+ with col_time:
162
+ t = st.time_input("Chọn giờ", datetime.now().time())
163
+
164
+ selected_dt = combine(d, t)
165
+
166
+ # 2. Nhập Cảm xúc và Cấp độ
167
+ col_mood, col_level = st.columns([2, 3]) # Chia tỷ lệ cột cho cân đối
168
+ with col_mood:
169
+ mood = st.selectbox(
170
+ "Cảm xúc",
171
+ mood_options,
172
+ index=2 # Mặc định chọn "Bình thường"
173
+ )
174
+ with col_level:
175
+ level = st.select_slider(
176
+ "Cấp độ",
177
+ options=[1, 2, 3, 4, 5],
178
+ value=3 # Mặc định cấp độ trung bình
179
+ )
180
+
181
+ # 3. Nội dung nhật ký
182
+ content = st.text_area(
183
+ "Hôm nay bạn nghĩ gì?",
184
+ height=250,
185
+ placeholder="Viết suy nghĩ của bạn ở đây..."
186
+ )
187
+
188
+ if st.button("💾 Lưu nhật ký", use_container_width=True):
189
+ if content.strip():
190
+ supabase.table("journals").insert({
191
+ "user_id": user.id,
192
+ "content": content,
193
+ "mood": mood,
194
+ "mood_level": level,
195
+ "created_at": selected_dt.isoformat()
196
+ }).execute()
197
+
198
+ st.success("Đã lưu nhật ký ✨")
199
+ st.rerun()
200
+ else:
201
+ st.warning("Nội dung đang trống")
202
+
203
+ # ===== RIGHT: LIST =====
204
+ with right:
205
+ st.subheader("📜 Nhật ký của bạn")
206
+
207
+ data = supabase.table("journals") \
208
+ .select("*") \
209
+ .eq("user_id", user.id) \
210
+ .order("created_at", desc=True) \
211
+ .execute()
212
+
213
+ if not data.data:
214
+ st.info("Chưa có nhật ký nào")
215
+ else:
216
+ for row in data.data:
217
+ # Format ngày cho gọn
218
+ created_dt = datetime.fromisoformat(
219
+ row["created_at"].replace("Z", "+00:00")
220
+ ).astimezone(ZoneInfo("Asia/Ho_Chi_Minh"))
221
+
222
+ date_label = created_dt.strftime("%d/%m/%Y – %H:%M")
223
+
224
+ # Dropdown theo ngày
225
+ with st.expander(f"📅 {date_label}", expanded=False):
226
+
227
+ # Tạo 2 cột để sửa Mood và Level
228
+ col_m, col_l = st.columns(2)
229
+
230
+ try:
231
+ old_mood_idx = mood_options.index(row["mood"])
232
+ except:
233
+ old_mood_idx = 2 # Mặc định là Bình thường nếu có lỗi
234
+
235
+ with col_m:
236
+ new_mood = st.selectbox(
237
+ "Cảm xúc",
238
+ mood_options,
239
+ index=old_mood_idx,
240
+ key=f"mood_{row['id']}"
241
+ )
242
+
243
+ with col_l:
244
+ new_level = st.select_slider(
245
+ "Cấp độ",
246
+ options=[1, 2, 3, 4, 5],
247
+ value=row.get("mood_level", 3),
248
+ key=f"level_{row['id']}"
249
+ )
250
+
251
+ new_content = st.text_area(
252
+ "Nội dung",
253
+ row["content"],
254
+ key=row["id"],
255
+ height=120
256
+ )
257
+
258
+ col_u, col_d = st.columns(2)
259
+
260
+ if col_u.button("✏️ Cập nhật", key=f"u{row['id']}"):
261
+ supabase.table("journals").update({
262
+ "content": new_content,
263
+ "mood": new_mood,
264
+ "mood_level": new_level,
265
+ "updated_at": datetime.utcnow().isoformat()
266
+ }).eq("id", row["id"]).execute()
267
+ st.rerun()
268
+
269
+ if col_d.button("🗑️ Xoá", key=f"d{row['id']}"):
270
+ supabase.table("journals").delete() \
271
+ .eq("id", row["id"]).execute()
272
+ st.rerun()