pililover commited on
Commit
97b7267
·
1 Parent(s): e1e34c6

First commit

Browse files
src/app.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from utils import initialize_firebase, load_css
3
+ from auth import (
4
+ verify_firebase_token,
5
+ register_user_to_mongo,
6
+ get_user_profile,
7
+ )
8
+
9
+ # ==== Cấu hình trang và tải CSS ====
10
+ st.set_page_config(page_title="Đăng nhập - Stock Insights", page_icon="🔮", layout="centered")
11
+
12
+ auth_fb = initialize_firebase()
13
+
14
+ # Khởi tạo page mặc định
15
+ if "page" not in st.session_state:
16
+ st.session_state.page = "report"
17
+
18
+ # ==== Giao diện Đăng nhập / Đăng ký ====
19
+ if "uid" not in st.session_state:
20
+ # Set layout về centered cho trang đăng nhập
21
+
22
+ st.markdown("<h1>Stock Insights 🔮</h1>", unsafe_allow_html=True)
23
+ st.markdown("<p class='auth-subheader'>Chào mừng! Vui lòng đăng nhập hoặc đăng ký.</p>", unsafe_allow_html=True)
24
+ load_css()
25
+
26
+ login_tab, register_tab = st.tabs(["✨ Đăng nhập", "📝 Đăng ký"])
27
+
28
+ # --- FORM ĐĂNG NHẬP ---
29
+ with login_tab:
30
+ email_login = st.text_input("Email", key="email_login", placeholder="you@example.com")
31
+ password_login = st.text_input("Mật khẩu", type="password", key="password_login", placeholder="••••••••")
32
+ st.markdown("<div style='margin-top: 1rem;'></div>", unsafe_allow_html=True)
33
+ if st.button("Đăng nhập", key="login_btn", use_container_width=True):
34
+ if not email_login or not password_login:
35
+ st.warning("Vui lòng nhập đầy đủ email và mật khẩu.")
36
+ else:
37
+ try:
38
+ user = auth_fb.sign_in_with_email_and_password(email_login, password_login)
39
+ id_token = user["idToken"]
40
+ info = verify_firebase_token(id_token)
41
+ if info:
42
+ st.session_state["uid"] = info["uid"]
43
+ st.session_state["user_email"] = info.get("email", "")
44
+ profile = get_user_profile(info["uid"])
45
+ st.session_state["user_name"] = profile.get("user_name", "") if profile else ""
46
+ st.rerun()
47
+ except Exception as e:
48
+ st.error("Sai email hoặc mật khẩu!")
49
+
50
+ # --- FORM ĐĂNG KÝ ---
51
+ with register_tab:
52
+ with st.form("registration_form", clear_on_submit=True):
53
+ user_name_reg = st.text_input("Tên người dùng", placeholder="Nguyen Van A")
54
+ email_reg = st.text_input("Email", placeholder="you@example.com")
55
+ password_reg = st.text_input("Mật khẩu", type="password", placeholder="••••••••")
56
+ password_confirm_reg = st.text_input("Nhập lại mật khẩu", type="password", placeholder="••••••••")
57
+ st.markdown("<div style='margin-top: 1rem;'></div>", unsafe_allow_html=True)
58
+ submitted = st.form_submit_button("Đăng ký", use_container_width=True)
59
+
60
+ if submitted:
61
+ # ... (Giữ nguyên logic đăng ký) ...
62
+ st.success("Đăng ký thành công! Giờ bạn có thể đăng nhập.")
63
+
64
+ # ==== Giao diện chính sau khi đăng nhập ====
65
+ else:
66
+ # --- Thanh điều hướng tùy chỉnh ---
67
+ st.markdown('<div class="nav-container">', unsafe_allow_html=True)
68
+
69
+ # Sử dụng st.columns để đặt các nút cạnh nhau
70
+ col1, col2, col3 = st.columns(3)
71
+
72
+ with col1:
73
+ is_active = st.session_state.page == "report"
74
+ if st.button("📊 Báo cáo", use_container_width=True, key="nav_report"):
75
+ st.session_state.page = "report"
76
+ # Reset trạng thái báo cáo cũ
77
+ st.session_state.pop("selected_report", None)
78
+ st.session_state["show_form"] = True
79
+ if is_active:
80
+ st.markdown('<style>button[data-testid="stButton-nav_report"] {border-color: #30cfd0; color: #ffffff; box-shadow: 0 0 15px rgba(48, 207, 208, 0.4);}</style>', unsafe_allow_html=True)
81
+
82
+ with col2:
83
+ is_active = st.session_state.page == "history"
84
+ st.button("📜 Lịch sử", use_container_width=True, on_click=lambda: st.session_state.update(page="history"),
85
+ type="secondary" if not is_active else "primary",
86
+ key="nav_history")
87
+ if is_active:
88
+ st.markdown('<style>button[data-testid="stButton-nav_history"] {border-color: #30cfd0; color: #ffffff; box-shadow: 0 0 15px rgba(48, 207, 208, 0.4);}</style>', unsafe_allow_html=True)
89
+
90
+ with col3:
91
+ is_active = st.session_state.page == "account"
92
+ st.button("⚙️ Tài khoản", use_container_width=True, on_click=lambda: st.session_state.update(page="account"),
93
+ type="secondary" if not is_active else "primary",
94
+ key="nav_account")
95
+ if is_active:
96
+ st.markdown('<style>button[data-testid="stButton-nav_account"] {border-color: #30cfd0; color: #ffffff; box-shadow: 0 0 15px rgba(48, 207, 208, 0.4);}</style>', unsafe_allow_html=True)
97
+
98
+ st.markdown('</div>', unsafe_allow_html=True)
99
+
100
+ # --- Hiển thị nội dung trang tương ứng ---
101
+ if st.session_state.page == "report":
102
+ from pages.page_report import main as report_main
103
+ report_main()
104
+ elif st.session_state.page == "history":
105
+ from pages.page_history import main as history_main
106
+ history_main()
107
+ elif st.session_state.page == "account":
108
+ from pages.page_account import main as account_main
109
+ account_main()
src/auth.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ import firebase_admin
4
+ from firebase_admin import credentials, auth as firebase_auth
5
+ from pymongo import MongoClient
6
+ from bson.binary import Binary
7
+ import json
8
+
9
+ load_dotenv()
10
+
11
+ # cred_path = os.getenv("FIREBASE_ADMIN_KEY")
12
+ # if not cred_path:
13
+ # raise ValueError("FIREBASE_ADMIN_KEY chưa được khai báo trong .env hoặc đường dẫn bị sai!")
14
+ # if not firebase_admin._apps:
15
+ # cred = credentials.Certificate(cred_path)
16
+ # firebase_admin.initialize_app(cred)
17
+
18
+ firebase_admin_json = os.getenv("FIREBASE_ADMIN_JSON")
19
+ if firebase_admin_json:
20
+ with open("serviceAccountKey.json", "w") as f:
21
+ f.write(firebase_admin_json)
22
+ cred_path = "serviceAccountKey.json"
23
+ else:
24
+ cred_path = os.getenv("FIREBASE_ADMIN_KEY")
25
+
26
+ if not cred_path:
27
+ raise ValueError("FIREBASE_ADMIN_KEY chưa được khai báo trong .env hoặc đường dẫn bị sai!")
28
+ if not firebase_admin._apps:
29
+ cred = credentials.Certificate(cred_path)
30
+ firebase_admin.initialize_app(cred)
31
+
32
+ mongo_uri = os.getenv("MONGO_URI")
33
+ mongo_dbname = os.getenv("MONGO_DBNAME")
34
+
35
+
36
+ def get_mongo_collection():
37
+ client = MongoClient(mongo_uri)
38
+ db = client[mongo_dbname]
39
+ return db["users"]
40
+
41
+
42
+ def verify_firebase_token(id_token):
43
+ try:
44
+ decoded = firebase_auth.verify_id_token(id_token)
45
+ return decoded
46
+ except Exception as e:
47
+ print("Token verify fail:", e)
48
+ return None
49
+
50
+
51
+ def register_user_to_mongo(uid, email, user_name):
52
+ users = get_mongo_collection()
53
+
54
+ if not users.find_one({"uid": uid}):
55
+ print("Registering new user:", uid, email, user_name)
56
+ users.insert_one({"uid": uid, "email": email, "user_name": user_name})
57
+ return True
58
+
59
+
60
+ def save_avatar(uid, file_bytes):
61
+ users = get_mongo_collection()
62
+ users.update_one(
63
+ {"uid": uid}, {"$set": {"avatar_blob": Binary(file_bytes)}}, upsert=True
64
+ )
65
+
66
+
67
+ def get_avatar_blob(uid):
68
+ users = get_mongo_collection()
69
+ user = users.find_one({"uid": uid}, {"avatar_blob": 1})
70
+ return user.get("avatar_blob") if user and "avatar_blob" in user else None
71
+
72
+
73
+ def get_user_profile(uid):
74
+ users = get_mongo_collection()
75
+ return users.find_one({"uid": uid})
76
+
77
+
78
+ def update_username_in_mongo(uid, new_username):
79
+ users = get_mongo_collection()
80
+ users.update_one({"uid": uid}, {"$set": {"user_name": new_username}})
src/pages/__pycache__/page_account.cpython-311.pyc ADDED
Binary file (4.36 kB). View file
 
src/pages/__pycache__/page_history.cpython-311.pyc ADDED
Binary file (2.86 kB). View file
 
src/pages/__pycache__/page_report.cpython-311.pyc ADDED
Binary file (7.27 kB). View file
 
src/pages/page_account.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from auth import (
3
+ get_user_profile,
4
+ update_username_in_mongo,
5
+ save_avatar,
6
+ get_avatar_blob,
7
+ )
8
+ from utils import load_css, render_avatar
9
+
10
+ def main():
11
+ load_css()
12
+
13
+ if "uid" not in st.session_state:
14
+ st.warning("Vui lòng đăng nhập để sử dụng tính năng này.")
15
+ st.page_link("StockInsights.py", label="Về trang Đăng nhập", icon="🏠")
16
+ st.stop()
17
+
18
+
19
+ # ==== Giao diện trang tài khoản ====
20
+ st.markdown("<h2>Thông tin Tài khoản</h2>", unsafe_allow_html=True)
21
+
22
+ _, col_center, _ = st.columns([1, 2, 1])
23
+
24
+ with col_center:
25
+ profile = get_user_profile(st.session_state["uid"])
26
+
27
+ render_avatar(st.session_state["uid"], st, get_avatar_blob)
28
+
29
+ st.html(f"<h3 style='text-align: center; color: #ffffff; margin-bottom: 0.25rem; font-weight: 600;'>{profile.get('user_name', '')}</h3>")
30
+ st.html(f"<p style='text-align: center; color: #94a3b8; margin-bottom: 2rem;'>{st.session_state['user_email']}</p>")
31
+
32
+ with st.expander("⚙️ Chỉnh sửa thông tin"):
33
+ new_username = st.text_input("Tên người dùng mới", value=profile.get("user_name", ""), key="edit_username")
34
+ if st.button("Lưu tên mới", use_container_width=True, key="save_name"):
35
+ if new_username:
36
+ update_username_in_mongo(st.session_state["uid"], new_username)
37
+ st.success("Đã cập nhật tên người dùng!")
38
+ st.rerun()
39
+ else:
40
+ st.warning("Tên người dùng không được để trống.")
41
+
42
+ st.markdown("<hr style='margin: 1rem 0; border-color: rgba(100, 116, 139, 0.3);'>", unsafe_allow_html=True)
43
+
44
+ file = st.file_uploader("Thay đổi ảnh đại diện (png, jpg, jpeg)", type=["png", "jpg", "jpeg"])
45
+ if st.button("Lưu avatar", use_container_width=True, key="save_avatar"):
46
+ if file:
47
+ save_avatar(st.session_state["uid"], file.read())
48
+ st.success("Đã lưu avatar!")
49
+ st.rerun()
50
+ else:
51
+ st.warning("Hãy chọn ảnh trước khi lưu.")
52
+
53
+ st.markdown("<div style='margin-top: 1rem;'></div>", unsafe_allow_html=True)
54
+
55
+ if st.button("Đăng xuất", use_container_width=True, key="logout_btn"):
56
+ st.session_state.clear()
57
+ st.rerun()
58
+ #st.switch_page("StockInsights.py")
src/pages/page_history.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ # st.set_page_config(page_title="Lịch sử - Stock Insights", page_icon="🔮", layout="centered")
3
+
4
+ import os
5
+ from pymongo import MongoClient
6
+ from report_generator import show_report
7
+ from utils import load_css
8
+
9
+ def main():
10
+ mongo_uri = os.getenv("MONGO_URI")
11
+ mongo_dbname = os.getenv("MONGO_DBNAME")
12
+
13
+ def get_db():
14
+ client = MongoClient(mongo_uri)
15
+ db = client[mongo_dbname]
16
+ return db["reports"]
17
+
18
+ reports_history = get_db()
19
+
20
+ load_css()
21
+
22
+ if "uid" not in st.session_state:
23
+ st.warning("Vui lòng đăng nhập để sử dụng tính năng này.")
24
+ st.page_link("StockInsights.py", label="Về trang Đăng nhập", icon="🏠")
25
+ st.stop()
26
+
27
+ # ==== Giao diện trang lịch sử ====
28
+ st.markdown("<h2>Các báo cáo đã tạo</h2>", unsafe_allow_html=True)
29
+ search_code = st.text_input("Tìm theo mã cổ phiếu", "").upper()
30
+
31
+ query = {"uid": st.session_state["uid"]}
32
+ if search_code:
33
+ query["report_data.stock_code"] = {"$regex": f"^{search_code}", "$options": "i"}
34
+
35
+ history = list(reports_history.find(query).sort("created_at", -1))
36
+
37
+ if history:
38
+ for item in history:
39
+ stock_code = item["report_data"]["stock_code"]
40
+ period = item["report_data"]["report_period"]
41
+ with st.expander(f"{stock_code}: {period}"):
42
+ show_report(item["report_data"], item["summary"], item["report_data"]["stock_code"])
43
+ else:
44
+ st.info("Chưa có báo cáo nào cả")
src/pages/page_report.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ from pymongo import MongoClient
4
+ import pandas as pd
5
+ from datetime import datetime
6
+ from report_generator import generate_stock_report, show_report
7
+ from utils import load_css, call_genai_summary
8
+
9
+ def main():
10
+ mongo_uri = os.getenv("MONGO_URI")
11
+ mongo_dbname = os.getenv("MONGO_DBNAME")
12
+
13
+ def get_db():
14
+ client = MongoClient(mongo_uri)
15
+ db = client[mongo_dbname]
16
+ return db["reports"]
17
+
18
+ reports_history = get_db()
19
+
20
+
21
+ load_css()
22
+
23
+ if "uid" not in st.session_state:
24
+ st.warning("Vui lòng đăng nhập để sử dụng tính năng này.")
25
+ st.page_link("StockInsights.py", label="Về trang Đăng nhập", icon="🏠")
26
+ st.stop()
27
+
28
+ # ==== Giao diện trang báo cáo ===
29
+ st.markdown("<h2>Báo cáo Cổ phiếu Thông minh</h2>", unsafe_allow_html=True)
30
+
31
+ # st.markdown("<div class='report-container'>", unsafe_allow_html=True)
32
+
33
+ col_history, col_main = st.columns([1, 3])
34
+
35
+ # ===== LEFT: Report History =====
36
+ with col_history:
37
+ st.markdown("<h4>Báo cáo đã xem</h4>", unsafe_allow_html=True)
38
+
39
+ # Initialize history list
40
+ if "reports_history_list" not in st.session_state:
41
+ query = {"uid": st.session_state["uid"]}
42
+ st.session_state["reports_history_list"] = list(
43
+ reports_history.find(query).sort("created_at", -1)
44
+ )
45
+
46
+ history = st.session_state["reports_history_list"]
47
+
48
+ if not history:
49
+ st.info("Chưa có báo cáo nào được lưu.")
50
+ else:
51
+ for idx, report in enumerate(history):
52
+ if st.button(
53
+ f"{report['report_data'].get('stock_code', 'N/A')} ({report['report_data'].get('report_period', 'N/A')})",
54
+ key=f"history_btn_{idx}"
55
+ ):
56
+ # Update the selected report in session state
57
+ st.session_state["selected_report"] = report
58
+ st.session_state["show_form"] = False # Hide form when viewing history
59
+
60
+ # ===== RIGHT: Report View=====
61
+ with col_main:
62
+ if st.session_state.get("selected_report") and not st.session_state.get("show_form", False):
63
+ selected = st.session_state["selected_report"]
64
+ show_report(selected["report_data"], selected["summary"], selected["report_data"]["stock_code"])
65
+
66
+ # Tạo báo cáo mới
67
+ if st.button("Tạo báo cáo mới", key="new_report_btn"):
68
+ st.session_state["show_form"] = True
69
+ st.session_state.pop("selected_report", None)
70
+ st.rerun()
71
+
72
+ else:
73
+ with st.form("report_form"):
74
+ stock_code_input = st.text_input(
75
+ "Nhập mã cổ phiếu (ví dụ: VIC, HPG...)", value="HPG").upper()
76
+
77
+ col_start, col_end = st.columns(2)
78
+ with col_start:
79
+ start_date = st.date_input(
80
+ "Từ ngày", value=pd.to_datetime("2025-05-01"))
81
+ with col_end:
82
+ end_date = st.date_input("Đến ngày", value=datetime.now())
83
+
84
+ submitted = st.form_submit_button("Tạo báo cáo", use_container_width=True)
85
+
86
+ if submitted and stock_code_input:
87
+ with st.spinner(f'Đang tổng hợp và phân tích dữ liệu cho mã {stock_code_input}...'):
88
+ report_data = generate_stock_report(
89
+ stock_code_input, (str(start_date), str(end_date)))
90
+
91
+ if report_data and (report_data["overall_sentiment"]["positive_mentions"] > 0 or report_data["overall_sentiment"]["negative_mentions"] > 0):
92
+ summary = call_genai_summary(
93
+ report_data, stock_code_input, (str(start_date), str(end_date)))
94
+ else:
95
+ summary = f"Không tìm thấy đủ dữ liệu nổi bật cho mã **{stock_code_input}** trong khoảng thời gian đã chọn để tạo tóm tắt AI."
96
+
97
+ # Save to MongoDB
98
+ inserted_id = reports_history.insert_one({
99
+ "uid": st.session_state["uid"],
100
+ "report_data": report_data,
101
+ "summary": summary,
102
+ "created_at": datetime.utcnow()
103
+ }).inserted_id
104
+
105
+ # Add new report to top of history
106
+ new_report = {
107
+ "_id": inserted_id,
108
+ "report_data": report_data,
109
+ "summary": summary,
110
+ "created_at": datetime.utcnow()
111
+ }
112
+ st.session_state["reports_history_list"].insert(0, new_report)
113
+
114
+ # Show new report
115
+ st.session_state["selected_report"] = new_report
116
+ st.session_state["show_form"] = False
117
+ st.rerun()
src/report_generator.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import pandas as pd
3
+ import os
4
+ from datetime import datetime
5
+ import streamlit as st
6
+
7
+ # def get_db_path():
8
+ # db_path = "../database/stock_insights.db"
9
+ # if not os.path.exists(db_path) and os.path.exists("/tmp/stock_insights.db"):
10
+ # db_path = "/tmp/stock_insights.db"
11
+ # return db_path
12
+
13
+ # download from Hugging Face dataset
14
+ def ensure_db():
15
+ db_path = os.path.join("database", "stock_insights.db")
16
+ if not os.path.exists(db_path):
17
+ from huggingface_hub import hf_hub_download
18
+ os.makedirs("database", exist_ok=True)
19
+ hf_hub_download(
20
+ repo_id="PuppetLover/stock_insights",
21
+ filename="stock_insights.db",
22
+ repo_type="dataset",
23
+ local_dir="database",
24
+ local_dir_use_symlinks=False,
25
+ )
26
+
27
+ ensure_db()
28
+
29
+ def generate_stock_report(stock_code, time_period):
30
+
31
+ start_date, end_date = time_period
32
+ today = datetime.now().date()
33
+ db_path = os.path.join("database", "stock_insights.db")
34
+
35
+ report = {
36
+ "stock_code": stock_code,
37
+ "report_period": f"{start_date} to {end_date}"
38
+ }
39
+
40
+ with sqlite3.connect(db_path) as conn:
41
+ # Tạo bảng tạm relevant_articles
42
+ conn.execute("DROP TABLE IF EXISTS relevant_articles;")
43
+ conn.execute("""
44
+ CREATE TEMP TABLE relevant_articles AS
45
+ SELECT DISTINCT article_id FROM entities
46
+ WHERE entity_text =?
47
+ AND entity_type IN ('STOCK', 'COMPANY')
48
+ AND confidence = 'high'
49
+ AND article_id IN (
50
+ SELECT article_id FROM articles WHERE publish_date BETWEEN ? AND ?
51
+ );
52
+ """, (stock_code, start_date, end_date))
53
+
54
+ # 1. OVERALL SENTIMENT
55
+ q_sentences = """
56
+ SELECT s.sentiment_score, s.sentiment_label, a.publish_date
57
+ FROM sentences s
58
+ JOIN articles a ON s.article_id = a.article_id
59
+ WHERE s.article_id IN (
60
+ SELECT s2.sentence_id FROM sentences s2
61
+ WHERE s2.article_id IN (SELECT article_id FROM relevant_articles)
62
+ )
63
+ AND s.sentiment_score IS NOT NULL;
64
+ """
65
+ df_sent = pd.read_sql_query(q_sentences, conn)
66
+
67
+ if not df_sent.empty:
68
+ df_sent['publish_date'] = pd.to_datetime(df_sent['publish_date']).dt.date
69
+ df_sent['days_ago'] = (today - df_sent['publish_date']).apply(lambda x: x.days)
70
+ df_sent['weight'] = 1 / (df_sent['days_ago'] + 1)
71
+ weighted_score = (df_sent['sentiment_score'] * df_sent['weight']).sum() / df_sent['weight'].sum()
72
+ # Chuẩn hóa nhãn sentiment về lower-case
73
+ df_sent['sentiment_label'] = df_sent['sentiment_label'].str.lower()
74
+ sentiment_counts = df_sent['sentiment_label'].value_counts().to_dict()
75
+ trend = "Tích cực" if weighted_score > 0.1 else "Tiêu cực" if weighted_score < -0.1 else "Trung tính"
76
+ else:
77
+ weighted_score, sentiment_counts, trend = 0.0, {}, "Không có dữ liệu"
78
+
79
+ report["overall_sentiment"] = {
80
+ "score": weighted_score,
81
+ "trend": trend,
82
+ "positive_mentions": sentiment_counts.get("positive", 0),
83
+ "negative_mentions": sentiment_counts.get("negative", 0),
84
+ "neutral_mentions": sentiment_counts.get("neutral", 0)
85
+ }
86
+
87
+ # 2. KEY EVENTS, RISKS, PRICE ACTIONS
88
+ def get_key_entities(entity_type):
89
+ query = f"""
90
+ SELECT
91
+ e.entity_text,
92
+ COUNT(e.entity_id) as count,
93
+ AVG(s.sentiment_score) as avg_sentiment
94
+ FROM entities e
95
+ JOIN sentences s ON e.sentence_id = s.sentence_id
96
+ WHERE e.article_id IN (SELECT article_id FROM relevant_articles)
97
+ AND e.entity_type =?
98
+ GROUP BY e.entity_text
99
+ ORDER BY count DESC
100
+ LIMIT 5;
101
+ """
102
+ df = pd.read_sql_query(query, conn, params=(entity_type,))
103
+ def score_to_label(score):
104
+ if score is None: return "N/A"
105
+ return "Tích cực" if score > 0.1 else "Tiêu cực" if score < -0.1 else "Trung tính"
106
+ df['sentiment'] = df['avg_sentiment'].apply(score_to_label)
107
+ return df.to_dict('records')
108
+
109
+ report["key_events"] = get_key_entities('EVENT')
110
+ report["key_price_actions"] = get_key_entities('PRICE_ACTION')
111
+ report["key_risks_mentioned"] = get_key_entities('RISK')
112
+
113
+ # 3. TOP RELATED ENTITIES
114
+ q_related = """
115
+ SELECT e.entity_type, e.entity_text
116
+ FROM entities e
117
+ WHERE e.article_id IN (SELECT article_id FROM relevant_articles)
118
+ AND e.entity_text!=?
119
+ AND e.entity_type IN ('STOCK', 'COMPANY', 'PERSON');
120
+ """
121
+ df_related = pd.read_sql_query(q_related, conn, params=(stock_code,))
122
+ top_related = {}
123
+ if not df_related.empty:
124
+ for etype in ['STOCK', 'COMPANY', 'PERSON']:
125
+ top_related[etype.lower() + 's'] = df_related[df_related['entity_type'] == etype]['entity_text'].value_counts().head(3).index.tolist()
126
+ report["top_related_entities"] = top_related
127
+
128
+ # 4. SOURCE ARTICLES
129
+ q_articles = """
130
+ SELECT a.title, a.source_url, s.sentiment_label
131
+ FROM articles a
132
+ JOIN sentences s ON a.article_id = s.article_id
133
+ WHERE a.article_id IN (SELECT article_id FROM relevant_articles)
134
+ GROUP BY a.article_id
135
+ ORDER BY a.publish_date DESC
136
+ LIMIT 5;
137
+ """
138
+ df_articles = pd.read_sql_query(q_articles, conn)
139
+ report["source_articles"] = df_articles.to_dict('records')
140
+
141
+ return report
142
+
143
+ # --- HIỂN THỊ BÁO CÁO ---
144
+ def show_report(report_data, summary, stock_code_input):
145
+ st.markdown(
146
+ f"<h3 style='text-align: center; color: #30cfd0; margin-top:2rem;'>Báo cáo Phân tích cho {report_data.get('stock_code', stock_code_input)}</h3>", unsafe_allow_html=True)
147
+ st.markdown(
148
+ f"<p style='text-align: center; color: #94a3b8;'>Giai đoạn: {report_data.get('report_period', 'N/A')}</p>", unsafe_allow_html=True)
149
+
150
+ st.markdown("#### 🤖 Tóm tắt từ AI")
151
+ st.info(summary)
152
+
153
+ # Tổng quan cảm xúc
154
+ st.markdown("#### 📊 Tổng quan Cảm xúc")
155
+ sentiment = report_data['overall_sentiment']
156
+ score = sentiment['score']
157
+ trend_color = "normal"
158
+ if sentiment['trend'] == "Tích cực":
159
+ trend_color = "normal"
160
+ if sentiment['trend'] == "Tiêu cực":
161
+ trend_color = "inverse"
162
+
163
+ st.metric(
164
+ label="Điểm Cảm xúc (có trọng số thời gian)",
165
+ value=f"{score:.2f}" if score is not None else "N/A",
166
+ delta=sentiment['trend'],
167
+ delta_color=trend_color
168
+ )
169
+
170
+ col1, col2, col3 = st.columns(3)
171
+ col1.metric("👍 Tích cực", sentiment['positive_mentions'])
172
+ col2.metric("👎 Tiêu cực", sentiment['negative_mentions'])
173
+ col3.metric("😐 Trung tính", sentiment['neutral_mentions'])
174
+
175
+ # Các bảng chi tiết
176
+ st.markdown("---")
177
+
178
+ col_events, col_risks = st.columns(2)
179
+ with col_events:
180
+ st.markdown("#### ⚡ Sự kiện Nổi bật")
181
+ if report_data["key_events"]:
182
+ # Kiểm tra key thực tế
183
+ df_events = pd.DataFrame(report_data["key_events"])
184
+ if 'avg_sentiment' in df_events.columns:
185
+ df_events = df_events.rename(
186
+ columns={'entity_text': 'Sự kiện', 'avg_sentiment': 'Sentiment'})
187
+ show_cols = ['Sự kiện', 'count', 'Sentiment']
188
+ elif 'sentiment' in df_events.columns:
189
+ df_events = df_events.rename(
190
+ columns={'entity_text': 'Sự kiện'})
191
+ show_cols = ['Sự kiện', 'count', 'sentiment']
192
+ else:
193
+ df_events = df_events.rename(
194
+ columns={'entity_text': 'Sự kiện'})
195
+ show_cols = ['Sự kiện', 'count']
196
+ st.dataframe(df_events[show_cols], use_container_width=True)
197
+ else:
198
+ st.write("Không có sự kiện nổi bật.")
199
+
200
+ with col_risks:
201
+ st.markdown("#### ⚠️ Rủi ro được đề cập")
202
+ if report_data["key_risks_mentioned"]:
203
+ df_risks = pd.DataFrame(report_data["key_risks_mentioned"])
204
+ if 'avg_sentiment' in df_risks.columns:
205
+ df_risks = df_risks.rename(
206
+ columns={'entity_text': 'Rủi ro', 'avg_sentiment': 'Sentiment'})
207
+ show_cols = ['Rủi ro', 'count', 'Sentiment']
208
+ elif 'sentiment' in df_risks.columns:
209
+ df_risks = df_risks.rename(
210
+ columns={'entity_text': 'Rủi ro'})
211
+ show_cols = ['Rủi ro', 'count', 'sentiment']
212
+ else:
213
+ df_risks = df_risks.rename(
214
+ columns={'entity_text': 'Rủi ro'})
215
+ show_cols = ['Rủi ro', 'count']
216
+ st.dataframe(df_risks[show_cols], use_container_width=True)
217
+ else:
218
+ st.write("Không có rủi ro nổi bật.")
219
+
220
+ st.markdown("#### 📈 Hành động Giá Chính")
221
+ if report_data["key_price_actions"]:
222
+ df_price = pd.DataFrame(report_data["key_price_actions"])
223
+ if 'avg_sentiment' in df_price.columns:
224
+ df_price = df_price.rename(
225
+ columns={'entity_text': 'Hành động giá', 'avg_sentiment': 'Sentiment'})
226
+ show_cols = ['Hành động giá', 'count', 'Sentiment']
227
+ elif 'sentiment' in df_price.columns:
228
+ df_price = df_price.rename(
229
+ columns={'entity_text': 'Hành động giá'})
230
+ show_cols = ['Hành động giá', 'count', 'sentiment']
231
+ else:
232
+ df_price = df_price.rename(
233
+ columns={'entity_text': 'Hành động giá'})
234
+ show_cols = ['Hành động giá', 'count']
235
+ st.dataframe(df_price[show_cols], use_container_width=True)
236
+ else:
237
+ st.write("Không có hành động giá nổi bật.")
238
+
239
+ # Thực thể liên quan
240
+ st.markdown("---")
241
+ st.markdown("#### 🔗 Các Thực thể Liên quan nhiều nhất")
242
+ related = report_data['top_related_entities']
243
+ if any(related.values()):
244
+ for etype, entities in related.items():
245
+ if entities:
246
+ st.markdown(
247
+ f"**{etype.replace('_', ' ').title()}:** {', '.join(entities)}")
248
+ else:
249
+ st.write("Không tìm thấy thực thể liên quan nổi bật.")
250
+
251
+ # Nguồn bài viết
252
+ st.markdown("---")
253
+ st.markdown("#### 📰 Nguồn Bài viết Tham khảo")
254
+ if report_data["source_articles"]:
255
+ for article in report_data["source_articles"]:
256
+ st.markdown(
257
+ f"- [{article['title']}]({article['source_url']}) - *Cảm xúc: {article['sentiment_label']}*")
258
+ else:
259
+ st.write("Không có bài viết nào trong khoảng thời gian này.")
260
+
261
+ st.markdown("</div>", unsafe_allow_html=True)
src/requirements.txt ADDED
Binary file (7.24 kB). View file
 
src/streamlit_app.py DELETED
@@ -1,40 +0,0 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/utils.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pyrebase
3
+ import os
4
+ import base64
5
+ import json
6
+ import requests
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ # ==== Firebase Config & Initialization ====
12
+ def initialize_firebase():
13
+ """Khởi tạo và trả về các đối tượng Firebase. Sử dụng singleton pattern."""
14
+ if "firebase_app" not in st.session_state:
15
+ firebase_config = {
16
+ "apiKey": os.getenv("FIREBASE_API_KEY"),
17
+ "authDomain": os.getenv("FIREBASE_AUTH_DOMAIN"),
18
+ "projectId": os.getenv("FIREBASE_PROJECT_ID"),
19
+ "storageBucket": os.getenv("FIREBASE_STORAGE_BUCKET"),
20
+ "messagingSenderId": os.getenv("FIREBASE_MESSAGING_SENDER_ID"),
21
+ "appId": os.getenv("FIREBASE_APP_ID"),
22
+ "databaseURL": os.getenv("FIREBASE_DATABASE_URL", ""),
23
+ }
24
+ st.session_state.firebase_app = pyrebase.initialize_app(firebase_config)
25
+
26
+ auth_fb = st.session_state.firebase_app.auth()
27
+ return auth_fb
28
+
29
+ # ==== CSS Dùng chung ====
30
+ def load_css():
31
+ """Tải CSS theme Cyberpunk Neon."""
32
+ st.markdown("""
33
+ <style>
34
+ /* === Hide default Streamlit elements === */
35
+ section[data-testid="stSidebar"] {display: none;}
36
+ header {visibility: hidden;}
37
+
38
+ /* === Main container styling === */
39
+ .stApp {
40
+ background-color: #0d1117;
41
+ color: #c9d1d9;
42
+ }
43
+
44
+ /* === Main content area styling === */
45
+ .main .block-container {
46
+ max-width: 1100px; /* Rộng hơn cho layout mới */
47
+ padding: 1rem 1.5rem;
48
+ }
49
+ .not-logged-in .main .block-container {
50
+ max-width: 450px;
51
+ }
52
+
53
+ /* === Custom Navigation Bar === */
54
+ .nav-container {
55
+ display: flex;
56
+ justify-content: center;
57
+ gap: 1rem;
58
+ margin-bottom: 2rem;
59
+ padding: 0.5rem;
60
+ background-color: rgba(30, 41, 59, 0.5);
61
+ border-radius: 12px;
62
+ border: 1px solid rgba(100, 116, 139, 0.3);
63
+ }
64
+ .nav-container .stButton>button {
65
+ background: transparent;
66
+ border: 2px solid transparent;
67
+ transition: all 0.3s ease;
68
+ font-weight: 600;
69
+ color: #94a3b8;
70
+ }
71
+ .nav-container .stButton>button:hover {
72
+ color: #ffffff;
73
+ border-color: rgba(48, 207, 208, 0.5);
74
+ box-shadow: none;
75
+ transform: none; /* FIX: Vô hiệu hóa hiệu ứng transform cho nút nav */
76
+ }
77
+ /* Style for the ACTIVE button */
78
+ .nav-container .stButton>button.active-nav-button {
79
+ color: #ffffff;
80
+ border-color: #30cfd0;
81
+ box-shadow: 0 0 15px rgba(48, 207, 208, 0.4);
82
+ }
83
+
84
+ /* === Card styling with "frosted glass" effect === */
85
+ div[data-testid="stTabs-panel"], .report-container {
86
+ background-color: rgba(30, 41, 59, 0.5);
87
+ backdrop-filter: blur(12px);
88
+ border-radius: 16px;
89
+ padding: 2.5rem;
90
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
91
+ border: 1px solid rgba(100, 116, 139, 0.3);
92
+ }
93
+
94
+ /* === General Button styling === */
95
+ .stButton>button {
96
+ border-radius: 8px;
97
+ border: 1px solid #30cfd0;
98
+ padding: 12px 20px;
99
+ color: white;
100
+ background: linear-gradient(90deg, #30cfd0, #330867);
101
+ transition: all 0.3s ease-in-out;
102
+ font-weight: 600;
103
+ }
104
+ .stButton>button:hover {
105
+ box-shadow: 0 0 20px #30cfd0;
106
+ transform: translateY(-2px);
107
+ }
108
+
109
+ /* === Input fields styling === */
110
+ .stTextInput label, .stDateInput label {
111
+ color: #c9d1d9 !important;
112
+ font-weight: 600;
113
+ margin-bottom: 0.5rem;
114
+ }
115
+ .stTextInput>div>div>input, .stDateInput>div>div>input {
116
+ background-color: rgba(15, 23, 42, 0.5);
117
+ border: 1px solid #64748b;
118
+ border-radius: 8px;
119
+ padding: 12px;
120
+ color: #ffffff;
121
+ }
122
+ .stTextInput>div>div>input:focus, .stDateInput>div>div>input:focus {
123
+ border-color: #30cfd0;
124
+ box-shadow: 0 0 10px rgba(48, 207, 208, 0.5);
125
+ }
126
+
127
+ /* === Header styling === */
128
+ h1, h2 {
129
+ text-align: center;
130
+ color: #ffffff;
131
+ font-weight: 700;
132
+ letter-spacing: 1px;
133
+ text-shadow: 0 0 10px rgba(48, 207, 208, 0.5);
134
+ }
135
+ </style>
136
+ """, unsafe_allow_html=True)
137
+
138
+ # ==== Hàm Render Avatar ====
139
+ def render_avatar(uid, container, get_avatar_blob_func):
140
+ avatar_bytes = get_avatar_blob_func(uid)
141
+ if avatar_bytes:
142
+ img_base64 = base64.b64encode(avatar_bytes).decode()
143
+ 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);">'
144
+ else:
145
+ avatar_html = """
146
+ <div style='border-radius:50%; background:linear-gradient(135deg, #30cfd0, #330867);
147
+ width:120px; height:120px; display:flex; align-items:center; justify-content:center;
148
+ box-shadow:0 0 20px rgba(48, 207, 208, 0.3);'>
149
+ <span style='font-size:3em; color:#fff;'>👤</span>
150
+ </div>
151
+ <div style='margin-top:8px; color:#94a3b8; font-size:0.9em;'>Chưa có avatar</div>
152
+ """
153
+ container.html(f"<div style='display:flex; flex-direction:column; align-items:center; margin-bottom: 1rem;'>{avatar_html}</div>")
154
+
155
+
156
+ # ==== Hàm gọi Gemini API ====
157
+ def call_genai_summary(report_data, stock_code, time_period):
158
+ api_key = os.getenv("GEMINI_API_KEY")
159
+ if not api_key:
160
+ st.error("Vui lòng cung cấp GEMINI_API_KEY trong file .env")
161
+ return "Lỗi: Chưa cấu hình API Key."
162
+
163
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
164
+ prompt = f"""
165
+ 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:
166
+ {json.dumps(report_data, ensure_ascii=False, indent=2)}
167
+ """
168
+ payload = {"contents": [{"parts": [{"text": prompt}]}]}
169
+
170
+ try:
171
+ response = requests.post(url, json=payload, timeout=45)
172
+ response.raise_for_status()
173
+ data = response.json()
174
+ return data["candidates"][0]["content"]["parts"][0]["text"]
175
+ except Exception as e:
176
+ return f"Lỗi khi gọi Gemini API: {e}"