Spaces:
Sleeping
Sleeping
File size: 6,694 Bytes
97b7267 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
import streamlit as st
import pyrebase
import os
import base64
import json
import requests
from dotenv import load_dotenv
load_dotenv()
# ==== Firebase Config & Initialization ====
def initialize_firebase():
"""Khởi tạo và trả về các đối tượng Firebase. Sử dụng singleton pattern."""
if "firebase_app" not in st.session_state:
firebase_config = {
"apiKey": os.getenv("FIREBASE_API_KEY"),
"authDomain": os.getenv("FIREBASE_AUTH_DOMAIN"),
"projectId": os.getenv("FIREBASE_PROJECT_ID"),
"storageBucket": os.getenv("FIREBASE_STORAGE_BUCKET"),
"messagingSenderId": os.getenv("FIREBASE_MESSAGING_SENDER_ID"),
"appId": os.getenv("FIREBASE_APP_ID"),
"databaseURL": os.getenv("FIREBASE_DATABASE_URL", ""),
}
st.session_state.firebase_app = pyrebase.initialize_app(firebase_config)
auth_fb = st.session_state.firebase_app.auth()
return auth_fb
# ==== CSS Dùng chung ====
def load_css():
"""Tải CSS theme Cyberpunk Neon."""
st.markdown("""
<style>
/* === Hide default Streamlit elements === */
section[data-testid="stSidebar"] {display: none;}
header {visibility: hidden;}
/* === Main container styling === */
.stApp {
background-color: #0d1117;
color: #c9d1d9;
}
/* === Main content area styling === */
.main .block-container {
max-width: 1100px; /* Rộng hơn cho layout mới */
padding: 1rem 1.5rem;
}
.not-logged-in .main .block-container {
max-width: 450px;
}
/* === Custom Navigation Bar === */
.nav-container {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 2rem;
padding: 0.5rem;
background-color: rgba(30, 41, 59, 0.5);
border-radius: 12px;
border: 1px solid rgba(100, 116, 139, 0.3);
}
.nav-container .stButton>button {
background: transparent;
border: 2px solid transparent;
transition: all 0.3s ease;
font-weight: 600;
color: #94a3b8;
}
.nav-container .stButton>button:hover {
color: #ffffff;
border-color: rgba(48, 207, 208, 0.5);
box-shadow: none;
transform: none; /* FIX: Vô hiệu hóa hiệu ứng transform cho nút nav */
}
/* Style for the ACTIVE button */
.nav-container .stButton>button.active-nav-button {
color: #ffffff;
border-color: #30cfd0;
box-shadow: 0 0 15px rgba(48, 207, 208, 0.4);
}
/* === Card styling with "frosted glass" effect === */
div[data-testid="stTabs-panel"], .report-container {
background-color: rgba(30, 41, 59, 0.5);
backdrop-filter: blur(12px);
border-radius: 16px;
padding: 2.5rem;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
border: 1px solid rgba(100, 116, 139, 0.3);
}
/* === General Button styling === */
.stButton>button {
border-radius: 8px;
border: 1px solid #30cfd0;
padding: 12px 20px;
color: white;
background: linear-gradient(90deg, #30cfd0, #330867);
transition: all 0.3s ease-in-out;
font-weight: 600;
}
.stButton>button:hover {
box-shadow: 0 0 20px #30cfd0;
transform: translateY(-2px);
}
/* === Input fields styling === */
.stTextInput label, .stDateInput label {
color: #c9d1d9 !important;
font-weight: 600;
margin-bottom: 0.5rem;
}
.stTextInput>div>div>input, .stDateInput>div>div>input {
background-color: rgba(15, 23, 42, 0.5);
border: 1px solid #64748b;
border-radius: 8px;
padding: 12px;
color: #ffffff;
}
.stTextInput>div>div>input:focus, .stDateInput>div>div>input:focus {
border-color: #30cfd0;
box-shadow: 0 0 10px rgba(48, 207, 208, 0.5);
}
/* === Header styling === */
h1, h2 {
text-align: center;
color: #ffffff;
font-weight: 700;
letter-spacing: 1px;
text-shadow: 0 0 10px rgba(48, 207, 208, 0.5);
}
</style>
""", unsafe_allow_html=True)
# ==== Hàm Render Avatar ====
def render_avatar(uid, container, get_avatar_blob_func):
avatar_bytes = get_avatar_blob_func(uid)
if avatar_bytes:
img_base64 = base64.b64encode(avatar_bytes).decode()
avatar_html = f'<img src="data:image/png;base64,{img_base64}" style="border-radius:50%; border:4px solid #30cfd0; width:120px; height:120px; object-fit:cover; box-shadow:0 0 20px rgba(48, 207, 208, 0.5);">'
else:
avatar_html = """
<div style='border-radius:50%; background:linear-gradient(135deg, #30cfd0, #330867);
width:120px; height:120px; display:flex; align-items:center; justify-content:center;
box-shadow:0 0 20px rgba(48, 207, 208, 0.3);'>
<span style='font-size:3em; color:#fff;'>👤</span>
</div>
<div style='margin-top:8px; color:#94a3b8; font-size:0.9em;'>Chưa có avatar</div>
"""
container.html(f"<div style='display:flex; flex-direction:column; align-items:center; margin-bottom: 1rem;'>{avatar_html}</div>")
# ==== Hàm gọi Gemini API ====
def call_genai_summary(report_data, stock_code, time_period):
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
st.error("Vui lòng cung cấp GEMINI_API_KEY trong file .env")
return "Lỗi: Chưa cấu hình API Key."
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
prompt = f"""
Hãy tóm tắt ngắn gọn, chuyên nghiệp về mã cổ phiếu {stock_code} trong giai đoạn {time_period[0]} đến {time_period[1]} dựa trên dữ liệu JSON sau:
{json.dumps(report_data, ensure_ascii=False, indent=2)}
"""
payload = {"contents": [{"parts": [{"text": prompt}]}]}
try:
response = requests.post(url, json=payload, timeout=45)
response.raise_for_status()
data = response.json()
return data["candidates"][0]["content"]["parts"][0]["text"]
except Exception as e:
return f"Lỗi khi gọi Gemini API: {e}"
|