91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
||
import type { ChatSkillOption } from '../utils/chatSkills';
|
||
import { ChatSkillIcon } from './ChatSkillIcons';
|
||
|
||
export function ChatSkillPicker({
|
||
skills,
|
||
disabled,
|
||
onboardingActive = false,
|
||
onSelect,
|
||
onPrefill,
|
||
}: {
|
||
skills: ChatSkillOption[];
|
||
disabled?: boolean;
|
||
onboardingActive?: boolean;
|
||
onSelect: (prompt: string, skillId?: string) => void | Promise<void>;
|
||
onPrefill?: (prompt: string, skillId?: string) => void;
|
||
}) {
|
||
const [open, setOpen] = useState(false);
|
||
const wrapRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const handleClick = (event: MouseEvent) => {
|
||
if (wrapRef.current && !wrapRef.current.contains(event.target as Node)) {
|
||
setOpen(false);
|
||
}
|
||
};
|
||
const handleKey = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') setOpen(false);
|
||
};
|
||
document.addEventListener('click', handleClick);
|
||
document.addEventListener('keydown', handleKey);
|
||
return () => {
|
||
document.removeEventListener('click', handleClick);
|
||
document.removeEventListener('keydown', handleKey);
|
||
};
|
||
}, [open]);
|
||
|
||
if (skills.length === 0) return null;
|
||
|
||
const handlePick = (skill: ChatSkillOption) => {
|
||
setOpen(false);
|
||
const prompt = skill.buildPrompt(skill.skillName);
|
||
if (skill.prefillOnly) {
|
||
onPrefill?.(prompt, skill.id);
|
||
return;
|
||
}
|
||
void onSelect(prompt, skill.id);
|
||
};
|
||
|
||
return (
|
||
<div className={`chat-skill-picker${open ? ' open' : ''}`} ref={wrapRef}>
|
||
<button
|
||
type="button"
|
||
className={`chat-skill-trigger${onboardingActive ? ' chat-control-onboarding-active' : ''}`}
|
||
disabled={disabled}
|
||
aria-expanded={open}
|
||
aria-haspopup="menu"
|
||
title="选择 Skill 并发送"
|
||
onClick={() => setOpen((prev) => !prev)}
|
||
>
|
||
<ChatSkillIcon id="spark" className="chat-skill-trigger-icon" />
|
||
<span className="chat-skill-trigger-label">skill</span>
|
||
{onboardingActive && (
|
||
<span className="chat-control-onboarding-tip" role="status">点击 Skill,更快捷完成任务</span>
|
||
)}
|
||
</button>
|
||
|
||
{open && (
|
||
<div className="chat-skill-menu" role="menu" aria-label="Skill 列表">
|
||
{skills.map((skill) => (
|
||
<button
|
||
key={skill.id}
|
||
type="button"
|
||
className="chat-skill-menu-item"
|
||
role="menuitem"
|
||
onClick={() => handlePick(skill)}
|
||
>
|
||
<span className="chat-skill-menu-leading">
|
||
<ChatSkillIcon id={skill.icon} className="chat-skill-menu-icon" />
|
||
<span className="chat-skill-menu-label">{skill.label}</span>
|
||
</span>
|
||
<span className="chat-skill-menu-tag">skill</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|