"""Phần thói quen dùng token, và nhận xét của AI — R08-T13. Hai phần chồng lên nhau: * **Tóm tắt thói quen** — dựng tại chỗ từ số liệu đã gộp: việc nào tốn token nhất, chia theo nguồn, trung bình mỗi lượt, ngày và giờ bận nhất. * **Nhận xét của AI** — chỉ chạy khi người dùng bấm. Gửi đi **các con số đã gộp**, không bao giờ gửi nội dung câu chat gốc; đó là ranh giới cố ý. Nút "áp dụng chiến lược tiết kiệm" chỉ hiện sau khi có nhận xét, và vẫn hỏi lại trước khi đổi cấu hình — nó bật nén tự động và hạ ngưỡng nén, tức là đổi hành vi của mọi lượt chat sau đó. """ from __future__ import annotations from typing import List from PySide6.QtCore import Signal from PySide6.QtWidgets import ( QHBoxLayout, QLabel, QMessageBox, QPushButton, QTextBrowser, QVBoxLayout, QWidget, ) from ...core import usage_tracker as ut from ...core.worker import AgentWorker from ...i18n import tr from ...ui.icons import icon from ...ui.widgets import fmt_tokens as _fmt_tokens class HabitsWidget(QWidget): #: Có gì cần nói với người dùng (lỗi, đã áp dụng xong…). status_message = Signal(str) #: Người dùng đồng ý áp dụng chiến lược tiết kiệm. strategy_approved = Signal() def __init__(self, ctx, parent=None): super().__init__(parent) self.ctx = ctx self._worker = None v = QVBoxLayout(self) v.setContentsMargins(0, 0, 0, 0) head = QHBoxLayout() self.title = QLabel() self.title.setStyleSheet("font-weight:600;") self.apply_strategy_btn = QPushButton() self.apply_strategy_btn.setIcon(icon("bolt")) self.apply_strategy_btn.setVisible(False) self.apply_strategy_btn.clicked.connect(self._ap_dung) self.ai_analyze_btn = QPushButton() self.ai_analyze_btn.setIcon(icon("sparkle")) self.ai_analyze_btn.clicked.connect(self.phan_tich) head.addWidget(self.title, 1) head.addWidget(self.apply_strategy_btn) head.addWidget(self.ai_analyze_btn) v.addLayout(head) self.habits = QTextBrowser() self.habits.setOpenExternalLinks(False) v.addWidget(self.habits) self.ai_title = QLabel() self.ai_title.setStyleSheet("font-weight:600;") self.ai_title.setVisible(False) v.addWidget(self.ai_title) self.ai_advice = QTextBrowser() self.ai_advice.setVisible(False) v.addWidget(self.ai_advice) # ---- tóm tắt --------------------------------------------------------- def set_summary(self, s: dict, co_du_lieu: bool) -> None: lines: List[str] = [] if not co_du_lieu: lines.append("%s" % tr("dashboard.no_data")) self.habits.setHtml("".join(lines)) return lines.append("%s" % tr("dashboard.h_top")) lines.append("
    ") for label, tok in s["top_labels"]: pct = int(tok * 100 / s["total"]) if s["total"] else 0 lines.append("
  1. %s — %s tokens (%d%%)
  2. " % (label[:60], _fmt_tokens(tok), pct)) lines.append("
") src_parts = ", ".join( "%s: %s" % (tr("app.tab.%s" % k) if k in ("cowork", "code") else k, _fmt_tokens(v)) for k, v in s["by_source"]) lines.append("%s: %s
" % (tr("dashboard.h_by_source"), src_parts)) lines.append("%s: %s tokens
" % (tr("dashboard.h_avg"), _fmt_tokens(s["avg_per_turn"]))) if s["busiest_day"]: lines.append("%s: %s
" % (tr("dashboard.h_busiest_day"), s["busiest_day"])) if s["busiest_hour"] is not None: h = s["busiest_hour"] lines.append("%s: %02d:00–%02d:59
" % (tr("dashboard.h_busiest_hour"), h, h)) if s["estimated_share"] > 0: lines.append("%s" % tr("dashboard.estimated_note", pct=int(s["estimated_share"] * 100))) self.habits.setHtml("".join(lines)) # ---- nhận xét của AI ------------------------------------------------- def phan_tich(self) -> None: """Gửi CÁC CON SỐ ĐÃ GỘP (không bao giờ gửi nội dung chat) cho provider đang chọn, rồi hiện nhận xét về thói quen và cách tiết kiệm token.""" if self._worker is not None: return events = ut.load_events(*self._khoang()) if not events: self.status_message.emit(tr("dashboard.no_data")) return summary = ut.summarize(events) self.ai_analyze_btn.setEnabled(False) self.ai_analyze_btn.setText(tr("dashboard.ai_analyzing")) ctx = self.ctx def job(worker: AgentWorker): from ...i18n import get_language prompt = ut.build_ai_analysis_prompt(summary, get_language()) reply = ctx.build_active_provider().chat( [{"role": "user", "content": prompt}], cancel=worker.stop_event) return {"text": (reply.get("content") or "").strip()} def done(result: dict) -> None: self._xong() text = result.get("text") or "" if text: self.ai_title.setText(tr("dashboard.ai_advice_title")) self.ai_title.setVisible(True) self.ai_advice.setMarkdown(text) self.ai_advice.setVisible(True) self.apply_strategy_btn.setVisible(True) def failed(err: str) -> None: self._xong() self.status_message.emit(str(err)) w = AgentWorker(job) w.finished_ok.connect(done) w.failed.connect(failed) self._worker = w w.start() def _xong(self) -> None: self._worker = None self.ai_analyze_btn.setEnabled(True) self.ai_analyze_btn.setText(tr("dashboard.ai_analyze_btn")) #: Dashboard gán hàm trả về (start, end) của khoảng đang lọc. _khoang = staticmethod(lambda: (None, None)) # ---- áp dụng chiến lược tiết kiệm ------------------------------------ def _ap_dung(self) -> None: if QMessageBox.question(self, tr("dashboard.strategy_title"), tr("dashboard.strategy_confirm")) != QMessageBox.Yes: return cx = self.ctx.config.data.setdefault("context", {}) cx["auto_compact"] = True cx["compact_threshold"] = 0.6 # nén ở 60% cửa sổ (trước ~80%) cx["compress_before_send"] = True # gộp ngữ cảnh trước mỗi lượt self.ctx.save() self.strategy_approved.emit() self.status_message.emit(tr("dashboard.strategy_applied"))