Files
memind/src/components/ChatSkillPicker.tsx
T

91 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}