9.4 KiB
9.4 KiB
Iframe Skill 选择功能实现计划
Context
实现 iframe 与宿主页之间的 skill 选择通信功能:
- 点击预选 Skill 的 Suggestion 按钮时,向宿主页发送
{ type: 'selectSkill', skill_id },宿主页返回{ skill_id, title } - 点击 IframeSkillDialogButton 时,向宿主页发送
{ type: 'openSkillDialog', openSkillDialog: true },宿主页返回用户选择的{ skill_id, title } - 在 IframeSkillDialogButton 右侧显示可删除的 Tag 标签(显示 title)
目前 skill 为单选,但设计需考虑后期多选扩展。
现有代码分析
关键文件
src/components/workspace/input-box.tsx- 包含IframeSkillDialogButton(L942-958) 和SuggestionList(L854-921)src/components/ui/badge.tsx- Badge 组件,可用于创建可删除 Tagsrc/hooks/use-mobile.ts- 现有 hook 示例src/core/i18n/locales/types.ts(L85-89) - suggestions 类型定义src/core/i18n/locales/zh-CN.ts(L101-127) - 中文 suggestion 数据
IframeSkillDialogButton 现状
// L942-958: 目前只是一个按钮,点击时打开文件对话框
function IframeSkillDialogButton({ className }: { className?: string }) {
const { t } = useI18n();
const attachments = usePromptInputAttachments();
return (
<Tooltip content={t.inputBox.selectSkill}>
<PromptInputButton onClick={() => attachments.openFileDialog()}>
{/* SVG icon */}
</PromptInputButton>
</Tooltip>
);
}
位置:在 input-box.tsx L496 处使用:<IframeSkillDialogButton className="px-2!"/>
SuggestionList 现状
// L854-921: 渲染预定义的建议按钮列表
{t.inputBox.suggestions.map((suggestion) => (
<Suggestion
key={suggestion.suggestion}
icon={suggestion.icon}
suggestion={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion.prompt)}
/>
))}
Files to Modify
1. 新建 Hook: src/hooks/use-iframe-skill.ts
创建 iframe skill 通信 hook:
import { useState, useEffect, useCallback } from 'react';
// 消息类型常量
const MESSAGE_TYPES = {
SELECT_SKILL: 'selectSkill',
OPEN_SKILL_DIALOG: 'openSkillDialog',
} as const;
// Skill 数据类型
interface SkillData {
skill_id: string;
title: string;
}
// Hook 返回类型
interface UseIframeSkillReturn {
selectedSkill: SkillData | null;
sendSelectSkill: (skill_id: string) => void;
openSkillDialog: () => void;
clearSkill: () => void;
}
// 从 URL query 参数解析 skill 数据
function parseSkillFromQuery(): SkillData | null {
const params = new URLSearchParams(window.location.search);
const skill_id = params.get('skill_id');
const title = params.get('title');
if (skill_id && title) {
return { skill_id, title };
}
return null;
}
export function useIframeSkill(): UseIframeSkillReturn {
const [selectedSkill, setSelectedSkill] = useState<SkillData | null>(() => parseSkillFromQuery());
// 发送选择预定义 skill
const sendSelectSkill = useCallback((skill_id: string) => {
window.parent.postMessage({ type: MESSAGE_TYPES.SELECT_SKILL, skill_id }, '*');
}, []);
// 打开 skill 选择对话框
const openSkillDialog = useCallback(() => {
window.parent.postMessage({ type: MESSAGE_TYPES.OPEN_SKILL_DIALOG, openSkillDialog: true }, '*');
}, []);
// 清除选中
const clearSkill = useCallback(() => {
setSelectedSkill(null);
}, []);
// 监听 URL 变化(宿主页通过修改 query 参数返回数据)
useEffect(() => {
const handleUrlChange = () => {
const skill = parseSkillFromQuery();
if (skill) {
setSelectedSkill(skill);
}
};
// 监听 popstate 事件(URL 变化时触发)
window.addEventListener('popstate', handleUrlChange);
// 使用 MutationObserver 监听 URL 变化(某些情况下 popstate 不触发)
const originalPushState = history.pushState;
history.pushState = function(...args) {
originalPushState.apply(history, args);
handleUrlChange();
};
return () => {
window.removeEventListener('popstate', handleUrlChange);
history.pushState = originalPushState;
};
}, []);
return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill };
}
2. 修改类型定义: src/core/i18n/locales/types.ts (L85-89)
扩展 suggestions 类型,添加可选的 skill_id 字段:
// 原代码
suggestions: {
suggestion: string;
prompt: string;
icon: LucideIcon;
}[];
// 修改为
suggestions: {
suggestion: string;
prompt: string;
icon: LucideIcon;
skill_id?: string; // 新增:可选的 skill_id,用于 iframe 通信
}[];
3. 修改 i18n 数据: src/core/i18n/locales/zh-CN.ts (L101-127)
为需要发送 skill_id 的 suggestion 添加字段:
suggestions: [
{
suggestion: "论文写作",
prompt: "撰写一篇关于[主题]的学术论文...",
icon: PenLineIcon,
skill_id: "paper-writing", // 新增
},
// ... 其他建议类似添加
],
4. 修改组件: src/components/workspace/input-box.tsx
4.1 导入 hook (文件顶部)
import { useIframeSkill } from "@/hooks/use-iframe-skill";
import { Badge } from "@/components/ui/badge";
import { XIcon } from "lucide-react";
4.2 修改 IframeSkillDialogButton (L942-958)
function IframeSkillDialogButton({ className }: { className?: string }) {
const { t } = useI18n();
const { selectedSkill, openSkillDialog, clearSkill } = useIframeSkill();
return (
<div className="flex items-center gap-2">
<Tooltip content={t.inputBox.selectSkill}>
<PromptInputButton
className={cn("group px-2! hover:!bg-[#EAE2F5]", className)}
onClick={openSkillDialog} // 改为调用 openSkillDialog
>
{/* SVG icon 保持不变 */}
</PromptInputButton>
</Tooltip>
{/* 显示选中的 skill Tag */}
{selectedSkill && (
<Badge variant="secondary" className="gap-1 pr-1">
{selectedSkill.title}
<button
onClick={clearSkill}
className="ml-1 rounded-full hover:bg-muted-foreground/20"
>
<XIcon className="size-3" />
</button>
</Badge>
)}
</div>
);
}
4.3 修改 SuggestionList (L854-921)
function SuggestionList() {
const { t } = useI18n();
const { textInput } = usePromptInputController();
const { sendSelectSkill } = useIframeSkill();
const handleSuggestionClick = useCallback(
(suggestion: { prompt: string; skill_id?: string }) => {
// 如果有 skill_id,发送给宿主页
if (suggestion.skill_id) {
sendSelectSkill(suggestion.skill_id);
return; // 可选:是否继续填充 prompt
}
// 原有逻辑
if (!suggestion.prompt) return;
textInput.setInput(suggestion.prompt);
// ... 其余代码不变
},
[textInput, sendSelectSkill],
);
return (
<Suggestions className="min-h-16 w-fit items-start">
{t.inputBox.suggestions.map((suggestion) => (
<Suggestion
key={suggestion.suggestion}
icon={suggestion.icon}
suggestion={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion)}
/>
))}
{/* DropdownMenu 部分不变 */}
</Suggestions>
);
}
Message Protocol
发送到宿主页 (iframe → parent)
使用 window.parent.postMessage():
// 选择预定义 skill
{ type: 'selectSkill', skill_id: string }
// 打开 skill 选择对话框
{ type: 'openSkillDialog', openSkillDialog: true }
宿主页返回 (parent → iframe)
宿主页通过修改 iframe 的 URL query 参数返回数据:
?skill_id=xxx&title=xxx
iframe 监听 URL 变化获取数据。
Implementation Steps
-
创建 useIframeSkill hook
- 新建文件
src/hooks/use-iframe-skill.ts - 实现 postMessage 发送和 message 事件监听
- 新建文件
-
扩展 i18n 类型
- 修改
src/core/i18n/locales/types.ts - 添加 skill_id 可选字段
- 修改
-
修改 SuggestionList
- 导入 useIframeSkill hook
- 修改 handleSuggestionClick,检测 skill_id 时发送消息
-
修改 IframeSkillDialogButton
- 导入 useIframeSkill hook 和 Badge 组件
- 点击调用 openSkillDialog
- 显示选中的 skill Tag,支持删除
-
添加 i18n 数据
- 为 zh-CN.ts 和 en-US.ts 的 suggestions 添加 skill_id
Verification
- 在 iframe 环境中测试:
- 点击带 skill_id 的 Suggestion,确认消息发送(可在宿主页控制台查看)
- 模拟宿主页返回
{ skill_id: 'test', title: '测试技能' },确认 Tag 显示正确 - 点击 IframeSkillDialogButton,确认 openSkillDialog 消息发送
- 点击 Tag 的 X 按钮,确认删除功能正常
Notes
- 目前 skill 为单选,selectedSkill 类型为
SkillData | null - 后期扩展多选时,改为
SkillData[]数组类型 - iframe → 宿主页:使用
postMessage - 宿主页 → iframe:通过修改 URL query 参数
?skill_id=xxx&title=xxx - hook 通过监听 URL 变化(popstate + history.pushState 劫持)获取宿主页返回的数据