From 07eba5c4028982eb0750fbaad34875dfa3a231b3 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 29 Jul 2026 14:34:46 +0800 Subject: [PATCH] Polish web workspace visual foundation --- CHANGELOG.md | 4 + PROGRESS.md | 5 +- core/__init__.py | 2 +- tests/test_static_vendor.py | 23 ++ web/static/dev.html | 30 +- web/static/ui.css | 589 ++++++++++++++++++++++++++++++++++++ 6 files changed, 621 insertions(+), 32 deletions(-) create mode 100644 web/static/ui.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 897480c..f976465 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 工程口径的完整记录见 `PROGRESS.md` / git log。 +## 0.60.8 — 2026-07-29 + +- 网页工作台完成一轮整体视觉优化:任务、对话和文件三栏层级更清晰,长回复阅读更舒适,按钮、输入框、弹窗、状态反馈及手机端触控尺寸也更加统一。 + ## 0.60.7 — 2026-07-29 - 新对话的目录选择框现在也能直接创建工作目录:输入新名称后选择创建即可,不再需要旁边的额外按钮和第二个输入框。 diff --git a/PROGRESS.md b/PROGRESS.md index fdbaef7..3f2886a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-07-29(组合框内直接新建目录,bump 0.60.7) +最后更新:2026-07-29(Web 工作台 Visual Polish v1,bump 0.60.8) --- @@ -23,6 +23,7 @@ ### 2026-07-29 +- **07-29 / 0.60.8 / Web 工作台 Visual Polish v1**:新增零构建 `web/static/ui.css` 作为内部轻量视觉基础层,集中颜色、间距、圆角、阴影与 focus/disabled 等控件状态;收敛三栏层级、任务/资源导航、对话正文与工具过程块、输入区、文件栏、modal/toast 及 ≤640px 触控尺寸,不改 DOM 契约、API 或业务交互。新增 CSS 文件接入、MIME 与 `no-cache` 回归测试;相关 Python 20 项 + Node 5 项通过,浏览器连接不可用故截图级视觉验收留真实页面复核。 - **07-29 / 0.60.7 / 组合框内直接新建工作目录**:新对话目录 combobox 增加 allow-create 语义:输入时继续过滤已有目录,无完全同名项时在同一下拉内提供“创建工作目录「名称」”,明确选择后才进入新建状态,失焦搜索词不会误提交;删除外部新建按钮与二级输入框。纯前端改动,无 API、schema、migration 或目录绑定语义变化;全量 387 测试通过(17 项按环境跳过)。 - **07-29 / 0.60.6 / 新对话目录选择改可搜索组合框**:将上一版独立搜索框 + 原生 select 合并为单一 combobox,同一输入区完成已选目录展示、即时过滤和下拉选择,并补方向键、Enter、Esc、失焦恢复与 ARIA 状态;最近目录置顶、明确选择 working_dir 和新建目录入口均保持不变。纯前端改动,无依赖、API、schema 或 migration;全量 387 测试通过(17 项按环境跳过)。 - **07-29 / 0.60.5 / 新对话目录选择提速**:保留首发前明确选择 working_dir 的归档约束;本机最近实际使用目录在候选中自动选中、置顶并标记,目录达到 6 个时按需显示搜索框,目录较少时维持原有简洁界面;同步精简“文件保存位置”文案。纯前端改动,无 API、schema、migration 或目录绑定语义变化;相关静态资源与无 DB 路由 18 项测试通过。 @@ -268,7 +269,7 @@ web/{background,scheduler_runner,wechat_runner}.py ← lifespan 后台协程按 web/{runs,common,schemas,model_gate,userfiles}.py ← BG worker/共享 helper/请求体/档位门控/路径安全 web/auth.py ~190 ← 邮箱密码 + platform_key → JWT web/broker.py / sinks.py / pptx_render.py / admin.py -web/static/dev.html + js/*.js ← dev SPA 零构建 ES module(main.js 入口;skills.js=技能 modal;kb.js=知识库 modal) +web/static/dev.html + ui.css + js/*.js ← dev SPA 零构建;ui.css=轻量视觉基础层,main.js=入口 web/static/vendor/ ~1 MB ← jszip / docx-preview / xlsx tests/ 360 项 ← golden/rendering 基线 + 路由两套(nodb/db)+ DB 套件(ZCBOT_TEST_DB_URL 门控) ───────────────────────────────── diff --git a/core/__init__.py b/core/__init__.py index a7b730a..7b55ac0 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.60.7" +__version__ = "0.60.8" diff --git a/tests/test_static_vendor.py b/tests/test_static_vendor.py index 74a6ec4..682ecdf 100644 --- a/tests/test_static_vendor.py +++ b/tests/test_static_vendor.py @@ -11,6 +11,7 @@ from web.static_files import NoCacheStaticFiles ROOT = Path(__file__).resolve().parents[1] STATIC_DIR = ROOT / "web" / "static" DEV_HTML = STATIC_DIR / "dev.html" +UI_CSS = STATIC_DIR / "ui.css" JS_DIR = STATIC_DIR / "js" VENDOR_DIR = STATIC_DIR / "vendor" / "markdown" @@ -42,6 +43,16 @@ class StaticVendorTests(unittest.TestCase): self.assertTrue(path.exists(), f"missing vendored asset: {path}") self.assertGreater(path.stat().st_size, 0, f"empty vendored asset: {path}") + def test_dev_html_uses_local_ui_foundation(self) -> None: + html = DEV_HTML.read_text(encoding="utf-8") + + self.assertIn('href="ui.css"', html) + self.assertTrue(UI_CSS.exists(), f"missing UI foundation: {UI_CSS}") + css = UI_CSS.read_text(encoding="utf-8") + self.assertIn("--accent:", css) + self.assertIn(":focus-visible", css) + self.assertIn("@media (max-width: 640px)", css) + def test_frontend_surfaces_context_and_cache_stats(self) -> None: src = _frontend_source() @@ -62,6 +73,18 @@ class StaticVendorTests(unittest.TestCase): self.assertEqual(resp.status_code, 200) self.assertEqual(resp.headers.get("cache-control"), "no-cache") + def test_ui_css_is_served_as_css_with_revalidation(self) -> None: + app = FastAPI() + app.mount("/static", NoCacheStaticFiles(directory=str(STATIC_DIR)), name="static") + client = TestClient(app) + + resp = client.get("/static/ui.css") + + self.assertEqual(resp.status_code, 200) + self.assertTrue(resp.headers.get("content-type", "").startswith("text/css")) + self.assertEqual(resp.headers.get("cache-control"), "no-cache") + self.assertIn("--accent:", resp.text) + if __name__ == "__main__": unittest.main() diff --git a/web/static/dev.html b/web/static/dev.html index 6aae7cd..e96db2f 100644 --- a/web/static/dev.html +++ b/web/static/dev.html @@ -13,35 +13,6 @@ + diff --git a/web/static/ui.css b/web/static/ui.css new file mode 100644 index 0000000..f125b8d --- /dev/null +++ b/web/static/ui.css @@ -0,0 +1,589 @@ +/* + * zcbot UI foundation + * + * The application intentionally stays build-free. This file is the shared + * visual layer for the main SPA: tokens, control states and restrained + * workspace polish. Feature-specific layout remains next to dev.html until + * it is reused by a second surface. + */ + +:root { + color-scheme: light; + + /* Neutral workspace palette */ + --bg: #f3f5f7; + --panel: #ffffff; + --panel-subtle: #f8f9fb; + --panel-muted: #f4f6f8; + --border: #dde2e7; + --border-soft: #e9edf1; + --text: #20242a; + --text-strong: #15181d; + --muted: #6f7782; + + /* Institute red is deliberately reserved for primary emphasis. */ + --accent: #b83a31; + --accent-hover: #a9322a; + --accent-soft: #fbecea; + --accent-ring: rgba(184, 58, 49, 0.16); + + --hover: #f0f3f5; + --code-bg: #f3f5f7; + --user-bg: #f1f5fa; + --asst-bg: #ffffff; + + --c-green: #238653; + --c-green-bg: #eaf6ef; + --c-green-bd: #a9d9bd; + --c-blue: #2d6f9f; + --c-blue-bg: #edf5fa; + --c-blue-bd: #b5d5e7; + --c-purple: #7c5192; + --c-purple-bg: #f5eff8; + --c-purple-bd: #d8c4e1; + --c-orange: #bf6a20; + --c-orange-bg: #fdf3e8; + --c-orange-bd: #ebc79f; + --c-red: #b83a31; + --c-red-bg: #fbecea; + --c-red-bd: #e7b3ae; + + /* One compact radius and spacing vocabulary. */ + --r-sm: 4px; + --r-md: 6px; + --r-lg: 8px; + --r-xl: 10px; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + + --shadow-float: 0 10px 30px rgba(24, 32, 41, 0.12); + --shadow-card: 0 18px 48px rgba(24, 32, 41, 0.16); + --shadow-card-lg: + 0 24px 72px rgba(24, 32, 41, 0.14), + 0 2px 8px rgba(24, 32, 41, 0.06); + --mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, monospace; + --t: background-color 0.15s ease, border-color 0.15s ease, + color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease; +} + +body { + font-size: 14px; + line-height: 1.55; + color: var(--text); + background: var(--bg); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +button, +input, +textarea, +select { + transition: var(--t); +} + +button { + min-height: 32px; + padding: 5px 11px; + font-weight: 500; +} + +button.small { + min-height: 28px; +} + +button:hover:not(:disabled) { + border-color: #cbd2d9; +} + +button.primary { + background: var(--accent); + border-color: var(--accent); + box-shadow: 0 1px 2px rgba(73, 25, 21, 0.16); +} + +button.primary:hover:not(:disabled) { + background: var(--accent-hover); + border-color: var(--accent-hover); + filter: none; +} + +button:disabled { + opacity: 0.46; +} + +button:focus-visible, +a:focus-visible, +input:focus-visible, +textarea:focus-visible, +select:focus-visible, +[tabindex]:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +input:not([type="checkbox"]):not([type="radio"]):not([type="file"]), +textarea, +select { + min-height: 34px; + border-color: var(--border); +} + +input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):hover, +textarea:hover, +select:hover { + border-color: #c7ced5; +} + +input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus, +textarea:focus, +select:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-ring); +} + +::placeholder { + color: #9aa1aa; +} + +/* Application shell */ + +#app.ready { + background: var(--bg); +} + +header { + min-height: 48px; + padding: 9px 16px; + border-bottom-color: var(--border); + box-shadow: 0 1px 0 rgba(24, 32, 41, 0.02); +} + +header .brand { + gap: 10px; +} + +header .brand .logo { + width: 26px; + height: 26px; + box-shadow: 0 2px 8px rgba(184, 58, 49, 0.2); +} + +header .title { + color: var(--text-strong); + font-size: 15px; + letter-spacing: 0.1px; +} + +header .who { + color: var(--muted); +} + +.pane { + border-color: var(--border); +} + +#pane-left, +#pane-right { + background: var(--panel-subtle); +} + +#pane-mid { + background: var(--panel); +} + +.splitter { + background: var(--bg); +} + +.splitter::before { + background: #d7dde3; +} + +.pane-head { + min-height: 44px; + padding: 8px 12px; + background: rgba(248, 249, 251, 0.96); + border-bottom-color: var(--border-soft); +} + +.pane-head .label { + color: var(--text-strong); + font-size: 13px; + letter-spacing: 0.2px; +} + +#pane-left .pane-head + .pane-head { + background: var(--panel); +} + +#task-scroll, +#file-list { + scrollbar-color: #cbd1d7 transparent; + scrollbar-width: thin; +} + +/* Task and resource navigation */ + +#channel-cards { + padding: 8px 10px 4px; +} + +.channel-card { + border-color: rgba(7, 158, 82, 0.26); + background: rgba(7, 158, 82, 0.045); +} + +#task-list { + padding: 4px 8px 8px; +} + +.task-row { + margin: 3px 0; + padding: 9px 10px; + border: 1px solid transparent; + border-radius: var(--r-md); +} + +.task-row:hover { + background: var(--panel); + border-color: var(--border-soft); + box-shadow: 0 1px 3px rgba(24, 32, 41, 0.04); +} + +.task-row.active { + padding-left: 8px; + background: var(--accent-soft); + border-color: #f0d2cf; + border-left: 3px solid var(--accent); + box-shadow: none; +} + +.task-row .desc { + color: var(--text-strong); +} + +.task-row .meta { + color: var(--muted); +} + +.task-row .badge, +.sk-badge { + border-radius: var(--r-sm); +} + +#rail-resources { + padding: 8px; + border-top-color: var(--border-soft); + background: var(--panel); +} + +#rail-resources > button { + min-height: 48px; + border-color: transparent; + color: #59616b; + background: transparent; +} + +#rail-resources > button:hover { + color: var(--accent); + border-color: #f0d2cf; + background: var(--accent-soft); +} + +/* Menus and lightweight cards */ + +#floating-menu, +#media-model-pop { + border-color: var(--border); + border-radius: var(--r-lg); + box-shadow: var(--shadow-float); +} + +.dd-item { + padding: 7px 14px; +} + +.dd-item:hover { + background: var(--panel-muted); +} + +.sk-item { + padding: 9px 10px; + border-color: var(--border-soft); + background: var(--panel); +} + +.sk-item:hover { + border-color: #d2a09b; + background: var(--accent-soft); +} + +.sk-item.active { + border-color: var(--accent); + background: var(--accent-soft); +} + +.empty, +.sk-empty { + color: var(--muted); + line-height: 1.7; +} + +/* Conversation: assistant content reads like a document, not a bubble stack. */ + +#chat-meta { + min-height: 40px; + padding: 8px 16px; + background: var(--panel); + border-bottom-color: var(--border-soft); +} + +#chat-stream { + gap: 14px; + padding: 20px clamp(16px, 3vw, 36px) 24px; + background: var(--panel); + scrollbar-color: #cbd1d7 transparent; + scrollbar-width: thin; +} + +.msg { + padding: 10px 13px; + border-color: var(--border-soft); + border-radius: var(--r-lg); +} + +.msg.user { + background: var(--user-bg); + border-color: #dce6f0; +} + +.msg.assistant { + padding: 8px 0; + border-color: transparent; + border-radius: 0; +} + +.msg.assistant.live-run { + padding: 12px 14px; + border-color: rgba(184, 58, 49, 0.2); + border-radius: var(--r-lg); + box-shadow: + 0 0 0 1px rgba(184, 58, 49, 0.05), + 0 8px 24px rgba(184, 58, 49, 0.06); +} + +.msg .role { + margin-bottom: 4px; + color: #7d858f; + font-family: inherit; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.2px; +} + +.msg .body { + color: var(--text); + font-size: 15px; + line-height: 1.7; +} + +.msg .body p { + margin: 0.65em 0; +} + +.msg .body h1, +.msg .body h2, +.msg .body h3 { + color: var(--text-strong); + line-height: 1.35; +} + +.msg-actions { + margin-top: 8px; +} + +.tool-call, +.reasoning { + color: #4f5863; +} + +.tool-call summary, +.reasoning summary { + padding: 6px 8px; + border: 1px solid var(--border-soft); + border-radius: var(--r-md); + background: var(--panel-muted); +} + +.tool-call summary:hover, +.reasoning summary:hover { + background: var(--hover); +} + +.tool-call pre { + border: 1px solid var(--border-soft); + background: var(--code-bg); +} + +#chat-form { + gap: 8px; + padding: 12px 16px 14px; + border-top-color: var(--border-soft); + background: var(--panel); + box-shadow: 0 -8px 24px rgba(24, 32, 41, 0.035); +} + +#chat-input { + min-height: 72px; + padding: 10px 12px; + line-height: 1.6; + border-radius: var(--r-lg); + background: #fcfcfd; +} + +#chat-form .row { + align-items: center; +} + +#chat-action { + min-width: 64px; +} + +/* File pane */ + +.crumbs { + min-height: 38px; + padding: 9px 12px; + background: var(--panel); + border-bottom-color: var(--border-soft); +} + +.file-row { + min-height: 38px; + padding: 7px 12px; + border-bottom-color: var(--border-soft); +} + +.file-row:hover { + background: var(--panel); +} + +.file-row .name { + color: #30363d; +} + +.storage-foot { + padding: 8px 12px; + border-top-color: var(--border-soft); + background: var(--panel); +} + +.storage-foot .bar { + height: 5px; + background: #e5e9ed; +} + +/* Dialogs and notifications */ + +.modal { + padding: 16px; + background: rgba(22, 28, 35, 0.46); +} + +.modal > .card { + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.72); + box-shadow: var(--shadow-card); +} + +#chpw-modal h3, +#app-dialog h3, +#changelog-modal h3, +#skills-modal h3, +#wechat-modal h3, +#crons-modal h3, +#memory-modal h3, +#kb-modal h3 { + min-height: 48px; + padding: 13px 18px; + color: var(--text-strong); + border-bottom-color: var(--border-soft); +} + +#app-dialog .body, +#chpw-modal .body { + padding: 18px; +} + +#app-dialog .actions, +#chpw-modal .actions, +#src-picker-modal .actions { + padding: 12px 18px; + border-top-color: var(--border-soft); + background: var(--panel-subtle); +} + +.toast, +.app-msg { + border: 1px solid var(--border-soft); + border-radius: var(--r-lg); + box-shadow: var(--shadow-float); +} + +/* Respect system motion preferences for polish-only transitions too. */ + +@media (prefers-reduced-motion: reduce) { + button, + input, + textarea, + select { + transition: none; + } +} + +@media (max-width: 640px) { + header { + min-height: 46px; + padding: 7px 10px; + } + + .pane-head { + min-height: 42px; + } + + #chat-stream { + gap: 12px; + padding: 14px 12px 18px; + } + + .msg { + max-width: 97%; + } + + .msg .body { + font-size: 15px; + line-height: 1.68; + } + + #chat-form { + padding: 10px; + } + + #chat-input { + min-height: 64px; + } + + #chat-form .row button { + min-height: 36px; + } + + .modal { + padding: 12px; + } +}