from __future__ import annotations
import base64
from datetime import datetime, timezone
from pathlib import Path
import streamlit as st
try:
from streamlit_extras.metric_cards import style_metric_cards
except ImportError:
style_metric_cards = None
from app.config import PROJECT_ROOT
MASCOT_DIR = PROJECT_ROOT / "assets" / "mascots"
def mascot_path(name: str) -> Path:
path = MASCOT_DIR / f"{name}.png"
return path if path.exists() else MASCOT_DIR / "chatvns.png"
def mascot_data_uri(name: str) -> str:
path = mascot_path(name)
if not path.exists():
return ""
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
return f"data:image/png;base64,{encoded}"
def inject_app_css() -> None:
st.markdown(
"""
""",
unsafe_allow_html=True,
)
def render_hero(ticker_count: int, top_k: int) -> None:
image_uri = mascot_data_uri("chatvns")
st.markdown(
f"""
Trợ lý chứng khoán Việt Nam
ChatVNS
Trợ lý RAG cho dữ liệu chứng khoán Việt Nam — trả lời ngắn gọn, ưu tiên dữ liệu mới nhất và luôn chỉ rõ nguồn.
{ticker_count} mã có dữ liệu
Truy xuất kết hợp · Top {top_k}
Ưu tiên trích nguồn
""",
unsafe_allow_html=True,
)
def render_welcome() -> None:
image_uri = mascot_data_uri("welcome")
st.markdown(
f"""
Chào bạn, mình là ChatVNS
Hỏi về giá gần nhất, báo cáo doanh nghiệp, tin tức hoặc chỉ báo kỹ thuật. Chọn một gợi ý bên dưới để bắt đầu.
""",
unsafe_allow_html=True,
)
def render_trust_note() -> None:
image_uri = mascot_data_uri("trust")
st.markdown(
f"""
Nguồn được lấy từ dữ liệu đã thu thập và đưa trực tiếp vào ngữ cảnh của câu trả lời.
""",
unsafe_allow_html=True,
)
def render_disclaimer() -> None:
st.markdown(
"""
Lưu ý: Nội dung do AI tổng hợp nhằm mục đích tham khảo, không phải khuyến nghị mua/bán hay tư vấn đầu tư cá nhân. Dữ liệu thị trường có thể có độ trễ; hãy kiểm tra lại với nguồn giao dịch chính thức trước khi ra quyết định.
""",
unsafe_allow_html=True,
)
def freshness_label(crawled_at: str | None) -> tuple[str, str]:
if not crawled_at:
return "Chưa xác định thời điểm cập nhật", "chatvns-stale"
try:
parsed = datetime.fromisoformat(str(crawled_at).replace("Z", "+00:00"))
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
age = datetime.now(timezone.utc) - parsed.astimezone(timezone.utc)
minutes = max(0, int(age.total_seconds() // 60))
if minutes < 60:
return f"Cập nhật {minutes} phút trước", "chatvns-fresh"
hours = minutes // 60
if hours < 24:
return f"Cập nhật {hours} giờ trước", "chatvns-fresh" if hours < 4 else "chatvns-stale"
return f"Cập nhật {hours // 24} ngày trước", "chatvns-stale"
except (TypeError, ValueError):
return "Không đọc được thời điểm cập nhật", "chatvns-stale"
def apply_metric_card_style() -> None:
if style_metric_cards is not None:
style_metric_cards(
background_color="#ffffff",
border_left_color="#0877e8",
border_color="#d8eaff",
box_shadow=True,
)
def render_section_header(label: str, description: str = "") -> None:
st.subheader(label, divider="blue")
if description:
st.caption(description)