Merge remote-tracking branch 'origin/codex/editor-asset-library' into codex/ui-extraction-model-integrate
This commit is contained in:
@@ -3,9 +3,14 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import * as clipboardService from '../../services/clipboard';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasMetadataModalView } from './ImageCanvasMetadataModalView';
|
||||
|
||||
vi.mock('../../services/clipboard', () => ({
|
||||
copyTextToClipboard: vi.fn(),
|
||||
}));
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
return {
|
||||
id: 'layer-1',
|
||||
@@ -67,6 +72,51 @@ describe('ImageCanvasMetadataModalView', () => {
|
||||
expect(within(dialog).getByText('generated/object.png')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('copies the visible metadata snapshot without restoring internal prompt fields', async () => {
|
||||
vi.mocked(clipboardService.copyTextToClipboard).mockResolvedValueOnce(true);
|
||||
|
||||
render(
|
||||
<ImageCanvasMetadataModalView
|
||||
layer={createLayer({
|
||||
prompt: '内部组装 Prompt 不应复制',
|
||||
model: 'gpt-image-2',
|
||||
taskId: 'gpt-image-2-task-123',
|
||||
objectKey: 'generated/object.png',
|
||||
generationInputs: {
|
||||
fields: [{ title: '生成提示词', value: '清爽游戏按钮' }],
|
||||
references: [
|
||||
{
|
||||
title: '参考图',
|
||||
label: '角色立绘',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-reference',
|
||||
},
|
||||
],
|
||||
},
|
||||
})}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', { name: '图片信息' });
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '复制图片信息' }));
|
||||
|
||||
expect(clipboardService.copyTextToClipboard).toHaveBeenCalledWith(
|
||||
[
|
||||
'图片类型:生成图片',
|
||||
'生成输入:\n生成提示词:清爽游戏按钮\n参考图:角色立绘 (项目资源 · resource-reference)',
|
||||
'Model:gpt-image-2',
|
||||
'Resolution:1024 x 768 px',
|
||||
'Task:123',
|
||||
'Object:generated/object.png',
|
||||
].join('\n'),
|
||||
);
|
||||
expect(clipboardService.copyTextToClipboard).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('内部组装 Prompt 不应复制'),
|
||||
);
|
||||
expect(await within(dialog).findByText('已复制')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders nanobanana2 instead of the raw Gemini image model id', () => {
|
||||
render(
|
||||
<ImageCanvasMetadataModalView
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
import { useCopyFeedback } from '../common/useCopyFeedback';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import { formatTaskIdForDisplay } from './ImageCanvasExportModel';
|
||||
import {
|
||||
@@ -16,10 +20,50 @@ function formatReferenceType(refType: 'project-resource' | 'asset') {
|
||||
return refType === 'project-resource' ? '项目资源' : '素材';
|
||||
}
|
||||
|
||||
function formatLayerDurationForDisplay(layer: CanvasLayer) {
|
||||
return formatCanvasDurationMetric(layer.durationSeconds).replace(
|
||||
/^时长\s*/u,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
function formatGenerationInputsForCopy(layer: CanvasLayer) {
|
||||
const fields = layer.generationInputs?.fields ?? [];
|
||||
const references = layer.generationInputs?.references ?? [];
|
||||
|
||||
if (!fields.length && !references.length) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return [
|
||||
...fields.map((field) => `${field.title}:${field.value}`),
|
||||
...references.map(
|
||||
(reference) =>
|
||||
`${reference.title}:${reference.label} (${formatReferenceType(
|
||||
reference.refType,
|
||||
)} · ${reference.refId})`,
|
||||
),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function buildLayerMetadataCopyText(layer: CanvasLayer, mediaLabel: string) {
|
||||
return [
|
||||
`${mediaLabel}类型:${formatLayerImageType(layer)}`,
|
||||
`生成输入:\n${formatGenerationInputsForCopy(layer)}`,
|
||||
`Model:${layer.model ? getEditorImageModelDisplayName(layer.model) : '-'}`,
|
||||
layer.mediaType === 'audio'
|
||||
? `时长:${formatLayerDurationForDisplay(layer)}`
|
||||
: `Resolution:${layer.originalWidth} x ${layer.originalHeight} px`,
|
||||
`Task:${formatTaskIdForDisplay(layer.taskId)}`,
|
||||
`Object:${layer.objectKey ?? layer.assetObjectId ?? '-'}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function ImageCanvasMetadataModalView({
|
||||
layer,
|
||||
onClose,
|
||||
}: ImageCanvasMetadataModalViewProps) {
|
||||
const { copyState, copyText, resetCopyState } = useCopyFeedback();
|
||||
const mediaLabel =
|
||||
layer?.mediaType === 'audio'
|
||||
? '音频'
|
||||
@@ -28,6 +72,20 @@ export function ImageCanvasMetadataModalView({
|
||||
: layer?.mediaType === 'image-sequence'
|
||||
? '序列帧'
|
||||
: '图片';
|
||||
const metadataText = layer ? buildLayerMetadataCopyText(layer, mediaLabel) : '';
|
||||
|
||||
useEffect(() => {
|
||||
resetCopyState();
|
||||
}, [metadataText, resetCopyState]);
|
||||
|
||||
const copyMetadata = () => {
|
||||
if (!metadataText) {
|
||||
return;
|
||||
}
|
||||
|
||||
void copyText(metadataText);
|
||||
};
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={Boolean(layer)}
|
||||
@@ -37,6 +95,20 @@ export function ImageCanvasMetadataModalView({
|
||||
onClose={onClose}
|
||||
panelClassName="image-canvas-editor__metadata-dialog"
|
||||
bodyClassName="image-canvas-editor__metadata-body"
|
||||
footerClassName="image-canvas-editor__metadata-footer"
|
||||
footer={
|
||||
layer ? (
|
||||
<CopyFeedbackButton
|
||||
state={copyState}
|
||||
idleLabel="复制信息"
|
||||
accessibleLabel={`复制${mediaLabel}信息`}
|
||||
actionSurface="platform"
|
||||
actionFullWidth
|
||||
className="image-canvas-editor__metadata-copy-button sm:w-auto"
|
||||
onClick={copyMetadata}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{layer ? (
|
||||
<dl className="image-canvas-editor__metadata-grid">
|
||||
@@ -90,10 +162,7 @@ export function ImageCanvasMetadataModalView({
|
||||
<>
|
||||
<dt>时长</dt>
|
||||
<dd>
|
||||
{formatCanvasDurationMetric(layer.durationSeconds).replace(
|
||||
/^时长\s*/u,
|
||||
'',
|
||||
)}
|
||||
{formatLayerDurationForDisplay(layer)}
|
||||
</dd>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -8430,6 +8430,9 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
|
||||
.image-canvas-editor__metadata-body {
|
||||
padding: 0;
|
||||
-webkit-touch-callout: default;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.image-canvas-editor__generate-dialog-body {
|
||||
@@ -8561,6 +8564,14 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.image-canvas-editor__metadata-footer {
|
||||
padding: 0.72rem 0.85rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__metadata-copy-button {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.image-canvas-editor {
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user