Revert "feat(frontend): 增强建议快捷skill工具多层提示交互并更新计划状态"

This reverts commit eed425e965.
This commit is contained in:
肖应宇 2026-04-19 11:38:35 +08:00
parent cbbae3dbd2
commit 24a97ef7d7
14 changed files with 42 additions and 329 deletions

View File

@ -79,5 +79,5 @@ Plans:
- [x] 07-02-PLAN.md — gap closure修复 ContextMenu 自动引用、提示前缀唯一化、Skill 使用 id 拼接
---
*Milestone status:* `in_progress`
*Next command:* `/gsd-execute-phase 6`
*Milestone status:* `complete`
*Next command:* `/gsd-new-milestone`

View File

@ -2,7 +2,7 @@
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: v1.0 milestone in progress
status: v1.0 milestone complete
last_updated: "2026-04-17T06:09:01.300Z"
last_activity: 2026-04-17
progress:
@ -20,13 +20,13 @@ progress:
See: .planning/PROJECT.md (updated 2026-04-07)
**Core value:** Keep the frontend visually familiar while preserving and hardening new-system behavior end to end.
**Current focus:** Milestone v1.0 in progress
**Current focus:** Milestone v1.0 completed
## Workflow State
- Current workflow: milestone execution (v1.0)
- Next workflow: execute-phase
- Next command: /gsd-execute-phase 6
- Current workflow: milestone complete (v1.0)
- Next workflow: new-milestone
- Next command: /gsd-new-milestone
## Artifacts
@ -53,6 +53,5 @@ See: .planning/PROJECT.md (updated 2026-04-07)
|---|-------------|------|--------|-----------|
| 260415-owq | 归档当前git diff为Phase 06验收后补丁检查改动、更新06-UAT/06-VERIFICATION/06-SUMMARY(必要时)与STATE再做原子提交 | 2026-04-15 | atomic | [260415-owq-git-diff-phase-06-06-uat-06-verification](./quick/260415-owq-git-diff-phase-06-06-uat-06-verification/) |
| 260416-koe | 归档 Phase 06 明确指代(“这张图”)语义修复到 GSD 流程(已验收,通过人工确认,免验证) | 2026-04-16 | pending | [260416-koe-phase-06](./quick/260416-koe-phase-06/) |
| 260417-kcb | suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage | 2026-04-17 | pending | [260417-kcb-suggestion-hover-dropdown-child-tooltip-](./quick/260417-kcb-suggestion-hover-dropdown-child-tooltip-/) |
Last activity: 2026-04-17 - Completed quick task 260417-kcb: suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage
Last activity: 2026-04-17

View File

@ -79,5 +79,5 @@ Plans:
- [x] 07-02-PLAN.md — gap closure修复 ContextMenu 自动引用、提示前缀唯一化、Skill 使用 id 拼接
---
*Milestone status:* `in_progress`
*Next command:* `/gsd-execute-phase 6`
*Milestone status:* `complete`
*Next command:* `/gsd-new-milestone`

View File

@ -1,40 +0,0 @@
# Quick Task 260417-kcb: suggestion hover dropdown + child tooltip + bootstrap ids/sessionStorage - Context
**Gathered:** 2026-04-17
**Status:** Ready for planning
<domain>
## Task Boundary
实现 suggestion 多字内容展示:
- 悬浮 suggestion 显示 dropdown内容来自 children
- 悬浮 dropdown 菜单项显示 tooltip内容来自 detail
- 点击 suggestion将 children 的 skill id 组合为数组并发送 bootstrap
- 点击 dropdown 菜单项:仅发送当前 id 进行 bootstrap
- 发起 bootstrap 时同步更新 sessionStorage
</domain>
<decisions>
## Implementation Decisions
### Dropdown Trigger
- 使用 suggestion hover 打开 dropdown同时保留 suggestion click 行为。
### Tooltip Source
- tooltip 文案优先使用 children.detail无 detail 时回退到 skill name。
### Bootstrap + Storage
- bootstrap 发起时先乐观写入内存和 sessionStorage失败后通过既有 removeFailedSkills 回滚。
### the agent's Discretion
- 不变更现有 API 入参与消息结构语义,仅扩展可选字段与前端交互。
</decisions>
<specifics>
## Specific Ideas
新增 children.detail 可选属性以支撑菜单项 tooltip。
</specifics>

View File

@ -1,37 +0,0 @@
---
mode: quick-full
must_haves:
truths:
- suggestion hover 能看到 children dropdown
- dropdown 菜单项 hover 能看到 detail tooltip
- suggestion click 发送 children 全量 id bootstrap
- dropdown item click 仅发送当前 id bootstrap
- bootstrap 发起时 sessionStorage 立即同步,失败可回滚
artifacts:
- frontend/src/components/workspace/input-box.tsx
- frontend/src/hooks/use-iframe-skill.ts
- frontend/src/core/i18n/locales/types.ts
- frontend/src/core/iframe-messages.ts
- frontend/src/core/i18n/locales/zh-CN.ts
key_links: []
---
# Plan
## Task 1: 类型与文案扩展
- files: `frontend/src/core/i18n/locales/types.ts`, `frontend/src/core/iframe-messages.ts`, `frontend/src/core/i18n/locales/zh-CN.ts`
- action: 为 children 增加 `detail` 可选字段并补充中文建议数据。
- verify: TypeScript 通过i18n suggestions 可读取 detail。
- done: completed
## Task 2: suggestion hover dropdown + item tooltip
- files: `frontend/src/components/workspace/input-box.tsx`
- action: 引入 hover 打开 dropdown菜单项展示 childrentooltip 展示 detail。
- verify: 交互符合需求,主 suggestion 与子项点击行为分离。
- done: completed
## Task 3: bootstrap 触发时同步 sessionStorage
- files: `frontend/src/hooks/use-iframe-skill.ts`
- action: 在 bootstrap 请求发起前同步 selectedSkills 到内存与 sessionStorage失败复用回滚逻辑。
- verify: `pnpm -s typecheck` 通过。
- done: completed

View File

@ -1,12 +0,0 @@
# Quick Task 260417-kcb - Research
## Findings
1. 当前 `SuggestionList` 已具备 children 优先 bootstrap 机制,改造点集中在 UI 展示和事件分流。
2. 代码库已有 Radix `DropdownMenu``Tooltip` 封装,直接复用可最小化风险。
3. `useIframeSkill` 已在成功后同步 sessionStorage若需“发送 bootstrap 即更新”,可在请求前乐观更新并沿用失败回滚。
## Integration Notes
- 受影响文件:`input-box.tsx`、`use-iframe-skill.ts`、i18n 类型与 zh-CN 文案。
- 不涉及后端接口变更。

View File

@ -1,15 +0,0 @@
# Quick Task 260417-kcb - Summary
## Delivered
- 新增 `detail` 可选属性并在中文 suggestions 中补充 detail 文案。
- suggestion 悬浮时可见 dropdown内容来自 children。
- dropdown 菜单项悬浮时显示 tooltip内容来自 detail无 detail 回退 name
- 点击 suggestionbootstrap 使用 children 的全部 id。
- 点击 dropdown 菜单项bootstrap 仅携带该菜单项 id。
- bootstrap 发起时即更新 sessionStorage 与本地 selectedSkills失败走回滚。
## Validation
- `cd frontend && pnpm -s typecheck` passed
- `cd frontend && pnpm -s lint` failed仓库已有历史 lint 问题,非本次引入)

View File

@ -1,14 +0,0 @@
status: passed
# Verification
- [x] suggestion hover shows dropdown children
- [x] dropdown item hover shows tooltip(detail)
- [x] suggestion click bootstraps all child ids
- [x] dropdown item click bootstraps only selected id
- [x] bootstrap start updates sessionStorage
- [x] TypeScript check passed
## Residual Risk
- 未在本地启动浏览器进行可视化手动回归;建议在真实页面快速 smoke test 一次。

View File

@ -937,7 +937,6 @@ function SuggestionList({
}) {
const { t } = useI18n();
const { textInput } = usePromptInputController();
const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null);
const suggestions = t.inputBox.suggestions;
const promptSuggestions = suggestions.filter(
(
@ -1003,114 +1002,18 @@ function SuggestionList({
},
[bootstrapAndLockSkills, isBootstrapping, textInput],
);
const handleSuggestionChildClick = useCallback(
({
childSkill,
suggestionTitle,
}: {
childSkill: SelectedSkillPayloadItem;
suggestionTitle: string;
}) => {
if (isBootstrapping) return;
setOpenDropdownKey(null);
const id = String(childSkill.id).trim();
const name = childSkill.name?.trim() ?? "";
if (!id || !name) return;
void bootstrapAndLockSkills({
selectedSkills: [{ id, name, detail: childSkill.detail }],
title: suggestionTitle,
});
},
[bootstrapAndLockSkills, isBootstrapping],
);
return (
<Suggestions
className="min-h-16 w-fit items-start"
data-testid="welcome-suggestions"
>
{promptSuggestions.map((suggestion) => (
(() => {
const childSkills = (suggestion.children ?? [])
.map((item) => ({
id: String(item.id).trim(),
name: item.name?.trim() ?? "",
detail: item.detail?.trim() ?? "",
}))
.filter(
(
item,
): item is {
id: string;
name: string;
detail: string;
} => Boolean(item.id) && Boolean(item.name),
);
if (childSkills.length === 0) {
return (
<Suggestion
key={suggestion.suggestion}
icon={suggestion.icon}
suggestion={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion)}
/>
);
}
const dropdownKey = suggestion.suggestion;
return (
<DropdownMenu
key={dropdownKey}
modal={false}
open={openDropdownKey === dropdownKey}
onOpenChange={(open) => setOpenDropdownKey(open ? dropdownKey : null)}
>
<DropdownMenuTrigger asChild>
<Suggestion
icon={suggestion.icon}
suggestion={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion)}
onMouseEnter={() => setOpenDropdownKey(dropdownKey)}
/>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="min-w-[240px] rounded-[20px] p-[20px]"
onMouseEnter={() => setOpenDropdownKey(dropdownKey)}
onMouseLeave={() => setOpenDropdownKey(null)}
>
<DropdownMenuLabel className="p-0 text-[14px] text-[#333333]">
{suggestion.suggestion}
</DropdownMenuLabel >
<DropdownMenuSeparator className="mx-0 mt-[20px] mb-0" />
<DropdownMenuGroup className=" pt-[20px] px-0">
{childSkills.map((item) => (
<Tooltip
key={`${dropdownKey}-${item.id}`}
content={item.detail || item.name}
side="right"
>
<DropdownMenuItem
className="cursor-pointer rounded-[10px] px-3 py-2"
onSelect={(event) => {
event.preventDefault();
handleSuggestionChildClick({
childSkill: item,
suggestionTitle: suggestion.suggestion,
});
}}
>
{item.name}
</DropdownMenuItem>
</Tooltip>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
})()
<Suggestion
key={suggestion.suggestion}
icon={suggestion.icon}
suggestion={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion)}
/>
))}
</Suggestions>
);

View File

@ -1,7 +1,5 @@
"use client";
import { Children, type ComponentProps, isValidElement } from "react";
import {
Tooltip as TooltipPrimitive,
TooltipContent,
@ -11,23 +9,15 @@ import {
export function Tooltip({
children,
content,
side,
...props
}: {
children: React.ReactNode;
side?: ComponentProps<typeof TooltipContent>["side"];
content?: React.ReactNode;
}) {
const hasSingleElementChild =
Children.count(children) === 1 && isValidElement(children);
const triggerChild = hasSingleElementChild ? children : <span>{children}</span>;
return (
<TooltipPrimitive delayDuration={500} {...props}>
<TooltipTrigger asChild>{triggerChild}</TooltipTrigger>
<TooltipContent side={side}>
<span className="whitespace-pre-line">{content}</span>
</TooltipContent>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
</TooltipPrimitive>
);
}

View File

@ -3,7 +3,6 @@ import type { LucideIcon } from "lucide-react";
export interface SelectedSkillPayloadItem {
id: string | number;
name: string;
detail?: string;
}
export interface Translations {

View File

@ -126,66 +126,31 @@ export const zhCN: Translations = {
prompt:
"为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。",
icon: PenLineIcon,
children: [
{
id: "6057",
name: "生辰解语",
detail:
"四柱八字命理分析。\n当用户询问八字、四柱、命理、算命、Bazi、运势预测、命盘分析\n或想了解其八字命盘、运势、大运、流年时请使用此功能。",
},
],
children: [{ id: "6057", name: "生辰解语" }],
},
{
suggestion: "小红书种草",
prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。",
icon: CompassIcon,
children: [
{
id: "6099",
name: "小红书笔记智造官",
detail:
"根据用户需求及提供资料,撰写小红书笔记内容(标题与正文)。\n生成图片卡片封面及正文卡片并支持发布小红书笔记。",
},
],
children: [{ id: "6099", name: "小红书笔记智造官" }],
},
{
suggestion: "精美报告",
prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。",
icon: GraduationCapIcon,
children: [
{
id: "6100",
name: "MD 转 PDF 助手",
detail:
"将 Markdown.md文件转换为专业排版的 PDF 文档。\n支持将 markdown 转换为 PDF。\n支持将 .md 文件转为 .pdf。\n支持从 markdown 生成 PDF 报告。\n适用于中文技术报告、学术论文、文档编写及各类专业排版需求。",
},
],
children: [{ id: "6100", name: "MD 转 PDF 助手" }],
},
{
suggestion: "excel数据处理",
prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。",
icon: MicroscopeIcon,
children: [
{
id: "17",
name: "Excel处理",
detail:
"全面的电子表格创建、编辑与分析功能。\n支持公式运算、格式设置、数据分析和可视化呈现。\n当 Claude 需要处理各类电子表格文件(如 .xlsx、.xlsm、.csv、.tsv 等格式)时,可执行以下操作:\n(1) 创建包含公式与格式的新表格;\n(2) 读取或分析表格数据;\n(3) 在保留公式的前提下修改现有表格;\n(4) 在表格内进行数据分析与可视化处理;\n(5) 重新计算公式。",
},
],
children: [{ id: "17", name: "Excel处理" }],
},
{
suggestion: "营销策划",
prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。",
icon: ShapesIcon,
children: [
{
id: "217",
name: "产品营销背景",
detail:
"当用户需要创建或更新产品营销背景文档时使用。\n可用于沉淀目标用户、市场环境与竞争格局等关键信息。",
},
],
children: [{ id: "217", name: "产品营销背景" }],
},
],
suggestionsCreate: [

View File

@ -68,7 +68,6 @@ export interface SelectedSkillMessage {
export interface SelectedSkillPayloadItem {
id: string | number;
name: string;
detail?: string;
}
type UnknownRecord = Record<string, unknown>;
@ -108,16 +107,8 @@ export function isSelectedSkillsMessage(
if (!skill) return false;
const id = skill.id;
const name = skill.name;
const detail = skill.detail;
const isValidId = typeof id === "string" || typeof id === "number";
const isValidDetail =
detail === undefined || typeof detail === "string";
return (
isValidId &&
typeof name === "string" &&
name.trim().length > 0 &&
isValidDetail
);
return isValidId && typeof name === "string" && name.trim().length > 0;
});
}

View File

@ -30,26 +30,6 @@ function getThreadStorageKey(threadId?: string | null): string | null {
return `${STORAGE_KEYS.byThreadPrefix}${normalized}`;
}
function persistSkillsToSessionStorage(
skills: SkillData[],
threadId?: string | null,
) {
const threadKey = getThreadStorageKey(threadId);
if (skills.length === 0) {
window.sessionStorage.removeItem(STORAGE_KEYS.latest);
if (threadKey) {
window.sessionStorage.removeItem(threadKey);
}
return;
}
const payload = JSON.stringify(skills);
window.sessionStorage.setItem(STORAGE_KEYS.latest, payload);
if (threadKey) {
window.sessionStorage.setItem(threadKey, payload);
}
}
function parseStoredSkills(raw: string | null): SkillData[] {
if (!raw) return [];
try {
@ -235,8 +215,21 @@ export function useIframeSkill(
// 4. 选择变化时同步到 sessionStorage
useEffect(() => {
// 空数组也要同步到存储,避免 UI 状态与缓存不一致
persistSkillsToSessionStorage(selectedSkills, threadId);
const threadKey = getThreadStorageKey(threadId);
if (selectedSkills.length === 0) {
// 空数组也要同步到存储,避免 UI 状态与缓存不一致
window.sessionStorage.removeItem(STORAGE_KEYS.latest);
if (threadKey) {
window.sessionStorage.removeItem(threadKey);
}
return;
}
const payload = JSON.stringify(selectedSkills);
window.sessionStorage.setItem(STORAGE_KEYS.latest, payload);
if (threadKey) {
window.sessionStorage.setItem(threadKey, payload);
}
}, [selectedSkills, threadId]);
// 发送选择预定义 skill
@ -293,15 +286,6 @@ export function useIframeSkill(
});
try {
// 发起 bootstrap 时立即同步缓存;失败会在 removeFailedSkills 中回滚。
const normalizedSkills = selectedSkills.map((item) => ({
skill_id: String(item.id),
title: item.name,
}));
setSelectedSkill(normalizedSkills[0] ?? null);
setSelectedSkills(normalizedSkills);
persistSkillsToSessionStorage(normalizedSkills, threadId);
const result = await bootstrapRemoteSkill({
thread_id: threadId,
content_ids,
@ -324,12 +308,12 @@ export function useIframeSkill(
}
sendSelectSkill(selectedSkills);
const latestSkills = selectedSkills.map((item) => ({
const normalizedSkills = selectedSkills.map((item) => ({
skill_id: String(item.id),
title: item.name,
}));
setSelectedSkill(latestSkills[0] ?? null);
setSelectedSkills(latestSkills);
setSelectedSkill(normalizedSkills[0] ?? null);
setSelectedSkills(normalizedSkills);
toast.success(t.skills.loadSuccessWithTitle(title), {
description: result.message || t.skills.createdFiles(result.created_files),