Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| from datetime import datetime | |
| import os | |
| import uuid | |
| import requests | |
| import json | |
| import time | |
| from functools import lru_cache | |
| # ============================================================================== | |
| # 1. CẤU HÌNH VÀ HẰNG SỐ | |
| # ============================================================================== | |
| # Thiết lập API Keys từ Biến môi trường (Cần đặt trong Hugging Face Secrets) | |
| GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") | |
| VOUCHER_AUTH_TOKEN = os.environ.get("VOUCHER_AUTH_TOKEN", "Token TGHFVZbSBxPoAF-4_al6NU-h47fzcvyQ") | |
| # API Endpoints | |
| GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent" | |
| VOUCHER_API_URL = "https://api.accesstrade.vn/v1/offers_informations" | |
| PRODUCTS_API_URL = "https://api.accesstrade.vn/v2/tiktokshop_product_feeds" | |
| # CSV Files | |
| TRANSACTIONS_CSV = 'transactions.csv' | |
| INVESTMENTS_CSV = 'investments.csv' | |
| # User ID cố định cho mô hình CSV đơn giản | |
| MOCK_USER_ID = 'family_finance_user' | |
| # ============================================================================== | |
| # 2. XỬ LÝ DỮ LIỆU CSV (PANDAS) | |
| # ============================================================================== | |
| def load_data(file_path, default_columns): | |
| """Tải dữ liệu từ CSV hoặc tạo DataFrame mới nếu file không tồn tại.""" | |
| try: | |
| df = pd.read_csv(file_path) | |
| # Đảm bảo các cột ngày tháng được parse đúng cách | |
| if 'date' in df.columns: | |
| df['date'] = pd.to_datetime(df['date']) | |
| if 'purchaseDate' in df.columns: | |
| df['purchaseDate'] = pd.to_datetime(df['purchaseDate']) | |
| # Bổ sung các cột bị thiếu nếu cần | |
| for col in default_columns: | |
| if col not in df.columns: | |
| df[col] = pd.NA | |
| except FileNotFoundError: | |
| df = pd.DataFrame(columns=default_columns) | |
| # Khởi tạo một số dữ liệu mock nếu file không tồn tại | |
| if file_path == TRANSACTIONS_CSV: | |
| mock_data = { | |
| 'id': ['t1', 't2', 't3'], | |
| 'user_id': [MOCK_USER_ID] * 3, | |
| 'type': ['INCOME', 'EXPENSE', 'EXPENSE'], | |
| 'category': ['Lương', 'Ăn uống', 'Giải trí'], | |
| 'amount': [30000000, 5000000, 2000000], | |
| 'date': [datetime(2025, 6, 1), datetime(2025, 6, 5), datetime(2025, 6, 15)], | |
| 'description': ['Lương tháng 6', 'Chi phí ăn uống gia đình', 'Xem phim và du lịch ngắn'], | |
| } | |
| df = pd.DataFrame(mock_data) | |
| elif file_path == INVESTMENTS_CSV: | |
| mock_data = { | |
| 'id': ['i1', 'i2'], | |
| 'user_id': [MOCK_USER_ID] * 2, | |
| 'ticker': ['VNM', 'FTSE'], | |
| 'quantity': [500, 100], | |
| 'averagePrice': [75000, 150000], | |
| 'currentValue': [78000, 145000], | |
| 'purchaseDate': [datetime(2024, 1, 10), datetime(2024, 5, 20)], | |
| } | |
| df = pd.DataFrame(mock_data) | |
| # Sắp xếp theo ngày giảm dần | |
| if 'date' in df.columns: | |
| df = df.sort_values(by='date', ascending=False) | |
| return df | |
| def save_data(df, file_path): | |
| """Lưu DataFrame vào CSV.""" | |
| df.to_csv(file_path, index=False) | |
| # Tải dữ liệu lần đầu | |
| transaction_cols = ['id', 'user_id', 'type', 'category', 'amount', 'date', 'description'] | |
| investment_cols = ['id', 'user_id', 'ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate'] | |
| transactions_df = load_data(TRANSACTIONS_CSV, transaction_cols) | |
| investments_df = load_data(INVESTMENTS_CSV, investment_cols) | |
| # ============================================================================== | |
| # 3. HÀM HỖ TRỢ CHUNG | |
| # ============================================================================== | |
| def format_currency(amount): | |
| """Định dạng tiền tệ VND.""" | |
| if pd.isna(amount): | |
| return "N/A" | |
| return f"{int(amount):,} VND".replace(",", ".") | |
| def format_percentage(value): | |
| """Định dạng phần trăm.""" | |
| if pd.isna(value): | |
| return "N/A" | |
| return f"{value:.2f}%" | |
| def get_current_month_data(df): | |
| """Lọc dữ liệu giao dịch trong tháng hiện tại.""" | |
| if df.empty or 'date' not in df.columns: | |
| return pd.DataFrame(columns=transaction_cols) | |
| today = datetime.now() | |
| start_of_month = datetime(today.year, today.month, 1) | |
| # Lọc giao dịch của người dùng hiện tại (MOCK_USER_ID) | |
| filtered_df = df[df['user_id'] == MOCK_USER_ID] | |
| return filtered_df[ | |
| (filtered_df['date'] >= start_of_month) & | |
| (filtered_df['date'] <= today) | |
| ] | |
| # ============================================================================== | |
| # 4. LOGIC TABS | |
| # ============================================================================== | |
| def update_dataframes(): | |
| """Tải lại DataFrames từ CSV (được gọi sau mỗi thao tác ghi).""" | |
| global transactions_df, investments_df | |
| transactions_df = load_data(TRANSACTIONS_CSV, transaction_cols) | |
| investments_df = load_data(INVESTMENTS_CSV, investment_cols) | |
| return transactions_df, investments_df | |
| # --- TAB DASHBOARD --- | |
| def fetch_market_news(): | |
| """Tải tin tức thị trường từ Gemini AI.""" | |
| system_prompt = "Act as an expert financial news editor for the Vietnamese market. Provide a brief, neutral, and up-to-date summary on the current status (last 24-48 hours) of the following markets: Chứng khoán Việt Nam (VNIndex), Giá Vàng (SJC/Thế giới), and Bitcoin/Crypto. Your response MUST be in Vietnamese, use markdown headings for markets, use bullet points for facts, and should include 2-3 web sources using the source link format required for grounding metadata." | |
| user_query = "Tóm tắt tình hình thị trường Chứng khoán, Vàng, và Crypto hiện tại." | |
| result = call_gemini_api(user_query, system_prompt, use_search=True) | |
| if result.get("error"): | |
| return gr.update(visible=True, value=result["error"]), "" | |
| # Xây dựng markdown output bao gồm nguồn dẫn | |
| markdown_output = result["text"] | |
| if result.get("sources"): | |
| sources_md = "\n\n**Nguồn Tham Khảo:**\n" | |
| sources_md += "\n".join([f"- [{s['title'] or s['uri']}]({s['uri']})" for s in result["sources"][:3]]) | |
| markdown_output += sources_md | |
| return gr.update(visible=False), markdown_output | |
| def calculate_dashboard_stats(transactions_df, investments_df): | |
| """Tính toán các chỉ số Dashboard.""" | |
| # 1. Stats Thu Chi Tháng này | |
| monthly_df = get_current_month_data(transactions_df) | |
| total_income = monthly_df[monthly_df['type'] == 'INCOME']['amount'].sum() | |
| total_expense = monthly_df[monthly_df['type'] == 'EXPENSE']['amount'].sum() | |
| net_savings = total_income - total_expense | |
| # 2. Stats Đầu tư | |
| investments_df_user = investments_df[investments_df['user_id'] == MOCK_USER_ID] | |
| total_invested = (investments_df_user['quantity'] * investments_df_user['averagePrice']).sum() | |
| total_current_value = (investments_df_user['quantity'] * investments_df_user['currentValue']).sum() | |
| net_return = total_current_value - total_invested | |
| roi = (net_return / total_invested) * 100 if total_invested > 0 else 0 | |
| # 3. Phân tích chi tiêu | |
| expense_by_category = monthly_df[monthly_df['type'] == 'EXPENSE'].groupby('category')['amount'].sum().reset_index() | |
| expense_by_category['amount_formatted'] = expense_by_category['amount'].apply(format_currency) | |
| expense_by_category = expense_by_category.sort_values(by='amount', ascending=False) | |
| # Chuẩn bị output cho Gradio | |
| stats_output = [ | |
| format_currency(total_income), | |
| format_currency(total_expense), | |
| format_currency(net_savings), | |
| format_percentage(roi), | |
| ] | |
| # Dataframe cho Phân tích | |
| if expense_by_category.empty: | |
| expense_table = pd.DataFrame({'Danh Mục': ['N/A'], 'Số Tiền': ['N/A']}) | |
| else: | |
| # Lỗi logic: cần dùng expense_by_category để tạo expense_table | |
| expense_table = expense_by_category[['category', 'amount_formatted']].rename(columns={'category': 'Danh Mục', 'amount_formatted': 'Số Tiền'}) | |
| return stats_output + [expense_table] | |
| # --- TAB TRANSACTIONS --- | |
| def add_transaction(transactions_df, type, category, amount, date, description): | |
| """Thêm giao dịch mới vào DataFrame và lưu CSV.""" | |
| if not amount or float(amount) <= 0: | |
| return gr.update(visible=True, value="Lỗi: Số tiền phải lớn hơn 0."), transactions_df.head(10)[['type', 'category', 'amount', 'date', 'description']] | |
| new_row = pd.DataFrame([{ | |
| 'id': str(uuid.uuid4()), | |
| 'user_id': MOCK_USER_ID, | |
| 'type': type, | |
| 'category': category, | |
| 'amount': float(amount), | |
| 'date': pd.to_datetime(date), | |
| 'description': description | |
| }]) | |
| transactions_df = pd.concat([new_row, transactions_df], ignore_index=True) | |
| save_data(transactions_df, TRANSACTIONS_CSV) | |
| # Tải lại DF và cập nhật bảng | |
| transactions_df, _ = update_dataframes() | |
| return gr.update(visible=False), transactions_df[['type', 'category', 'amount', 'date', 'description']].head(10) | |
| def delete_transaction(transactions_df, evt: gr.SelectData): | |
| """Xóa giao dịch khỏi DataFrame và lưu CSV.""" | |
| if not evt.value: | |
| return transactions_df[['type', 'category', 'amount', 'date', 'description']].head(10) | |
| # Lấy ID từ hàng được chọn | |
| selected_row = transactions_df.iloc[evt.index[0]] | |
| transaction_id = selected_row['id'] | |
| # Xóa hàng | |
| transactions_df = transactions_df[transactions_df['id'] != transaction_id] | |
| save_data(transactions_df, TRANSACTIONS_CSV) | |
| # Tải lại DF và cập nhật bảng | |
| transactions_df, _ = update_dataframes() | |
| return transactions_df[['type', 'category', 'amount', 'date', 'description']].head(10) | |
| # --- TAB INVESTMENTS --- | |
| def add_investment(investments_df, ticker, quantity, average_price, current_value, purchase_date): | |
| """Thêm khoản đầu tư mới vào DataFrame và lưu CSV.""" | |
| if not ticker or not quantity or float(quantity) <= 0 or float(average_price) <= 0: | |
| return gr.update(visible=True, value="Lỗi: Vui lòng nhập đầy đủ Ticker, Số lượng, và Giá mua."), investments_df[['ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate']].head(10) | |
| new_row = pd.DataFrame([{ | |
| 'id': str(uuid.uuid4()), | |
| 'user_id': MOCK_USER_ID, | |
| 'ticker': ticker.upper(), | |
| 'quantity': float(quantity), | |
| 'averagePrice': float(average_price), | |
| 'currentValue': float(current_value) if current_value else float(average_price), | |
| 'purchaseDate': pd.to_datetime(purchase_date) | |
| }]) | |
| investments_df = pd.concat([new_row, investments_df], ignore_index=True) | |
| save_data(investments_df, INVESTMENTS_CSV) | |
| # Tải lại DF và cập nhật bảng | |
| _, investments_df = update_dataframes() | |
| return gr.update(visible=False), investments_df[['ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate']].head(10) | |
| def delete_investment(investments_df, evt: gr.SelectData): | |
| """Xóa khoản đầu tư khỏi DataFrame và lưu CSV.""" | |
| if not evt.value: | |
| return investments_df[['ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate']].head(10) | |
| selected_row = investments_df.iloc[evt.index[0]] | |
| investment_id = selected_row['id'] | |
| investments_df = investments_df[investments_df['id'] != investment_id] | |
| save_data(investments_df, INVESTMENTS_CSV) | |
| # Tải lại DF và cập nhật bảng | |
| _, investments_df = update_dataframes() | |
| return investments_df[['ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate']].head(10) | |
| def calculate_investment_report(investments_df): | |
| """Tính toán báo cáo đầu tư tổng thể.""" | |
| df_user = investments_df[investments_df['user_id'] == MOCK_USER_ID] | |
| if df_user.empty: | |
| return ["0 VND"] * 3 + ["0.00%"], [pd.DataFrame({'Tổng Quan': ['Chưa có dữ liệu đầu tư.']}), pd.DataFrame()] | |
| total_invested = (df_user['quantity'] * df_user['averagePrice']).sum() | |
| total_current_value = (df_user['quantity'] * df_user['currentValue']).sum() | |
| net_return = total_current_value - total_invested | |
| roi = (net_return / total_invested) * 100 if total_invested > 0 else 0 | |
| summary_data = { | |
| 'Chỉ Số': ['Tổng Vốn Đầu Tư', 'Giá Trị Hiện Tại', 'Lãi/Lỗ Ròng', 'ROI Ròng'], | |
| 'Giá Trị': [ | |
| format_currency(total_invested), | |
| format_currency(total_current_value), | |
| format_currency(net_return), | |
| format_percentage(roi) | |
| ] | |
| } | |
| # Báo cáo chi tiết ROI theo từng khoản | |
| df_report = df_user.copy() | |
| df_report['Lãi/Lỗ (%)'] = ((df_report['currentValue'] - df_report['averagePrice']) / df_report['averagePrice']) * 100 | |
| df_report['Giá Mua TB'] = df_report['averagePrice'].apply(format_currency) | |
| df_report['Giá Hiện Tại'] = df_report['currentValue'].apply(format_currency) | |
| df_report['Lãi/Lỗ Ròng'] = (df_report['currentValue'] - df_report['averagePrice']) * df_report['quantity'] | |
| df_report['Lãi/Lỗ Ròng'] = df_report['Lãi/Lỗ Ròng'].apply(format_currency) | |
| report_table = df_report[['ticker', 'quantity', 'Giá Mua TB', 'Giá Hiện Tại', 'Lãi/Lỗ Ròng', 'Lãi/Lỗ (%)']].rename(columns={'ticker': 'Mã CK', 'quantity': 'Số Lượng'}) | |
| return [ | |
| format_currency(total_invested), | |
| format_currency(total_current_value), | |
| format_currency(net_return), | |
| format_percentage(roi), | |
| ], [pd.DataFrame(summary_data), report_table] | |
| # --- TAB REPORTS (Báo cáo Thu Chi) --- | |
| def calculate_report_table(transactions_df, report_type): | |
| """Tính toán báo cáo thu chi chi tiết theo Tuần, Tháng, Quý, Năm.""" | |
| if transactions_df.empty: | |
| return pd.DataFrame({'Ngày/Kỳ': ['N/A'], 'Thu Nhập': [0], 'Chi Tiêu': [0], 'Tiết Kiệm Ròng': [0]}) | |
| df = transactions_df.copy() | |
| df['date'] = pd.to_datetime(df['date']) | |
| # Xử lý các loại báo cáo | |
| if report_type == 'WEEK': | |
| # Sử dụng tuần ISO (dt.isocalendar().week) | |
| df['period'] = df['date'].dt.isocalendar().week.astype(str) + '-' + df['date'].dt.isocalendar().year.astype(str) | |
| group_col = 'period' | |
| col_name = 'Tuần-Năm' | |
| elif report_type == 'MONTH': | |
| df['period'] = df['date'].dt.to_period('M').astype(str) | |
| group_col = 'period' | |
| col_name = 'Tháng-Năm' | |
| elif report_type == 'QUARTER': | |
| df['period'] = df['date'].dt.to_period('Q').astype(str) | |
| group_col = 'period' | |
| col_name = 'Quý-Năm' | |
| elif report_type == 'YEAR': | |
| df['period'] = df['date'].dt.to_period('Y').astype(str) | |
| group_col = 'period' | |
| col_name = 'Năm' | |
| else: # Mặc định là Tháng | |
| df['period'] = df['date'].dt.to_period('M').astype(str) | |
| group_col = 'period' | |
| col_name = 'Tháng-Năm' | |
| # Tạo bảng pivot | |
| pivot_df = df.pivot_table(index=group_col, columns='type', values='amount', aggfunc='sum', fill_value=0) | |
| if 'INCOME' not in pivot_df.columns: | |
| pivot_df['INCOME'] = 0 | |
| if 'EXPENSE' not in pivot_df.columns: | |
| pivot_df['EXPENSE'] = 0 | |
| pivot_df['Tiết Kiệm Ròng'] = pivot_df['INCOME'] - pivot_df['EXPENSE'] | |
| # Định dạng tiền tệ | |
| pivot_df['Thu Nhập'] = pivot_df['INCOME'].apply(format_currency) | |
| pivot_df['Chi Tiêu'] = pivot_df['EXPENSE'].apply(format_currency) | |
| pivot_df['Tiết Kiệm Ròng'] = pivot_df['Tiết Kiệm Ròng'].apply(format_currency) | |
| final_report = pivot_df[[ 'Thu Nhập', 'Chi Tiêu', 'Tiết Kiệm Ròng']].reset_index() | |
| final_report = final_report.rename(columns={group_col: col_name}) | |
| # Sắp xếp theo kỳ giảm dần | |
| final_report = final_report.sort_index(ascending=False) | |
| return final_report | |
| # --- TAB AI VÀ API CALLS --- | |
| # Thêm cache để tránh gọi API nhiều lần | |
| def call_gemini_api(user_query, system_prompt, use_search=False, is_json=False): | |
| """Hàm gọi API Gemini với logic retry và cache.""" | |
| if not GEMINI_API_KEY: | |
| return {"error": "GEMINI_API_KEY chưa được cấu hình."} | |
| payload = { | |
| "contents": [{"parts": [{"text": user_query}]}], | |
| "systemInstruction": {"parts": [{"text": system_prompt}]} | |
| } | |
| if use_search: | |
| payload["tools"] = [{"google_search": {}}] | |
| if is_json: | |
| payload["generationConfig"] = { | |
| "responseMimeType": "application/json", | |
| "responseSchema": { | |
| "type": "OBJECT", | |
| "properties": { | |
| "suggestions": {"type": "ARRAY", "items": {"type": "STRING"}}, | |
| "actions": {"type": "ARRAY", "items": {"type": "STRING"}} | |
| } | |
| } | |
| } | |
| headers = {'Content-Type': 'application/json'} | |
| for i in range(3): # 3 lần thử với exponential backoff | |
| try: | |
| response = requests.post(f"{GEMINI_API_URL}?key={GEMINI_API_KEY}", headers=headers, json=payload, timeout=20) | |
| response.raise_for_status() | |
| result = response.json() | |
| text = result.get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '') | |
| if is_json: | |
| return json.loads(text) | |
| # Xử lý nguồn dẫn (grounding) | |
| sources = [] | |
| grounding_metadata = result.get('candidates', [{}])[0].get('groundingMetadata', {}) | |
| if grounding_metadata and grounding_metadata.get('groundingAttributions'): | |
| sources = [{ | |
| 'uri': attr.get('web', {}).get('uri'), | |
| 'title': attr.get('web', {}).get('title') | |
| } for attr in grounding_metadata['groundingAttributions']] | |
| return {"text": text, "sources": sources} | |
| except requests.exceptions.RequestException as e: | |
| if i < 2: | |
| time.sleep(2 ** i) # Backoff | |
| continue | |
| return {"error": f"Lỗi kết nối API Gemini: {e}"} | |
| except json.JSONDecodeError: | |
| return {"error": "Lỗi phân tích JSON từ phản hồi AI."} | |
| return {"error": "Đã hết số lần thử kết nối API Gemini."} | |
| def fetch_investment_suggestions(): | |
| """Lấy gợi ý đầu tư vĩ mô từ Gemini.""" | |
| system_prompt = "Act as a leading global financial advisor. Analyze the current macroeconomic situation and future projections (e.g., inflation, interest rates, currency movements) and provide specific, actionable investment suggestions. Your output MUST be a JSON object with two arrays: 'suggestions' (3 high-level themes) and 'actions' (2 specific actions/sectors). DO NOT use any markdown, only raw JSON." | |
| user_query = "Phân tích tình hình vĩ mô hiện tại và đề xuất 3 chủ đề đầu tư sinh lời tốt hơn và 2 hành động cụ thể trong 6 tháng tới. Trả lời bằng tiếng Việt." | |
| result = call_gemini_api(user_query, system_prompt, use_search=True, is_json=True) | |
| if result.get("error"): | |
| return result["error"], "" | |
| suggestions_md = "\n".join([f"- {s}" for s in result.get("suggestions", [])]) | |
| actions_md = "\n".join([f"- {a}" for a in result.get("actions", [])]) | |
| output = f"**3 Chủ Đề Đầu Tư Cấp Cao:**\n{suggestions_md}\n\n**2 Hành Động/Lĩnh Vực Cụ Thể:**\n{actions_md}" | |
| return "", output | |
| def fetch_expense_suggestions(transactions_df): | |
| """Lấy gợi ý tiết kiệm từ Gemini dựa trên lịch sử chi tiêu.""" | |
| system_prompt = "Act as a personalized financial coach for a family. Analyze the provided expense history (category and total amount) and give 3 specific, actionable tips on how to save money and 2 related tips on taking advantage of discounts/vouchers (referencing the current market). Your response MUST be in Vietnamese and use markdown bullet points. Do not include a greeting or conclusion. Start with a brief summary of the highest spending categories." | |
| # Chuẩn bị lịch sử chi tiêu | |
| monthly_df = transactions_df[transactions_df['type'] == 'EXPENSE'] | |
| expense_by_category = monthly_df.groupby('category')['amount'].sum().sort_values(ascending=False) | |
| expense_history_text = ", ".join([f"{cat}: {format_currency(amount)}" for cat, amount in expense_by_category.items()]) | |
| user_query = f"Dựa trên lịch sử chi tiêu sau: {expense_history_text}. Hãy đưa ra gợi ý chi tiêu tiết kiệm hơn." | |
| result = call_gemini_api(user_query, system_prompt, use_search=False) | |
| if result.get("error"): | |
| return result["error"], "" | |
| return "", result["text"] | |
| # --- TAB SHOPPING API CALLS --- | |
| def call_accesstrade_api(url, params={}): | |
| """Hàm gọi API AccessTrade.""" | |
| if not VOUCHER_AUTH_TOKEN: | |
| return {"error": "VOUCHER_AUTH_TOKEN chưa được cấu hình."} | |
| headers = {'Authorization': VOUCHER_AUTH_TOKEN} | |
| for i in range(3): | |
| try: | |
| response = requests.get(url, headers=headers, params=params, timeout=15) | |
| response.raise_for_status() | |
| data = response.json() | |
| return data.get('data', []) | |
| except requests.exceptions.RequestException as e: | |
| if i < 2: | |
| time.sleep(2 ** i) | |
| continue | |
| return {"error": f"Lỗi kết nối API AccessTrade: {e}"} | |
| return {"error": "Đã hết số lần thử kết nối API AccessTrade."} | |
| def fetch_vouchers(): | |
| """Tải mã giảm giá.""" | |
| vouchers = call_accesstrade_api(VOUCHER_API_URL, params={'limit': 20}) | |
| if isinstance(vouchers, dict) and 'error' in vouchers: | |
| return gr.update(visible=True, value=vouchers['error']), pd.DataFrame() | |
| df = pd.DataFrame(vouchers) | |
| df['domain'] = df['domain'].fillna('Khác') | |
| df['coupon_code'] = df['coupons'].apply(lambda x: x[0]['coupon_code'] if x and len(x) > 0 else 'N/A') | |
| df['coupon_desc'] = df['coupons'].apply(lambda x: x[0]['coupon_desc'] if x and len(x) > 0 else 'Không mô tả') | |
| # Chỉ trả về các cột cần hiển thị | |
| return gr.update(visible=False), df[['name', 'domain', 'coupon_code', 'coupon_desc', 'end_time', 'aff_link']] | |
| def fetch_products(keywords, sort_field, sort_order): | |
| """Tải và sắp xếp sản phẩm TikTok Shop.""" | |
| products = call_accesstrade_api(PRODUCTS_API_URL, params={ | |
| 'limit': 50, | |
| 'title_keywords': keywords, | |
| 'sort_field': sort_field, | |
| 'sort_direction': sort_order, | |
| }) | |
| if isinstance(products, dict) and 'error' in products: | |
| return gr.update(visible=True, value=products['error']), pd.DataFrame() | |
| df = pd.DataFrame(products) | |
| # Xử lý các cột phức tạp và NaN | |
| df['price'] = df.apply(lambda row: row['sale_price'] if row['sale_price'] else row['price'], axis=1) | |
| df['price_formatted'] = df['price'].apply(format_currency) | |
| df['commission_rate'] = df['commission_rate'].fillna(0).astype(float).apply(lambda x: f"{x:.2f}%") | |
| df['category_name'] = df['categories'].apply(lambda x: x[0]['category_name_show'] if x and len(x) > 0 else 'Khác') | |
| # Chỉ trả về các cột cần hiển thị | |
| return gr.update(visible=False), df[['image', 'name', 'domain', 'price_formatted', 'commission_rate', 'category_name', 'aff_link', 'content']] | |
| def fetch_trending_suggestions(keywords): | |
| """Lấy gợi ý sản phẩm trending từ Gemini.""" | |
| system_prompt = "Act as an e-commerce trend analyst for the Vietnamese market. Based on the user's search query, identify the top 3 currently trending or highly-purchased products/categories related to that query across major e-commerce platforms (Shopee, Lazada, TikTok Shop). Provide 3 specific bullet points describing the trend (e.g., 'focus on eco-friendly materials', 'high demand for dual-function devices'). Your output MUST be in Vietnamese and use markdown bullet points. Do not include a greeting or conclusion." | |
| user_query = f"Phân tích sản phẩm trending liên quan đến từ khóa tìm kiếm này: {keywords}" | |
| result = call_gemini_api(user_query, system_prompt, use_search=True) | |
| if result.get("error"): | |
| return result["error"], "" | |
| return "", result["text"] | |
| def analyze_product_review(product_df, evt: gr.SelectData): | |
| """Phân tích đánh giá sản phẩm giả lập bằng Gemini.""" | |
| if product_df.empty or not evt.value: | |
| return "Vui lòng chọn một sản phẩm trong bảng.", "" | |
| selected_row = product_df.iloc[evt.index[0]] | |
| product_name = selected_row['name'] | |
| product_content = selected_row['content'] | |
| system_prompt = "Act as a comprehensive product review sentiment analyzer. Based on the product name and description provided, generate 3 specific bullet points for POSITIVE aspects (potential strengths) and 3 bullet points for NEGATIVE aspects (potential weaknesses or common complaints). Then give an overall sentiment score (1 to 5 stars, e.g., '4/5 stars'). The output MUST be in Vietnamese, use markdown bullet points, and follow the structure: POSITIVE: [list]; NEGATIVE: [list]; SENTIMENT: [score]. DO NOT use any other text, greeting, or conclusion." | |
| user_query = f"Phân tích nhận xét cho sản phẩm này: Tên: {product_name}. Mô tả: {product_content}" | |
| result = call_gemini_api(user_query, system_prompt, use_search=False) | |
| if result.get("error"): | |
| return product_name, result["error"] | |
| return product_name, result["text"] | |
| # ============================================================================== | |
| # 5. GRADIO INTERFACE BUILDER | |
| # ============================================================================== | |
| with gr.Blocks(title="Ứng Dụng Quản Lý Tài Chính Gia Đình", theme=gr.themes.Soft()) as demo: | |
| # Biến trạng thái để lưu DataFrame hiện tại | |
| transactions_state = gr.State(transactions_df) | |
| investments_state = gr.State(investments_df) | |
| product_results_state = gr.State(pd.DataFrame()) | |
| gr.Markdown("# 💸 FINTECH FAMILY - Quản Lý Thu Chi & Đầu Tư 🚀") | |
| gr.Markdown("Đây là ứng dụng quản lý tài chính toàn diện, sử dụng CSV để lưu dữ liệu và Gemini AI để gợi ý thông minh.") | |
| with gr.Tabs(): | |
| # --- TAB 1: DASHBOARD (Tổng Quan) --- | |
| with gr.TabItem("Tổng Quan"): | |
| gr.Markdown("## Báo Cáo Tổng Hợp Tháng Này") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### Thu Chi Cá Nhân") | |
| with gr.Row(): | |
| stat_income = gr.Textbox(label="Tổng Thu", interactive=False) | |
| stat_expense = gr.Textbox(label="Tổng Chi", interactive=False) | |
| stat_savings = gr.Textbox(label="Tiết Kiệm Ròng", interactive=False) | |
| gr.Markdown("### Phân Tích Chi Tiêu Theo Danh Mục") | |
| # Sửa lỗi: Bỏ row_headers | |
| expense_category_table = gr.Dataframe(headers=['Danh Mục', 'Số Tiền'], wrap=True, interactive=False) | |
| with gr.Column(): | |
| gr.Markdown("### Đầu Tư & ROI Ròng") | |
| with gr.Row(): | |
| stat_roi = gr.Textbox(label="ROI Danh Mục Ròng", interactive=False) | |
| gr.Markdown("### Tin Tức Thị Trường (Gemini AI)") | |
| news_error = gr.Markdown(visible=False, label="Lỗi") | |
| news_output = gr.Markdown("Đang tải tin tức...") | |
| # Tải tin tức ngay khi tab được chọn lần đầu | |
| demo.load(fn=fetch_market_news, outputs=[news_error, news_output], show_progress="minimal") | |
| # Nút cập nhật toàn bộ Dashboard | |
| gr.Button("Cập Nhật Dashboard").click( | |
| fn=calculate_dashboard_stats, | |
| inputs=[transactions_state, investments_state], | |
| outputs=[stat_income, stat_expense, stat_savings, stat_roi, expense_category_table] | |
| ) | |
| # --- TAB 2: TRANSACTIONS (Thu Chi) --- | |
| with gr.TabItem("Quản Lý Thu Chi"): | |
| gr.Markdown("## Ghi Lại Giao Dịch") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| with gr.Group(): # Thay thế gr.Box() bằng gr.Group() | |
| gr.Markdown("### Thêm Giao Dịch Mới") | |
| with gr.Row(): | |
| type_input = gr.Radio(choices=['INCOME', 'EXPENSE'], label="Loại Giao Dịch", value='EXPENSE') | |
| category_input = gr.Dropdown(choices=['Ăn uống', 'Nhà ở', 'Di chuyển', 'Lương', 'Khác'], label="Danh Mục", value='Ăn uống') | |
| amount_input = gr.Number(label="Số Tiền (VND)", minimum=0) | |
| date_input = gr.Textbox(label="Ngày (YYYY-MM-DD)", value=datetime.now().strftime('%Y-%m-%d')) | |
| description_input = gr.Textbox(label="Mô Tả") | |
| # Sửa lỗi: Bỏ type='error' | |
| tx_error_msg = gr.Markdown(visible=False, label="Lỗi") | |
| add_tx_btn = gr.Button("Ghi Lại Giao Dịch", variant="primary") | |
| with gr.Column(scale=2): | |
| gr.Markdown("### Lịch Sử Giao Dịch Gần Đây (Chọn hàng để xóa)") | |
| # Sửa lỗi: Bỏ row_headers và visible_cols | |
| tx_table = gr.Dataframe( | |
| headers=['Loại', 'Danh Mục', 'Số Tiền', 'Ngày', 'Mô Tả'], # Bỏ ID khỏi header | |
| wrap=True, | |
| interactive=False, | |
| ) | |
| gr.Markdown("💡 **Lưu ý:** Dữ liệu được lưu trong file `transactions.csv`.") | |
| # Logic: Thêm giao dịch | |
| add_tx_btn.click( | |
| fn=add_transaction, | |
| inputs=[transactions_state, type_input, category_input, amount_input, date_input, description_input], | |
| outputs=[tx_error_msg, tx_table] | |
| ).success( | |
| # Cập nhật state và hiển thị bảng chỉ với các cột cần thiết | |
| fn=lambda df: (df, df[['type', 'category', 'amount', 'date', 'description']].head(10)), | |
| inputs=[transactions_state], | |
| outputs=[transactions_state, tx_table] | |
| ) | |
| # Logic: Xóa giao dịch | |
| tx_table.select( | |
| fn=delete_transaction, | |
| inputs=[transactions_state], | |
| outputs=[tx_table] | |
| ).success( | |
| # Cập nhật state và hiển thị bảng chỉ với các cột cần thiết | |
| fn=lambda df: (df, df[['type', 'category', 'amount', 'date', 'description']].head(10)), | |
| inputs=[transactions_state], | |
| outputs=[transactions_state, tx_table] | |
| ) | |
| # --- TAB 3: INVESTMENTS (Đầu Tư) --- | |
| with gr.TabItem("Quản Lý Đầu Tư"): | |
| gr.Markdown("## Quản Lý Danh Mục Đầu Tư") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| with gr.Group(): # Thay thế gr.Box() bằng gr.Group() | |
| gr.Markdown("### Thêm Khoản Đầu Tư Mới") | |
| ticker_input = gr.Textbox(label="Mã Cổ Phiếu/Ticker", placeholder="Ví dụ: VNM, BTC") | |
| quantity_input = gr.Number(label="Số Lượng", minimum=0) | |
| avg_price_input = gr.Number(label="Giá Mua Trung Bình", minimum=0) | |
| current_value_input = gr.Number(label="Giá Hiện Tại (Cập nhật thủ công)", minimum=0) | |
| purchase_date_input = gr.Textbox(label="Ngày Mua (YYYY-MM-DD)", value=datetime.now().strftime('%Y-%m-%d')) | |
| # Sửa lỗi: Bỏ type='error' | |
| inv_error_msg = gr.Markdown(visible=False, label="Lỗi") | |
| add_inv_btn = gr.Button("Thêm Khoản Đầu Tư", variant="primary") | |
| with gr.Column(scale=2): | |
| gr.Markdown("### Danh Mục Hiện Tại (Chọn hàng để xóa)") | |
| # Sửa lỗi: Bỏ row_headers và visible_cols | |
| inv_table = gr.Dataframe( | |
| headers=['Ticker', 'Số Lượng', 'Giá Mua TB', 'Giá Hiện Tại', 'Ngày Mua'], # Bỏ ID khỏi header | |
| wrap=True, | |
| interactive=False, | |
| ) | |
| gr.Markdown("💡 **Lưu ý:** Dữ liệu được lưu trong file `investments.csv`.") | |
| # Logic: Thêm đầu tư | |
| add_inv_btn.click( | |
| fn=add_investment, | |
| inputs=[investments_state, ticker_input, quantity_input, avg_price_input, current_value_input, purchase_date_input], | |
| outputs=[inv_error_msg, inv_table] | |
| ).success( | |
| fn=lambda df: (df, df[['ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate']].head(10)), | |
| inputs=[investments_state], | |
| outputs=[investments_state, inv_table] | |
| ) | |
| # Logic: Xóa đầu tư | |
| inv_table.select( | |
| fn=delete_investment, | |
| inputs=[investments_state], | |
| outputs=[inv_table] | |
| ).success( | |
| fn=lambda df: (df, df[['ticker', 'quantity', 'averagePrice', 'currentValue', 'purchaseDate']].head(10)), | |
| inputs=[investments_state], | |
| outputs=[investments_state, inv_table] | |
| ) | |
| # Báo cáo đầu tư | |
| gr.Markdown("## Báo Cáo Tỷ Suất Đầu Tư Ròng") | |
| with gr.Row(): | |
| inv_total_invested = gr.Textbox(label="Tổng Vốn Đầu Tư", interactive=False) | |
| inv_current_value = gr.Textbox(label="Giá Trị Hiện Tại", interactive=False) | |
| inv_net_return = gr.Textbox(label="Lãi/Lỗ Ròng", interactive=False) | |
| inv_roi = gr.Textbox(label="ROI Ròng (%)", interactive=False) | |
| # Sửa lỗi: Bỏ row_headers | |
| inv_report_table = gr.Dataframe( | |
| headers=['Mã CK', 'Số Lượng', 'Giá Mua TB', 'Giá Hiện Tại', 'Lãi/Lỗ Ròng', 'Lãi/Lỗ (%)'], | |
| interactive=False | |
| ) | |
| gr.Button("Tính Toán Báo Cáo ROI").click( | |
| fn=calculate_investment_report, | |
| inputs=[investments_state], | |
| outputs=[inv_total_invested, inv_current_value, inv_net_return, inv_roi, inv_report_table] | |
| ) | |
| # --- TAB 4: REPORTS (Báo Cáo Thu Chi) --- | |
| with gr.TabItem("Báo Cáo Thu Chi"): | |
| gr.Markdown("## Báo Cáo Thống Kê Thu Chi Theo Kỳ") | |
| report_period_input = gr.Radio( | |
| choices=['WEEK', 'MONTH', 'QUARTER', 'YEAR'], | |
| label="Chọn Kỳ Báo Cáo", | |
| value='MONTH' | |
| ) | |
| # Sửa lỗi: Bỏ row_headers | |
| report_table_output = gr.Dataframe( | |
| headers=['Kỳ', 'Thu Nhập', 'Chi Tiêu', 'Tiết Kiệm Ròng'], | |
| interactive=False, | |
| wrap=True | |
| ) | |
| gr.Button("Xem Báo Cáo").click( | |
| fn=calculate_report_table, | |
| inputs=[transactions_state, report_period_input], | |
| outputs=[report_table_output] | |
| ) | |
| # --- TAB 5: AI GỢI Ý --- | |
| with gr.TabItem("AI Gợi Ý"): | |
| gr.Markdown("## Gemini AI Cố Vấn Tài Chính") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### 🚀 Đề Xuất Đầu Tư (Dựa trên Vĩ mô)") | |
| inv_ai_output = gr.Markdown("Chưa có đề xuất.") | |
| inv_ai_error = gr.Markdown(visible=False, label="Lỗi") # Sửa lỗi: Bỏ type='error' | |
| gr.Button("Phân Tích & Đề Xuất Đầu Tư").click( | |
| fn=fetch_investment_suggestions, | |
| outputs=[inv_ai_error, inv_ai_output] | |
| ) | |
| with gr.Column(): | |
| gr.Markdown("### 💰 Gợi Ý Chi Tiêu Tiết Kiệm (Dựa trên Lịch sử)") | |
| expense_ai_output = gr.Markdown("Chưa có gợi ý.") | |
| expense_ai_error = gr.Markdown(visible=False, label="Lỗi") # Sửa lỗi: Bỏ type='error' | |
| gr.Button("Phân Tích & Gợi Ý Tiết Kiệm").click( | |
| fn=fetch_expense_suggestions, | |
| inputs=[transactions_state], | |
| outputs=[expense_ai_error, expense_ai_output] | |
| ) | |
| # --- TAB 6: SHOPPING (Mua Sắm Thông Minh) --- | |
| with gr.TabItem("Mua Sắm Thông Minh"): | |
| gr.Markdown("## Tìm Kiếm & So Sánh Sản Phẩm TikTok Shop") | |
| with gr.Row(): | |
| search_input = gr.Textbox(label="Từ Khóa Sản Phẩm", value="dép", placeholder="Ví dụ: áo khoác, điện thoại") | |
| sort_field_input = gr.Radio(choices=['COMMISSION', 'PRICE'], label="Sắp xếp theo", value='PRICE') | |
| sort_order_input = gr.Radio(choices=['DESC', 'ASC'], label="Thứ tự", value='DESC') | |
| search_btn = gr.Button("Tìm Kiếm Sản Phẩm & Xu Hướng", variant="primary") | |
| # Sản phẩm Trending AI | |
| gr.Markdown("### 💡 Sản Phẩm Trending (Gợi ý từ AI)") | |
| trending_ai_output = gr.Markdown("Nhấn tìm kiếm để AI phân tích xu hướng...") | |
| trending_ai_error = gr.Markdown(visible=False, label="Lỗi") # Sửa lỗi: Bỏ type='error' | |
| # Bảng kết quả sản phẩm | |
| gr.Markdown("### Kết Quả Tìm Kiếm & So Sánh Giá") | |
| product_search_error = gr.Markdown(visible=False, label="Lỗi") # Sửa lỗi: Bỏ type='error' | |
| # Sửa lỗi: Bỏ row_headers và visible_cols | |
| product_table = gr.Dataframe( | |
| headers=['Ảnh', 'Tên Sản Phẩm', 'Sàn', 'Giá', 'Hoa Hồng (%)', 'Danh Mục', 'Link'], | |
| interactive=False, | |
| ) | |
| # Phân tích AI | |
| gr.Markdown("### Phân Tích Đánh Giá Sản Phẩm") | |
| with gr.Row(): | |
| product_name_output = gr.Textbox(label="Sản Phẩm Được Chọn", interactive=False) | |
| review_ai_output = gr.Markdown("Chọn một hàng trong bảng kết quả để phân tích.") | |
| # Logic Tìm kiếm | |
| search_btn.click( | |
| fn=lambda: [gr.update(visible=True, value="Đang tải sản phẩm..."), gr.update(visible=True, value="Đang phân tích xu hướng...")], | |
| outputs=[product_search_error, trending_ai_error] # Reset errors | |
| ).then( | |
| fn=fetch_products, | |
| inputs=[search_input, sort_field_input, sort_order_input], | |
| outputs=[product_search_error, product_table] | |
| ).then( | |
| fn=fetch_trending_suggestions, | |
| inputs=[search_input], | |
| outputs=[trending_ai_error, trending_ai_output] | |
| ) | |
| # Logic Phân tích AI Review | |
| product_table.select( | |
| fn=analyze_product_review, | |
| inputs=[product_table], | |
| outputs=[product_name_output, review_ai_output] | |
| ) | |
| # Logic Voucher (Tải sẵn) | |
| with gr.Group(): # Thay thế gr.Box() bằng gr.Group() | |
| gr.Markdown("### Mã Giảm Giá & Ưu Đãi Tiết Kiệm") | |
| voucher_error = gr.Markdown(visible=False, label="Lỗi") # Sửa lỗi: Bỏ type='error' | |
| # Sửa lỗi: Bỏ row_headers và visible_cols | |
| voucher_table = gr.Dataframe( | |
| headers=['Tên', 'Sàn', 'Mã Coupon', 'Mô Tả Coupon', 'Hạn', 'Link'], | |
| interactive=False | |
| ) | |
| demo.load(fn=fetch_vouchers, outputs=[voucher_error, voucher_table], show_progress="minimal") | |
| # Logic: Cập nhật DataFrames sau khi ứng dụng load lần đầu | |
| demo.load(fn=update_dataframes, outputs=[transactions_state, investments_state], show_progress="minimal").then( | |
| fn=calculate_dashboard_stats, | |
| inputs=[transactions_state, investments_state], | |
| outputs=[stat_income, stat_expense, stat_savings, stat_roi, expense_category_table] | |
| ) | |
| # Chạy ứng dụng | |
| if __name__ == "__main__": | |
| demo.launch(share=True) | |