9b4a25799f
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
3.3 KiB
TypeScript
114 lines
3.3 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { WechatAccountButton } from './WechatAccountButton';
|
|
const HEADER_POPOVER_OPEN_EVENT = 'tkmind:header-popover-open';
|
|
const HEADER_POPOVER_ID = 'header-more-menu';
|
|
|
|
type MenuItem = {
|
|
id: string;
|
|
label: string;
|
|
onClick: () => void;
|
|
disabled?: boolean;
|
|
danger?: boolean;
|
|
};
|
|
|
|
export function ChatHeaderMoreMenu({
|
|
items,
|
|
returnTo,
|
|
showWechat = false,
|
|
}: {
|
|
items: MenuItem[];
|
|
returnTo?: string;
|
|
showWechat?: boolean;
|
|
}) {
|
|
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]);
|
|
|
|
useEffect(() => {
|
|
const handleOtherPopoverOpen = (event: Event) => {
|
|
const detail = (event as CustomEvent<{ id?: string }>).detail;
|
|
if (detail?.id && detail.id !== HEADER_POPOVER_ID) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
window.addEventListener(HEADER_POPOVER_OPEN_EVENT, handleOtherPopoverOpen);
|
|
return () => window.removeEventListener(HEADER_POPOVER_OPEN_EVENT, handleOtherPopoverOpen);
|
|
}, []);
|
|
|
|
return (
|
|
<div className={`header-more-menu${open ? ' open' : ''}`} ref={wrapRef}>
|
|
<button
|
|
type="button"
|
|
className="header-icon-btn"
|
|
aria-label="更多操作"
|
|
aria-expanded={open}
|
|
aria-haspopup="menu"
|
|
onClick={() =>
|
|
setOpen((prev) => {
|
|
const next = !prev;
|
|
if (next) {
|
|
window.dispatchEvent(
|
|
new CustomEvent(HEADER_POPOVER_OPEN_EVENT, {
|
|
detail: { id: HEADER_POPOVER_ID },
|
|
}),
|
|
);
|
|
}
|
|
return next;
|
|
})
|
|
}
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
<circle cx="12" cy="5" r="1.75" />
|
|
<circle cx="12" cy="12" r="1.75" />
|
|
<circle cx="12" cy="19" r="1.75" />
|
|
</svg>
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="header-more-dropdown" role="menu" aria-label="更多操作">
|
|
{items.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
className={`header-more-item${item.danger ? ' header-more-item-danger' : ''}`}
|
|
role="menuitem"
|
|
disabled={item.disabled}
|
|
onClick={() => {
|
|
setOpen(false);
|
|
item.onClick();
|
|
}}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
{showWechat && (
|
|
<>
|
|
<div className="header-more-divider" role="separator" />
|
|
<div className="header-more-wechat">
|
|
<WechatAccountButton returnTo={returnTo} onBeforeAction={() => setOpen(false)} />
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|