e6b3199490
之前遇到几次弹窗的样式不生效,bug 现在在画布agent的删除对话弹窗又出现了, 这个pr把类似的修改集中到最下层的UnifiedModal,适用于所有的modal, 一些特殊的modal有特殊处理. 画布agent的删除对话弹窗  --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/122 Reviewed-by: 段舒康 <kdletters@qq.com> Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
273 lines
8.1 KiB
TypeScript
273 lines
8.1 KiB
TypeScript
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
|
|
import type { ImageCanvasActionResult } from '@/src/components/image-editor/ImageCanvasActionsContext.ts';
|
|
|
|
import { ImageCanvasEditorPortal } from '../ImageCanvasEditorPortal.tsx';
|
|
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: ImageCanvasActionResult | 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: ImageCanvasActionResult | null;
|
|
}) {
|
|
if (pendingAction === action) {
|
|
switch (action) {
|
|
case EditorAgentRightClickAction.FocusCanvas:
|
|
return '定位中';
|
|
case EditorAgentRightClickAction.DownloadAsset:
|
|
return '下载中';
|
|
case EditorAgentRightClickAction.ReferenceImage:
|
|
return '引用中';
|
|
case EditorAgentRightClickAction.CopyText:
|
|
case EditorAgentRightClickAction.CopyImage:
|
|
return '复制中';
|
|
}
|
|
}
|
|
|
|
const actionResult = resultAction === action ? result : null;
|
|
if (!actionResult) {
|
|
return idleLabel;
|
|
}
|
|
|
|
switch (action) {
|
|
case EditorAgentRightClickAction.FocusCanvas:
|
|
if (actionResult.successed) return '已定位';
|
|
switch (actionResult.reason) {
|
|
case 'not-found-on-canva':
|
|
return '画布上不存在';
|
|
case 'other':
|
|
default:
|
|
return '失败';
|
|
}
|
|
case EditorAgentRightClickAction.DownloadAsset:
|
|
if (actionResult.successed) return '已下载';
|
|
return '下载失败';
|
|
case EditorAgentRightClickAction.ReferenceImage:
|
|
if (actionResult.successed) return '已引用';
|
|
return '引用失败';
|
|
case EditorAgentRightClickAction.CopyText:
|
|
case EditorAgentRightClickAction.CopyImage:
|
|
if (actionResult.successed) return '已复制';
|
|
return '复制失败';
|
|
}
|
|
}
|
|
|
|
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<HTMLDivElement | null>(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]);
|
|
|
|
return (
|
|
<ImageCanvasEditorPortal>
|
|
<div
|
|
ref={menuRef}
|
|
className="image-canvas-editor__context-menu"
|
|
role="menu"
|
|
aria-label={
|
|
target.kind === 'text' ? '消息右键菜单' : '消息素材右键菜单'
|
|
}
|
|
style={{
|
|
left: position?.x ?? x,
|
|
top: position?.y ?? y,
|
|
zIndex: 60,
|
|
}}
|
|
onContextMenu={(event) => event.preventDefault()}
|
|
>
|
|
{target.kind === 'text' ? (
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() => onAction(EditorAgentRightClickAction.CopyText)}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.CopyText,
|
|
idleLabel: '复制文本',
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
) : (
|
|
<>
|
|
{target.asset.kind === 'generated_media' &&
|
|
target.asset.resourceId?.trim() ? (
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() =>
|
|
onAction(EditorAgentRightClickAction.FocusCanvas)
|
|
}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.FocusCanvas,
|
|
idleLabel: '在画布中定位',
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
) : null}
|
|
{target.asset.mediaType === 'image' ? (
|
|
<>
|
|
{contextAssetMediaSrc(target.asset).trim() ? (
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() =>
|
|
onAction(EditorAgentRightClickAction.ReferenceImage)
|
|
}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.ReferenceImage,
|
|
idleLabel: '引用',
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() =>
|
|
onAction(EditorAgentRightClickAction.CopyImage)
|
|
}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.CopyImage,
|
|
idleLabel: '复制图片',
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
</>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() =>
|
|
onAction(EditorAgentRightClickAction.DownloadAsset)
|
|
}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.DownloadAsset,
|
|
idleLabel: assetDownloadLabel(target.asset),
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</ImageCanvasEditorPortal>
|
|
);
|
|
}
|