smartfinance / app.py
danghungithp's picture
Update app.py
7a30309 verified
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)