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>
94 lines
4.4 KiB
Python
94 lines
4.4 KiB
Python
"""Hàng thẻ số liệu trên đầu Dashboard — R08-T13.
|
||
|
||
Năm con số (tổng / vào / ra / cache / chi phí) cộng thẻ Ngân sách.
|
||
|
||
Bố cục không phải sáu ô bằng nhau: chi phí là con số màn hình này sinh ra để
|
||
trả lời, nên nó chiếm một thẻ cao gấp đôi bên trái, bốn con số phụ xếp 2×2
|
||
bên cạnh. Sáu ô bằng nhau thì không có gì nói cho người dùng biết cái nào
|
||
đáng nhìn trước.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from PySide6.QtCore import Signal
|
||
from PySide6.QtWidgets import QGridLayout, QWidget
|
||
|
||
from ...core import usage_tracker as ut
|
||
from ...i18n import tr
|
||
from ...ui.icons import icon
|
||
from ...ui.widgets import BudgetCard, StatCard
|
||
from ...ui.widgets import fmt_tokens as _fmt_tokens
|
||
|
||
|
||
class TokenUsageCardWidget(QWidget):
|
||
#: Người dùng bấm Áp dụng trên thẻ Ngân sách. Widget không tự ghi cấu hình
|
||
#: — nó chỉ báo; ai sở hữu cấu hình thì người đó ghi.
|
||
budget_applied = Signal(float)
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
grid = QGridLayout(self)
|
||
grid.setContentsMargins(0, 0, 0, 0)
|
||
grid.setSpacing(8)
|
||
|
||
self.card_total = StatCard()
|
||
self.card_in = StatCard()
|
||
self.card_out = StatCard()
|
||
self.card_cache = StatCard()
|
||
self.card_cost = StatCard().as_hero()
|
||
|
||
# Thẻ chi phí bên trái, chiếm cả hai hàng; bốn con số phụ lấp khối 2×2.
|
||
grid.addWidget(self.card_cost, 0, 0, 2, 1)
|
||
for i, card in enumerate((self.card_total, self.card_in,
|
||
self.card_out, self.card_cache)):
|
||
grid.addWidget(card, i // 2, 1 + i % 2)
|
||
|
||
self.budget_card = BudgetCard()
|
||
self.budget_card.apply_btn.setIcon(icon("check"))
|
||
self.budget_card.apply_btn.clicked.connect(
|
||
lambda: self.budget_applied.emit(self.budget_card.budget_spin.value()))
|
||
grid.addWidget(self.budget_card, 0, 3, 2, 1)
|
||
|
||
# Cột thẻ chi phí và cột Ngân sách rộng hơn bốn ô nhỏ.
|
||
for col, stretch in ((0, 3), (1, 2), (2, 2), (3, 3)):
|
||
grid.setColumnStretch(col, stretch)
|
||
|
||
# ---- nạp số liệu -----------------------------------------------------
|
||
|
||
def set_usage(self, s: dict, costs: dict, pricing: dict) -> None:
|
||
est_note = (tr("dashboard.estimated_note", pct=int(s["estimated_share"] * 100))
|
||
if s["estimated_share"] > 0 else "")
|
||
self.card_total.set(tr("dashboard.card_total"), _fmt_tokens(s["total"]),
|
||
tr("dashboard.card_turns", n=s["turns"]))
|
||
self.card_in.set(tr("dashboard.card_in"), _fmt_tokens(s["in"]),
|
||
ut.format_cost(costs["in"], pricing))
|
||
self.card_out.set(tr("dashboard.card_out"), _fmt_tokens(s["out"]),
|
||
ut.format_cost(costs["out"], pricing))
|
||
self.card_cache.set(tr("dashboard.card_cache"), _fmt_tokens(s["cache"]),
|
||
ut.format_cost(costs["cache"], pricing))
|
||
self.card_cost.set(tr("dashboard.card_cost"),
|
||
ut.format_cost(sum(costs.values()), pricing, digits=2),
|
||
est_note)
|
||
|
||
def set_budget(self, status, pricing: dict, convert) -> None:
|
||
"""``status`` là None khi người dùng chưa đặt ngân sách nào."""
|
||
if status is None:
|
||
self.budget_card.set(tr("usage.budget_title"), "—",
|
||
tr("usage.budget_no_budget"))
|
||
self.budget_card.budget_spin.setValue(0.0)
|
||
return
|
||
|
||
ccy = pricing.get("currency", "USD")
|
||
value = ("%s / %s" % (ut.format_cost(status["remaining_usd"], pricing, digits=2),
|
||
ut.format_cost(status["amount_usd"], pricing, digits=2)))
|
||
pct = int(round(status["pct_used"] * 100))
|
||
sub = (tr("usage.budget_over_warning") if status["over_85"]
|
||
else tr("usage.budget_used_pct", pct=pct))
|
||
self.budget_card.set(tr("usage.budget_title"), value, sub,
|
||
warn=status["over_85"])
|
||
|
||
# Ô nhập giữ giá trị ngân sách HIỆN TẠI, nhưng không giẫm lên thứ người
|
||
# dùng đang gõ dở.
|
||
if not self.budget_card.budget_spin.hasFocus():
|
||
self.budget_card.budget_spin.setValue(
|
||
round(convert(status["amount_usd"], "USD", ccy), 2))
|