Feature/fsg gamma team ui fix #3
@@ -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"},
|
||||
|
||||
@@ -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
@@ -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 ""
|
||||
|
||||
Reference in New Issue
Block a user