问题
新建对话页的两个示例任务分组「搭建网页应用」和「搭建和优化智能体」被实现成了单选式手风琴:始终恰好有一个分组展开。
这导致用户无法:
- 点击当前已展开的分组将其收起;
- 同时展开两个分组;
- 同时收起两个分组。
界面使用了展开/收起箭头和 aria-expanded,视觉与无障碍语义都更像独立 disclosure control,但实际行为更接近 tab,容易让人误以为点击没有生效。
复现步骤
- 打开 Penguin Harness Web UI 的新建对话页。
- 默认「搭建网页应用」处于展开状态。
- 再次点击「搭建网页应用」。
- 点击「搭建和优化智能体」。
实际结果:
- 第 3 步没有任何变化,当前分组无法收起;
- 第 4 步会展开第二组并自动收起第一组;
- 任意时刻都只能有一个分组展开。
代码确认
该行为在当前 origin/main(aeffc8a)中被显式写死,并非偶发渲染问题:
- openFolder 的类型是不可空的单个 ExampleFolderId,并默认取第一个分组;代码注释也明确写着 ALWAYS exactly one:
|
/** |
|
* The open example folder — bookmark-style, and ALWAYS exactly one: selecting another closes |
|
* the previous, and clicking the open one is a no-op rather than collapsing it. Never |
|
* nullable on purpose. With every folder the same length, "one open" is what makes the |
|
* block's height a constant: the examples area can neither collapse to bare folder rows nor |
|
* grow, so nothing below it shifts as folders are switched. |
|
*/ |
|
const [openFolder, setOpenFolder] = useState<ExampleFolderId>(EXAMPLE_FOLDERS[0].id); |
- 每个分组是否展开由 folder.id === openFolder 决定;点击只执行 setOpenFolder(folder.id),因此点击当前分组是 no-op,点击其他分组必然替换前一个:
|
{EXAMPLE_FOLDERS.map((folder) => { |
|
const open = folder.id === openFolder; |
|
return ( |
|
<div key={folder.id}> |
|
{/* A tab, not a disclosure: the open folder stays open (clicking it is a |
|
no-op) and carries the selected fill, so the block always shows one |
|
folder's examples and its height never changes. */} |
|
<button |
|
type="button" |
|
aria-expanded={open} |
|
onClick={() => setOpenFolder(folder.id)} |
- 分组清单注释同样规定 exactly ONE folder is open at a time:
|
/** |
|
* Draft-screen example cards, filed into collapsible folders in display order. |
|
* |
|
* Folders are what lets the showcase grow past a flat list: exactly ONE folder is open at a |
|
* time (bookmark-style — opening one closes the other), so the block's height is one row per |
|
* folder plus the open folder's own rows, never the whole catalog. Adding an example means |
|
* appending it to the folder it belongs to, not lengthening the page. |
|
* |
|
* Keep the folders similarly sized: the draft page reserves no scroll area for this block, so |
|
* a folder much longer than its siblings is what would make the height jump between them. |
这一设计由 PR #130 引入,动机是保持示例区高度恒定:
#130
期望行为
将两个分组作为彼此独立的 disclosure controls:
- 点击已展开分组,只收起该分组;
- 点击已收起分组,只展开该分组;
- 支持全部展开、全部收起,以及任意组合;
- aria-expanded 始终与各分组的真实状态一致。
实现上可以将单值 openFolder 改为 Set 或等价的逐组布尔状态。新建对话页外层已经是 overflow-y-auto,全部展开导致的高度增长可以由页面滚动承接。
建议增加一个前端测试,覆盖默认状态、点击当前组收起、两组同时展开、两组同时收起四种状态。
问题
新建对话页的两个示例任务分组「搭建网页应用」和「搭建和优化智能体」被实现成了单选式手风琴:始终恰好有一个分组展开。
这导致用户无法:
界面使用了展开/收起箭头和 aria-expanded,视觉与无障碍语义都更像独立 disclosure control,但实际行为更接近 tab,容易让人误以为点击没有生效。
复现步骤
实际结果:
代码确认
该行为在当前 origin/main(aeffc8a)中被显式写死,并非偶发渲染问题:
penguin-harness/packages/web/src/features/chat/draft-view.tsx
Lines 444 to 451 in aeffc8a
penguin-harness/packages/web/src/features/chat/draft-view.tsx
Lines 531 to 541 in aeffc8a
penguin-harness/packages/web/src/features/chat/example-tasks.ts
Lines 1 to 10 in aeffc8a
这一设计由 PR #130 引入,动机是保持示例区高度恒定:
#130
期望行为
将两个分组作为彼此独立的 disclosure controls:
实现上可以将单值 openFolder 改为 Set 或等价的逐组布尔状态。新建对话页外层已经是 overflow-y-auto,全部展开导致的高度增长可以由页面滚动承接。
建议增加一个前端测试,覆盖默认状态、点击当前组收起、两组同时展开、两组同时收起四种状态。