feat/update-playground-ui #1

Open
zhuxiaomin wants to merge 43 commits from feat/update-playground-ui into main
Owner

close #issue_number

我已确认该 PR 已自测通过,相关截图如下:

[//]: # "请按照以下格式关联 issue" [//]: # "请在提交 PR 前确认所提交的功能可用,附上截图即可,这将有助于项目维护者 review & merge 该 PR,谢谢" [//]: # "项目维护者一般仅在周末处理 PR,因此如若未能及时回复希望能理解" [//]: # "请在提交 PR 之前删除上面的注释" close #issue_number 我已确认该 PR 已自测通过,相关截图如下:
Promote the original Playground (multi-tab iframe shell embedding
ChatGPT Next, chatgpt-web-midjourney-proxy, AMA 问天 and OpenCat with
auto-injected playground token) to a dedicated public page at
/qrouter. The header gains a QRouter entry to the right of Price,
the homepage hero gains a Try QRouter CTA between Get Started and
View Pricing, and the control-panel / admin chat surfaces keep
working unchanged.

Header
- New QRouter outlined Button with chat-bubble icon, primary color
  on /qrouter, gated by siteInfo.qrouter_enabled
- Mobile Popper menu mirrors the desktop order and re-adds the
  legacy Playground entry (gated by builtin_chat_enabled) so the
  existing IA is preserved

Hero
- New 'Try QRouter' outlined-primary Button between 'Get Started'
  and 'View Pricing', same icon, localized for en/zh/zh-HK/ja

Routing
- /qrouter mounts the recovered views/Playground component
- /playground kept as an alias for backwards compatibility
- /panel/playground in the admin layout still works

Runtime flag
- New QRouterEnabled Go constant + qrouter_enabled /api/status field
- Frontend siteInfo defaults updated to include qrouter_enabled: true
- The legacy builtin_chat_enabled / chat_link / chat_links fields
  remain untouched so admin chatLinkSettings, QuickStartCard, and
  the Telegram /apikey [Next Chat][AMA][OpenCat] line all keep
  their original behaviour

Plan
- docs/plans/2026-06-28-qrouter-chat-standalone.md rewritten to
  reflect the actual delivery: the chat surface is the recovered
  Playground iframe shell, not a self-hosted React chat. The
  Amendment section records the prior self-hosted attempt and
  why it was reverted.
feat(qrouter): self-hosted chat client (replace iframe shell on /qrouter)
Some checks failed
neorouter docker image / build-and-push (push) Failing after 4s
13e8761a23
Build a real QRouter-owned chat product on /qrouter, replacing the
multi-tab iframe shell that embedded NextChat / chatgpt-web-midjourney-proxy /
AMA 问天 / OpenCat. The legacy /playground and /panel/playground routes
keep the iframe shell for power users.

Backend (Go)
- Three new GORM models with soft delete: QRouterConversation,
  QRouterMessage (with parent_id / branch_index for regenerate / edit
  branches), QRouterAttachment (content-addressed by sha256, stored
  under data/qrouter/attachments/).
- 12 REST endpoints under /api/qrouter/* covering conversation CRUD,
  message append + PATCH (used for streaming deltas), regenerate,
  branch, share enable/disable, soft + hard delete, attachment
  upload/download, search, and a public share view at
  /api/qrouter/shared/:token.
- Token resolver picks a user-owned 'QRouter'-tagged token first,
  falls back to first enabled user token, auto-provisions a
  'sys_qrouter' unlimited-quota token for users with zero tokens,
  and falls back to the legacy 'sys_playground' for anonymous
  visitors. The picked token is returned to the client which then
  calls /v1/chat/completions directly with SSE streaming.
- Daily 03:00 cron purges soft-deleted conversations older than 30
  days, cascading to messages and on-disk attachment files.

Frontend (React)
- New views/QRouter/ tree with .qrouter-root layout, design tokens
  from docs/plans/2026-06-28-qrouter-ui-spec.md, MUI-mode ->
  html[data-theme] bridge on mount.
- ConversationList (left rail) with today / yesterday / earlier
  grouping + soft-delete affordance.
- MessageStream + MessageBubble (WeChat-style user / left-aligned AI
  with gradient avatar and model tag).
- Composer (sticky bottom) with model picker, textarea (Enter to
  send, Shift+Enter newline, auto-grow), send/stop toggle.
- chatStream service: fetch + ReadableStream + AbortController, SSE
  parsing with line-buffered state, periodic PATCH to persist deltas,
  finalize on done/stopped/error.
- TrashPage with restore / permanent-delete.
- SearchPage with title + body LIKE search.
- SharePage at /qrouter/s/:token for public read-only views.

Tests
- 7 model tests (CRUD, soft delete, share rotate, branch index,
  hard delete cascade, list filters).
- 6 service tests (access control, share, branch, streaming update,
  search, soft/hard delete).
- 1 cron test (30-day purge keeps recent, purges old, spares live).

Plan
- docs/plans/2026-06-28-003-feat-qrouter-self-hosted-chat.md captures
  the full design + 8 v2 follow-up beads (markdown rendering, branch
  UI, model presets + temperature/max_tokens, keyboard shortcuts,
  anon-to-account upgrade, FTS search, image -> prompt, mobile drawer
  animation).

/playground and /panel/playground are unchanged - the legacy
multi-tab iframe shell stays available for users who bookmarked it
or rely on the Midjourney proxy / AMA / OpenCat integrations.

Build: go build ./... clean, go test ./... green, npm run build
(workspace) clean.
Design token spec for the QRouter self-hosted chat UI: light/dark
theme variables, layout primitives, message stream + composer +
modal patterns, accessibility and motion guidelines. Drove the
qrouter-chat.css in feat(qrouter) 13e8761.
This reverts commit 3957c0ebe9.
Revert "feat(qrouter): self-hosted chat client (replace iframe shell on /qrouter)"
Some checks failed
neorouter docker image / build-and-push (push) Failing after 3s
49d1775c27
This reverts commit 13e8761a23.
feat(qrouter): re-skinned playground with demo visual language (3-pane layout)
Some checks failed
neorouter docker image / build-and-push (push) Failing after 3s
a1d813fc4c
Apply the UI contract at docs/plans/2026-06-29-qrouter-ui-contract.md
v1.0: /qrouter is now a re-skinned /playground, not a self-hosted chat
client. UI borrows from ./demo (3-pane layout, glassmorphism panels,
dark color tokens, Noto Sans SC); functionality is unchanged from
/playground (4 third-party chat apps as tabs, per-user playground
token auto-injection via /api/token/playground, admin
chatLinkSettings, Telegram bot /apikey hint).

Layout (3-pane grid, 100% viewport height)
- side panel (260px): brand (cyan dot + green ring) + tips card +
  account card + footer
- body: 48px topbar (breadcrumb + actions: reload, copy token,
  theme toggle, right-panel toggle) + 40px tab bar (4 third-party
  apps with cyan underline) + flex 1 iframe container with overlay
  states (token loading, token error, protocol-only chat, iframe
  status pill)
- right panel (320px, collapsible, drag-resize handle): current chat
  card + usage tip + token status + actions (reload, open in new
  tab / launch protocol)

Tokens (scoped to .qrouter-root, never leaks to MUI)
- --bg / --bg-panel / --bg-input / --bg-card / --bg-hover
- --text / --muted / --muted2
- --c1 / --c2 (blue→green gradient)
- --accent / --accent-blue / --accent-warn
- --radius / --radius-md / --radius-pill
- --sans (Noto Sans SC) / --serif (Noto Serif SC, brand title only)
- light-mode override via [data-theme-mode='light']

Third-party apps (4)
- NextChat: iframe + token
- chatgpt-web-midjourney-proxy: iframe + token (Midjourney image gen)
- AMA 问天: ama://set-api-key?server=...&key=... (protocol launch)
- OpenCat: opencat://team/join?domain=...&token=... (protocol launch)
  AMA/OpenCat show a 'launch in desktop app' overlay (not iframe);
  iframes are used for NextChat / Midjourney.
  visible tabs are filtered by siteInfo.chat_links; admin can toggle
  show on/off in OperationSetting > chatLinkSettings.

Behavior
- Token auto-fetched on mount; re-fetched when user identity changes
  (login/logout)
- Iframe src is lazy: only the active tab's src is built on first
  activation, then cached. Switching tabs doesn't unload the iframe.
- /playground redirects to /qrouter via Navigate replace.
- /panel/playground keeps the legacy MUI Playground for admin (no
  change).

Out of scope (per contract)
- No self-hosted chat, no message persistence, no conversation
  list, no share / trash / search / branches
- All 14 'not done' items from contract §1.4 are explicitly not
  implemented
fix(qrouter): 100% /playground functional parity — fix 1-app + 0-app edge cases
Some checks failed
neorouter docker image / build-and-push (push) Failing after 3s
8e9658c83e
After the re-skin in a1d813f, two subtle /playground behaviors were
missing:

1. When only 1 chat link is visible, /playground renders the iframe
   directly without showing a tab bar (subCard single-app branch).
   My /qrouter always showed the tab bar even with 1 link, which
   didn't match.

2. When 0 chat links are visible, /playground showed a SubCard
   'No playground available'. My /qrouter left the iframe area
   blank in that case.

3. The filter for 'show' was 'show !== false' (lenient — kept
   undefined). /playground uses 'show' truthy (strict — excludes
   undefined). Changed to match.

Changes
- Filter: 'l.show' (truthy) instead of 'l.show !== false'
- Tab bar: wrapped in 'visibleLinks.length > 1' so 0 / 1 link case
  skips the tab UI (matches /playground)
- iframe area: added a 'No playground available' overlay shown when
  visibleLinks.length === 0 && !tokenLoading && !tokenError

The existing 3 modes (loading, token error, iframe / protocol
overlay) are preserved and remain higher priority than the new
'No playground' state.

Re-skin additions (3-pane layout, theme toggle, side panel, right
panel, protocol launch for AMA/OpenCat, reload / copy token / open
in new tab buttons) are kept — they're demo-UI shell features, not
/playground functionality.
fix(qrouter): visibleLinks key 派生 / API 401 守护 / 暗色侧栏 glass 限定
Some checks failed
neorouter docker image / build-and-push (push) Failing after 4s
2ffc827326
1. visibleLinks 派生 key from name — 修 React key 警告 + iframe 永不渲染
   - CHAT_LINKS 项只有 name 没 key,导致 useState(visibleLinks[0]?.key || null)
     拿到 null → activeKey 变 undefined → activeSrc = null → iframe 不渲染
   - 在 QRouter 内本地派生 key,不污染共享代码(/playground 也用 getChatLinks)

2. API 401 守护 — res === undefined 时不再 .data 抛错
   - utils/api.js 的 response 拦截器在 401 / 网络错误时返回 undefined(不是
     Promise.reject),所以 .then((res) => res.data) 会炸
   - QRouter 显式判 res/data 缺失,走 tokenError 路径

3. --bg-base 加入 light 模式 — iframe 区不再漆黑
   - 原 light 模式只覆写 --bg(实际是 panel 用的变量),没覆 --bg-base
   - 实际页面背景是 --bg-base,导致 light 模式下 iframe 区仍然深色

4. 侧栏 glassmorphism 渐变限定到暗色 — light 模式用纯白面板
   - 原侧栏硬写 dark gradient,light 模式下一片暗黑
   - 改成默认 var(--bg-panel),只在 :not(:where([data-theme-mode="light"]))
     下加 dark gradient

5. /playground 路由恢复为原版(MUI Playground),/qrouter 为新版
   - 之前把 /playground 重定向到 /qrouter;实测后保留双路由并存

Co-Authored-By: Mavis <mavis@mavis.local>
fix(qrouter): mobile layout collapse - body 显式 grid-column:2 + iframe-wrap 改 absolute
Some checks failed
neorouter docker image / build-and-push (push) Failing after 3s
868949f784
Root cause: 在 .qrouter-root 的 grid 容器里,.qrouter-body (grid item, 在
1fr 列) 的 intrinsic width 算成 0 — 因为它的子元素 (topbar/tab bar) 的
display 模式 + iframe-wrap 的 absolute positioning 都不贡献父容器
intrinsic width。width: 100% 算成 0,width: 100vw 在桌面 overflow,
display: flex + min-width: 0 + width: 100% 全都失败。

修法:
1. .qrouter-body 显式 grid-column: 2 - 强制放进 1fr 轨,不再 auto-place
2. 改用 display: block; position: relative; width: 100% - body 是简单
   block,作为 .qrouter-iframe-wrap 的 absolute 定位 anchor
3. .qrouter-iframe-wrap 改 position: absolute; inset: <top+tab> 0 0 0 -
   精确铺满 topbar + tab bar 之下的剩余区域
4. main 元素 改 div role=main - 避免 main 元素的 user-agent 样式干扰
   (虽然 main 不是 root cause,但 div 更稳)

验证: 1389 / 1024 / 900 / 768 四个 breakpoint 全部 0 横向 overflow,
body 宽 = 1fr cell 宽,iframe-wrap 填满剩余高度。

Co-Authored-By: Mavis <mavis@mavis.local>
refactor(qrouter): 极简化为裸 iframe 容器,移除所有 chrome
Some checks failed
neorouter docker image / build-and-push (push) Failing after 3s
fe26edbd4e
按用户要求,/qrouter 现在只保留第三方 chat app 的对话主区:
- 去掉左侧 QRouter 品牌/提示/账号栏
- 去掉顶部面包屑 + 4 个图标按钮(刷新/复制 token/主题/侧栏)
- 去掉 tab 切换栏(默认取第一个 visible chat link,siteInfo 控制优先级)
- 去掉右侧 当前 chat/使用提示/Token 状态/操作 4 张卡片
- 保留 token 注入逻辑(iframe 还要用)
- 保留 4 个 overlay: 无链接 / loading / 错误 / 桌面协议唤起
- 保留 status pill(右下角,显示 token 注入状态)

JSX 从 559 行缩到 200 行,CSS 从 750 行缩到 220 行。
净减少 ~900 行。

不再需要 mobile media query(没有响应式面板了),
不再需要 grid-column:2 / position:relative 的 body hack
(iframe-wrap 直接 inset: 0 填满 root)。
不再需要 rightCollapsed / sideMobileOpen / onCopyToken /
onReloadActive / onOpenExternal / onToggleTheme 等 handler
和 CHAT_DESCRIPTIONS 常量。

Co-Authored-By: Mavis <mavis@mavis.local>
feat(qrouter): 真正全屏 — 去 status pill + MinimalLayout 隐藏 AppBar/Footer
Some checks failed
neorouter docker image / build-and-push (push) Failing after 3s
991df5d4d6
1. /qrouter 去 status pill
   - 移除 .qrouter-iframe-status JSX 块 + 相关 state (iframeLoading /
     iframeError / onLoad / onError handler)
   - 移除 .qrouter-iframe-status CSS + @keyframes qrouter-pulse
   - iframe 现在没有 onLoad/onError 处理(就是裸 iframe)

2. MinimalLayout 支持 bare route
   - 加 BARE_ROUTES 数组 + useLocation 检测 pathname
   - /qrouter 命中时:
     * 不渲染 AppBar(顶部 Qine nav 整个去掉)
     * 不渲染 Footer(底部版本号那条)
     * Outlet Box marginTop 改成 0,minHeight 改成 100vh
   - 其他路由(Home / About / Login / /playground / /price 等)
     行为不变,AppBar 1 个、Footer 1 个、marginTop 64px
   - 用数组 + includes 检测,后续要加新的全屏页直接 push

验证:
  /           AppBar 1,Footer 1 (正常)
  /playground AppBar 1,Footer 1 (正常)
  /qrouter    AppBar 0,Footer 0 (全屏)
  iframe 0,0 起 1389x868 铺满整个 viewport

Co-Authored-By: Mavis <mavis@mavis.local>
feat(web): build playground chat subsystem
Some checks failed
neorouter docker image / build-and-push (push) Failing after 12s
3833e4638d
fix playground app shell route
Some checks failed
neorouter docker image / build-and-push (push) Has been cancelled
ac2edd6d36
fix playground mode rail
Some checks failed
neorouter docker image / build-and-push (push) Failing after 9s
843d5a3813
remove playground hero and footer hint
Some checks failed
neorouter docker image / build-and-push (push) Has been cancelled
d7d5d61fb1
show only playground main content
Some checks failed
neorouter docker image / build-and-push (push) Has been cancelled
765742aab0
fix bare route frame matching
Some checks failed
neorouter docker image / build-and-push (push) Failing after 14s
431a5f72fa
make qrouter standalone chat proxy
Some checks failed
neorouter docker image / build-and-push (push) Has been cancelled
554c423fa6
make playground standalone chat proxy
Some checks failed
neorouter docker image / build-and-push (push) Failing after 5s
1034b0d134
history typo /playgound was being accepted as a sibling of /playground in both routes
and the MinimalLayout BARE_ROUTES list. retire it cleanly:
  - server-side authoritative 301 redirect in web-router (NoRoute + explicit GET handler)
  - drop the typo route from OtherRoutes
  - drop the typo from MinimalLayout BARE_ROUTES

also merge the 5-line views/Playground wrapper into views/QRouter. /playground is
the canonical entry, /qrouter is kept as legacy alias pointing to the same element.
model.GetTokenByName already scopes by (user_id, name), so cross-user leakage
isn't an issue, but the controller was returning the key of any token matching
that pair regardless of status. fix:
  - require Status=Enabled when reusing
  - auto-delete disabled/expired token before recreating
  - default ExpiredTime to 30 days instead of permanent
  - clarify in the docstring that UnlimitedQuota=true is bound to user-group
    quota billing in model/token.go:IncreaseUserQuota, so this isn't a quota bypass
getChatLinks directly JSON.parse'd localStorage values, which would crash
any view importing it (QRouter, Dashboard, QuickStartCard) if the user or a
migration tool corrupted siteInfo. wrap with safeParse() and a numeric sort that
handles NaN/invalid sort fields. spread CHAT_LINKS into a fresh array before
mutating, so the const source stays clean.

replace the placeholder markers '{key}' / '{server}' with '__PLAYGROUND_KEY__'
and '__PLAYGROUND_SERVER__' so we don't replace literal occurrences of the
old markers in URL templates (especially JSON in fragment hashes). keep the
old names as a secondary fallback for backward compatibility with admin-set
chat_links in the database. use split/join to only match the first occurrence.
the iframe was loaded into a third-party chat app with full mic/camera
permissions and no sandbox. tighten:
  - sandbox='allow-scripts allow-same-origin allow-forms allow-popups
    allow-popups-to-escape-sandbox allow-storage-access-by-user-activation'
  - trim allow to clipboard-read/write only; drop microphone, camera

theme bridge:
  - drop the side-effect on document.documentElement.dataset.theme — that was
    leaking past the qrouter-root subtree and into other views
  - switch to wrapper div className qrouter-mode-{dark,light}
  - inject --qrouter-accent from theme.palette.primary.main via inline style,
    so the project theme + ThemeColorButton picker both flow through
  - rewrite qrouter.css: light is default on .qrouter-root, dark overrides on
    .qrouter-mode-dark, accent reads from var(--qrouter-accent)
the /qrouter header entry was the destination in three different visual
states (desktop pill, mobile icon item, mobile plain item). unify by extracting
ChatMenuLink with variant='desktop' | 'mobile-icon' | 'mobile-plain'. isActive
matches both /playground (canonical) and /qrouter (legacy alias) so the link
highlights correctly when a user lands on the alias via an old bookmark.

all Header entries (desktop + mobile drawer) and the Hero CTA now jump to the
canonical /playground path. /qrouter remains navigable as a legacy alias.
visual consistency with the product name.
End-to-end coverage for the review items landed in this branch:

- playground-redirect.spec.js — /playgound → /playground 301 (P0-1)
- playground-iframe.spec.js  — iframe sandbox + trimmed allow (P1-3)
- playground-theme.spec.js   — wrapper className + accent sync (P1-2)

specs require an external backend + vite dev server (PLAYWRIGHT_BASE_URL
overridable). one-time install: 'aube install && npx playwright install
chromium'. see web/tests/e2e/README.md for run instructions and env vars.
- Remove /qrouter alias route; /playground is the only chat entry
- Drop QRouterEnabled Go constant and qrouter_enabled /api/status field
- Remove qrouter i18n namespace (~60 lines, zero code references) and
  unused menu.qrouter / dashboard_index.quickStart* keys; rewrite
  home.hero.tryQrouter copy to 'Playground' across 4 locales
- Strip qrouter_enabled gates from Hero, Header, and config defaults;
  unify header chat entry to mobile-icon variant and t('playground')
- Delete two QRouter plan docs in docs/plans/

/playground still renders the same standalone chat iframe proxy.
views/QRouter/ directory and qrouter-* CSS class names are kept
(medium-scope cleanup; rename to Playground/ is out of scope).
- en_US: Playground → Chat
- ja_JP: 遊び場 → チャット
- zh_CN/zh_HK already 聊天, no change

Header ChatMenuLink (web/src/layout/MinimalLayout/Header/index.jsx)
uses t('playground'), so this changes the homepage right-top button
label across all locales.
- mv web/src/views/QRouter → web/src/views/Playground
  (including styles/qrouter.css → styles/playground.css)
- routes/OtherRoutes.jsx: const QRouter → Playground,
  import path views/QRouter → views/Playground
- views/Playground/index.jsx: className qrouter-root / qrouter-mode-* /
  qrouter-iframe-* / qrouter-btn, inline var --qrouter-accent, and
  every comment + export default renamed
- styles/playground.css: .qrouter-* / --qrouter-accent → .playground-* /
  --playground-accent
- tests/e2e/playground-iframe.spec.js + playground-theme.spec.js:
  locator / assertion / describe / comments updated

The /qrouter alias route is preserved in tests/e2e/playground-redirect.spec.js
(intentional — see bf3632b scope). The /qrouter alias was removed from the
router by bf3632b, so this only renames internal code names.
UI 调整而非功能重构。所有 chat link / token 注入 / iframe sandbox/allow 行为
完全保留;样式严格 scope 在 .playground-root 内,不污染其他 view。

新增:
- TopBar: 浮动玻璃面板, 不抢屏幕高度; hamburger / ⌘K shortcut / 主题快切 / 全屏切换
- SideDrawer: 默认展开, 320px 玻璃面板, 列出 chat links (今天/昨天/本周/更早)
- CommandLauncher: ⌘K / Ctrl+K 模态搜索, 上下键 + Enter 切换上游, Esc 关闭
- Overlay: empty/loading/error 三态玻璃面板 (lucide flat outline 图标, 冷蓝主色)
- tokens.css: --pg-* 全套 design token (颜色/半径/间距/字号/motion/shadow/glass),
  作用域锁 .playground-root / .playground-mode-light / .playground-mode-dark,
  不写 :root, 不动 document.documentElement
- playground-ui-isolation.spec.js: 验证 data-theme 不被改动, :root 不含 --pg-*,
  iframe 指纹字符级保持, ⌘K/Ctrl+K + Esc 工作

修改:
- index.jsx: 接入新组件; 主题快切走规范 SET_THEME Redux 派发 (与 ThemeButton 同路)
- playground.css: 删除 :root { --playground-accent } 兜底块 (改由 inline style 注入)

E2E: 7/8 通过. 唯一失败 (/playgound 301) 为 pre-existing vite dev SPA fallback
问题, base commit 下同样失败, 与本次重构无关.

Co-Authored-By: Claude <noreply@anthropic.com>
Plan v3.1.5 (E-series) + v3.2 (task-style double topbar):

3-theme system (E.3):
- New .playground-mode-{light,dark,midnight} className, localStorage
  key playground-mode (not the global 'theme'); pgMode state + persist
- ModeSwitcher.jsx: 3-mode cycle button (sun/moon/star), placed in
  outer 56px TopBar; TopBar refactored from themeMode/onToggleThemeMode
  to pgMode/onChangePgMode; does NOT dispatch SET_THEME, so <html>
  data-theme stays clean (verified by playground-ui-isolation spec)
- All --pg-* tokens stay scoped to .playground-root; light=#f0f5ff,
  dark=#0a1633, midnight=#000814; alpha=0.40 glass + 20px blur +
  blue shadow + inset highlight
- playground-theme.spec.js updated to use playground-mode key + cycle
  light/dark/midnight

task-style double topbar (v3.2.1-v3.2.3):
- New .pg-iframe-stage flex container holds 48px MainTopBar (top) +
  flex 1 .pg-iframe-wrap (bottom)
- MainTopBar.jsx: 48px glass bar, left = chat_link name + status dot
  (online/loading/error/idle), center = "Playground" eyebrow, right
  = masked token (sk-****xxxx) + info button → StatusPopover
- iframe borderless + transparent bg, "融入" into canvas by way of
  MainTopBar's bottom edge as the implicit frame
- StatusPopover.jsx: 320px glass panel anchored top-right under
  MainTopBar; chat_link metadata + token status; Esc closes (Esc
  priority: statusOpen > launcherOpen)
- Mobile (<768px): MainTopBar hidden to save vertical space

Constraints honored:
- iframe sandbox/allow/referrerPolicy untouched
- replaceChatPlaceholders / API.get('/api/token/playground') /
  getChatLinks unchanged
- 3-theme localStorage key persists, no <html data-theme> pollution
- SideDrawer / CommandLauncher / fullscreen / Cmd+K / Esc all retained
- No task elements that Playground lacks are copied (no right panel,
  no middle panel, no input area, no chat scroll, no stats view)

Co-Authored-By: Claude <noreply@anthropic.com>
playground-design-baseline.spec.js captures 3 PNGs of the chrome
(after hiding the third-party iframe via display:none, 800ms blur settle):

- light @ 1440x900
- dark @ 1440x900
- mobile @ 375x812 (light)

Uses playground-mode localStorage key (not the global 'theme') so the
spec survives v3.1.5 3-theme system. CI does NOT pixel-diff (fragile);
human review of PNGs is the gate.

docs/glassmorphism-baseline.md documents:
- PNG locations under web/tests/e2e/__screenshots__/
- When to regenerate (any token/CSS/component change in Playground/)
- v3.2 visual deltas (double topbar, borderless iframe, popover)
- Regeneration command: aube test:e2e playground-design-baseline

Co-Authored-By: Claude <noreply@anthropic.com>
This pull request has changes conflicting with the target branch.
  • web/package.json
  • web/src/views/Dashboard/index.jsx
  • web/src/views/Setting/component/ChatLinksDataGrid.jsx
  • web/src/views/Setting/component/OperationSetting.jsx
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/update-playground-ui:feat/update-playground-ui
git switch feat/update-playground-ui

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff feat/update-playground-ui
git switch feat/update-playground-ui
git rebase main
git switch main
git merge --ff-only feat/update-playground-ui
git switch feat/update-playground-ui
git rebase main
git switch main
git merge --no-ff feat/update-playground-ui
git switch main
git merge --squash feat/update-playground-ui
git switch main
git merge --ff-only feat/update-playground-ui
git switch main
git merge feat/update-playground-ui
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference

ai/neorouter!1
No description provided.