fix(artifact): 调整 artifact 文件预览底部间距

- 注释掉 ArtifactContent 中的 paddingBottom 占位 div
- 为 markdown 预览容器添加 cn 工具函数支持
This commit is contained in:
肖应宇 2026-03-17 10:27:14 +08:00
parent 4950350494
commit 4318b8392a
9 changed files with 446 additions and 36 deletions

View File

@ -0,0 +1,305 @@
# Iframe Skill 选择功能实现计划
## Context
实现 iframe 与宿主页之间的 skill 选择通信功能:
1. 点击预选 Skill 的 Suggestion 按钮时,向宿主页发送 `{ type: 'selectSkill', skill_id }`,宿主页返回 `{ skill_id, title }`
2. 点击 IframeSkillDialogButton 时,向宿主页发送 `{ type: 'openSkillDialog', openSkillDialog: true }`,宿主页返回用户选择的 `{ skill_id, title }`
3. 在 IframeSkillDialogButton 右侧显示可删除的 Tag 标签(显示 title
目前 skill 为单选,但设计需考虑后期多选扩展。
## 现有代码分析
### 关键文件
- `src/components/workspace/input-box.tsx` - 包含 `IframeSkillDialogButton` (L942-958) 和 `SuggestionList` (L854-921)
- `src/components/ui/badge.tsx` - Badge 组件,可用于创建可删除 Tag
- `src/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 现状
```tsx
// 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 现状
```tsx
// 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
```typescript
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` 字段:
```typescript
// 原代码
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 添加字段:
```typescript
suggestions: [
{
suggestion: "论文写作",
prompt: "撰写一篇关于[主题]的学术论文...",
icon: PenLineIcon,
skill_id: "paper-writing", // 新增
},
// ... 其他建议类似添加
],
```
### 4. 修改组件: `src/components/workspace/input-box.tsx`
#### 4.1 导入 hook (文件顶部)
```typescript
import { useIframeSkill } from "@/hooks/use-iframe-skill";
import { Badge } from "@/components/ui/badge";
import { XIcon } from "lucide-react";
```
#### 4.2 修改 IframeSkillDialogButton (L942-958)
```typescript
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)
```typescript
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()`
```typescript
// 选择预定义 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
1. **创建 useIframeSkill hook**
- 新建文件 `src/hooks/use-iframe-skill.ts`
- 实现 postMessage 发送和 message 事件监听
2. **扩展 i18n 类型**
- 修改 `src/core/i18n/locales/types.ts`
- 添加 skill_id 可选字段
3. **修改 SuggestionList**
- 导入 useIframeSkill hook
- 修改 handleSuggestionClick检测 skill_id 时发送消息
4. **修改 IframeSkillDialogButton**
- 导入 useIframeSkill hook 和 Badge 组件
- 点击调用 openSkillDialog
- 显示选中的 skill Tag支持删除
5. **添加 i18n 数据**
- 为 zh-CN.ts 和 en-US.ts 的 suggestions 添加 skill_id
## Verification
1. 在 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 劫持)获取宿主页返回的数据

View File

@ -68,7 +68,6 @@ export default function ChatPage() {
}
},
});
console.log(thread.values.todos);
const handleSubmit = useCallback(
(message: PromptInputMessage) => {
@ -141,7 +140,7 @@ export default function ChatPage() {
)}
>
<InputBox
className={cn("bg-[#FBFAFC] w-full -translate-y-4 ")}
className={cn("bg-[#FBFAFC] w-full -translate-y-4 rounded-[20px] ")}
isNewThread={isNewThread}
threadId={threadId}
autoFocus={isNewThread}

View File

@ -144,7 +144,7 @@ export const ArtifactContent = ({
...props
}: ArtifactContentProps) => (
<div
className={cn("min-h-0 flex-1 overflow-auto p-4", className)}
className={cn("min-h-0 flex-1 overflow-auto p-4", className)}
{...props}
/>
);

View File

@ -247,6 +247,7 @@ export function ArtifactFileDetail({
src={urlOfArtifact({ filepath, threadId, isMock })}
/>
)}
{/* <div style={{ height: `${paddingBottom}px` }} /> */}
</ArtifactContent>
</Artifact>
);
@ -261,13 +262,14 @@ export function ArtifactFilePreview({
}) {
if (language === "markdown") {
return (
<div className="size-full px-4">
<div className={cn("size-full px-4")}>
<Streamdown
className="size-full"
{...streamdownPlugins}
components={{ a: CitationLink }}
>
{content ?? ""}
</Streamdown>
</div>
);

View File

@ -42,6 +42,7 @@ import {
} from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button";
import { ConfettiButton } from "@/components/ui/confetti-button";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
@ -82,6 +83,7 @@ import {
import { useThread } from "./messages/context";
import { ModeHoverGuide } from "./mode-hover-guide";
import { Tooltip } from "./tooltip";
import { useIframeSkill } from "@/hooks/use-iframe-skill";
type InputMode = "flash" | "thinking" | "pro" | "ultra";
@ -453,10 +455,13 @@ export function InputBox({
// height和padding都为0来隐藏
!effectiveIsFocused && "invisible h-[0px] p-[0px] opacity-0 translate-y-2 pointer-events-none"
)}>
{/* ========== 左侧工具栏 ========== */}
<PromptInputTools>
<AddAttachmentsButton className="px-2!" />
<PromptInputActionMenu>
{/* <ModeHoverGuide
{/* 附件上传按钮 */}
<AddAttachmentsButton />
{/* [已禁用] 模式选择器触发器 (flash/thinking/pro/ultra) */}
{/*<PromptInputActionMenu>
<ModeHoverGuide
mode={
context.mode === "flash" ||
context.mode === "thinking" ||
@ -493,8 +498,10 @@ export function InputBox({
</div>
</PromptInputActionMenuTrigger>
</ModeHoverGuide> */}
<IframeSkillDialogButton className="px-2!"/>
<PromptInputActionMenuContent className="w-80">
{/* Skill 选择按钮 (iframe 与宿主页通信) */}
<IframeSkillDialogButton className="px-2!"/>
{/* [已禁用] 模式选择下拉菜单内容 */}
{/* <PromptInputActionMenuContent className="w-80">
<DropdownMenuGroup>
<DropdownMenuLabel className="text-muted-foreground text-xs">
{t.inputBox.mode}
@ -626,6 +633,7 @@ export function InputBox({
</DropdownMenuGroup>
</PromptInputActionMenuContent>
</PromptInputActionMenu>
{/* [已禁用] 推理强度选择器 (minimal/low/medium/high) */}
{supportReasoningEffort && context.mode !== "flash" && (
<PromptInputActionMenu>
<PromptInputActionMenuTrigger className="gap-1! px-2!">
@ -737,8 +745,10 @@ export function InputBox({
</PromptInputActionMenu>
)}
</PromptInputTools>
{/* ========== 右侧工具栏 ========== */}
<PromptInputTools>
<ModelSelector
{/* [已禁用] 模型选择器 */}
{/* <ModelSelector
open={modelDialogOpen}
onOpenChange={setModelDialogOpen}
>
@ -768,13 +778,14 @@ export function InputBox({
))}
</ModelSelectorList>
</ModelSelectorContent>
</ModelSelector>
</ModelSelector> */}
{/* 占位符 */}
<div className="w-[150px]"></div>
</PromptInputTools>
</PromptInputFooter>
{/* 移动出来 */}
<PromptInputSubmit
className="absolute right-3 bottom-3 z-[20]"
className="absolute right-3 bottom-3 z-[20] border-0"
disabled={disabled}
variant="outline"
status={status}
@ -850,21 +861,29 @@ export function InputBox({
</div>
);
}
// 快速选择skillbutton
function SuggestionList() {
const { t } = useI18n();
const { textInput } = usePromptInputController();
const { sendSelectSkill } = useIframeSkill();
const handleSuggestionClick = useCallback(
(prompt: string | undefined) => {
if (!prompt) return;
textInput.setInput(prompt);
(suggestion: { prompt: string; skill_id?: string }) => {
// 如果有 skill_id发送给宿主页
if (suggestion.skill_id) {
sendSelectSkill(suggestion.skill_id);
return;
}
// 原有逻辑
if (!suggestion.prompt) return;
textInput.setInput(suggestion.prompt);
setTimeout(() => {
const textarea = document.querySelector<HTMLTextAreaElement>(
"textarea[name='message']",
);
if (textarea) {
const selStart = prompt.indexOf("[");
const selEnd = prompt.indexOf("]");
const selStart = suggestion.prompt.indexOf("[");
const selEnd = suggestion.prompt.indexOf("]");
if (selStart !== -1 && selEnd !== -1) {
textarea.setSelectionRange(selStart, selEnd + 1);
textarea.focus();
@ -872,7 +891,7 @@ function SuggestionList() {
}
}, 500);
},
[textInput],
[textInput, sendSelectSkill],
);
return (
<Suggestions className="min-h-16 w-fit items-start">
@ -889,7 +908,7 @@ function SuggestionList() {
key={suggestion.suggestion}
icon={suggestion.icon}
suggestion={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion.prompt)}
onClick={() => handleSuggestionClick(suggestion)}
/>
))}
<DropdownMenu>
@ -905,7 +924,7 @@ function SuggestionList() {
!("type" in suggestion) && (
<DropdownMenuItem
key={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion.prompt)}
onClick={() => handleSuggestionClick(suggestion)}
>
{suggestion.icon && <suggestion.icon className="size-4" />}
{suggestion.suggestion}
@ -919,14 +938,14 @@ function SuggestionList() {
</Suggestions>
);
}
// 上传附件
function AddAttachmentsButton({ className }: { className?: string }) {
const { t } = useI18n();
const attachments = usePromptInputAttachments();
return (
<Tooltip content={t.inputBox.addAttachments}>
<PromptInputButton
className={cn("group px-2! hover:!bg-[#EAE2F5]", className)}
className={cn("group px-2! hover:bg-[#EAE2F5] ", className)}
onClick={() => attachments.openFileDialog()}
>
<svg width="18" height="15" viewBox="0 0 18 15" fill="none" xmlns="http://www.w3.org/2000/svg" className="transition-[stroke] duration-200 [&>path]:transition-[fill,stroke] [&>path]:duration-200 [&>path:first-child]:group-hover:fill-[#8E47F0] [&>path:last-child]:group-hover:stroke-[#8E47F0]">
@ -938,21 +957,34 @@ function AddAttachmentsButton({ className }: { className?: string }) {
</Tooltip>
);
}
// 启动iframeSkillDialog
function IframeSkillDialogButton({ className }: { className?: string }) {
const { t } = useI18n();
const attachments = usePromptInputAttachments();
return (
<Tooltip content={t.inputBox.selectSkill}>
<PromptInputButton
className={cn("group px-2! hover:!bg-[#EAE2F5]", className)}
onClick={() => attachments.openFileDialog()}
>
const { selectedSkill, openSkillDialog, clearSkill } = useIframeSkill();
<svg xmlns="http://www.w3.org/2000/svg" className="size-4 transition-[stroke] duration-200 [&>path]:transition-[stroke] [&>path]:duration-200 [&>path]:group-hover:stroke-[#8E47F0]" viewBox="0 0 12 16" fill="none">
<path d="M3.7998 0.5H9.19922C9.24033 0.5 9.26852 0.518136 9.28516 0.541992C9.30124 0.565318 9.30411 0.588767 9.29395 0.613281H9.29297L7.43066 5.07422L7.1416 5.76758H11.3994C11.4295 5.76765 11.4474 5.77552 11.459 5.7832C11.4724 5.79207 11.4846 5.80503 11.4922 5.82129C11.4997 5.83745 11.5013 5.85253 11.5 5.86328C11.4989 5.87156 11.4953 5.88556 11.4785 5.9043L2.87891 15.4629V15.4639C2.85396 15.4914 2.83406 15.4971 2.82031 15.499C2.80144 15.5016 2.77553 15.4981 2.74902 15.4844C2.72225 15.4705 2.70837 15.453 2.70312 15.4424C2.70056 15.4372 2.69457 15.4253 2.70312 15.3936V15.3926L4.30273 9.49512L4.47461 8.86426H0.600586C0.559682 8.86424 0.531324 8.84587 0.514648 8.82227C0.498608 8.79944 0.496551 8.777 0.505859 8.75293L3.70508 0.558594C3.71075 0.544183 3.72173 0.529788 3.73828 0.518555C3.74688 0.51277 3.75704 0.508037 3.76758 0.504883L3.7998 0.5Z" stroke="#150033"/>
</svg>
</PromptInputButton>
</Tooltip>
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}
>
<svg xmlns="http://www.w3.org/2000/svg" className="size-4 transition-[stroke] duration-200 [&>path]:transition-[stroke] [&>path]:duration-200 [&>path]:group-hover:stroke-[#8E47F0]" viewBox="0 0 12 16" fill="none">
<path d="M3.7998 0.5H9.19922C9.24033 0.5 9.26852 0.518136 9.28516 0.541992C9.30124 0.565318 9.30411 0.588767 9.29395 0.613281H9.29297L7.43066 5.07422L7.1416 5.76758H11.3994C11.4295 5.76765 11.4474 5.77552 11.459 5.7832C11.4724 5.79207 11.4846 5.80503 11.4922 5.82129C11.4997 5.83745 11.5013 5.85253 11.5 5.86328C11.4989 5.87156 11.4953 5.88556 11.4785 5.9043L2.87891 15.4629V15.4639C2.85396 15.4914 2.83406 15.4971 2.82031 15.499C2.80144 15.5016 2.77553 15.4981 2.74902 15.4844C2.72225 15.4705 2.70837 15.453 2.70312 15.4424C2.70056 15.4372 2.69457 15.4253 2.70312 15.3936V15.3926L4.30273 9.49512L4.47461 8.86426H0.600586C0.559682 8.86424 0.531324 8.84587 0.514648 8.82227C0.498608 8.79944 0.496551 8.777 0.505859 8.75293L3.70508 0.558594C3.71075 0.544183 3.72173 0.529788 3.73828 0.518555C3.74688 0.51277 3.75704 0.508037 3.76758 0.504883L3.7998 0.5Z" stroke="#150033"/>
</svg>
</PromptInputButton>
</Tooltip>
{selectedSkill && (
<Badge variant="secondary" className="gap-1 px-[15px] py-[4px] text-[#8E47F0] bg-[#EAE2F5]">
{selectedSkill.title}
<button
onClick={clearSkill}
className="ml-1 rounded-full hover:bg-muted-foreground/20"
>
<XIcon className="size-3" />
</button>
</Badge>
)}
</div>
);
}

View File

@ -107,26 +107,31 @@ export const enUS: Translations = {
suggestion: "Paper Writing",
prompt: "Write an academic paper about [topic], including abstract, introduction, body and references.",
icon: PenLineIcon,
skill_id: "paper-writing",
},
{
suggestion: "Report Generation",
prompt: "Analyze [topic] in depth and generate a well-structured research report.",
icon: MicroscopeIcon,
skill_id: "report-generation",
},
{
suggestion: "Copywriting",
prompt: "Create a complete planning proposal and promotional copy for [project/event].",
icon: ShapesIcon,
skill_id: "planning-copywriting",
},
{
suggestion: "PPT Generation",
prompt: "Generate a PPT presentation outline and content about [topic].",
icon: GraduationCapIcon,
skill_id: "ppt-generation",
},
{
suggestion: "Document Processing",
prompt: "Process [document] with reading, summarizing, translating or format conversion.",
icon: CompassIcon,
skill_id: "document-processing",
},
],
suggestionsCreate: [

View File

@ -86,6 +86,7 @@ export interface Translations {
suggestion: string;
prompt: string;
icon: LucideIcon;
skill_id?: string;
}[];
suggestionsCreate: (
| {

View File

@ -103,26 +103,31 @@ export const zhCN: Translations = {
suggestion: "论文写作",
prompt: "撰写一篇关于[主题]的学术论文,包含摘要、引言、正文和参考文献。",
icon: PenLineIcon,
skill_id: "1",
},
{
suggestion: "报告生成",
prompt: "深入分析[主题],生成一份结构清晰的调研报告。",
icon: MicroscopeIcon,
skill_id: "2",
},
{
suggestion: "策划文案",
prompt: "为[项目/活动]撰写一份完整的策划方案和宣传文案。",
icon: ShapesIcon,
skill_id: "3",
},
{
suggestion: "PPT生成",
prompt: "生成一个关于[主题]的PPT演示文稿大纲和内容。",
icon: GraduationCapIcon,
skill_id: "4",
},
{
suggestion: "文档处理",
prompt: "对[文档]进行阅读、总结、翻译或格式转换等处理。",
icon: CompassIcon,
skill_id: "5",
},
],
suggestionsCreate: [

View File

@ -0,0 +1,61 @@
import { useState, useEffect, useCallback } from 'react';
import { useSearchParams } from 'next/navigation';
// 消息类型常量
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;
}
export function useIframeSkill(): UseIframeSkillReturn {
const searchParams = useSearchParams();
const skillIdFromQuery = searchParams.get('skill_id');
const titleFromQuery = searchParams.get('title');
const [selectedSkill, setSelectedSkill] = useState<SkillData | null>(null);
// 监听 query 参数变化
useEffect(() => {
console.log('[useIframeSkill] Query params changed - skill_id:', skillIdFromQuery, 'title:', titleFromQuery);
if (skillIdFromQuery && titleFromQuery) {
const skill = { skill_id: skillIdFromQuery, title: titleFromQuery };
console.log('[useIframeSkill] Setting skill from URL:', skill);
setSelectedSkill(skill);
}
}, [skillIdFromQuery, titleFromQuery]);
// 发送选择预定义 skill
const sendSelectSkill = useCallback((skill_id: string) => {
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };
console.log('[useIframeSkill] sendSelectSkill:', message);
window.parent.postMessage(message, '*');
}, []);
// 打开 skill 选择对话框
const openSkillDialog = useCallback(() => {
const message = { type: MESSAGE_TYPES.OPEN_SKILL_DIALOG, openSkillDialog: true };
console.log('[useIframeSkill] openSkillDialog:', message);
window.parent.postMessage(message, '*');
}, []);
// 清除选中
const clearSkill = useCallback(() => {
setSelectedSkill(null);
}, []);
return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill };
}