统一快速编辑与改造生成弹窗底盘
复用 ImageCanvasBasicGenerationComposerView 作为快速编辑面板容器 复用 ImageCanvasBasicGenerationComposerView 作为改造对话框容器 为基础底盘增加可配置化参数,覆盖标题、提示词、按钮、参考图和引用源能力 生成器参数与状态可由外层覆盖,支持快速编辑重绘等场景定制 Quick-edit 参考图使用通用参考图 Slot,不再手写组件
This commit is contained in:
@@ -16,14 +16,27 @@ import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import type {
|
||||
CharacterReferenceImage,
|
||||
GenerateDialogState,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
||||
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
||||
import {
|
||||
calculateEditorImageGenerationPrice,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { calculateEditorImageGenerationPrice } from './ImageCanvasGenerationModel';
|
||||
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
||||
|
||||
type ReferenceLabelFormatter = (
|
||||
reference: CharacterReferenceImage,
|
||||
index: number,
|
||||
) => string;
|
||||
|
||||
type ReferenceTone =
|
||||
| 'default'
|
||||
| 'spec'
|
||||
| 'icon'
|
||||
| 'ui'
|
||||
| 'quick-edit'
|
||||
| 'video'
|
||||
| 'audio';
|
||||
|
||||
type ImageCanvasBasicGenerationComposerViewProps = {
|
||||
dialog: GenerateDialogState;
|
||||
style: CSSProperties;
|
||||
@@ -37,11 +50,41 @@ type ImageCanvasBasicGenerationComposerViewProps = {
|
||||
anchor: HTMLElement | null,
|
||||
placement: 'above' | 'below',
|
||||
) => CSSProperties;
|
||||
onRequestUpload: (target: 'generation-reference') => void;
|
||||
onRequestUpload: (target: UploadTarget) => void;
|
||||
onPickReferenceFromCanvas?: () => void;
|
||||
onToggleReferenceMenu?: () => void;
|
||||
onRememberImageModel?: (model: string) => void;
|
||||
onSubmit: (dialog: GenerateDialogState) => void;
|
||||
dialogLabel?: string;
|
||||
includeDimensions?: boolean;
|
||||
includeModel?: boolean;
|
||||
includeReferences?: boolean;
|
||||
promptLabel?: string;
|
||||
promptPlaceholder?: string;
|
||||
optionLabelPrefix?: string;
|
||||
dimensionRatioAriaLabelPrefix?: string;
|
||||
dimensionSizeAriaLabelPrefix?: string;
|
||||
referenceUploadTarget?: UploadTarget;
|
||||
referenceButtonIcon?: ReactNode;
|
||||
referenceButtonLabel?: string;
|
||||
referenceButtonAriaLabel?: string;
|
||||
referenceMenuLabel?: string;
|
||||
referenceSlotTone?: ReferenceTone;
|
||||
referenceSlotDisabled?: boolean;
|
||||
referenceLabelFormatter?: ReferenceLabelFormatter;
|
||||
referenceAriaLabelFormatter?: ReferenceLabelFormatter;
|
||||
referenceRemoveLabelFormatter?: ReferenceLabelFormatter;
|
||||
showReferenceAddButton?: boolean;
|
||||
formClassName?: string;
|
||||
footerClassName?: string;
|
||||
promptClassName?: string;
|
||||
referenceSlotClassName?: string;
|
||||
submitButtonClassName?: string;
|
||||
submitLabel?: string;
|
||||
submitAriaLabel?: string;
|
||||
submittingStatusLabel?: string;
|
||||
};
|
||||
|
||||
function resetFailedDialogStatus(dialog: GenerateDialogState) {
|
||||
return {
|
||||
...dialog,
|
||||
@@ -50,31 +93,6 @@ function resetFailedDialogStatus(dialog: GenerateDialogState) {
|
||||
};
|
||||
}
|
||||
|
||||
function ReferenceChip({
|
||||
reference,
|
||||
index,
|
||||
onRemove,
|
||||
}: {
|
||||
reference: CharacterReferenceImage;
|
||||
index: number;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
const label = reference.label || `参考图${index + 1}`;
|
||||
return (
|
||||
<ImageCanvasReferenceSlot
|
||||
tone="default"
|
||||
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
||||
imageSrc={reference.src}
|
||||
objectKey={reference.objectKey}
|
||||
label={label}
|
||||
ariaLabel={label}
|
||||
title={reference.label}
|
||||
onRemove={onRemove}
|
||||
removeLabel={`删除${label}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ImageCanvasBasicGenerationComposerView({
|
||||
dialog,
|
||||
style,
|
||||
@@ -86,12 +104,60 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
renderEditorPortal = (node) => node,
|
||||
buildPortalMenuStyle = () => ({}),
|
||||
onRequestUpload,
|
||||
onPickReferenceFromCanvas,
|
||||
onToggleReferenceMenu,
|
||||
onRememberImageModel = () => {},
|
||||
onSubmit,
|
||||
dialogLabel,
|
||||
includeDimensions = true,
|
||||
includeModel = true,
|
||||
includeReferences = true,
|
||||
promptLabel,
|
||||
promptPlaceholder,
|
||||
optionLabelPrefix,
|
||||
dimensionRatioAriaLabelPrefix,
|
||||
dimensionSizeAriaLabelPrefix,
|
||||
referenceUploadTarget = 'generation-reference',
|
||||
referenceButtonIcon = <ImageIcon className="h-4 w-4" aria-hidden="true" />,
|
||||
referenceButtonLabel = '参考图',
|
||||
referenceButtonAriaLabel = '添加参考图',
|
||||
referenceMenuLabel = '参考图来源',
|
||||
referenceSlotTone = 'default',
|
||||
referenceSlotDisabled = false,
|
||||
referenceLabelFormatter,
|
||||
referenceAriaLabelFormatter,
|
||||
referenceRemoveLabelFormatter,
|
||||
showReferenceAddButton = true,
|
||||
formClassName,
|
||||
footerClassName,
|
||||
promptClassName = 'image-canvas-editor__generation-prompt',
|
||||
referenceSlotClassName,
|
||||
submitButtonClassName = 'image-canvas-editor__generation-submit',
|
||||
submitLabel = '生成',
|
||||
submitAriaLabel = '生成',
|
||||
submittingStatusLabel = '生成中',
|
||||
}: ImageCanvasBasicGenerationComposerViewProps) {
|
||||
const references = dialog.generationReferences ?? [];
|
||||
const isQuickEdit = dialog.mode === 'quick-edit';
|
||||
const resolvedDialogLabel =
|
||||
dialogLabel ?? optionLabelPrefix ?? (isQuickEdit ? '改造图片' : '生成图片');
|
||||
const resolvedPromptLabel =
|
||||
promptLabel ?? (isQuickEdit ? '改造提示词' : '生成提示词');
|
||||
const resolvedPromptPlaceholder =
|
||||
promptPlaceholder ??
|
||||
(isQuickEdit ? '想怎么改造这张图?' : '今天想生成什么画面?');
|
||||
const resolvedOptionLabelPrefix =
|
||||
optionLabelPrefix ?? (isQuickEdit ? '改造图片' : '生成图片');
|
||||
const resolvedReferenceButtonLabel = referenceButtonLabel;
|
||||
const resolvedReferenceButtonAriaLabel =
|
||||
referenceButtonAriaLabel ?? `添加${resolvedReferenceButtonLabel}`;
|
||||
const hasReferenceMenu = includeReferences && onRequestUpload;
|
||||
const finalFormClassName =
|
||||
formClassName ??
|
||||
'image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image';
|
||||
const finalFooterClassName =
|
||||
footerClassName ?? 'image-canvas-editor__generation-composer-footer';
|
||||
|
||||
useImageCanvasFloatingOptionDismiss({
|
||||
isOpen: isGenerationReferenceMenuOpen,
|
||||
boundaryRefs: [generationReferenceButtonRef],
|
||||
@@ -101,10 +167,10 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image"
|
||||
className={finalFormClassName}
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label={isQuickEdit ? '改造图片' : '生成图片'}
|
||||
aria-label={resolvedDialogLabel}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
@@ -113,102 +179,133 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
{references.map((reference, index) => (
|
||||
<ReferenceChip
|
||||
key={reference.id}
|
||||
reference={reference}
|
||||
index={index}
|
||||
onRemove={() =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
generationReferences: (
|
||||
currentDialog.generationReferences ?? []
|
||||
).filter((item) => item.id !== reference.id),
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<ImageCanvasReferenceSlot
|
||||
buttonRef={generationReferenceButtonRef}
|
||||
tone="default"
|
||||
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
||||
label="参考图"
|
||||
ariaLabel="添加参考图"
|
||||
disabled={dialog.status === 'generating'}
|
||||
isAdd
|
||||
onClick={() => onToggleReferenceMenu?.()}
|
||||
/>
|
||||
</div>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
aria-label={isQuickEdit ? '改造提示词' : '生成提示词'}
|
||||
value={dialog.prompt}
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder={isQuickEdit ? '想怎么改造这张图?' : '今天想生成什么画面?'}
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
prompt: event.target.value,
|
||||
{includeReferences ? (
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
{references.map((reference, index) => {
|
||||
const label =
|
||||
(referenceLabelFormatter?.(reference, index) ??
|
||||
reference.label) ||
|
||||
`参考图${index + 1}`;
|
||||
const ariaLabel =
|
||||
referenceAriaLabelFormatter?.(reference, index) ?? label;
|
||||
const removeLabel =
|
||||
referenceRemoveLabelFormatter?.(reference, index) ??
|
||||
`删除${ariaLabel}`;
|
||||
|
||||
return (
|
||||
<ImageCanvasReferenceSlot
|
||||
key={reference.id}
|
||||
tone={referenceSlotTone}
|
||||
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
||||
imageSrc={reference.src}
|
||||
objectKey={reference.objectKey}
|
||||
label={label}
|
||||
ariaLabel={ariaLabel}
|
||||
title={reference.label}
|
||||
onRemove={() =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
generationReferences: (
|
||||
currentDialog.generationReferences ?? []
|
||||
).filter((item) => item.id !== reference.id),
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
removeLabel={removeLabel}
|
||||
className={referenceSlotClassName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{showReferenceAddButton ? (
|
||||
<ImageCanvasReferenceSlot
|
||||
buttonRef={generationReferenceButtonRef}
|
||||
tone={referenceSlotTone}
|
||||
icon={referenceButtonIcon}
|
||||
label={resolvedReferenceButtonLabel}
|
||||
ariaLabel={resolvedReferenceButtonAriaLabel}
|
||||
disabled={
|
||||
dialog.status === 'generating' || referenceSlotDisabled
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="image-canvas-editor__generation-composer-footer">
|
||||
<ImageCanvasGenerationImageOptionsView
|
||||
dialog={dialog}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
includeDimensions
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
optionLabelPrefix={isQuickEdit ? '改造图片' : '生成图片'}
|
||||
cost={calculateEditorImageGenerationPrice({
|
||||
model: dialog.imageModel,
|
||||
imageSize: dialog.imageSize,
|
||||
})}
|
||||
submitLabel="生成"
|
||||
submitAriaLabel="生成"
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
isAdd
|
||||
onClick={() => onToggleReferenceMenu?.()}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
aria-label={resolvedPromptLabel}
|
||||
value={dialog.prompt}
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder={resolvedPromptPlaceholder}
|
||||
size="sm"
|
||||
density="compact"
|
||||
className={promptClassName}
|
||||
onChange={(event) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...resetFailedDialogStatus(currentDialog),
|
||||
prompt: event.target.value,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{dialog.status === 'generating' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="info"
|
||||
surface="platform"
|
||||
size="xs"
|
||||
className="image-canvas-editor__generate-status"
|
||||
role="status"
|
||||
>
|
||||
生成中
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
size="xs"
|
||||
className="image-canvas-editor__generate-status"
|
||||
role="alert"
|
||||
>
|
||||
{dialog.errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
<div className={finalFooterClassName}>
|
||||
<ImageCanvasGenerationImageOptionsView
|
||||
dialog={dialog}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
includeDimensions={includeDimensions}
|
||||
includeModel={includeModel}
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
dimensionRatioAriaLabelPrefix={dimensionRatioAriaLabelPrefix}
|
||||
dimensionSizeAriaLabelPrefix={dimensionSizeAriaLabelPrefix}
|
||||
optionLabelPrefix={resolvedOptionLabelPrefix}
|
||||
cost={calculateEditorImageGenerationPrice({
|
||||
model: dialog.imageModel,
|
||||
imageSize: dialog.imageSize,
|
||||
})}
|
||||
submitLabel={submitLabel}
|
||||
submitAriaLabel={submitAriaLabel}
|
||||
submitButtonClassName={submitButtonClassName}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
/>
|
||||
</div>
|
||||
{dialog.status === 'generating' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="info"
|
||||
surface="platform"
|
||||
size="xs"
|
||||
className="image-canvas-editor__generate-status"
|
||||
role="status"
|
||||
>
|
||||
{submittingStatusLabel}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
size="xs"
|
||||
className="image-canvas-editor__generate-status"
|
||||
role="alert"
|
||||
>
|
||||
{dialog.errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
</form>
|
||||
{isGenerationReferenceMenuOpen && generationReferenceButtonRef
|
||||
{hasReferenceMenu &&
|
||||
isGenerationReferenceMenuOpen &&
|
||||
generationReferenceButtonRef
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
||||
label="参考图来源"
|
||||
label={referenceMenuLabel}
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
generationReferenceButtonRef.current,
|
||||
@@ -218,7 +315,11 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen?.(false);
|
||||
setIsPickingGenerationReferenceFromCanvas?.(true);
|
||||
if (onPickReferenceFromCanvas) {
|
||||
onPickReferenceFromCanvas();
|
||||
} else {
|
||||
setIsPickingGenerationReferenceFromCanvas?.(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
从画布中选择
|
||||
@@ -227,7 +328,7 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen?.(false);
|
||||
setIsPickingGenerationReferenceFromCanvas?.(false);
|
||||
onRequestUpload('generation-reference');
|
||||
onRequestUpload(referenceUploadTarget);
|
||||
}}
|
||||
>
|
||||
上传图片
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { type Dispatch, type SetStateAction } from 'react';
|
||||
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
import {
|
||||
calculateEditorImageGenerationPrice,
|
||||
IMAGE_MODEL_GPT_IMAGE_2,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
|
||||
import type { GenerateDialogState } from './ImageCanvasEditorTypes';
|
||||
|
||||
type ImageCanvasEditGenerationModalViewProps = {
|
||||
@@ -40,70 +38,32 @@ export function ImageCanvasEditGenerationModalView({
|
||||
bodyClassName="image-canvas-editor__generate-dialog-body"
|
||||
>
|
||||
{dialog?.mode === 'edit' ? (
|
||||
<form
|
||||
className="image-canvas-editor__generate-form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
<ImageCanvasBasicGenerationComposerView
|
||||
dialog={{
|
||||
...dialog,
|
||||
mode: 'edit',
|
||||
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
generationReferences: [],
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__generate-body">
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
aria-label="生成提示词"
|
||||
value={dialog.prompt}
|
||||
disabled={dialog.status === 'generating'}
|
||||
size="sm"
|
||||
density="roomy"
|
||||
className="image-canvas-editor__generate-prompt"
|
||||
placeholder="描述你想如何修改这张图片"
|
||||
onChange={(event) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...currentDialog,
|
||||
prompt: event.target.value,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
{dialog.status === 'generating' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="info"
|
||||
surface="platform"
|
||||
size="xs"
|
||||
className="image-canvas-editor__generate-status"
|
||||
role="status"
|
||||
>
|
||||
修改中
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
{dialog.status === 'failed' ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
surface="platform"
|
||||
size="xs"
|
||||
className="image-canvas-editor__generate-status"
|
||||
role="alert"
|
||||
>
|
||||
{dialog.errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
size="sm"
|
||||
className="image-canvas-editor__generate-submit"
|
||||
disabled={dialog.status === 'generating'}
|
||||
>
|
||||
{dialog.status === 'generating'
|
||||
? '修改中'
|
||||
: `修改${editPrice}泥点`}
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</form>
|
||||
style={{}}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
onRequestUpload={() => {}}
|
||||
onSubmit={onSubmit}
|
||||
dialogLabel="修改图片"
|
||||
includeDimensions={false}
|
||||
includeModel={false}
|
||||
includeReferences={false}
|
||||
promptPlaceholder="描述你想如何修改这张图片"
|
||||
submitLabel="修改"
|
||||
submitAriaLabel={`修改${editPrice}泥点`}
|
||||
submittingStatusLabel="修改中"
|
||||
formClassName="image-canvas-editor__generate-form"
|
||||
footerClassName="image-canvas-editor__generate-body"
|
||||
promptClassName="image-canvas-editor__generate-prompt"
|
||||
submitButtonClassName="image-canvas-editor__generate-submit"
|
||||
/>
|
||||
) : null}
|
||||
</UnifiedModal>
|
||||
);
|
||||
|
||||
@@ -28,8 +28,11 @@ type ImageCanvasGenerationImageOptionsViewProps = {
|
||||
dialog: GenerateDialogState;
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
includeDimensions: boolean;
|
||||
includeModel?: boolean;
|
||||
onRememberImageModel: (model: string) => void;
|
||||
optionLabelPrefix?: string;
|
||||
dimensionRatioAriaLabelPrefix?: string;
|
||||
dimensionSizeAriaLabelPrefix?: string;
|
||||
cost?: number;
|
||||
submitLabel?: string;
|
||||
submitAriaLabel?: string;
|
||||
@@ -87,11 +90,13 @@ function OptionChoice({
|
||||
children,
|
||||
selected,
|
||||
className,
|
||||
ariaLabel,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
selected: boolean;
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
@@ -100,6 +105,7 @@ function OptionChoice({
|
||||
className={['image-canvas-editor__option-popover-choice', className]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
aria-label={ariaLabel}
|
||||
aria-pressed={selected}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -112,8 +118,11 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
dialog,
|
||||
setGenerateDialog,
|
||||
includeDimensions,
|
||||
includeModel = true,
|
||||
onRememberImageModel,
|
||||
optionLabelPrefix,
|
||||
dimensionRatioAriaLabelPrefix,
|
||||
dimensionSizeAriaLabelPrefix,
|
||||
cost,
|
||||
submitLabel = '生成',
|
||||
submitAriaLabel = '生成',
|
||||
@@ -138,10 +147,15 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
imageSize: selection.imageSize,
|
||||
});
|
||||
const isGenerating = dialog.status === 'generating';
|
||||
const isModelLocked = Boolean(normalizedLockedModel);
|
||||
const isModelLocked = Boolean(normalizedLockedModel) && includeModel;
|
||||
const labelPrefix = optionLabelPrefix ?? '生成图片';
|
||||
const ratioAriaLabelPrefix = dimensionRatioAriaLabelPrefix?.trim() ?? '';
|
||||
const sizeAriaLabelPrefix = dimensionSizeAriaLabelPrefix?.trim() ?? '';
|
||||
const dismissOpenPanel = useCallback(() => setOpenPanel(null), []);
|
||||
|
||||
const formatDimensionOptionLabel = (prefix: string, value: string) =>
|
||||
prefix ? `${prefix} ${value}` : value;
|
||||
|
||||
useImageCanvasFloatingOptionDismiss({
|
||||
isOpen: openPanel !== null,
|
||||
boundaryRefs: [dimensionsButtonRef, modelButtonRef],
|
||||
@@ -236,6 +250,10 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
key={aspectRatio}
|
||||
selected={selection.aspectRatio === aspectRatio}
|
||||
className="image-canvas-editor__option-popover-choice--ratio"
|
||||
ariaLabel={formatDimensionOptionLabel(
|
||||
ratioAriaLabelPrefix,
|
||||
aspectRatio,
|
||||
)}
|
||||
onClick={() => updateDialog({ aspectRatio })}
|
||||
>
|
||||
<span
|
||||
@@ -257,6 +275,10 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
<OptionChoice
|
||||
key={imageSize}
|
||||
selected={selection.imageSize === imageSize}
|
||||
ariaLabel={formatDimensionOptionLabel(
|
||||
sizeAriaLabelPrefix,
|
||||
imageSize,
|
||||
)}
|
||||
onClick={() => updateDialog({ imageSize })}
|
||||
>
|
||||
{imageSize}
|
||||
@@ -270,80 +292,82 @@ export function ImageCanvasGenerationImageOptionsView({
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={[
|
||||
'image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model',
|
||||
isModelLocked
|
||||
? 'image-canvas-editor__readonly-generation-option'
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<PlatformInlineOptionButton
|
||||
ref={modelButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
||||
aria-label={`${labelPrefix}模型 ${selectedModelLabel}`}
|
||||
aria-expanded={isModelLocked ? undefined : openPanel === 'model'}
|
||||
disabled={isGenerating || isModelLocked}
|
||||
trailingIcon={
|
||||
isModelLocked ? undefined : <ChevronDown className="h-3 w-3" />
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isModelLocked) {
|
||||
togglePanel('model');
|
||||
}
|
||||
}}
|
||||
{includeModel ? (
|
||||
<div
|
||||
className={[
|
||||
'image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model',
|
||||
isModelLocked
|
||||
? 'image-canvas-editor__readonly-generation-option'
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<span className="image-canvas-editor__model-trigger-label">
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Cpu />
|
||||
</span>
|
||||
<span>{selectedModelLabel}</span>
|
||||
</span>
|
||||
</PlatformInlineOptionButton>
|
||||
{!isModelLocked && openPanel === 'model'
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--model image-canvas-editor__portal-menu"
|
||||
label={`${labelPrefix}模型选项`}
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(modelButtonRef.current, 'above')}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
<PlatformInlineOptionButton
|
||||
ref={modelButtonRef}
|
||||
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
||||
aria-label={`${labelPrefix}模型 ${selectedModelLabel}`}
|
||||
aria-expanded={isModelLocked ? undefined : openPanel === 'model'}
|
||||
disabled={isGenerating || isModelLocked}
|
||||
trailingIcon={
|
||||
isModelLocked ? undefined : <ChevronDown className="h-3 w-3" />
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isModelLocked) {
|
||||
togglePanel('model');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="image-canvas-editor__model-trigger-label">
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="image-canvas-editor__option-popover-items image-canvas-editor__option-popover-items--model">
|
||||
{EDITOR_IMAGE_MODEL_OPTIONS.map((option) => {
|
||||
const selected = selection.model === option.value;
|
||||
return (
|
||||
<OptionChoice
|
||||
key={option.value}
|
||||
selected={selected}
|
||||
className="image-canvas-editor__option-popover-choice--model"
|
||||
onClick={() => updateImageModel(option.value)}
|
||||
>
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
<Cpu />
|
||||
</span>
|
||||
<span>{selectedModelLabel}</span>
|
||||
</span>
|
||||
</PlatformInlineOptionButton>
|
||||
{!isModelLocked && openPanel === 'model'
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__option-popover image-canvas-editor__option-popover--model image-canvas-editor__portal-menu"
|
||||
label={`${labelPrefix}模型选项`}
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(modelButtonRef.current, 'above')}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="image-canvas-editor__option-popover-items image-canvas-editor__option-popover-items--model">
|
||||
{EDITOR_IMAGE_MODEL_OPTIONS.map((option) => {
|
||||
const selected = selection.model === option.value;
|
||||
return (
|
||||
<OptionChoice
|
||||
key={option.value}
|
||||
selected={selected}
|
||||
className="image-canvas-editor__option-popover-choice--model"
|
||||
onClick={() => updateImageModel(option.value)}
|
||||
>
|
||||
<Cpu />
|
||||
</span>
|
||||
<span>{option.label}</span>
|
||||
<Check
|
||||
className="image-canvas-editor__option-selected-check"
|
||||
data-visible={selected}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</OptionChoice>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<span
|
||||
className="image-canvas-editor__model-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Cpu />
|
||||
</span>
|
||||
<span>{option.label}</span>
|
||||
<Check
|
||||
className="image-canvas-editor__option-selected-check"
|
||||
data-visible={selected}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</OptionChoice>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
) : null}
|
||||
{typeof cost === 'number' ? (
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user