"""Màn Dashboard — khung lắp ráp (R08-T13). Ba mảng đã bóc sang ``presentation/dashboard/``: token_usage_card_widget.py 5 thẻ số liệu + thẻ Ngân sách usage_chart_widget.py biểu đồ theo tuần / tháng / năm habits_widget.py thói quen dùng token + nhận xét của AI File này còn ba việc: dựng hàng điều khiển (bộ lọc khoảng áp cho CẢ màn — thẻ, biểu đồ và thói quen đều đi theo nó), nạp dữ liệu một lần rồi chia cho ba widget, và tự làm mới mỗi 30 giây. """ from __future__ import annotations from datetime import timedelta from typing import Dict from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtWidgets import ( QComboBox, QHBoxLayout, QLabel, QPushButton, QScrollArea, QVBoxLayout, QWidget, ) from ..core import usage_tracker as ut from ..i18n import on_language_changed, tr from ..presentation.dashboard.habits_widget import HabitsWidget from ..presentation.dashboard.token_usage_card_widget import TokenUsageCardWidget from ..presentation.dashboard.usage_chart_widget import UsageChartWidget from ..state import AppContext from .icons import icon class DashboardTab(QWidget): status_message = Signal(str) _PERIODS = ("today", "week", "month", "all") def __init__(self, ctx: AppContext): super().__init__() self.ctx = ctx outer = QVBoxLayout(self) scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QScrollArea.NoFrame) content = QWidget() scroll.setWidget(content) outer.addWidget(scroll) root = QVBoxLayout(content) self.cards = TokenUsageCardWidget() self.chart_panel = UsageChartWidget() self.habits_panel = HabitsWidget(ctx) # ---- hàng tiêu đề ------------------------------------------------ head = QHBoxLayout() self._title = QLabel() self._title.setStyleSheet("font-weight:700; font-size:15px;") self.refresh_btn = QPushButton("") self.refresh_btn.setIcon(icon("refresh")) self.refresh_btn.setFixedWidth(34) self.refresh_btn.clicked.connect(self.refresh) head.addWidget(self._title, 1) head.addWidget(self.refresh_btn) root.addLayout(head) # ---- hàng điều khiển --------------------------------------------- # Hai hàng, gom theo việc control làm gì, thay vì chín widget xâu trên # một dòng nơi tiêu đề, bộ lật ngày, hai ô chọn biểu đồ, ô chọn tiền tệ # và nút Làm mới đọc thành một dải không phân biệt được. # Hàng 1 là "tôi đang ở đâu"; hàng 2 là "tôi đang xem cái gì". for g in ("week", "month", "year"): self.chart_panel.gran_combo.addItem(tr("dashboard.gran_%s" % g), g) self.chart_panel.gran_combo.currentIndexChanged.connect(self._on_gran_changed) for m in ("cost", "tokens"): self.chart_panel.metric_combo.addItem(tr("dashboard.metric_%s" % m), m) self.chart_panel.filter_changed.connect(self.refresh) # Ô chọn tiền hiển thị — dời từ thẻ Token Usage của Monitoring sang đây, # ngay cạnh Làm mới; hai màn vẫn dùng chung khoá usage.currency nên đổi # ở đây là đổi cả hai nơi. self.currency_lbl = QLabel() self.currency_lbl.setObjectName("hint") self.currency_combo = QComboBox() for cur in ut.SUPPORTED_CURRENCIES: self.currency_combo.addItem(cur, cur) idx = self.currency_combo.findData( (self.ctx.config.data.get("usage") or {}).get("currency", "USD")) self.currency_combo.setCurrentIndex(max(0, idx)) self.currency_combo.currentIndexChanged.connect(self._on_currency_changed) controls = QHBoxLayout() controls.setSpacing(6) controls.addWidget(self.chart_panel.prev_btn) # lật khoảng controls.addWidget(self.chart_panel.period_lbl) controls.addWidget(self.chart_panel.next_btn) controls.addSpacing(12) controls.addWidget(self.chart_panel.gran_combo) # biểu đồ vẽ gì controls.addWidget(self.chart_panel.metric_combo) controls.addStretch(1) controls.addWidget(self.currency_lbl) # tiền hiện thế nào controls.addWidget(self.currency_combo) root.addLayout(controls) # ---- ba mảng nội dung -------------------------------------------- root.addWidget(self.cards) root.addWidget(self.chart_panel) self.habits_panel.habits.setMinimumHeight(160) self.habits_panel.ai_advice.setMinimumHeight(140) self.habits_panel.ai_advice.setOpenExternalLinks(False) self.habits_panel._khoang = self._period_range root.addWidget(self.habits_panel, 1) self.cards.budget_applied.connect(self._apply_budget) self.habits_panel.status_message.connect(self.status_message) # Tự làm mới mỗi 30 giây để con số đi theo việc đang chạy. self._timer = QTimer(self) self._timer.setInterval(30_000) self._timer.timeout.connect(self.refresh) self._timer.start() on_language_changed(self._retranslate) self.refresh() # ---- cầu tương thích ------------------------------------------------- # Ba checker trong tools/ đọc thẳng tên cũ. Giữ nguyên đường vào; nơi ở # thật của chúng nay là ba widget con. card_total = property(lambda self: self.cards.card_total) card_in = property(lambda self: self.cards.card_in) card_out = property(lambda self: self.cards.card_out) card_cache = property(lambda self: self.cards.card_cache) card_cost = property(lambda self: self.cards.card_cost) budget_card = property(lambda self: self.cards.budget_card) chart = property(lambda self: self.chart_panel.chart) gran_combo = property(lambda self: self.chart_panel.gran_combo) metric_combo = property(lambda self: self.chart_panel.metric_combo) chart_prev_btn = property(lambda self: self.chart_panel.prev_btn) chart_next_btn = property(lambda self: self.chart_panel.next_btn) _chart_period_lbl = property(lambda self: self.chart_panel.period_lbl) _chart_title = property(lambda self: self.chart_panel.title) _habits_title = property(lambda self: self.habits_panel.title) _ai_title = property(lambda self: self.habits_panel.ai_title) habits = property(lambda self: self.habits_panel.habits) ai_advice = property(lambda self: self.habits_panel.ai_advice) ai_analyze_btn = property(lambda self: self.habits_panel.ai_analyze_btn) apply_strategy_btn = property(lambda self: self.habits_panel.apply_strategy_btn) # ---- helpers --------------------------------------------------------- def _pricing(self) -> Dict: from ..core import model_pricing as mp mp.sync_to_usage(self.ctx.config) # tổng/chi phí lấy thẳng từ bảng giá return {**ut.DEFAULT_PRICING, **(self.ctx.config.data.get("usage") or {})} def _period_range(self): """Khoảng đang chọn dạng (đầu, cuối) bao gồm cả hai đầu — nó điều khiển cả màn: thẻ số liệu, biểu đồ và thói quen.""" gran = self.chart_panel.gran_combo.currentData() or "week" start, end = ut.period_bounds(gran, self.chart_panel.offset) return start, end - timedelta(days=1) # load_events tính cả ngày cuối def _on_gran_changed(self, *_a) -> None: self.chart_panel.offset = 0 # đổi độ mịn → quay về khoảng hiện tại self.refresh() # bộ lọc điều khiển CẢ màn def _on_currency_changed(self, _idx: int) -> None: cur = self.currency_combo.currentData() if not cur: return self.ctx.config.data.setdefault("usage", {})["currency"] = cur self.ctx.save() self.refresh() def _apply_budget(self, amount: float) -> None: """Ghi giá trị ở ô nhập thành ngân sách mới — mở một chu kỳ số dư mới, phần đã tiêu trước thời điểm này không còn được tính.""" ccy = (self.ctx.config.data.get("usage") or {}).get("currency", "USD") ut.set_budget(self.ctx.config, amount, ccy) self.ctx.save() self._refresh_budget() def _refresh_budget(self) -> None: from ..core import model_pricing as mp pricing = self._pricing() self.cards.set_budget( ut.budget_status(self.ctx.config), pricing, lambda v, a, b: mp.convert(v, a, b, self.ctx.config)) def _retranslate(self) -> None: self._title.setText(tr("dashboard.title")) self.refresh_btn.setToolTip(tr("dashboard.refresh_tooltip")) self.currency_lbl.setText(tr("monitoring.overview_currency")) self.currency_combo.setToolTip(tr("dashboard.currency_tooltip")) self.habits_panel.ai_analyze_btn.setText(tr("dashboard.ai_analyze_btn")) self.habits_panel.ai_analyze_btn.setToolTip(tr("dashboard.ai_analyze_tooltip")) self.habits_panel.apply_strategy_btn.setText(tr("dashboard.strategy_btn")) self.habits_panel.apply_strategy_btn.setToolTip(tr("dashboard.strategy_tooltip")) self.habits_panel.title.setText(tr("dashboard.habits_title")) self.chart_panel.title.setText(tr("dashboard.chart_title")) self.chart_panel.prev_btn.setToolTip(tr("dashboard.chart_prev")) self.chart_panel.next_btn.setToolTip(tr("dashboard.chart_next")) self.budget_card.apply_btn.setToolTip(tr("usage.budget_apply_tooltip")) self.budget_card.budget_spin.setToolTip(tr("usage.budget_spin_tooltip")) self.refresh() # ---- nạp dữ liệu ----------------------------------------------------- def refresh(self) -> None: start, end = self._period_range() events = ut.load_events(start, end) pricing = self._pricing() s = ut.summarize(events) self.cards.set_usage(s, ut.cost_usd_events(events, pricing), pricing) self.habits_panel.set_summary(s, bool(events)) # Biểu đồ đọc TOÀN BỘ sự kiện rồi tự cắt theo khoảng — nó cần cả khoảng # liền trước để vẽ đường so sánh, thứ không nằm trong (start, end). self.chart_panel.refresh(ut.load_events(), pricing) self._refresh_budget()