## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Presentation settings package: Section widgets for provider, connector, routing, and general settings."""
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Mục Chung trong Cài đặt — R08-T07.
|
||||
|
||||
Ngôn ngữ, giao diện, khay hệ thống, và dòng gợi ý cuối trang.
|
||||
|
||||
Bản trước khi tách dựng mục này thành một ``QFormLayout`` rời, gắn vào layout
|
||||
gốc, rồi ở đoạn lắp ráp lại gỡ ra để nhét vào hộp riêng — kèm một widget cao
|
||||
0 pixel làm mốc cuộn. Vòng vo đó chỉ tồn tại vì mục này không phải group box
|
||||
như bốn mục kia. Gói thành widget là hết: nó tự là một trang, không cần gỡ ra
|
||||
gắn vào, không cần mốc giả.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtWidgets import QFormLayout, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from ...i18n import LANGUAGES, tr
|
||||
from ...ui.widgets import SegmentedControl, ToggleSwitch
|
||||
|
||||
|
||||
class GeneralSettingsWidget(QWidget):
|
||||
"""Nhóm "Chung" trong Cài đặt: ngôn ngữ, giao diện, khởi động cùng hệ thống,
|
||||
thu về khay.
|
||||
"""
|
||||
def __init__(self, ctx, parent=None):
|
||||
"""Nhóm Cài đặt chung: ngôn ngữ, giao diện, thư mục dùng chung."""
|
||||
super().__init__(parent)
|
||||
data = ctx.config.data
|
||||
outer = QVBoxLayout(self)
|
||||
outer.setContentsMargins(0, 0, 0, 0)
|
||||
form = QFormLayout()
|
||||
|
||||
self.language_combo = SegmentedControl()
|
||||
for key, label in LANGUAGES.items():
|
||||
self.language_combo.addItem(label, key)
|
||||
_select(self.language_combo, ctx.config.language)
|
||||
form.addRow(tr("settings.language"), self.language_combo)
|
||||
|
||||
# Giao diện cũng có trên hàng tài khoản ở thanh bên (một cú bấm để lật
|
||||
# nhanh); đây là cùng một giá trị, nhưng có tên và có giải thích, cho
|
||||
# người đi tìm nó trong Cài đặt.
|
||||
self.theme_combo = SegmentedControl()
|
||||
for key in ("system", "dark", "light"):
|
||||
self.theme_combo.addItem(tr(f"settings.theme_{key}"), key)
|
||||
_select(self.theme_combo, getattr(ctx.config, "theme", "system"))
|
||||
form.addRow(tr("settings.theme"), self.theme_combo)
|
||||
|
||||
tray = data.get("tray", {})
|
||||
self.tray_chk = ToggleSwitch(tr("settings.tray_keep"))
|
||||
self.tray_chk.setChecked(bool(tray.get("minimize_on_close", True)))
|
||||
form.addRow("", self.tray_chk)
|
||||
self.notify_chk = ToggleSwitch(tr("settings.tray_notify"))
|
||||
self.notify_chk.setChecked(bool(tray.get("notify_on_done", True)))
|
||||
form.addRow("", self.notify_chk)
|
||||
|
||||
outer.addLayout(form)
|
||||
|
||||
note = QLabel(tr("settings.tip"))
|
||||
note.setObjectName("hint")
|
||||
note.setWordWrap(True) # không thì đúng một dòng này quyết định bề ngang dialog
|
||||
outer.addWidget(note)
|
||||
outer.addStretch(1)
|
||||
|
||||
# ---- lưu ------------------------------------------------------------
|
||||
|
||||
def apply_to(self, data: dict) -> None:
|
||||
"""Ghi các lựa chọn chung vào dict cấu hình.
|
||||
|
||||
Không tự áp theme ở đây: ``MainWindow._open_settings`` áp lại sau khi hộp
|
||||
thoại đóng, làm cả hai chỗ sẽ vẽ lại giao diện hai lần.
|
||||
"""
|
||||
data["language"] = self.language_combo.currentData()
|
||||
# MainWindow._open_settings áp lại giao diện sau khi dialog đóng, nên
|
||||
# ghi giá trị ở đây là đủ để nó có hiệu lực.
|
||||
data["theme"] = self.theme_combo.currentData()
|
||||
tray = data.setdefault("tray", {})
|
||||
tray["minimize_on_close"] = self.tray_chk.isChecked()
|
||||
tray["notify_on_done"] = self.notify_chk.isChecked()
|
||||
|
||||
|
||||
def _select(combo, value: str) -> None:
|
||||
"""Chọn mục mang dữ liệu ``value`` trong một combo; không có thì để nguyên."""
|
||||
i = combo.findData(value)
|
||||
if i >= 0:
|
||||
combo.setCurrentIndex(i)
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Mục Tham số trong Cài đặt — R08-T07.
|
||||
|
||||
Bóc từ ``ui/settings_dialog.py``. Ba nhóm con: đính kèm, cấu trúc/GraphRAG,
|
||||
và giới hạn tài nguyên sandbox.
|
||||
|
||||
Lưu ý khi đọc: nhóm thứ ba **hiện** ở đây nhưng **lưu** vào ``agent_security``
|
||||
chứ không phải một khoá riêng — nó vốn nằm ở mục Bảo mật sandbox rồi được dời
|
||||
sang đây cho gần các con số khác. Chỗ hiện và chỗ lưu khác nhau, đừng gộp.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtWidgets import QFormLayout, QGroupBox, QLabel, QSpinBox
|
||||
|
||||
from ...i18n import tr
|
||||
|
||||
|
||||
class ParameterSettingsWidget(QGroupBox):
|
||||
"""Nhóm "Tham số" trong Cài đặt: hạn mức token cho tệp đính kèm, ngữ cảnh,
|
||||
số lượt song song và các ngưỡng khác.
|
||||
"""
|
||||
def __init__(self, ctx, parent=None):
|
||||
"""Nhóm Tham số: giới hạn token, ngưỡng rủi ro và các mức chặn của agent."""
|
||||
super().__init__(tr("settings.group.parameter"), parent)
|
||||
data = ctx.config.data
|
||||
sec = ctx.config.agent_security
|
||||
form = QFormLayout(self)
|
||||
|
||||
def tieu_de(key: str) -> None:
|
||||
"""Thêm một dòng tiêu đề phụ vào form."""
|
||||
lbl = QLabel(tr(key))
|
||||
lbl.setStyleSheet("font-weight:600; margin-top:6px;")
|
||||
form.addRow(lbl)
|
||||
|
||||
# --- đính kèm ---
|
||||
att = data.get("attachments", {})
|
||||
tieu_de("settings.group.attachments")
|
||||
self.attach_files = QSpinBox()
|
||||
self.attach_files.setRange(1, 50)
|
||||
self.attach_files.setSuffix(tr("settings.max_files_suffix"))
|
||||
self.attach_files.setValue(max(1, int(att.get("max_files", 20))))
|
||||
self.attach_files.setToolTip(tr("settings.max_files_tooltip"))
|
||||
self.attach_tokens = QSpinBox()
|
||||
self.attach_tokens.setRange(1, 1000)
|
||||
self.attach_tokens.setSingleStep(5)
|
||||
self.attach_tokens.setSuffix(tr("settings.max_per_file_suffix"))
|
||||
# Lưu theo token, hiện theo nghìn token.
|
||||
self.attach_tokens.setValue(max(1, int(att.get("max_tokens", 500000)) // 1000))
|
||||
self.attach_tokens.setToolTip(tr("settings.max_per_file_tooltip"))
|
||||
form.addRow(tr("settings.max_files"), self.attach_files)
|
||||
form.addRow(tr("settings.max_per_file"), self.attach_tokens)
|
||||
|
||||
# --- cấu trúc / GraphRAG ---
|
||||
st = data.get("structure", {})
|
||||
tieu_de("settings.group.structure")
|
||||
self.struct_nodes = QSpinBox()
|
||||
self.struct_nodes.setRange(0, 100000)
|
||||
self.struct_nodes.setSpecialValueText(tr("settings.unlimited"))
|
||||
self.struct_nodes.setSuffix(tr("settings.nodes_suffix"))
|
||||
self.struct_nodes.setValue(max(0, int(st.get("max_nodes", 500))))
|
||||
self.struct_nodes.setToolTip(tr("settings.nodes_tooltip"))
|
||||
self.struct_edges = QSpinBox()
|
||||
self.struct_edges.setRange(0, 200000)
|
||||
self.struct_edges.setSpecialValueText(tr("settings.unlimited"))
|
||||
self.struct_edges.setSuffix(tr("settings.edges_suffix"))
|
||||
self.struct_edges.setValue(max(0, int(st.get("max_edges", 500))))
|
||||
self.struct_edges.setToolTip(tr("settings.edges_tooltip"))
|
||||
form.addRow(tr("settings.max_nodes"), self.struct_nodes)
|
||||
form.addRow(tr("settings.max_edges"), self.struct_edges)
|
||||
|
||||
# --- giới hạn sandbox (lưu vào agent_security) ---
|
||||
tieu_de("settings.group.sandbox_limits")
|
||||
self.sandbox_cpu = _gioi_han(" %", int(sec.get("resource_limit_cpu_percent", 0) or 0),
|
||||
hi=100_000)
|
||||
form.addRow(tr("settings.sandbox_cpu_label"), self.sandbox_cpu)
|
||||
self.sandbox_memory = _gioi_han(" MB", int(sec.get("resource_limit_memory_mb", 2048) or 2048))
|
||||
form.addRow(tr("settings.sandbox_memory_label"), self.sandbox_memory)
|
||||
self.sandbox_disk = _gioi_han(" MB", int(sec.get("resource_limit_disk_mb", 2048) or 2048))
|
||||
form.addRow(tr("settings.sandbox_disk_label"), self.sandbox_disk)
|
||||
|
||||
# ---- lưu ------------------------------------------------------------
|
||||
|
||||
def apply_to(self, data: dict) -> None:
|
||||
"""Ghi các tham số từ form vào dict cấu hình.
|
||||
|
||||
Ô nhập tính bằng NGHÌN token cho dễ đọc, nên phải nhân 1000 khi lưu.
|
||||
"""
|
||||
att = data.setdefault("attachments", {})
|
||||
att["max_tokens"] = self.attach_tokens.value() * 1000
|
||||
att["max_files"] = self.attach_files.value()
|
||||
st = data.setdefault("structure", {})
|
||||
st["max_nodes"] = self.struct_nodes.value()
|
||||
st["max_edges"] = self.struct_edges.value()
|
||||
|
||||
def apply_limits_to(self, sec: dict) -> None:
|
||||
"""Ba con số này thuộc ``agent_security``, không thuộc ``structure``."""
|
||||
sec["resource_limit_cpu_percent"] = self.sandbox_cpu.value()
|
||||
sec["resource_limit_memory_mb"] = self.sandbox_memory.value()
|
||||
sec["resource_limit_disk_mb"] = self.sandbox_disk.value()
|
||||
|
||||
|
||||
def _gioi_han(suffix: str, value: int, hi: int = 1_000_000) -> QSpinBox:
|
||||
"""Ô nhập số nguyên có hậu tố đơn vị, chặn trong khoảng 0..``hi``."""
|
||||
box = QSpinBox()
|
||||
box.setRange(0, hi)
|
||||
box.setSuffix(suffix)
|
||||
box.setSpecialValueText(tr("settings.sandbox_unlimited")) # 0 = không giới hạn
|
||||
box.setValue(value)
|
||||
return box
|
||||
@@ -0,0 +1,255 @@
|
||||
"""Mục AI Provider trong Cài đặt — R08-T07.
|
||||
|
||||
Chọn nhà cung cấp, base URL, API key, model — kèm hai nút Tải model và Test
|
||||
kết nối chạy ở luồng nền.
|
||||
|
||||
Điểm cần biết khi sửa: widget giữ **bản nháp cho từng provider**
|
||||
(``_staging``). Người dùng đổi sang provider khác rồi quay lại thì thấy đúng
|
||||
những gì mình vừa gõ, dù chưa bấm Lưu. Nếu đọc thẳng từ config thay vì từ bản
|
||||
nháp là mất phần đang gõ dở.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QSizePolicy, QWidget,
|
||||
)
|
||||
|
||||
from ...config import PROVIDER_LABELS
|
||||
from ...core.worker import AgentWorker
|
||||
from ...i18n import tr
|
||||
from ...ui.icons import icon
|
||||
|
||||
|
||||
class ProviderSettingsWidget(QGroupBox):
|
||||
"""Nhóm "Nhà cung cấp AI" trong hộp thoại Cài đặt.
|
||||
|
||||
Giữ một BẢN NHÁP riêng cho từng provider (``_staging``): đổi qua lại giữa
|
||||
các provider trong lúc chỉnh không được làm mất thứ vừa gõ, và chỉ khi
|
||||
bấm Lưu mới ghi xuống cấu hình thật.
|
||||
"""
|
||||
def __init__(self, ctx, parent=None):
|
||||
"""Nhóm Provider: endpoint, khoá API và model của từng provider.
|
||||
|
||||
Sửa trên một bản nháp (``_staging``) chứ không sửa thẳng cấu hình: người dùng
|
||||
bấm Huỷ thì mọi thay đổi biến mất, kể cả khoá API vừa gõ.
|
||||
"""
|
||||
super().__init__(tr("settings.group.provider"), parent)
|
||||
self.ctx = ctx
|
||||
data = ctx.config.data
|
||||
self._workers = []
|
||||
|
||||
self._staging: Dict[str, dict] = {
|
||||
key: dict(conf) for key, conf in data["providers"].items()
|
||||
}
|
||||
self.provider_combo = QComboBox()
|
||||
for key, label in PROVIDER_LABELS.items():
|
||||
self.provider_combo.addItem(label, key)
|
||||
_select(self.provider_combo, ctx.config.active_provider)
|
||||
self._current_key = self.provider_combo.currentData()
|
||||
|
||||
conf = self._staging.get(self._current_key, {})
|
||||
self.prov_base = QLineEdit(conf.get("base_url", ""))
|
||||
self.prov_key = QLineEdit(conf.get("api_key", ""))
|
||||
self.prov_key.setEchoMode(QLineEdit.Password)
|
||||
self.prov_model = _model_combo(conf.get("model", ""))
|
||||
self.prov_status = QLabel("")
|
||||
self.prov_status.setObjectName("hint")
|
||||
self.prov_status.setWordWrap(True)
|
||||
|
||||
form = QFormLayout(self)
|
||||
form.addRow(tr("settings.active_provider"), self.provider_combo)
|
||||
form.addRow(tr("settings.base_url"), self.prov_base)
|
||||
form.addRow(tr("settings.api_key"), self.prov_key)
|
||||
form.addRow(tr("settings.model"), self._hang_model())
|
||||
form.addRow("", self.prov_status)
|
||||
|
||||
self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
|
||||
|
||||
# ---- lưu ------------------------------------------------------------
|
||||
|
||||
def apply_to(self, data: dict) -> None:
|
||||
"""Ghi provider đang chọn và toàn bộ bản nháp vào dict cấu hình."""
|
||||
data["active_provider"] = self.provider_combo.currentData()
|
||||
self._stash()
|
||||
for key, staged in self._staging.items():
|
||||
data["providers"].setdefault(key, {}).update({
|
||||
"base_url": staged.get("base_url", ""),
|
||||
"api_key": staged.get("api_key", ""),
|
||||
"model": staged.get("model", ""),
|
||||
})
|
||||
|
||||
# ---- bản nháp từng provider -----------------------------------------
|
||||
|
||||
def _stash(self) -> None:
|
||||
"""Cất nội dung đang gõ vào bản nháp của provider hiện tại."""
|
||||
self._staging.setdefault(self._current_key, {}).update({
|
||||
"base_url": self.prov_base.text().strip(),
|
||||
"api_key": self.prov_key.text(),
|
||||
"model": self.prov_model.currentText().strip(),
|
||||
})
|
||||
|
||||
def _on_provider_changed(self) -> None:
|
||||
"""Đổi provider: cất bản nháp cũ rồi nạp bản nháp mới lên các ô nhập."""
|
||||
self._stash()
|
||||
self._current_key = self.provider_combo.currentData()
|
||||
conf = self._staging.get(self._current_key, {})
|
||||
self.prov_base.setText(conf.get("base_url", ""))
|
||||
self.prov_key.setText(conf.get("api_key", ""))
|
||||
self.prov_model.clear()
|
||||
if conf.get("model"):
|
||||
self.prov_model.addItem(conf["model"])
|
||||
self.prov_model.setCurrentText(conf["model"])
|
||||
else:
|
||||
self.prov_model.setCurrentText("")
|
||||
self.prov_status.setText("")
|
||||
|
||||
def _conf_hien_tai(self, provider: str) -> dict:
|
||||
"""Cấu hình dùng để thử kết nối/nạp model.
|
||||
|
||||
Provider đang mở thì lấy thẳng từ ô nhập (kể cả thứ chưa lưu); provider
|
||||
khác thì lấy từ bản nháp đã cất.
|
||||
"""
|
||||
if provider == self._current_key:
|
||||
return {"base_url": self.prov_base.text().strip(),
|
||||
"api_key": self.prov_key.text(),
|
||||
"model": self.prov_model.currentText().strip()}
|
||||
conf = self._staging.get(provider, {})
|
||||
return {"base_url": conf.get("base_url", ""), "api_key": conf.get("api_key", ""),
|
||||
"model": conf.get("model", "")}
|
||||
|
||||
# ---- hàng model + hai nút -------------------------------------------
|
||||
|
||||
def _hang_model(self) -> QWidget:
|
||||
"""Dựng hàng "Model": ô chọn model kèm nút nạp danh sách và nút thử kết nối."""
|
||||
row = QWidget()
|
||||
lay = QHBoxLayout(row)
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.addWidget(self.prov_model, 1)
|
||||
|
||||
btn = QPushButton(tr("settings.load"))
|
||||
btn.setIcon(icon("download"))
|
||||
btn.setToolTip(tr("settings.load_tooltip"))
|
||||
btn.clicked.connect(lambda: self._load_models(self.provider_combo.currentData()))
|
||||
lay.addWidget(btn)
|
||||
|
||||
test_btn = QPushButton(tr("settings.test_connection"))
|
||||
test_btn.setIcon(icon("flask"))
|
||||
test_btn.setToolTip(tr("settings.test_connection_tooltip"))
|
||||
test_btn.clicked.connect(lambda: self._test_connection(self.provider_combo.currentData()))
|
||||
lay.addWidget(test_btn)
|
||||
|
||||
# Hai nút giữ kích thước tự nhiên, combo là thứ phải nhường. Không có
|
||||
# dòng này thì bề rộng tối thiểu của hàng bằng combo cộng cả hai nút,
|
||||
# không co lại được, và dialog sinh ra thanh cuộn ngang.
|
||||
for b in (btn, test_btn):
|
||||
b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
row.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
|
||||
return row
|
||||
|
||||
# ---- việc chạy nền ---------------------------------------------------
|
||||
|
||||
def _load_models(self, provider: str) -> None:
|
||||
"""Nạp danh sách model của provider ở luồng nền.
|
||||
|
||||
Giữ nguyên model đang chọn ở đầu danh sách, kể cả khi máy chủ không trả
|
||||
về nó — người dùng có thể đang dùng một model không được liệt kê.
|
||||
"""
|
||||
conf = self._conf_hien_tai(provider)
|
||||
combo, status = self.prov_model, self.prov_status
|
||||
|
||||
def job(worker):
|
||||
"""Chạy nền: hỏi provider danh sách model."""
|
||||
from ...providers import build_provider
|
||||
prov = build_provider(provider, conf)
|
||||
return {"models": prov.list_models(), "error": getattr(prov, "last_error", "")}
|
||||
|
||||
def done(result):
|
||||
"""Đổ danh sách vào ô chọn, giữ model đang chọn ở đầu."""
|
||||
models = result.get("models") or []
|
||||
current = combo.currentText().strip()
|
||||
combo.clear()
|
||||
if current:
|
||||
combo.addItem(current)
|
||||
for m in models:
|
||||
if m != current:
|
||||
combo.addItem(m)
|
||||
combo.setCurrentText(current)
|
||||
if models:
|
||||
status.setText(tr("settings.loaded_models", n=len(models),
|
||||
provider=PROVIDER_LABELS.get(provider, provider)))
|
||||
else:
|
||||
status.setText(tr("settings.load_models_error",
|
||||
err=result.get("error", "")
|
||||
or tr("settings.load_models_error_unknown")))
|
||||
|
||||
self._chay_nen(job, done,
|
||||
lambda e: status.setText(tr("settings.load_failed", err=e)),
|
||||
tr("settings.loading_models"))
|
||||
|
||||
def _test_connection(self, provider: str) -> None:
|
||||
"""Thử kết nối tới provider ở luồng nền, tô xanh/đỏ kết quả."""
|
||||
conf = self._conf_hien_tai(provider)
|
||||
status = self.prov_status
|
||||
|
||||
def job(worker):
|
||||
"""Chạy nền: gọi thử provider để xác nhận kết nối."""
|
||||
from ...providers import build_provider
|
||||
ok, message = build_provider(provider, conf).test_connection()
|
||||
return {"ok": ok, "message": message}
|
||||
|
||||
def done(result):
|
||||
"""Hiện kết quả thử, tô xanh khi thành công và đỏ khi thất bại."""
|
||||
status.setText(result.get("message", ""))
|
||||
status.setStyleSheet("color: #090;" if result.get("ok") else "color: #c00;")
|
||||
|
||||
def failed(e):
|
||||
"""Thử kết nối ném lỗi: hiện lỗi bằng màu đỏ."""
|
||||
status.setText(str(e))
|
||||
status.setStyleSheet("color: #c00;")
|
||||
|
||||
self._chay_nen(job, done, failed, tr("settings.testing_connection"))
|
||||
|
||||
def _chay_nen(self, job, done, failed, dang_lam: str) -> None:
|
||||
"""Chạy một việc ở luồng nền và hiện dòng trạng thái trong lúc chờ.
|
||||
|
||||
Giữ tham chiếu tới worker trong ``self._workers``: để nó bị thu gom giữa
|
||||
chừng là luồng chết lặng lẽ, không lỗi, không kết quả.
|
||||
"""
|
||||
w = AgentWorker(job)
|
||||
w.finished_ok.connect(done)
|
||||
w.failed.connect(failed)
|
||||
# Giữ tham chiếu: worker bị thu gom giữa chừng là luồng chết lặng lẽ.
|
||||
self._workers.append(w)
|
||||
self.prov_status.setText(dang_lam)
|
||||
w.start()
|
||||
|
||||
|
||||
def _model_combo(value: str) -> QComboBox:
|
||||
"""Ô chọn model cho phép gõ tay.
|
||||
|
||||
Ép co lại theo bề rộng được cấp: mặc định Qt cho combo rộng bằng mục dài
|
||||
nhất, mà id model thì rất dài — hàng đó sẽ tràn ra ngoài hộp thoại và đẻ ra
|
||||
thanh cuộn ngang (tệ hơn ở màn 125%/150%). Phần bung ra để popup lo.
|
||||
"""
|
||||
combo = QComboBox()
|
||||
combo.setEditable(True)
|
||||
# Mặc định combo rộng bằng mục dài nhất; id model thì dài, nên hàng này
|
||||
# tràn ra ngoài dialog và đẻ ra thanh cuộn ngang (tệ hơn ở màn 125%/150%).
|
||||
# Cho nó co lại, phần bung ra để popup lo.
|
||||
combo.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
||||
combo.setMinimumContentsLength(8)
|
||||
combo.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed)
|
||||
if value:
|
||||
combo.addItem(value)
|
||||
combo.setCurrentText(value)
|
||||
return combo
|
||||
|
||||
|
||||
def _select(combo: QComboBox, value: str) -> None:
|
||||
"""Chọn mục mang dữ liệu ``value`` trong một combo; không có thì để nguyên."""
|
||||
i = combo.findData(value)
|
||||
if i >= 0:
|
||||
combo.setCurrentIndex(i)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Mục Auto Model Routing trong Cài đặt — R08-T07.
|
||||
|
||||
Bóc từ ``ui/settings_dialog.py`` (khối dòng 254-309 của bản trước khi tách).
|
||||
Widget tự dựng control, tự nạp giá trị, tự ghi trả về dict cấu hình. Dialog
|
||||
chỉ còn việc đặt nó vào chỗ và gọi ``apply_to`` lúc lưu.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QComboBox, QFormLayout, QGroupBox, QLabel, QLineEdit, QPushButton, QSpinBox,
|
||||
)
|
||||
|
||||
from ...i18n import tr
|
||||
|
||||
#: Các chế độ định tuyến. Danh sách này phải khớp ``config.py::AppConfig
|
||||
#: .ROUTING_MODES`` — Delta thêm "fallback" ở R03-T03 và nếu quên đồng bộ
|
||||
#: chỗ này thì người dùng không chọn được chế độ đó, mà không có lỗi nào báo.
|
||||
MODE_KEYS = (("off", "routing.mode_off"), ("auto", "routing.mode_auto"),
|
||||
("manual", "routing.mode_manual"))
|
||||
|
||||
POLICY_KEYS = (("quality", "routing.policy_quality"), ("cost", "routing.policy_cost"),
|
||||
("latency", "routing.policy_latency"),
|
||||
("balanced", "routing.policy_balanced"))
|
||||
|
||||
|
||||
class RoutingSettingsWidget(QGroupBox):
|
||||
"""Nhóm "Định tuyến model" trong Cài đặt: chế độ, chính sách, ngưỡng đổi và
|
||||
cấu hình trọng tài chấm điểm.
|
||||
"""
|
||||
def __init__(self, ctx, parent=None):
|
||||
"""Nhóm Định tuyến: chế độ tự chọn model và các tham số thăm dò."""
|
||||
super().__init__(tr("routing.settings_group"), parent)
|
||||
self.ctx = ctx
|
||||
routing = ctx.config.routing
|
||||
form = QFormLayout(self)
|
||||
|
||||
self.mode = QComboBox()
|
||||
for value, key in MODE_KEYS:
|
||||
self.mode.addItem(tr(key), value)
|
||||
_select(self.mode, routing.get("switch_mode", "off"))
|
||||
form.addRow(tr("routing.settings_mode"), self.mode)
|
||||
|
||||
self.policy = QComboBox()
|
||||
for value, key in POLICY_KEYS:
|
||||
self.policy.addItem(tr(key), value)
|
||||
_select(self.policy, routing.get("policy", "balanced"))
|
||||
form.addRow(tr("routing.settings_policy"), self.policy)
|
||||
|
||||
# Lưu dạng phân lẻ (0..1) nhưng hiện dạng phần trăm.
|
||||
self.min_gain = QSpinBox()
|
||||
self.min_gain.setRange(0, 100)
|
||||
self.min_gain.setSuffix(" %")
|
||||
self.min_gain.setValue(int(round(float(routing.get("min_score_gain", 0.05)) * 100)))
|
||||
form.addRow(tr("routing.settings_min_gain"), self.min_gain)
|
||||
|
||||
self.timeout = QSpinBox()
|
||||
self.timeout.setRange(5, 600)
|
||||
self.timeout.setSuffix(" s")
|
||||
self.timeout.setValue(int(routing.get("confirm_timeout_sec", 60) or 60))
|
||||
form.addRow(tr("routing.settings_timeout"), self.timeout)
|
||||
|
||||
self.interval = QSpinBox()
|
||||
self.interval.setRange(0, 720)
|
||||
self.interval.setSpecialValueText(tr("routing.mode_off")) # 0 = tắt
|
||||
self.interval.setSuffix(" h")
|
||||
self.interval.setValue(int(routing.get("reassess_interval_hours", 24) or 0))
|
||||
form.addRow(tr("routing.settings_interval"), self.interval)
|
||||
|
||||
self.concurrency = QSpinBox()
|
||||
self.concurrency.setRange(1, 16)
|
||||
self.concurrency.setValue(int(routing.get("per_provider_concurrency", 2) or 2))
|
||||
form.addRow(tr("routing.settings_concurrency"), self.concurrency)
|
||||
|
||||
self.judge = QLineEdit(routing.get("judge_model", ""))
|
||||
form.addRow(tr("routing.settings_judge"), self.judge)
|
||||
|
||||
self.reassess_btn = QPushButton(tr("routing.settings_reassess_now"))
|
||||
self.reassess_btn.clicked.connect(self._reassess_now)
|
||||
form.addRow("", self.reassess_btn)
|
||||
|
||||
hint = QLabel(tr("routing.settings_hint"))
|
||||
hint.setObjectName("hint")
|
||||
hint.setWordWrap(True)
|
||||
form.addRow(hint)
|
||||
|
||||
# ---- lưu ------------------------------------------------------------
|
||||
|
||||
def apply_to(self, data: dict) -> None:
|
||||
"""Ghi cấu hình định tuyến từ form vào dict cấu hình."""
|
||||
r = data.setdefault("routing", {})
|
||||
r["switch_mode"] = self.mode.currentData()
|
||||
r["policy"] = self.policy.currentData()
|
||||
r["min_score_gain"] = self.min_gain.value() / 100.0
|
||||
r["confirm_timeout_sec"] = self.timeout.value()
|
||||
r["reassess_interval_hours"] = self.interval.value()
|
||||
r["per_provider_concurrency"] = self.concurrency.value()
|
||||
r["judge_model"] = self.judge.text().strip()
|
||||
|
||||
# ---- đánh giá lại ngay ----------------------------------------------
|
||||
|
||||
def _reassess_now(self) -> None:
|
||||
"""Chạy đánh giá lại model ở nền."""
|
||||
try:
|
||||
service = self.ctx.routing()
|
||||
if service.is_reassessing():
|
||||
return
|
||||
self.reassess_btn.setEnabled(False)
|
||||
self.reassess_btn.setText(tr("routing.reassessing"))
|
||||
|
||||
def _done(result) -> None:
|
||||
self.reassess_btn.setEnabled(True)
|
||||
self.reassess_btn.setText(
|
||||
tr("routing.reassess_done", count=len(result or {})))
|
||||
|
||||
service.reassess_background(on_done=_done)
|
||||
except Exception: # noqa: BLE001 — bấm đánh giá lại không được làm sập Cài đặt
|
||||
self.reassess_btn.setEnabled(True)
|
||||
self.reassess_btn.setText(tr("routing.settings_reassess_now"))
|
||||
|
||||
|
||||
def _select(combo: QComboBox, value: str) -> None:
|
||||
"""Chọn mục mang dữ liệu ``value`` trong một combo; không có thì để nguyên."""
|
||||
i = combo.findData(value)
|
||||
if i >= 0:
|
||||
combo.setCurrentIndex(i)
|
||||
Reference in New Issue
Block a user