Spaces:
Running
Running
| import streamlit as st | |
| import time | |
| import os | |
| from supabase import create_client | |
| from datetime import datetime | |
| from zoneinfo import ZoneInfo | |
| from supabase_auth.errors import AuthApiError | |
| from transformers import pipeline | |
| def load_sentiment_model(): | |
| return pipeline( | |
| "sentiment-analysis", | |
| model="wonrax/phobert-base-vietnamese-sentiment" | |
| ) | |
| sentiment_model = load_sentiment_model() | |
| # ========== CONFIG ========== | |
| st.set_page_config( | |
| page_title="Personal Diary", | |
| page_icon="📔", | |
| layout="wide" | |
| ) | |
| SUPABASE_URL = os.environ.get("SUPABASE_URL") | |
| SUPABASE_KEY = os.environ.get("SUPABASE_KEY") | |
| supabase = create_client( | |
| SUPABASE_URL, | |
| SUPABASE_KEY | |
| ) | |
| # ========== STYLE ========== | |
| st.markdown(""" | |
| <style> | |
| .diary-card { | |
| padding: 1rem; | |
| border-radius: 12px; | |
| background-color: #f8f9fa; | |
| margin-bottom: 1rem; | |
| } | |
| .small-text { | |
| font-size: 0.8rem; | |
| color: #6c757d; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def signup(email, password): | |
| return supabase.auth.sign_up({ | |
| "email": email, | |
| "password": password | |
| }) | |
| def analyze_sentiment(text: str) -> str: | |
| result = sentiment_model(text)[0] | |
| return result["label"] # POS / NEG / NEU | |
| # ========== HEADER ========== | |
| st.title("📔 Personal Diary") | |
| st.caption("Ghi chép suy nghĩ mỗi ngày – phiên bản demo") | |
| # ========== LOGIN ========== | |
| if "user" not in st.session_state: | |
| st.subheader("🔐 Tài khoản") | |
| tab_login, tab_signup = st.tabs(["Đăng nhập", "Đăng ký"]) | |
| # ===== LOGIN ===== | |
| with tab_login: | |
| email = st.text_input("Email", key="login_email") | |
| password = st.text_input("Mật khẩu", type="password", key="login_pw") | |
| if st.button("➡️ Đăng nhập", use_container_width=True): | |
| res = supabase.auth.sign_in_with_password({ | |
| "email": email, | |
| "password": password | |
| }) | |
| if res.user: | |
| st.session_state.user = res.user | |
| st.success("Đăng nhập thành công") | |
| st.rerun() | |
| else: | |
| st.error("Sai email hoặc mật khẩu") | |
| # ===== SIGNUP ===== | |
| with tab_signup: | |
| email = st.text_input("Email đăng ký", key="signup_email") | |
| password = st.text_input( | |
| "Mật khẩu (tối thiểu 6 ký tự)", | |
| type="password", | |
| key="signup_pw" | |
| ) | |
| # init cooldown | |
| if "signup_cooldown_until" not in st.session_state: | |
| st.session_state.signup_cooldown_until = 0 | |
| now = time.time() | |
| cooldown_left = int(st.session_state.signup_cooldown_until - now) | |
| if cooldown_left > 0: | |
| st.warning(f"⏳ Vui lòng thử lại sau {cooldown_left} giây") | |
| signup_disabled = cooldown_left > 0 | |
| if st.button( | |
| "🆕 Tạo tài khoản", | |
| use_container_width=True, | |
| disabled=signup_disabled | |
| ): | |
| try: | |
| res = signup(email, password) | |
| if res.user: | |
| st.session_state.user = res.user | |
| st.success("Đăng ký thành công 🎉") | |
| st.rerun() | |
| else: | |
| st.error("Không thể đăng ký") | |
| except AuthApiError as e: | |
| msg = str(e) | |
| # Bắt lỗi rate limit 60s | |
| if "only request this after" in msg: | |
| # Mặc định 60s nếu parse không được | |
| wait_seconds = 60 | |
| # cố gắng parse số giây từ message | |
| import re | |
| match = re.search(r"after (\d+) seconds", msg) | |
| if match: | |
| wait_seconds = int(match.group(1)) | |
| st.session_state.signup_cooldown_until = time.time() + wait_seconds | |
| st.error(f"🚫 Bạn đã thử quá nhiều lần. Vui lòng đợi {wait_seconds} giây rồi thử lại.") | |
| st.rerun() | |
| else: | |
| st.error("Lỗi đăng ký: " + msg) | |
| # ========== MAIN APP ========== | |
| else: | |
| user = st.session_state.user | |
| # Top bar | |
| top_left, top_right = st.columns([4, 1]) | |
| with top_left: | |
| st.markdown(f"👋 Xin chào **{user.email}**") | |
| with top_right: | |
| if st.button("🚪 Đăng xuất", use_container_width=True): | |
| st.session_state.clear() | |
| st.rerun() | |
| st.divider() | |
| # Main layout | |
| left, right = st.columns([2, 3]) | |
| # ===== LEFT: WRITE ===== | |
| with left: | |
| st.subheader("✍️ Viết nhật ký") | |
| content = st.text_area( | |
| "Hôm nay bạn nghĩ gì?", | |
| height=250, | |
| placeholder="Viết suy nghĩ của bạn ở đây..." | |
| ) | |
| if st.button("💾 Lưu nhật ký", use_container_width=True): | |
| if content.strip(): | |
| sentiment = analyze_sentiment(content) | |
| supabase.table("journals").insert({ | |
| "user_id": user.id, | |
| "content": content, | |
| "sentiment": sentiment, | |
| "created_at": datetime.utcnow().isoformat() | |
| }).execute() | |
| st.success("Đã lưu nhật ký ✨") | |
| st.rerun() | |
| else: | |
| st.warning("Nội dung đang trống") | |
| # ===== RIGHT: LIST ===== | |
| with right: | |
| st.subheader("📜 Nhật ký của bạn") | |
| data = supabase.table("journals") \ | |
| .select("*") \ | |
| .eq("user_id", user.id) \ | |
| .order("created_at", desc=True) \ | |
| .execute() | |
| if not data.data: | |
| st.info("Chưa có nhật ký nào") | |
| else: | |
| for row in data.data: | |
| # Format ngày cho gọn | |
| created_dt = datetime.fromisoformat( | |
| row["created_at"].replace("Z", "+00:00") | |
| ).astimezone(ZoneInfo("Asia/Ho_Chi_Minh")) | |
| date_label = created_dt.strftime("%d/%m/%Y – %H:%M") | |
| # Dropdown theo ngày | |
| with st.expander(f"📅 {date_label}", expanded=False): | |
| new_content = st.text_area( | |
| "Nội dung", | |
| row["content"], | |
| key=row["id"], | |
| height=120 | |
| ) | |
| sentiment = row.get("sentiment", "N/A") | |
| emoji_map = { | |
| "Vui vẻ": "😊", | |
| "Buồn": "😔", | |
| "Trung tính": "😐" | |
| } | |
| st.caption(f"Cảm xúc: {emoji_map.get(sentiment, '❓')} {sentiment}") | |
| col_u, col_d = st.columns(2) | |
| if col_u.button("✏️ Cập nhật", key=f"u{row['id']}"): | |
| supabase.table("journals").update({ | |
| "content": new_content, | |
| "updated_at": datetime.utcnow().isoformat() | |
| }).eq("id", row["id"]).execute() | |
| st.rerun() | |
| if col_d.button("🗑️ Xoá", key=f"d{row['id']}"): | |
| supabase.table("journals").delete() \ | |
| .eq("id", row["id"]).execute() | |
| st.rerun() | |