6f181b36a1
生成队列按稳定请求标识去重并保持外部幂等哈希兼容 统一拒绝参考图超限并同步前端、后端、Provider 与 OpenAPI 契约 按真实归属重建生成引用并阻止直接持久化伪造来源 锁定参考图在途上传上下文并保留批量部分成功结果 关闭内部生成 POST 自动重试并补齐回归测试与项目文档 修正最新主线开发者密钥弹窗的导入排序门禁
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import { ImagePlus } from 'lucide-react';
|
|
import {
|
|
type CSSProperties,
|
|
type Dispatch,
|
|
type ReactNode,
|
|
type RefObject,
|
|
type SetStateAction,
|
|
} from 'react';
|
|
|
|
import { AutoGrowTextArea } from '../common/AutoGrowTextArea';
|
|
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
|
import {
|
|
PlatformFloatingMenu,
|
|
PlatformFloatingMenuItem,
|
|
} from '../common/PlatformFloatingMenu';
|
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
|
import { PlatformTextField } from '../common/PlatformTextField';
|
|
import type {
|
|
CharacterReferenceImage,
|
|
GenerateDialogState,
|
|
PublicationMaterialsGameInfo,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
|
import {
|
|
buildPublicationMaterialsPrompt,
|
|
calculateEditorImageModelPrice,
|
|
DEFAULT_PUBLICATION_GAME_INFO,
|
|
IMAGE_MODEL_GPT_IMAGE_2,
|
|
} from './ImageCanvasGenerationModel';
|
|
import type { PublicationMaterialsWorkflow } from './ImageCanvasPublicationMaterialsModel';
|
|
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
|
|
|
type ImageCanvasPublicationMaterialsDemoPanelViewProps = {
|
|
dialog: GenerateDialogState;
|
|
workflow: PublicationMaterialsWorkflow;
|
|
style: CSSProperties;
|
|
publicationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
|
isPublicationReferenceMenuOpen: boolean;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
setIsPublicationReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingPublicationReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
renderEditorPortal: (node: ReactNode) => ReactNode;
|
|
buildPortalMenuStyle: (
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
) => CSSProperties;
|
|
onRequestUpload: (target: UploadTarget) => void;
|
|
onRememberImageModel: (model: string) => void;
|
|
hasPendingImageReferenceUploads?: boolean;
|
|
onSubmit: (dialog: GenerateDialogState) => void;
|
|
};
|
|
|
|
function resetFailedDialogStatus(dialog: GenerateDialogState) {
|
|
return {
|
|
...dialog,
|
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
};
|
|
}
|
|
|
|
function updatePublicationGameInfoField(
|
|
currentDialog: GenerateDialogState | null,
|
|
key: keyof PublicationMaterialsGameInfo,
|
|
value: string,
|
|
): GenerateDialogState | null {
|
|
if (currentDialog?.mode !== 'publication') {
|
|
return currentDialog;
|
|
}
|
|
const publicationGameInfo = {
|
|
...DEFAULT_PUBLICATION_GAME_INFO,
|
|
...currentDialog.publicationGameInfo,
|
|
[key]: value,
|
|
};
|
|
return {
|
|
...resetFailedDialogStatus(currentDialog),
|
|
publicationGameInfo,
|
|
prompt: buildPublicationMaterialsPrompt(publicationGameInfo),
|
|
};
|
|
}
|
|
|
|
function ReferenceThumb({
|
|
reference,
|
|
index,
|
|
onRemove,
|
|
}: {
|
|
reference: CharacterReferenceImage;
|
|
index: number;
|
|
onRemove: () => void;
|
|
}) {
|
|
const label = reference.label || `参考图${index + 1}`;
|
|
return (
|
|
<ImageCanvasReferenceSlot
|
|
tone="default"
|
|
icon={<ImagePlus 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 ImageCanvasPublicationMaterialsDemoPanelView({
|
|
dialog,
|
|
workflow,
|
|
style,
|
|
publicationReferenceButtonRef,
|
|
isPublicationReferenceMenuOpen,
|
|
setGenerateDialog,
|
|
setIsPublicationReferenceMenuOpen,
|
|
setIsPickingPublicationReferenceFromCanvas,
|
|
renderEditorPortal,
|
|
buildPortalMenuStyle,
|
|
onRequestUpload,
|
|
onRememberImageModel,
|
|
hasPendingImageReferenceUploads = false,
|
|
onSubmit,
|
|
}: ImageCanvasPublicationMaterialsDemoPanelViewProps) {
|
|
const references = dialog.publicationReferences ?? [];
|
|
const publicationGameInfo = {
|
|
...DEFAULT_PUBLICATION_GAME_INFO,
|
|
...dialog.publicationGameInfo,
|
|
};
|
|
return (
|
|
<form
|
|
className="image-canvas-editor__character-composer image-canvas-editor__publication-composer"
|
|
style={style}
|
|
role="dialog"
|
|
aria-label={`${workflow.label}生成卡片`}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (dialog.status !== 'generating') {
|
|
onSubmit(dialog);
|
|
}
|
|
}}
|
|
>
|
|
<div className="image-canvas-editor__reference-strip">
|
|
{references.map((reference, index) => (
|
|
<ReferenceThumb
|
|
key={reference.id}
|
|
reference={reference}
|
|
index={index}
|
|
onRemove={() =>
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'publication'
|
|
? {
|
|
...resetFailedDialogStatus(currentDialog),
|
|
publicationReferences: (
|
|
currentDialog.publicationReferences ?? []
|
|
).filter((item) => item.id !== reference.id),
|
|
}
|
|
: currentDialog,
|
|
)
|
|
}
|
|
/>
|
|
))}
|
|
<ImageCanvasReferenceSlot
|
|
buttonRef={publicationReferenceButtonRef}
|
|
tone="default"
|
|
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
|
|
label="添加参考图"
|
|
ariaLabel="添加参考图"
|
|
disabled={dialog.status === 'generating'}
|
|
isAdd
|
|
onClick={() => setIsPublicationReferenceMenuOpen((open) => !open)}
|
|
/>
|
|
{isPublicationReferenceMenuOpen
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__publication-reference-menu image-canvas-editor__portal-menu"
|
|
label="宣发素材参考图来源"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(
|
|
publicationReferenceButtonRef.current,
|
|
'above',
|
|
)}
|
|
>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={() => {
|
|
setIsPickingPublicationReferenceFromCanvas(true);
|
|
setIsPublicationReferenceMenuOpen(false);
|
|
}}
|
|
>
|
|
从画布中选择
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={() => {
|
|
setIsPublicationReferenceMenuOpen(false);
|
|
onRequestUpload('publication-reference');
|
|
}}
|
|
>
|
|
上传图片
|
|
</PlatformFloatingMenuItem>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</div>
|
|
|
|
<div className="image-canvas-editor__publication-composer-fields">
|
|
<div className="image-canvas-editor__publication-composer-summary">
|
|
<span>{workflow.outputLabel}</span>
|
|
<strong>{workflow.sizeLabel}</strong>
|
|
</div>
|
|
<label className="image-canvas-editor__field-block">
|
|
<PlatformFieldLabel
|
|
variant="field"
|
|
className="image-canvas-editor__field-title"
|
|
>
|
|
游戏名
|
|
</PlatformFieldLabel>
|
|
<PlatformTextField
|
|
aria-label={`${workflow.label}游戏名`}
|
|
value={publicationGameInfo.gameName}
|
|
disabled={dialog.status === 'generating'}
|
|
placeholder="马戏团午夜惊魂"
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__publication-input"
|
|
onChange={(event) =>
|
|
setGenerateDialog((currentDialog) =>
|
|
updatePublicationGameInfoField(
|
|
currentDialog,
|
|
'gameName',
|
|
event.target.value,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="image-canvas-editor__field-block">
|
|
<PlatformFieldLabel
|
|
variant="field"
|
|
className="image-canvas-editor__field-title"
|
|
>
|
|
游戏分类
|
|
</PlatformFieldLabel>
|
|
<PlatformTextField
|
|
aria-label={`${workflow.label}游戏分类`}
|
|
value={publicationGameInfo.gameCategories}
|
|
disabled={dialog.status === 'generating'}
|
|
placeholder="非对称对抗"
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__publication-input"
|
|
onChange={(event) =>
|
|
setGenerateDialog((currentDialog) =>
|
|
updatePublicationGameInfoField(
|
|
currentDialog,
|
|
'gameCategories',
|
|
event.target.value,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label className="image-canvas-editor__field-block image-canvas-editor__publication-description-field">
|
|
<PlatformFieldLabel
|
|
variant="field"
|
|
className="image-canvas-editor__field-title"
|
|
>
|
|
一句话描述游戏
|
|
</PlatformFieldLabel>
|
|
<AutoGrowTextArea
|
|
aria-label={`${workflow.label}一句话描述游戏`}
|
|
value={publicationGameInfo.gameDescription}
|
|
disabled={dialog.status === 'generating'}
|
|
placeholder="神奇数字马戏团里,帕姆尼失控了,找到钥匙,开门逃离马戏团,千万别被抓住了"
|
|
className="image-canvas-editor__generation-prompt image-canvas-editor__publication-description"
|
|
onChange={(event) =>
|
|
setGenerateDialog((currentDialog) =>
|
|
updatePublicationGameInfoField(
|
|
currentDialog,
|
|
'gameDescription',
|
|
event.target.value,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
{dialog.status === 'failed' ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="platform"
|
|
size="xs"
|
|
className="image-canvas-editor__generate-status"
|
|
role="alert"
|
|
>
|
|
{dialog.errorMessage}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
|
|
<div className="image-canvas-editor__generation-composer-footer">
|
|
<ImageCanvasGenerationImageOptionsView
|
|
dialog={dialog}
|
|
setGenerateDialog={setGenerateDialog}
|
|
includeDimensions={false}
|
|
onRememberImageModel={onRememberImageModel}
|
|
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
|
|
lockedModel={IMAGE_MODEL_GPT_IMAGE_2}
|
|
optionLabelPrefix="宣发素材"
|
|
cost={calculateEditorImageModelPrice(
|
|
IMAGE_MODEL_GPT_IMAGE_2,
|
|
workflow.imageSize,
|
|
)}
|
|
submitLabel="生成"
|
|
submitAriaLabel="生成"
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
/>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|