import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { contextAssetMediaSrc, type EditorAgentContextAsset, EditorAgentRightClickAction, type RightClickMenuTarget, } from './common.ts'; type MessageBubbleRightClickMenuProps = { x: number; y: number; target: RightClickMenuTarget; pendingAction: EditorAgentRightClickAction | null; resultAction: EditorAgentRightClickAction | null; result: 'success' | 'error' | null; onAction: (action: EditorAgentRightClickAction) => void; onClose: () => void; }; const VIEWPORT_MARGIN = 8; function actionLabel({ action, idleLabel, pendingAction, resultAction, result, }: { action: EditorAgentRightClickAction; idleLabel: string; pendingAction: EditorAgentRightClickAction | null; resultAction: EditorAgentRightClickAction | null; result: 'success' | 'error' | null; }) { if (pendingAction === action) { return action === EditorAgentRightClickAction.DownloadAsset ? '下载中' : action === EditorAgentRightClickAction.ReferenceImage ? '引用中' : '复制中'; } if (resultAction !== action) { return idleLabel; } if (result === 'success') { return action === EditorAgentRightClickAction.DownloadAsset ? '已下载' : action === EditorAgentRightClickAction.ReferenceImage ? '已引用' : '已复制'; } if (result === 'error') { return action === EditorAgentRightClickAction.DownloadAsset ? '下载失败' : action === EditorAgentRightClickAction.ReferenceImage ? '引用失败' : '复制失败'; } return idleLabel; } function assetDownloadLabel(asset: EditorAgentContextAsset) { return `下载${ asset.mediaType === 'image' ? '图片' : asset.mediaType === 'video' ? '视频' : '音频' }`; } export function MessageBubbleRightClickMenu({ x, y, target, pendingAction, resultAction, result, onAction, onClose, }: MessageBubbleRightClickMenuProps) { const menuRef = useRef(null); const [position, setPosition] = useState<{ x: number; y: number } | null>( null, ); useLayoutEffect(() => { const menu = menuRef.current; if (!menu || typeof window === 'undefined') { return; } const rect = menu.getBoundingClientRect(); setPosition({ x: Math.min( Math.max(x, VIEWPORT_MARGIN), Math.max( VIEWPORT_MARGIN, window.innerWidth - rect.width - VIEWPORT_MARGIN, ), ), y: Math.min( Math.max(y, VIEWPORT_MARGIN), Math.max( VIEWPORT_MARGIN, window.innerHeight - rect.height - VIEWPORT_MARGIN, ), ), }); }, [target.kind, x, y]); useEffect(() => { const handlePointerDown = (event: PointerEvent) => { if (!menuRef.current?.contains(event.target as Node)) { onClose(); } }; const handleContextMenu = (event: MouseEvent) => { if (menuRef.current?.contains(event.target as Node)) { event.preventDefault(); } onClose(); }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { onClose(); } }; window.addEventListener('pointerdown', handlePointerDown); window.addEventListener('contextmenu', handleContextMenu, true); window.addEventListener('keydown', handleKeyDown); window.addEventListener('scroll', onClose, true); window.addEventListener('resize', onClose); return () => { window.removeEventListener('pointerdown', handlePointerDown); window.removeEventListener('contextmenu', handleContextMenu, true); window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('scroll', onClose, true); window.removeEventListener('resize', onClose); }; }, [onClose]); if (typeof document === 'undefined') { return null; } return createPortal(
event.preventDefault()} > {target.kind === 'text' ? ( ) : ( <> {target.asset.mediaType === 'image' ? ( <> {contextAssetMediaSrc(target.asset).trim() ? ( ) : null} ) : null} )}
, document.body, ); }