feat(ui): flat nav rail, compact assistant, responsive layouts
Implements the redesign from docs/ui-audit.html. Rearrangement only — no
feature was removed; every control that moved kept its handler, and the
gates that used to hide things now grey them out instead.
Navigation
* The rail is one flat list: the five Workspace sub-views sit at the top
level instead of behind an accordion, with Dashboard/Monitoring pinned
at the foot and Settings below them.
* Cowork and GraphRAG stay listed and greyed while no project is
selected, rather than vanishing and resizing the menu under the user.
* Monitoring keeps its eight sub-views in its own tab strip (unhidden)
instead of doubling the rail's length.
* _goto now moves the highlight itself, fixing a long-standing bug where
programmatic navigation left the rail pointing at the previous screen.
* Rail header gained the project picker and "New chat"; RECENTS lists the
active project's threads. Both are second views of existing state — the
Cowork toolbar button and the full History panel are untouched.
* Provider / language / theme moved from the top bar to an account row at
the foot of the rail (same widgets, same signals).
Screens
* Co4E: the flow tab strip is gone (per the design); Flow Status became a
toolbar toggle with its own way back, and the three icon-only tabs became
four labelled, foldable sections in one column. One flow open at a time
is the one capability this costs; background runs are unaffected.
* Dashboard: header split into two rows; cost promoted to a hero card.
* Monitoring Overview: one scrolling column of titled sections; the model
price table got its own full-width section instead of sharing a row with
the CPU meters.
* Settings and Task editor gained a section index down the left.
* Help dock: 84x64 launcher + chevron became one 26px dot that expands to
a labelled pill on hover; "hide to the edge" moved into the panel's menu.
Layout
* The window's minimum width dropped from 1453px to 768px. The main cause
was a QTabWidget taking its minimum from the widest page even when that
page is hidden, so Co4E was forcing Project and Cowork wide.
* Secondary panes fold themselves on a narrow window and restore when it
grows, never overriding a fold the user made.
* The long dialogs no longer scroll sideways at any font size.
Verification: tools/check_*.py build a real MainWindow offscreen against a
copy of ~/.cowork_local with the schedulers no-oped. check_design_parity.py
reads its checklist straight from the audit page's own proposals.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+317
-22
@@ -23,6 +23,20 @@ DOCS = REPO / "docs"
|
||||
MANIFEST = DOCS / "screens" / "manifest.json"
|
||||
OUT = DOCS / "ui-audit.html"
|
||||
|
||||
# Eight sections were written by hand and are richer than anything this script
|
||||
# produces. They live in a data module (rebuilt by tools/extract_handwritten.py)
|
||||
# and are merged in below, so ui-audit.html stays the ONE output file instead of
|
||||
# a generated file plus a hand-edited copy that drift apart.
|
||||
try:
|
||||
from audit_handwritten import EXTRA_CSS as HAND_CSS
|
||||
from audit_handwritten import EXTRA_JS as HAND_JS
|
||||
from audit_handwritten import SECTIONS as HAND_SECTIONS
|
||||
except ImportError: # pragma: no cover
|
||||
sys.path.insert(0, str(REPO / "tools"))
|
||||
from audit_handwritten import EXTRA_CSS as HAND_CSS
|
||||
from audit_handwritten import EXTRA_JS as HAND_JS
|
||||
from audit_handwritten import SECTIONS as HAND_SECTIONS
|
||||
|
||||
# Screenshots are inlined as data: URIs so the page is ONE self-contained file —
|
||||
# copy it anywhere and the images travel with it. `--external` opts out, leaving
|
||||
# the images as `screens/*.png` next to a much smaller HTML.
|
||||
@@ -43,11 +57,46 @@ STANDALONE = "--external" not in sys.argv
|
||||
RECENTS = ["📌 Gom số liệu doanh thu", "Dựng slide trình bày Q3"]
|
||||
|
||||
|
||||
def rail(active: str = "", project: str = "Báo cáo tài chính Q3") -> str:
|
||||
"""The proposed flat sidebar, with `active` highlighted."""
|
||||
# Floating on every screen, pinned bottom-right — same corner as the app.
|
||||
# The app spends 84×64px there: a 64px badge plus an 18px chevron beside it
|
||||
# (help_agent_widget.py:34-37). That is a lot of permanent real estate for a
|
||||
# thing you open a few times a day, so the proposal is one 26px dot. The label
|
||||
# moves to hover/tooltip and to the panel header; nothing is removed.
|
||||
DOCK_FAB = ('<div class="dock fab" title="AI Assistant — trợ lý cách dùng app">'
|
||||
'<span class="spark">✨</span></div>')
|
||||
DOCK_BADGE = DOCK_FAB # name kept: 16 screens already reference it
|
||||
|
||||
|
||||
def rail(active: str = "", project: str = "Báo cáo tài chính Q3",
|
||||
*, empty: bool = False) -> str:
|
||||
"""The proposed flat sidebar, with `active` highlighted.
|
||||
|
||||
`empty=True` renders the no-project state. Running the app with zero
|
||||
projects shows Cowork and GraphRAG simply *gone* from the menu; here they
|
||||
stay put but dimmed, and the actions that need a project are disabled with
|
||||
a reason rather than vanishing.
|
||||
"""
|
||||
items = ["Project", "Cowork", "Co4E", "Folder", "GraphRAG", "Schedule Task"]
|
||||
needs_project = {"Cowork", "GraphRAG"}
|
||||
rows = "".join(
|
||||
f'<div class="i{" on" if n == active else ""}">{n}</div>' for n in items)
|
||||
f'<div class="i{" on" if n == active else ""}'
|
||||
f'{" off" if empty and n in needs_project else ""}">{n}</div>'
|
||||
for n in items)
|
||||
if empty:
|
||||
return ('<div class="rail">'
|
||||
'<div class="menutog"><span>MENU</span><span class="chev">‹</span></div>'
|
||||
'<div class="rpick empty"><span>Chưa có project</span>'
|
||||
'<span class="cv">▾</span></div>'
|
||||
'<div class="newbtn off">+ Đoạn chat mới</div>'
|
||||
'<div class="hint">Tạo project trước</div>'
|
||||
f'{rows}'
|
||||
'<div class="sep"></div><div class="hd">RECENTS</div>'
|
||||
'<div class="i sm off">trống</div>'
|
||||
'<div class="grow"></div><div class="sep"></div>'
|
||||
'<div class="i">Dashboard</div><div class="i">Monitoring</div>'
|
||||
'<div class="i">Cài đặt</div>'
|
||||
'<div class="acct"><span>👤 local</span>'
|
||||
'<span class="lang">VN ▾</span><span class="thm">🌙</span></div></div>')
|
||||
recents = "".join(f'<div class="i sm">{t}</div>' for t in RECENTS)
|
||||
return (
|
||||
'<div class="rail">'
|
||||
@@ -65,7 +114,9 @@ def rail(active: str = "", project: str = "Báo cáo tài chính Q3") -> str:
|
||||
'<div class="grow"></div>'
|
||||
'<div class="sep"></div>'
|
||||
'<div class="i">Dashboard</div><div class="i">Monitoring</div>'
|
||||
'<div class="acct">👤 local · Ollama ▾</div>'
|
||||
'<div class="i">Cài đặt</div>'
|
||||
'<div class="acct"><span>👤 local</span>'
|
||||
'<span class="lang">VN ▾</span><span class="thm">🌙</span></div>'
|
||||
'</div>')
|
||||
|
||||
|
||||
@@ -92,6 +143,19 @@ def projbar(name: str = "") -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def grp(label: str, action: str = "", *, collapse: str = "") -> str:
|
||||
"""A list-group heading, optionally with its own create/manage button.
|
||||
|
||||
Putting "+" beside WORKFLOWS (and beside AGENTS) is what replaces the "+"
|
||||
that used to live on the flow tab strip: removing the strip removed its
|
||||
button too, and the create action has to land somewhere explicit.
|
||||
"""
|
||||
chev = {"left": "‹", "right": "›"}.get(collapse, "")
|
||||
tail = f'<span class="pchev">{chev}</span>' if chev else ""
|
||||
act = f'<span class="gact">{action}</span>' if action else ""
|
||||
return f'<div class="hd2 row">{label}<span class="ghd">{act}{tail}</span></div>'
|
||||
|
||||
|
||||
def li(text: str, sub: str = "", *, on: bool = False) -> str:
|
||||
"""One row in a list pane."""
|
||||
s = f'<span class="s">{sub}</span>' if sub else ""
|
||||
@@ -170,8 +234,13 @@ DESCRIPTIONS: dict[str, dict] = {
|
||||
"dialog-login": {"d": "Màn đăng nhập — <b>đã dựng xong nhưng không nơi nào gọi</b>. "
|
||||
"App khởi động thẳng với user \“local\”, quyền admin.",
|
||||
"r": [("3 trang", "Khởi tạo · Đăng nhập · Offline")]},
|
||||
"overlay-help-panel": {"d": "Robot trợ giúp nổi, có mặt trên mọi màn. Cố tình không có công cụ.",
|
||||
"r": [("3 trạng thái", "tab mép → huy hiệu → panel chat")]},
|
||||
"overlay-help-panel": {"d": "Trợ lý dùng app, nổi ở góc phải và có mặt trên mọi màn. "
|
||||
"Cố tình không có công cụ — chỉ hỏi đáp cách dùng.",
|
||||
"r": [("3 trạng thái", "tab mép phải → huy hiệu → panel 340×460, "
|
||||
"<b>luôn ghim góc dưới phải</b>"),
|
||||
("3 nút", "<b>›</b> ẩn vào cạnh phải · <b>—</b> thu nhỏ về huy hiệu · "
|
||||
"tab mép để hiện lại"),
|
||||
("Model", "chọn ở Monitoring ▸ Agents Admin, agent chức năng “help”")]},
|
||||
}
|
||||
|
||||
|
||||
@@ -281,21 +350,23 @@ ANALYSIS: dict[str, dict] = {
|
||||
'<div class="btn pri">▷ Chạy</div></div>'
|
||||
'<div class="r grow">'
|
||||
'<div class="c w26 pane">'
|
||||
'<div class="hd2 row">WORKFLOWS<span class="pchev">‹</span></div>'
|
||||
+ grp("WORKFLOWS", "+ Mới", collapse="left")
|
||||
+ li("Quy trình phát triển tính năng", "5 bước · đã lưu", on=True)
|
||||
+ li("Rà soát bảo mật định kỳ", "2 bước · đã lưu")
|
||||
+ li("Dựng báo cáo từ Excel", "3 bước · đã lưu")
|
||||
+ '<div class="hd2">AGENTS (5)</div>'
|
||||
+ grp("AGENTS (5)", "+ Mới")
|
||||
+ li("Phân tích yêu cầu", "ANALYST") + li("Thiết kế giải pháp", "ARCHITECT")
|
||||
+ li("Lập trình viên", "CODER") + li("Kiểm thử", "TESTER")
|
||||
+ li("Soạn tài liệu", "WRITER")
|
||||
+ '<div class="hd2">SKILLS (5)</div>'
|
||||
+ grp("SKILLS (5)", "Quản lý…")
|
||||
+ li("Rà soát bảo mật · Viết test trước · Chuẩn hoá Excel · …")
|
||||
+ '<div class="hd2">LẦN CHẠY (6)</div>'
|
||||
+ grp("LẦN CHẠY (6)")
|
||||
+ li("✓ Quy trình phát triển", "5/5 · 08-08 15:32")
|
||||
+ li("✕ Rà soát bảo mật", "3/5 · 08-06 16:32")
|
||||
+ li("■ Dựng báo cáo từ Excel", "1/5 · 08-04 18:32")
|
||||
+ '<div class="grow"></div></div>'
|
||||
+ '<div class="grow"></div>'
|
||||
'<div class="r tb"><div class="btn">✎</div><div class="btn">⧉</div>'
|
||||
'<div class="btn">🗑</div><div class="btn grow">▷ Chạy nền</div></div></div>'
|
||||
'<div class="c grow"><div class="b canvas grow">'
|
||||
'<div class="node ok">Phân tích yêu cầu</div><div class="arw">→</div>'
|
||||
'<div class="node ok">Thiết kế</div><div class="arw">→</div>'
|
||||
@@ -478,7 +549,18 @@ ANALYSIS: dict[str, dict] = {
|
||||
'<div class="hd2">SANDBOX & QUYỀN</div>'
|
||||
'<div class="r"><div class="b grow">Tệp: chỉ trong workspace · Mạng: chặn · '
|
||||
'Tiến trình: giới hạn 4</div></div>'
|
||||
'<div class="hd2">NHẬT KÝ GẦN ĐÂY</div>'
|
||||
'<div class="hd2 row">BẢNG GIÁ MODEL'
|
||||
'<span class="ghd"><span class="gact">Nhập · Xuất · Thêm · Tự dò</span>'
|
||||
'<span class="pchev">USD ▾</span></span></div>'
|
||||
'<div class="b tbl">'
|
||||
'<div class="tr th"><span>Model</span><span>Vào</span><span>Ra</span>'
|
||||
'<span>Cache</span><span>Đơn vị</span></div>'
|
||||
'<div class="tr"><span>qwen2.5-coder:7b</span><span>0.00</span><span>0.00</span>'
|
||||
'<span>0.00</span><span>/Mtok</span></div>'
|
||||
'<div class="tr"><span>gpt-4o-mini</span><span>0.15</span><span>0.60</span>'
|
||||
'<span>0.08</span><span>/Mtok</span></div></div>'
|
||||
'<div class="hd2 row">NHẬT KÝ GẦN ĐÂY'
|
||||
'<span class="ghd"><span class="gact">Xem tất cả</span></span></div>'
|
||||
'<div class="b grow"><span class="bad">✕ Chặn đọc personal.xlsx (ngoài sandbox)</span><br>'
|
||||
'<span class="ok">✓ pytest tests/test_stations.py → 4 passed</span><br>'
|
||||
'<span class="bad">✕ jira.create_issue — 401 token hết hạn</span></div>'
|
||||
@@ -509,6 +591,73 @@ ANALYSIS: dict[str, dict] = {
|
||||
+ li("image_gen", "Sinh ảnh — ☐ tắt")
|
||||
+ '<div class="grow"></div></div></div>'),
|
||||
},
|
||||
"overlay-help-panel": {
|
||||
"problems": [
|
||||
"<b>Hai vùng bấm cho một tính năng.</b> Huy hiệu mở, chevron ẩn — nằm sát nhau, "
|
||||
"dễ bấm nhầm.",
|
||||
"<b>Vùng bấm quá nhỏ.</b> Chevron rộng <b>18px</b>, tab mép <b>16px</b> "
|
||||
"(<code>help_agent_widget.py:36-38</code>) — dưới ngưỡng ~24px để bấm thoải mái, "
|
||||
"nhất là trên màn cảm ứng.",
|
||||
"<b>Ba trạng thái, thừa một.</b> “Nép mép” và “huy hiệu” đều nghĩa là <i>đang đóng</i>; "
|
||||
"người dùng phải học hai kiểu đóng và hai đường quay lại.",
|
||||
"<b>Chiếm 84×64px vĩnh viễn</b> ngay góc dưới phải (huy hiệu 64 + khe 2 + "
|
||||
"chevron 18 — <code>help_agent_widget.py:34-37</code>) — ở màn Cowork nó nằm đè "
|
||||
"lên vùng nút Gửi, dù cả ngày chỉ mở vài lần.",
|
||||
"Huy hiệu dùng <b>chính icon app</b> (<code>help_agent_widget.py:49-53</code>, "
|
||||
"dự phòng là glyph robot) nên nhìn không khác gì icon cửa sổ; nhãn "
|
||||
"“Trợ lý App” / “App Assistant” / “アプリアシスタント” nói <i>chỗ dùng</i> "
|
||||
"chứ không nói <i>nó là gì</i>.",
|
||||
],
|
||||
"changes": [
|
||||
"<b>Một chấm 26px, không chữ.</b> Bỏ luôn chevron rời — chỗ chiếm giảm từ "
|
||||
"<b>84×64 xuống 26×26</b> (<b>−88% diện tích</b>). Vẫn là một vùng bấm, "
|
||||
"26px ≥ ngưỡng bấm thoải mái.",
|
||||
"<b>Tên: “AI Assistant”</b> — giữ nguyên ở cả <b>3 ngôn ngữ</b>, sửa đúng một "
|
||||
"khoá <code>help_agent.title</code> (<code>i18n.py:470</code>) thay cho "
|
||||
"“App Assistant / Trợ lý App / アプリアシスタント”. Tên dài không còn là vấn đề "
|
||||
"vì nó không nằm trên màn lúc bình thường.",
|
||||
"<b>Chữ chỉ hiện khi rê chuột / focus bàn phím</b> — chấm nở thành pill "
|
||||
"“✨ AI Assistant”. Lúc bình thường màn hình không có chữ nào thừa.",
|
||||
"<b>“Ẩn trợ lý” dời vào menu ⋯</b> trong header panel, cạnh “Thu nhỏ”. "
|
||||
"Không mất chức năng — chỉ chuyển tới lúc người dùng <i>đang</i> tương tác.",
|
||||
"Thường ngày chỉ còn <b>2 trạng thái</b>: đóng ↔ mở. Ẩn hẳn thành lựa chọn hiếm.",
|
||||
"Tab mép nới từ <b>16px → 28px</b> cho bấm được.",
|
||||
"Ở màn có ô nhập dưới đáy (Cowork), chấm <b>nâng lên trên hàng nhập</b>, "
|
||||
"không đè nút Gửi.",
|
||||
],
|
||||
"wf": ('<div class="main dlg">'
|
||||
'<div class="r tb"><div class="ttl">Trợ lý — thu gọn còn một chấm</div></div>'
|
||||
'<div class="r grow">'
|
||||
# 1. at rest — drawn to scale beside the old footprint
|
||||
'<div class="c w24"><div class="lbl">Bình thường</div>'
|
||||
'<div class="b grow ctr2" style="gap:14px">'
|
||||
'<div class="oldbox">cũ 84×64</div>'
|
||||
'<div class="fab"><span class="spark">✨</span></div></div>'
|
||||
'<div class="s">26×26 · không chữ, không chevron · −88% diện tích</div></div>'
|
||||
# 2. hover — the label appears only on demand
|
||||
'<div class="c w24"><div class="lbl">Rê chuột / focus</div>'
|
||||
'<div class="b grow ctr2">'
|
||||
'<span class="fabpill"><span class="fab"><span class="spark">✨</span></span>'
|
||||
'AI Assistant</span></div>'
|
||||
'<div class="s">tên chỉ hiện lúc cần</div></div>'
|
||||
# 3. open — hide lives in the ⋯ menu
|
||||
'<div class="c grow"><div class="lbl">Mở — “Ẩn” nằm trong menu ⋯</div>'
|
||||
'<div class="b grow pnl">'
|
||||
'<div class="r tb phdr"><span class="spark">✨</span><b>AI Assistant</b>'
|
||||
'<div class="grow"></div><span class="mut">— ⋯</span></div>'
|
||||
'<div class="mnu"><div class="mi">Thu nhỏ về chấm</div>'
|
||||
'<div class="mi">Ẩn trợ lý vào cạnh phải</div>'
|
||||
'<div class="mi">Đổi model…</div></div>'
|
||||
'<div class="msg a">Xin chào Nam, mình giúp gì khi bạn dùng app?</div>'
|
||||
'<div class="grow"></div>'
|
||||
'<div class="r tb"><div class="inp grow">Hỏi về cách dùng app…</div>'
|
||||
'<div class="btn pri">Gửi</div></div></div></div>'
|
||||
# 4. hidden — wider edge tab
|
||||
'<div class="c w18"><div class="lbl">Đã ẩn</div>'
|
||||
'<div class="b grow ctr2"><div class="edge wide">‹</div></div>'
|
||||
'<div class="s">tab mép 28px</div></div>'
|
||||
'</div></div>'),
|
||||
},
|
||||
"dialog-settings": {
|
||||
"problems": [
|
||||
"Năm group cuộn dọc, không mục lục.",
|
||||
@@ -635,7 +784,15 @@ MOVES = {
|
||||
"self.project_list": "→ giữ ở màn Quản lý project + thêm thanh chọn đầu trang",
|
||||
"self.view_combo": "→ đổi thành cặp tab Kanban | Lịch",
|
||||
"self.flow_bar": "→ bỏ; chọn workflow từ danh sách trái",
|
||||
"self.flow_add_btn": "→ nút “+ Mới” cạnh tiêu đề WORKFLOWS "
|
||||
"(chỗ cũ là dải tab, đã bỏ nên phải có chỗ mới)",
|
||||
"self.ag_new_btn": "→ nút “+ Mới” cạnh tiêu đề AGENTS",
|
||||
"self.sk_manage_btn": "→ nút “Quản lý…” cạnh tiêu đề SKILLS",
|
||||
"self._msg_btn": "→ đổi thành cặp tab Đồ thị | Tin nhắn",
|
||||
# The chevron beside the launcher is 18px wide and sits next to a 64px
|
||||
# badge; the action survives, it just moves to where the user already is.
|
||||
"self.collapse_btn": "→ mục “Ẩn trợ lý vào cạnh phải” trong menu ⋯ ở header panel",
|
||||
"self.edge_tab": "Giữ — tab mép mở lại trợ lý, nới 16px → 28px",
|
||||
"self.search_edit": "→ lên sidebar cùng RECENTS",
|
||||
"self.search_btn": "→ lên sidebar cùng RECENTS",
|
||||
"self.refresh_btn": "→ lên sidebar cùng RECENTS",
|
||||
@@ -671,6 +828,45 @@ NEWCHAT = [
|
||||
"Giữ y nguyên — RECENTS refresh", "<span class='ok'>Không đổi.</span>"),
|
||||
]
|
||||
|
||||
# Surfaced by this audit, deliberately NOT done — each would add or change
|
||||
# behaviour, which the redesign's scope forbids.
|
||||
LATER = [
|
||||
("Xuất log cho Nhật ký gần đây",
|
||||
"Hiện chỉ có “Xem tất cả” nhảy sang Action Logs (<code>monitoring_tab.py:586</code>). "
|
||||
"Xuất ra tệp là chức năng mới.",
|
||||
"chức năng mới"),
|
||||
("Bỏ auto-refresh, thay bằng nút bấm",
|
||||
"Monitoring làm mới mỗi <b>3 giây</b> (<code>monitoring_tab.py:43</code>), "
|
||||
"Schedule <b>10 giây</b> (<code>schedule_task_tab.py:150</code>), "
|
||||
"Dashboard <b>30 giây</b> (<code>dashboard_tab.py:175</code>). "
|
||||
"Đề xuất: chỉ làm mới khi vào màn + một nút thủ công.",
|
||||
"đổi hành vi"),
|
||||
("Nút tạo skill mới",
|
||||
"Cả <code>SkillsDialog</code> lẫn <code>SkillManagerTab</code> đều không có. "
|
||||
"Chỉ tạo được qua AI / template / nhập / nhân bản. "
|
||||
"<code>SkillEditDialog</code> đã làm được việc này, chỉ thiếu lối vào.",
|
||||
"chức năng mới"),
|
||||
("Nút “chat mới” trong pane Lịch sử",
|
||||
"<code>sidebar.py:68</code> khai báo tín hiệu <code>new_chat</code>, "
|
||||
"<code>workspace_tab.py:241</code> đã nối — nhưng không nơi nào phát.",
|
||||
"hoàn thiện thứ đã dựng"),
|
||||
("Gọi <code>ensure_starter_project()</code>",
|
||||
"Hàm có docstring “đảm bảo luôn có ít nhất một project” nhưng không ai gọi, "
|
||||
"trong khi <code>refresh()</code> lại ghi “no auto-seed”. Hai chỗ mâu thuẫn.",
|
||||
"đổi hành vi"),
|
||||
("Mật khẩu Sandbox hard-code",
|
||||
"<code>settings_dialog.py:115</code> để mật khẩu mở khoá ngay trong mã nguồn.",
|
||||
"bảo mật"),
|
||||
("Hai lớp trùng tên <code>CustomAgent</code>",
|
||||
"<code>core/custom_agents.py:23</code> và <code>core/co4e.py:117</code> — "
|
||||
"khác trường, khác thư mục lưu.",
|
||||
"dọn mã"),
|
||||
("Sáu màn không có đường vào",
|
||||
"AccountsTab · LoginDialog · FlowBuilderDialog · AgentManagerTab · "
|
||||
"SkillManagerTab · McpServerEditDialog — tổng 64 control.",
|
||||
"quyết định giữ hay gỡ"),
|
||||
]
|
||||
|
||||
# Old location → new location for EVERY screen, so "nothing was removed" is
|
||||
# something the reader can check rather than take on trust.
|
||||
MAPPING = [
|
||||
@@ -912,7 +1108,7 @@ th,td{text-align:left;padding:6px 10px;border-bottom:1px solid var(--bd);vertica
|
||||
th{color:var(--mut);font-size:12px;text-transform:uppercase;letter-spacing:.05em}
|
||||
/* ---- wireframe vocabulary ---- */
|
||||
.wf{display:flex;height:570px;border:1px solid var(--bds);border-radius:var(--r);
|
||||
overflow:hidden;background:var(--bg);font-size:11px}
|
||||
overflow:hidden;background:var(--bg);font-size:11px;position:relative}
|
||||
/* The rail must never crop: its bottom group is real navigation. */
|
||||
.wf .rail{overflow:visible}
|
||||
.wf .rail{width:150px;flex:none;background:var(--nav);border-right:1px solid var(--navb);
|
||||
@@ -925,6 +1121,10 @@ padding:5px 7px;margin-bottom:5px;font-weight:600;display:flex;align-items:cente
|
||||
justify-content:space-between;gap:4px;line-height:1.3}
|
||||
.wf .rpick>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.wf .rpick .cv{color:var(--mut);font-weight:400;flex:none}
|
||||
.wf .rpick.empty{color:var(--fnt);font-weight:400;font-style:italic}
|
||||
.wf .i.off,.wf .newbtn.off{opacity:.45}
|
||||
.wf .newbtn.off{background:var(--bds);color:var(--tx)}
|
||||
.wf .hint{font-size:9px;color:var(--fnt);text-align:center;padding:2px 0 6px;font-style:italic}
|
||||
.wf .newbtn{flex:none}
|
||||
.wf .i{padding:5px 7px;border-radius:4px;color:var(--tx)}
|
||||
.wf .i.on{background:var(--navs);border-left:2px solid var(--ac);font-weight:600}
|
||||
@@ -933,7 +1133,11 @@ justify-content:space-between;gap:4px;line-height:1.3}
|
||||
.wf .scope{font-size:10px;font-weight:600;color:var(--tx);padding:2px 7px 4px}
|
||||
.wf .i.allp{color:var(--ac);font-style:italic}
|
||||
.wf .sep{height:1px;background:var(--navb);margin:5px 0}
|
||||
.wf .acct{margin-top:6px;padding:6px 7px;border-top:1px solid var(--navb);color:var(--mut);font-size:10px}
|
||||
.wf .acct{margin-top:6px;padding:6px 7px;border-top:1px solid var(--navb);
|
||||
color:var(--mut);font-size:10px;display:flex;align-items:center;gap:5px}
|
||||
.wf .acct .lang{margin-left:auto;background:var(--rz);border:1px solid var(--bds);
|
||||
border-radius:3px;padding:1px 5px;color:var(--tx);font-weight:600}
|
||||
.wf .acct .thm{font-size:11px}
|
||||
.wf .main,.wf .c{display:flex;flex-direction:column;gap:6px;padding:10px;flex:1;min-width:0}
|
||||
.wf .main.dlg{border:none}
|
||||
.wf .ttl{font-weight:700;font-size:13px;white-space:nowrap}
|
||||
@@ -963,6 +1167,50 @@ font-weight:700}
|
||||
margin-top:2px}
|
||||
.wf .hd2.row{display:flex;align-items:center;justify-content:space-between}
|
||||
.wf .pchev{color:var(--mut);font-size:12px;font-weight:400}
|
||||
.wf .ghd{display:flex;align-items:center;gap:6px}
|
||||
.wf .gact{color:var(--ac);font-weight:600;font-size:9.5px;letter-spacing:0}
|
||||
.wf .b.tbl{padding:0;overflow:hidden}
|
||||
.wf .tr{display:flex;padding:3px 8px;border-bottom:1px solid var(--bd);gap:6px}
|
||||
.wf .tr:last-child{border-bottom:none}
|
||||
.wf .tr span{flex:1}.wf .tr span:first-child{flex:2.4}
|
||||
.wf .tr.th{color:var(--fnt);font-size:9px;letter-spacing:.05em;font-weight:700}
|
||||
.wf .ctr2{display:flex;align-items:center;justify-content:center}
|
||||
.wf .edge{background:var(--sf);border:1px solid var(--bds);border-right:none;
|
||||
border-radius:5px 0 0 5px;padding:14px 5px;color:var(--mut)}
|
||||
.wf .edge.wide{padding:14px 10px;font-weight:700;color:var(--tx)}
|
||||
.wf .mnu{align-self:flex-end;background:var(--rz);border:1px solid var(--bds);
|
||||
border-radius:5px;padding:3px;min-width:52%;box-shadow:0 3px 10px rgba(16,32,64,.14)}
|
||||
.wf .mi{padding:4px 8px;border-radius:3px}
|
||||
.wf .mi:nth-child(2){background:var(--navs);font-weight:600}
|
||||
/* Sparkle badge — the teal-tinted "AI" chip, same convention as the app's
|
||||
existing ✨ AI buttons in Folder and Schedule. */
|
||||
/* The dock is anchored to the window corner — its position is unchanged. */
|
||||
.wf .main.anchor{position:relative}
|
||||
.wf .dock{position:absolute;right:10px;bottom:10px}
|
||||
.wf .dock.badgewrap{display:flex;align-items:center;gap:3px}
|
||||
.wf .dock .badge,.wf .dock.badge{background:#E6F6F4;border:1px solid #7FD0C4;
|
||||
border-radius:6px;padding:7px 11px;font-weight:700;color:#0F6E62;white-space:nowrap}
|
||||
/* "Ẩn vào cạnh phải" — a real button in the app, next to the launcher. */
|
||||
.wf .dock .chv{background:var(--sf);border:1px solid var(--bds);border-radius:4px;
|
||||
padding:6px 3px;color:var(--mut);font-size:11px;line-height:1}
|
||||
/* The proposed dot: 26px, no label, no neighbouring chevron. Drawn at the same
|
||||
scale as the wireframe around it so the size claim is visible, not asserted. */
|
||||
.wf .dock.fab,.wf .fab{width:26px;height:26px;border-radius:50%;background:#E6F6F4;
|
||||
border:1px solid #7FD0C4;display:flex;align-items:center;justify-content:center;
|
||||
font-size:12px;box-shadow:0 2px 6px rgba(16,32,64,.14)}
|
||||
/* Hover / keyboard focus only — the label is never on screen at rest. */
|
||||
.wf .fabpill{display:inline-flex;align-items:center;gap:5px;background:#E6F6F4;
|
||||
border:1px solid #7FD0C4;border-radius:13px;padding:4px 11px 4px 5px;
|
||||
font-weight:700;color:#0F6E62;white-space:nowrap;box-shadow:0 2px 6px rgba(16,32,64,.14)}
|
||||
.wf .fabpill .fab{box-shadow:none;width:18px;height:18px;font-size:10px}
|
||||
/* Old vs new footprint, drawn to scale beside each other. */
|
||||
.wf .oldbox{width:42px;height:32px;border:1px dashed var(--bds);border-radius:4px;
|
||||
display:flex;align-items:center;justify-content:center;color:var(--fnt);font-size:9px}
|
||||
.wf .dock.pnl{width:74%;height:76%;background:var(--sf);border:1px solid var(--bds);
|
||||
border-radius:6px;box-shadow:0 3px 10px rgba(16,32,64,.13)}
|
||||
.wf .phdr{border-bottom:1px solid var(--bd);padding-bottom:5px;gap:5px}
|
||||
.wf .spark{color:#0F9B8A}
|
||||
.wf .pnl{display:flex;flex-direction:column;gap:5px;padding:8px}
|
||||
/* The rail's own MENU collapse control (150px <-> 54px in the app). */
|
||||
.wf .menutog{display:flex;align-items:center;justify-content:space-between;
|
||||
color:var(--fnt);font-size:9px;letter-spacing:.1em;font-weight:700;padding:2px 6px 6px}
|
||||
@@ -1017,6 +1265,7 @@ line-height:1.7;background:var(--rz);border-radius:4px;padding:5px 7px}
|
||||
.wf .add{color:var(--ok)}.wf .del{color:var(--bad)}
|
||||
.wf .ok{color:var(--ok)}.wf .bad{color:var(--bad)}
|
||||
.wf .cm{color:var(--fnt)}.wf .kw{color:var(--ac)}.wf .fn{color:var(--warn)}
|
||||
.wf .w18{flex:none;width:18%}.wf .w24{flex:none;width:24%}
|
||||
.wf .w26{flex:none;width:26%}.wf .w28{flex:none;width:28%}.wf .w32{flex:none;width:32%}
|
||||
@media(max-width:760px){.wf{height:auto;flex-direction:column}.wf .rail{width:auto}}
|
||||
.tgl{position:fixed;top:16px;right:16px;z-index:9;background:var(--sf);color:var(--tx);
|
||||
@@ -1145,7 +1394,12 @@ def main() -> int:
|
||||
shot = f'<div class="miss">Không chụp được màn này<br><code>{err}</code></div>'
|
||||
sw = ""
|
||||
|
||||
wf = (f'<div class="cap">Đề xuất — bố cục mới</div><div class="wf">{a["wf"]}</div>'
|
||||
# The dock floats over the MAIN WINDOW. Modal dialogs cover it, so they
|
||||
# get none; section 27 draws its own (badge + open panel).
|
||||
dock = "" if (slug.startswith("dialog-")
|
||||
or slug == "overlay-help-panel") else DOCK_BADGE
|
||||
wf = (f'<div class="cap">Đề xuất — bố cục mới</div>'
|
||||
f'<div class="wf">{a["wf"]}{dock}</div>'
|
||||
if a["wf"] else
|
||||
'<div class="cap">Đề xuất — bố cục mới</div>'
|
||||
'<p class="mut">Giữ nguyên bố cục bên trong; chỉ đổi thanh menu sang danh sách phẳng.</p>')
|
||||
@@ -1159,9 +1413,15 @@ def main() -> int:
|
||||
f'<b>{lab}</b>{": " + txt if txt else ""}' for lab, txt in de["r"])
|
||||
legend = f'<p class="rg">{bits}</p>'
|
||||
|
||||
secs.append(f"""<section class="sec" id="{slug}">
|
||||
<div class="hd"><b>{n}. {esc(info['title'])}</b><span class="tag">{esc(info['note'])}</span>{sw}</div>
|
||||
<div class="bd">
|
||||
# Eight sections were written by hand (tools/audit_handwritten.py). Use
|
||||
# that markup verbatim, but keep the screenshot and the AST control
|
||||
# inventory generated so neither goes stale.
|
||||
hand = HAND_SECTIONS.get(slug)
|
||||
if hand:
|
||||
body = (hand.replace("{{SHOT}}", shot)
|
||||
.replace("{{CONTROLS}}", controls_table(slug, cidx)))
|
||||
else:
|
||||
body = f"""<div class="bd">
|
||||
{intro}
|
||||
<div class="cap">Hiện tại</div>{shot}
|
||||
{legend}
|
||||
@@ -1170,7 +1430,11 @@ def main() -> int:
|
||||
<div class="cols pc">
|
||||
<div><div class="cap">Vấn đề</div><ul class="pr">{''.join(f'<li>{p}</li>' for p in a['problems'])}</ul></div>
|
||||
<div><div class="cap">Thay đổi</div><ul class="pr">{''.join(f'<li>{c}</li>' for c in a['changes'])}</ul></div>
|
||||
</div></div></section>""")
|
||||
</div></div>"""
|
||||
|
||||
secs.append(f"""<section class="sec" id="{slug}">
|
||||
<div class="hd"><b>{n}. {esc(info['title'])}</b><span class="tag">{esc(info['note'])}</span>{sw}</div>
|
||||
{body}</section>""")
|
||||
|
||||
flows = "".join(
|
||||
f'<tr><td><b>{t}</b></td><td class="mut">{b}</td><td>{af}</td></tr>'
|
||||
@@ -1183,6 +1447,14 @@ def main() -> int:
|
||||
newchat = "".join(
|
||||
f'<tr><td><b>{a}</b></td><td class="mut">{o}</td><td>{n}</td><td>{v}</td></tr>'
|
||||
for a, o, n, v in NEWCHAT)
|
||||
later = "".join(f'<tr><td><b>{n}</b></td><td>{d}</td>'
|
||||
f'<td class="mut">{k}</td></tr>' for n, d, k in LATER)
|
||||
rail_has = rail("Cowork") + ('<div class="main"><div class="ttl">Cowork</div>'
|
||||
'<div class="b grow"></div></div>')
|
||||
rail_none = rail("Project", empty=True) + (
|
||||
'<div class="main"><div class="ttl">Quản lý project</div>'
|
||||
'<div class="b grow"><span class="mut">Chưa có project — bấm “+ Project mới”</span>'
|
||||
'</div></div>')
|
||||
shell_ctl = controls_table("__shell__", cidx)
|
||||
colls = "".join(
|
||||
f'<tr><td><b>{n}</b></td><td>{how}</td><td class="mut">{src}</td>'
|
||||
@@ -1201,7 +1473,8 @@ def main() -> int:
|
||||
|
||||
html = f"""<!doctype html><html lang="vi"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>CoworkLocal — Audit UI/UX</title><style>{CSS}</style></head><body>
|
||||
<title>CoworkLocal — Audit UI/UX</title><style>{CSS}
|
||||
{HAND_CSS}</style></head><body>
|
||||
<button class="tgl" id="tgl">🌙 Tối</button>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
@@ -1267,6 +1540,20 @@ Số còn lại bị ẩn, thành combo, thành nút, hoặc không tới đư
|
||||
<h3>Truy vết chi tiết: “Đoạn chat mới”</h3>
|
||||
<table><tr><th style="width:16%">Khía cạnh</th><th style="width:28%">Giao diện cũ</th>
|
||||
<th style="width:24%">Giao diện mới</th><th>Có đồng bộ không</th></tr>{newchat}</table>
|
||||
<div class="cols" style="align-items:start">
|
||||
<div><div class="cap">Có project</div><div class="wf demo">{rail_has}</div></div>
|
||||
<div><div class="cap">Chưa có project nào</div><div class="wf demo">{rail_none}</div></div>
|
||||
</div>
|
||||
<div class="note"><b>Khi chưa có project</b> (đã chạy thử app với 0 project):
|
||||
hiện nay <b>Cowork và GraphRAG biến mất</b> khỏi menu nên không chat được, mà không nói vì sao.
|
||||
Thiết kế mới <b>giữ nguyên cổng chặn đó</b> — vẫn không tạo chat được — nhưng hai mục vẫn nằm
|
||||
đúng chỗ, chỉ mờ đi; droplist ghi “Chưa có project”; nút chat mới bị khoá kèm lý do
|
||||
“Tạo project trước”.<br>
|
||||
<span class="mut">Ghi nhận thêm: lúc đó <code>ctx.active_project_id</code> vẫn giữ
|
||||
<code>'default'</code> — trỏ vào một project không tồn tại. Và
|
||||
<code>projects.ensure_starter_project()</code> (“đảm bảo luôn có ít nhất một project”)
|
||||
<b>không nơi nào gọi</b>.</span></div>
|
||||
|
||||
<div class="note bad"><b>Phát hiện:</b> <code>sidebar.py:68</code> khai báo tín hiệu
|
||||
<code>new_chat</code> và <code>workspace_tab.py:241</code> đã nối nó vào
|
||||
<code>_on_sidebar_new</code> — nhưng <b>không nơi nào phát tín hiệu này</b>
|
||||
@@ -1277,7 +1564,13 @@ Số còn lại bị ẩn, thành combo, thành nút, hoặc không tới đư
|
||||
<h2>Phần 3 — Từng màn hình</h2>
|
||||
{''.join(secs)}
|
||||
|
||||
<h2>Phần 4 — Màn chết (chỉ ghi nhận)</h2>
|
||||
<h2>Phần 4 — Phát triển lần sau</h2>
|
||||
<p class="mut">Những việc audit này phát hiện nhưng <b>cố ý không làm</b>, vì đều thêm
|
||||
hoặc đổi chức năng — ngoài phạm vi “chỉ sắp xếp lại”.</p>
|
||||
<table><tr><th style="width:26%">Việc</th><th>Chi tiết</th><th style="width:16%">Loại</th></tr>
|
||||
{later}</table>
|
||||
|
||||
<h2>Phần 5 — Màn chết (chỉ ghi nhận)</h2>
|
||||
<p class="mut">Sáu màn có trong code nhưng không tới được — tổng <b>64 control</b>
|
||||
(accounts 18 · flow_dialog 28 · agent_manager 7 · skill_manager 7 · mcp_servers 4).
|
||||
Không nằm trong kiểm kê phía trên vì không có đường nào tới; chỉ ghi nhận, không đề xuất gỡ.</p>
|
||||
@@ -1285,7 +1578,9 @@ Không nằm trong kiểm kê phía trên vì không có đường nào tới; c
|
||||
<div class="note warn">Ngoài phạm vi: <code>settings_dialog.py:115</code> hard-code mật khẩu
|
||||
Sandbox; hai lớp cùng tên <code>CustomAgent</code>
|
||||
(<code>custom_agents.py:23</code> · <code>co4e.py:117</code>).</div>
|
||||
</div><script>{JS}</script></body></html>"""
|
||||
</div><script>{JS}</script>
|
||||
{"".join(f"<script>{j}</script>" for j in HAND_JS)}
|
||||
</body></html>"""
|
||||
|
||||
dest = OUT
|
||||
dest.write_text(html, encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user