修复画布参考图上传删除链路
主站规范生成支持多张参考图 修复普通生成参考图删除与提交状态不同步 优化触屏设备删除按钮可达性并补充回归测试
This commit is contained in:
@@ -156,6 +156,49 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
|
||||
expect(screen.queryByText('参考图A')).toBeNull();
|
||||
});
|
||||
|
||||
it('removes only the selected reference when multiple references are present', () => {
|
||||
const submitGeneration = vi.fn();
|
||||
render(
|
||||
<BasicGenerationHarness
|
||||
initialDialog={createDialog({
|
||||
generationReferences: [
|
||||
{
|
||||
id: 'ref-a',
|
||||
label: '参考图A',
|
||||
src: 'data:image/png;base64,cmVmQQ==',
|
||||
},
|
||||
{
|
||||
id: 'ref-b',
|
||||
label: '参考图B',
|
||||
src: 'data:image/png;base64,cmVmQg==',
|
||||
},
|
||||
{
|
||||
id: 'ref-c',
|
||||
label: '参考图C',
|
||||
src: 'data:image/png;base64,cmVmQw==',
|
||||
},
|
||||
],
|
||||
})}
|
||||
onSubmit={submitGeneration}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '删除参考图B' }));
|
||||
expect(screen.queryByText('参考图B')).toBeNull();
|
||||
expect(screen.getByText('参考图A')).toBeTruthy();
|
||||
expect(screen.getByText('参考图C')).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '生成' }));
|
||||
expect(submitGeneration).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
generationReferences: [
|
||||
expect.objectContaining({ id: 'ref-a' }),
|
||||
expect.objectContaining({ id: 'ref-c' }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows only current image options and opens upward option panels', () => {
|
||||
render(<BasicGenerationHarness />);
|
||||
|
||||
|
||||
@@ -1666,8 +1666,14 @@ export function ImageCanvasEditorView({
|
||||
return generateDialog?.mode === 'spec'
|
||||
? {
|
||||
label: '规范参考图',
|
||||
currentCount: 0,
|
||||
limit: 1,
|
||||
currentCount:
|
||||
generateDialog.specType === 'icon'
|
||||
? 0
|
||||
: imageReferenceCount(generateDialog.generationReferences),
|
||||
limit:
|
||||
generateDialog.specType === 'icon'
|
||||
? 1
|
||||
: resolveDialogExtraImageReferenceLimit(generateDialog),
|
||||
contextId: generateDialogContextId,
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -1017,12 +1017,22 @@ function createNormalizedGenerationDialogDraft({
|
||||
characterView: getNormalizedString(sourceLayer, 'characterView'),
|
||||
customPrompt: getNormalizedString(sourceLayer, 'customPrompt'),
|
||||
},
|
||||
specReference:
|
||||
resolveNormalizedReferences(
|
||||
sourceLayer,
|
||||
'reference',
|
||||
availableLayers,
|
||||
)[0] ?? null,
|
||||
...(specType === 'icon'
|
||||
? {
|
||||
specReference:
|
||||
resolveNormalizedReferences(
|
||||
sourceLayer,
|
||||
'reference',
|
||||
availableLayers,
|
||||
)[0] ?? null,
|
||||
}
|
||||
: {
|
||||
generationReferences: resolveNormalizedReferences(
|
||||
sourceLayer,
|
||||
'reference',
|
||||
availableLayers,
|
||||
),
|
||||
}),
|
||||
},
|
||||
sourceLayer,
|
||||
);
|
||||
@@ -2104,7 +2114,15 @@ export function appendGenerationReference(
|
||||
}
|
||||
return {
|
||||
...resetFailedGenerationDialog(dialog),
|
||||
specReference: createCanvasLayerReference(layer),
|
||||
...(dialog.specType === 'icon' || dialog.specType === undefined
|
||||
? { specReference: createCanvasLayerReference(layer) }
|
||||
: {
|
||||
generationReferences: appendLimitedImageReferences(
|
||||
dialog.generationReferences,
|
||||
[createCanvasLayerReference(layer)],
|
||||
resolveDialogExtraImageReferenceLimit(dialog),
|
||||
),
|
||||
}),
|
||||
composerOpen: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1875,12 +1875,18 @@ export function buildSpecGenerationInputs(
|
||||
specType: SpecGenerationType,
|
||||
values: SpecFormValues,
|
||||
reference?: CharacterReferenceImage | null,
|
||||
extraReferences: CharacterReferenceImage[] = [],
|
||||
): CanvasGenerationInputs {
|
||||
const references = reference
|
||||
? createGenerationInputReference('参考图', reference, {
|
||||
id: 'reference',
|
||||
})
|
||||
: [];
|
||||
const references = [
|
||||
...(reference ? [reference] : []),
|
||||
...extraReferences,
|
||||
].flatMap((item, index) =>
|
||||
createGenerationInputReference(
|
||||
index === 0 ? '参考图' : `参考图${index + 1}`,
|
||||
item,
|
||||
{ id: index === 0 ? 'reference' : `reference-${index + 1}` },
|
||||
),
|
||||
);
|
||||
if (specType === 'custom') {
|
||||
return {
|
||||
version: 2,
|
||||
|
||||
@@ -756,6 +756,11 @@ export function buildImageGenerationSubmissionPlan({
|
||||
},
|
||||
};
|
||||
}
|
||||
const specReferences = dialog.generationReferences?.length
|
||||
? dialog.generationReferences
|
||||
: dialog.specReference
|
||||
? [dialog.specReference]
|
||||
: [];
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
@@ -763,18 +768,18 @@ export function buildImageGenerationSubmissionPlan({
|
||||
prompt: buildSpecPrompt(
|
||||
specType,
|
||||
specValues,
|
||||
Boolean(dialog.specReference?.src),
|
||||
specReferences.some((reference) => Boolean(reference.src)),
|
||||
),
|
||||
size: SPEC_GENERATION_SIZE,
|
||||
model: SPEC_GENERATION_MODEL,
|
||||
aspectRatio: SPEC_GENERATION_ASPECT_RATIO,
|
||||
imageSize: SPEC_GENERATION_IMAGE_SIZE,
|
||||
kind: 'spec',
|
||||
...(dialog.specReference?.src
|
||||
...(specReferences.length
|
||||
? {
|
||||
referenceImageSrcs: [
|
||||
resolveImageReferenceSubmissionSource(dialog.specReference),
|
||||
],
|
||||
referenceImageSrcs: specReferences.map(
|
||||
resolveImageReferenceSubmissionSource,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
@@ -787,7 +792,8 @@ export function buildImageGenerationSubmissionPlan({
|
||||
generationInputs: buildSpecGenerationInputs(
|
||||
specType,
|
||||
specValues,
|
||||
dialog.specReference,
|
||||
specReferences[0],
|
||||
specReferences.slice(1),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -144,7 +144,13 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
: 'spec-reference';
|
||||
const reference = isUiDesignDialog
|
||||
? dialog.uiDesignSpecReference
|
||||
: dialog.specReference;
|
||||
: dialog.specType === 'icon'
|
||||
? dialog.specReference
|
||||
: null;
|
||||
const extraSpecReferences =
|
||||
!isUiDesignDialog && dialog.specType !== 'icon'
|
||||
? (dialog.generationReferences ?? [])
|
||||
: [];
|
||||
const isGenerating = dialog.status === 'generating';
|
||||
const isIconSpec = dialog.mode === 'spec' && dialog.specType === 'icon';
|
||||
const [playSettingOptimization, setPlaySettingOptimization] =
|
||||
@@ -278,7 +284,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="image-canvas-editor__reference-strip">
|
||||
{isUiDesignDialog || dialog.specType ? (
|
||||
{isUiDesignDialog || dialog.specType === 'icon' ? (
|
||||
<ImageCanvasReferenceSlot
|
||||
buttonRef={generationReferenceButtonRef}
|
||||
tone={isUiDesignDialog ? 'ui' : 'spec'}
|
||||
@@ -337,6 +343,57 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
removeLabel={`删除${referenceLabel}`}
|
||||
/>
|
||||
) : null}
|
||||
{extraSpecReferences.map((item, index) => (
|
||||
<ImageCanvasReferenceSlot
|
||||
key={item.id}
|
||||
tone="default"
|
||||
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
|
||||
imageSrc={item.src}
|
||||
objectKey={item.objectKey}
|
||||
label={`参考图${index + 1}`}
|
||||
ariaLabel={item.label || `参考图${index + 1}`}
|
||||
title={item.label}
|
||||
disabled={isGenerating}
|
||||
onRemove={
|
||||
setGenerateDialog
|
||||
? () =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'spec'
|
||||
? {
|
||||
...currentDialog,
|
||||
status:
|
||||
currentDialog.status === 'failed'
|
||||
? 'idle'
|
||||
: currentDialog.status,
|
||||
errorMessage:
|
||||
currentDialog.status === 'failed'
|
||||
? undefined
|
||||
: currentDialog.errorMessage,
|
||||
generationReferences: (
|
||||
currentDialog.generationReferences ?? []
|
||||
).filter(
|
||||
(referenceItem) =>
|
||||
referenceItem.id !== item.id,
|
||||
),
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
removeLabel={`删除${item.label || `参考图${index + 1}`}`}
|
||||
/>
|
||||
))}
|
||||
{!isUiDesignDialog && dialog.specType !== 'icon' ? (
|
||||
<ImageCanvasReferenceSlot
|
||||
tone="default"
|
||||
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
|
||||
label="参考图"
|
||||
ariaLabel={referenceLabel}
|
||||
disabled={isGenerating}
|
||||
isAdd
|
||||
onClick={openReferenceMenu}
|
||||
/>
|
||||
) : null}
|
||||
{isUiDesignDialog
|
||||
? (dialog.generationReferences ?? []).map((item, index) => (
|
||||
<ImageCanvasReferenceSlot
|
||||
|
||||
@@ -171,7 +171,15 @@ export function applyGenerationReferenceUpload({
|
||||
return dialog?.mode === 'spec'
|
||||
? {
|
||||
...setFailedGenerationIdle(dialog),
|
||||
specReference: firstReference,
|
||||
...(dialog.specType === 'character' || dialog.specType === 'custom'
|
||||
? {
|
||||
generationReferences: appendLimitedImageReferences(
|
||||
dialog.generationReferences,
|
||||
references,
|
||||
resolveDialogExtraImageReferenceLimit(dialog),
|
||||
),
|
||||
}
|
||||
: { specReference: firstReference }),
|
||||
}
|
||||
: dialog;
|
||||
}
|
||||
|
||||
@@ -420,7 +420,10 @@ function resolveImageGenerationDialogReferences(
|
||||
dialog: GenerateDialogState,
|
||||
): CharacterReferenceImage[] {
|
||||
if (dialog.mode === 'spec') {
|
||||
return dialog.specReference ? [dialog.specReference] : [];
|
||||
return [
|
||||
...(dialog.specReference ? [dialog.specReference] : []),
|
||||
...(dialog.generationReferences ?? []),
|
||||
];
|
||||
}
|
||||
if (dialog.mode === 'character') {
|
||||
return [
|
||||
|
||||
@@ -528,24 +528,33 @@ export function useImageCanvasUploadWorkflow({
|
||||
return;
|
||||
}
|
||||
|
||||
const reservation = reserveImageReferenceUpload('spec-reference', 1);
|
||||
const reservation = reserveImageReferenceUpload(
|
||||
'spec-reference',
|
||||
Array.from(files).filter(isImageFile).length,
|
||||
);
|
||||
if (!reservation) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
||||
const imageFiles = Array.from(files).filter(isImageFile);
|
||||
const fileReferences = await Promise.all(
|
||||
imageFiles.map(async (file, index) => ({
|
||||
file,
|
||||
index,
|
||||
imageSrc: await readImageFileAsDataUrl(file),
|
||||
})),
|
||||
);
|
||||
if (!validateImageReferenceUploadReservation(reservation)) {
|
||||
return;
|
||||
}
|
||||
const references = await persistReferenceUploads(
|
||||
[
|
||||
{
|
||||
idPrefix: 'upload-spec-reference',
|
||||
file: imageFile,
|
||||
fallbackLabel: '参考图',
|
||||
imageSrc,
|
||||
},
|
||||
],
|
||||
fileReferences.map(({ file, imageSrc, index }) => ({
|
||||
idPrefix: 'upload-spec-reference',
|
||||
index,
|
||||
file,
|
||||
fallbackLabel: `参考图${index + 1}`,
|
||||
imageSrc,
|
||||
})),
|
||||
reservation,
|
||||
);
|
||||
if (
|
||||
|
||||
@@ -7033,6 +7033,14 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.image-canvas-editor__reference-chip-remove {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.image-canvas-editor__reference-chip-remove:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0;
|
||||
|
||||
Reference in New Issue
Block a user