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 @lru_cache(maxsize=32) 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)