Feature/fsg gamma team ui fix #3

Merged
gitea-admin merged 41 commits from feature/FSG_GammaTeam_UI_Fix into main 2026-08-20 12:12:59 +00:00
3 changed files with 190 additions and 12 deletions
Showing only changes of commit 9393fc1748 - Show all commits
+10 -4
View File
@@ -198,7 +198,7 @@ STRINGS: Dict[str, Dict[str, str]] = {
"vi": "Tạo project trước"},
# ---- workspace_tab.py (Projects — Claude-Projects style) -----------
"workspace.header": {"en": "Workspace — Projects", "ja": "ワークスペース — プロジェクト", "vi": "Workspace — Projects"},
"workspace.header": {"en": "Manage projects", "ja": "プロジェクト管理", "vi": "Quản lý project"},
"workspace.tab_cowork": {"en": "Cowork", "ja": "Cowork", "vi": "Cowork"},
"workspace.tab_graphrag": {"en": "GraphRAG", "ja": "GraphRAG", "vi": "GraphRAG"},
"workspace.tab_project": {"en": "Project", "ja": "プロジェクト", "vi": "Project"},
@@ -378,6 +378,12 @@ STRINGS: Dict[str, Dict[str, str]] = {
"các file đặt ở gốc thư mục đó (project knowledge)."),
},
"workspace.new_project": {"en": "New project", "ja": "新規プロジェクト", "vi": "Project mới"},
"workspace.projects_heading": {"en": "PROJECTS", "ja": "プロジェクト", "vi": "PROJECT"},
"workspace.folder_label": {"en": "Workspace folder", "ja": "作業フォルダ", "vi": "Thư mục làm việc"},
"workspace.counts": {
"en": "{chats} chats · {tasks} tasks",
"ja": "チャット {chats} · タスク {tasks}",
"vi": "{chats} đoạn chat · {tasks} task"},
"workspace.delete": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
"workspace.delete_confirm": {
"en": "Delete project “{name}”? Its conversations and files are kept (threads move to General).",
@@ -391,19 +397,19 @@ STRINGS: Dict[str, Dict[str, str]] = {
"vi": "Project của hội thoại này không còn tồn tại — không thể mở."},
"workspace.name": {"en": "Name", "ja": "名前", "vi": "Tên"},
"workspace.description": {"en": "Description", "ja": "説明", "vi": "Mô tả"},
"workspace.instructions": {"en": "Instructions (shared project context)", "ja": "指示(プロジェクト共有コンテキスト)", "vi": "Instructions (ngữ cảnh chung của project)"},
"workspace.instructions": {"en": "Instructions", "ja": "Instructions", "vi": "Instructions"},
"workspace.instructions_placeholder": {
"en": "e.g. \"All answers in Vietnamese. We are building the X reporting tool; always follow the naming rules …\"",
"ja": "例:「回答はすべて日本語で。X レポートツールを開発中。命名規則に従うこと …」",
"vi": "vd: \"Trả lời bằng tiếng Việt. Team đang xây tool báo cáo X; luôn theo quy tắc đặt tên …\"",
},
"workspace.browse": {"en": "Change folder…", "ja": "フォルダ変更…", "vi": "Đổi thư mục…"},
"workspace.browse": {"en": "Change", "ja": "変更", "vi": "Đổi"},
"workspace.browse_tooltip": {
"en": "Choose the project's workspace folder (agent sandbox + shared knowledge root)",
"ja": "プロジェクトのワークスペースフォルダを選択(エージェントのサンドボックス+共有ナレッジのルート)",
"vi": "Chọn thư mục workspace của project (sandbox của agent + gốc chứa knowledge chung)",
},
"workspace.open_folder": {"en": "Open folder", "ja": "フォルダを開く", "vi": "Mở thư mục"},
"workspace.open_folder": {"en": "Open", "ja": "開く", "vi": "Mở"},
"workspace.save": {"en": "Save project", "ja": "プロジェクトを保存", "vi": "Lưu project"},
"workspace.saved": {"en": "Saved project {name}.", "ja": "プロジェクト {name} を保存しました。", "vi": "Đã lưu project {name}."},
"workspace.threads": {"en": "Conversations in this project", "ja": "このプロジェクトの会話", "vi": "Hội thoại trong project này"},
+115
View File
@@ -0,0 +1,115 @@
"""Workspace ▸ Project against its wireframe (section 4).
The drawing: a "Quản lý project" title with + Project mới on its right, a caps
PROJECT heading over the list, every row carrying "N đoạn chat · M task", and
the form reading Tên / Mô tả / Instructions / Thư mục làm việc.
"""
from __future__ import annotations
import os
import sys
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO.parent))
sys.path.insert(0, str(Path(__file__).resolve().parent))
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from capture_screens import ( # noqa: E402
_apply_theme, _freeze_schedulers, _isolate_home, _load_fonts)
def main() -> int:
sandbox = _isolate_home()
from PySide6.QtCore import QPoint
from PySide6.QtWidgets import QApplication, QLabel
app = QApplication([])
_load_fonts()
_freeze_schedulers()
_apply_theme(app)
from cowork_local.config import CONFIG_DIR
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
from seed_demo_data import seed
seed()
from cowork_local.app import MainWindow
from cowork_local.config import AppConfig
from cowork_local.i18n import set_language, tr
from cowork_local.state import AppContext
set_language("vi")
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
win.resize(1400, 900)
win.show()
app.processEvents()
win._goto(win._ROW_WORKSPACE, win.workspace._project_tab_idx)
app.processEvents()
w = win.workspace
fails = []
# 1. title, and the create button on its row, to its right
print(f"tieu de: {w._header.text()!r}")
if w._header.text() != tr("workspace.header"):
fails.append("tieu de khong phai workspace.header")
h_pos = w._header.mapTo(w, QPoint(0, 0))
h_mid = h_pos.y() + w._header.height() // 2
b_pos = w._new_btn.mapTo(w, QPoint(0, 0))
b_mid = b_pos.y() + w._new_btn.height() // 2
aligned = abs(b_mid - h_mid) <= 8
after = b_pos.x() >= h_pos.x() + w._header.width()
print(f"nut '+ Project mới': tam y={b_mid} (tieu de {h_mid}) thang hang={aligned} "
f"| x={b_pos.x()} sau tieu de={after}")
if not (aligned and after):
fails.append("nut tao project khong nam cung hang, ben phai tieu de")
# 2. caps heading over the list
hdr = w._projects_hdr.text()
print(f"tieu de pane trai: {hdr!r}")
if not hdr or hdr != hdr.upper():
fails.append(f"tieu de pane trai chua viet hoa: {hdr!r}")
# 3. every row says how much is in the project
lst = w.project_list
if not lst.count():
fails.append("khong co project nao de kiem")
shown = 0
for i in range(lst.count()):
row = lst.itemWidget(lst.item(i))
labels = [l.text() for l in row.findChildren(QLabel)] if row else []
if len(labels) < 2:
fails.append(f"hang {i} khong co dong dem chat/task")
continue
shown += 1
if i < 2:
print(f" hang {i}: {labels[0]!r} / {labels[1]!r}")
# the sub-line must be the counts string, not the name repeated
if labels[1] == labels[0] or not any(ch.isdigit() for ch in labels[1]):
fails.append(f"hang {i}: dong phu khong phai so dem: {labels[1]!r}")
print(f"so hang co dong dem: {shown}/{lst.count()}")
# 4. the form reads as the drawing labels it
want = [tr("workspace.name"), tr("workspace.description"),
tr("workspace.instructions"), tr("workspace.folder_label")]
seen = [l.text() for l in w.findChildren(QLabel) if l.isVisible() and l.text()]
for label in want:
if label not in seen:
fails.append(f"thieu nhan {label!r}")
print(f"nhan form: {want}")
print()
for f in fails:
print("FAIL " + f)
print("PASS man Project khop ban ve" if not fails else f"{len(fails)} problem(s)")
sys.stdout.flush()
os._exit(1 if fails else 0)
if __name__ == "__main__":
raise SystemExit(main())
+65 -8
View File
@@ -32,6 +32,25 @@ from .osutil import open_folder
from .widgets import CollapseStrip
class _ProjectRow(QWidget):
"""A project in the list: its name, and under it how much is in it.
The drawing gives every row a second line — "2 đoạn chat · 3 task" — which
is the only thing on this screen that says a project holds anything at all.
"""
def __init__(self, name: str, counts: str):
super().__init__()
lay = QVBoxLayout(self)
lay.setContentsMargins(6, 4, 6, 4)
lay.setSpacing(0)
title = QLabel(name)
sub = QLabel(counts)
sub.setObjectName("hint")
lay.addWidget(title)
lay.addWidget(sub)
class WorkspaceTab(QWidget):
status_message = Signal(str)
open_chat = Signal(str, dict) # kind, conversation — open a thread in Cowork
@@ -98,12 +117,23 @@ class WorkspaceTab(QWidget):
self._sidebar = sidebar
root = QVBoxLayout(self)
# Title row — the drawing puts "+ Project mới" up here beside the title,
# not at the foot of the project list where it read as belonging to the
# list's own controls.
self._header = QLabel()
self._header.setStyleSheet("font-weight:700; font-size:15px;")
self._new_btn = QPushButton()
self._new_btn.setIcon(icon("plus"))
self._new_btn.setObjectName("primary")
self._new_btn.clicked.connect(self._create)
title_row = QHBoxLayout()
title_row.addWidget(self._header)
title_row.addStretch(1)
title_row.addWidget(self._new_btn)
root.addLayout(title_row)
self._hint = QLabel()
self._hint.setObjectName("hint")
self._hint.setWordWrap(True)
root.addWidget(self._header)
root.addWidget(self._hint)
self._split = QSplitter(Qt.Horizontal)
@@ -115,6 +145,9 @@ class WorkspaceTab(QWidget):
ll = QVBoxLayout(left)
ll.setContentsMargins(0, 0, 0, 0)
left_hdr = QHBoxLayout()
self._projects_hdr = QLabel()
self._projects_hdr.setObjectName("navSectionHdr")
left_hdr.addWidget(self._projects_hdr)
self._proj_collapse_btn = QPushButton()
self._proj_collapse_btn.setIcon(collapse_left_icon())
self._proj_collapse_btn.setFixedWidth(28)
@@ -126,15 +159,13 @@ class WorkspaceTab(QWidget):
self.project_list.currentItemChanged.connect(self._on_select)
ll.addWidget(self.project_list, 1)
btns = QHBoxLayout()
self._new_btn = QPushButton()
self._new_btn.setIcon(icon("plus"))
self._new_btn.setObjectName("primary")
self._new_btn.clicked.connect(self._create)
# Delete stays under the list it acts on. The drawing does not show it,
# but it does not show it moved either, and dropping a control is not
# something a layout pass gets to do.
self._del_btn = QPushButton()
self._del_btn.setIcon(icon("trash"))
self._del_btn.clicked.connect(self._delete)
btns.addWidget(self._new_btn, 1)
btns.addWidget(self._del_btn)
btns.addWidget(self._del_btn, 1)
ll.addLayout(btns)
self._projects_panel = left
@@ -240,6 +271,8 @@ class WorkspaceTab(QWidget):
rl.addWidget(self._instr_lbl)
rl.addWidget(self.instr_edit)
self._folder_hdr = QLabel()
rl.addWidget(self._folder_hdr)
folder_row = QHBoxLayout()
self.folder_lbl = QLabel()
self.folder_lbl.setObjectName("hint")
@@ -394,6 +427,8 @@ class WorkspaceTab(QWidget):
def _retranslate(self) -> None:
self._header.setText(tr("workspace.header"))
self._hint.setText(tr("workspace.hint"))
self._projects_hdr.setText(tr("workspace.projects_heading").upper())
self._folder_hdr.setText(tr("workspace.folder_label"))
self._new_btn.setText(tr("workspace.new_project"))
self._del_btn.setText(tr("workspace.delete"))
self._name_lbl.setText(tr("workspace.name"))
@@ -491,21 +526,43 @@ class WorkspaceTab(QWidget):
from ..core.projects import list_projects
keep = self._current_id
counts = self._project_counts()
self.project_list.blockSignals(True)
self.project_list.clear()
row_to_select = 0
for i, p in enumerate(list_projects()):
item = QListWidgetItem(p.name)
chats, tasks = counts.get(p.project_id, (0, 0))
item = QListWidgetItem()
item.setData(Qt.UserRole, p.project_id)
if p.description:
item.setToolTip(p.description)
self.project_list.addItem(item)
row = _ProjectRow(p.name, tr("workspace.counts", chats=chats, tasks=tasks))
item.setSizeHint(row.sizeHint())
self.project_list.setItemWidget(item, row)
if p.project_id == keep:
row_to_select = i
self.project_list.blockSignals(False)
self.project_list.setCurrentRow(row_to_select)
self._load_current()
@staticmethod
def _project_counts():
"""{project_id: (chats, tasks)} — read once per refresh, not per row."""
from ..core.history import list_conversations
from ..core.tasks import list_tasks
out: dict = {}
for conv in list_conversations():
pid = conv.get("project_id") or "default"
chats, tasks = out.get(pid, (0, 0))
out[pid] = (chats + 1, tasks)
for task in list_tasks():
pid = task.get("project_id") or "default"
chats, tasks = out.get(pid, (0, 0))
out[pid] = (chats, tasks + 1)
return out
def _selected_id(self) -> str:
item = self.project_list.currentItem()
return item.data(Qt.UserRole) if item else ""