token_usage_card_widget.py 93 5 thẻ số liệu + thẻ Ngân sách
usage_chart_widget.py 119 biểu đồ tuần/tháng/năm + đường so sánh
habits_widget.py 171 thói quen dùng token + nhận xét của AI
Ba widget THẬT, không phải mixin — khác với shell và Co4E, ba mảng này tách
bạch trên màn hình và không đọc state của nhau. Giao tiếp bằng signal:
budget_applied, filter_changed, status_message.
Điểm cần biết: các nút lật khoảng và hai ô chọn thuộc về UsageChartWidget
nhưng được Dashboard nhấc lên hàng điều khiển ở trên. Chúng là control của
biểu đồ, chỉ hiển thị ở chỗ khác.
Giữ 18 cầu tương thích cho tên cũ vì check_dashboard, check_design_parity và
check_controls_alive đọc thẳng self.card_total, self._chart_period_lbl...
756 test xanh. check_dashboard, check_design_parity, check_controls_alive qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
120 lines
4.9 KiB
Python
120 lines
4.9 KiB
Python
"""Biểu đồ mức dùng theo khoảng thời gian — R08-T13.
|
||
|
||
Cắt khoảng đang chọn thành từng phần: TUẦN → 7 ngày (T2–CN) · THÁNG → các
|
||
tuần W1…Wn · NĂM → 12 tháng.
|
||
|
||
Đường nét đứt là mốc so sánh với khoảng liền trước cùng loại — "tuần trước" ở
|
||
chế độ tuần, "tháng trước" ở chế độ tháng. Nó vẽ ở mức trung bình mỗi điểm của
|
||
khoảng trước để nằm đúng thang đo, còn nhãn thì hiện % thay đổi của tổng.
|
||
|
||
Các nút lật khoảng và hai ô chọn nằm ở đây nhưng được đặt lên hàng điều khiển
|
||
của Dashboard — chúng thuộc về biểu đồ, chỉ hiển thị ở chỗ khác.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from PySide6.QtCore import Qt, Signal
|
||
from PySide6.QtWidgets import (
|
||
QComboBox, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget,
|
||
)
|
||
|
||
from ...core import usage_tracker as ut
|
||
from ...i18n import tr
|
||
from ...theme import current_palette
|
||
from ...ui.icons import icon
|
||
from ...ui.spline_chart import SplineChart
|
||
from ...ui.widgets import fmt_tokens as _fmt_tokens
|
||
|
||
_REF_KEY = {"week": "dashboard.ref_last_week",
|
||
"month": "dashboard.ref_last_month",
|
||
"year": "dashboard.ref_last_year"}
|
||
|
||
|
||
class UsageChartWidget(QWidget):
|
||
#: Người dùng đổi khoảng/độ mịn/chỉ số — Dashboard nạp lại cả màn, không
|
||
#: riêng biểu đồ, vì thẻ số liệu cũng đi theo bộ lọc.
|
||
filter_changed = Signal()
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.offset = 0 # 0 = khoảng hiện tại; số âm = lùi về trước
|
||
|
||
# --- các control, sẽ được Dashboard nhấc lên hàng điều khiển ---
|
||
self.prev_btn = QPushButton()
|
||
self.prev_btn.setIcon(icon("chevron-left"))
|
||
self.prev_btn.setFixedWidth(30)
|
||
self.prev_btn.clicked.connect(self._lui)
|
||
|
||
self.period_lbl = QLabel()
|
||
self.period_lbl.setObjectName("hint")
|
||
self.period_lbl.setAlignment(Qt.AlignCenter)
|
||
self.period_lbl.setMinimumWidth(170)
|
||
|
||
self.next_btn = QPushButton()
|
||
self.next_btn.setIcon(icon("chevron-right"))
|
||
self.next_btn.setFixedWidth(30)
|
||
self.next_btn.clicked.connect(self._toi)
|
||
|
||
self.gran_combo = QComboBox()
|
||
self.metric_combo = QComboBox()
|
||
self.metric_combo.currentIndexChanged.connect(self.filter_changed)
|
||
|
||
# --- thân widget: tiêu đề + biểu đồ ---
|
||
v = QVBoxLayout(self)
|
||
v.setContentsMargins(0, 0, 0, 0)
|
||
head = QHBoxLayout()
|
||
self.title = QLabel()
|
||
self.title.setStyleSheet("font-weight:600;")
|
||
head.addWidget(self.title, 1)
|
||
v.addLayout(head)
|
||
self.chart = SplineChart()
|
||
v.addWidget(self.chart)
|
||
|
||
# ---- lật khoảng ------------------------------------------------------
|
||
|
||
def _lui(self) -> None:
|
||
self.offset -= 1
|
||
self.filter_changed.emit()
|
||
|
||
def _toi(self) -> None:
|
||
# Không cho đi quá khoảng hiện tại: tương lai thì chưa có dữ liệu.
|
||
if self.offset < 0:
|
||
self.offset += 1
|
||
self.filter_changed.emit()
|
||
|
||
# ---- vẽ --------------------------------------------------------------
|
||
|
||
def refresh(self, events, pricing: dict) -> None:
|
||
gran = self.gran_combo.currentData() or "week"
|
||
metric = self.metric_combo.currentData() or "cost"
|
||
parts = ut.period_breakdown(events, gran, pricing, offset=self.offset)
|
||
|
||
mi = 0 if metric == "tokens" else 1 # (nhãn, token, tiền)
|
||
pts = [(row[0], float(row[mi + 1])) for row in parts]
|
||
|
||
# Dạng tiền rút gọn: hộp nhãn trục y hẹp, format_cost đầy đủ (tới 4 số
|
||
# lẻ với USD) tràn ra ngoài và che mất con số.
|
||
fmt = (_fmt_tokens if metric == "tokens"
|
||
else (lambda v: ut.format_cost_compact(v, pricing)))
|
||
|
||
cur = ut.period_totals(events, gran, pricing, self.offset)
|
||
prev = ut.period_totals(events, gran, pricing, self.offset - 1)
|
||
refs = []
|
||
if prev[mi] > 0:
|
||
# Màu mờ có chủ ý: đây là mốc tham chiếu, không được tranh chấp
|
||
# với đường dữ liệu màu nhấn.
|
||
refs.append((prev[mi] / max(1, len(parts)),
|
||
"%s %s" % (tr(_REF_KEY.get(gran, _REF_KEY["week"])),
|
||
_delta(cur[mi], prev[mi])),
|
||
current_palette().text_muted))
|
||
self.chart.set_reference_lines(refs)
|
||
self.chart.set_data(pts, fmt, tr("dashboard.metric_%s" % metric))
|
||
self.period_lbl.setText(ut.period_range_label(gran, self.offset))
|
||
self.next_btn.setEnabled(self.offset < 0)
|
||
|
||
|
||
def _delta(cur: float, prev: float) -> str:
|
||
if prev <= 0:
|
||
return ""
|
||
pct = (cur - prev) / prev * 100.0
|
||
return "(%+.0f%%)" % pct
|