Files
Genarrative/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx
T
suzmii 35d3a92afd 快速编辑提示词支持 @ 引用素材,@ 面板补标签筛选
- 快速编辑面板提示词换成与聊天同一个 `ResourceReferenceInput`,`@` 候选、插入动作与引用模型全部复用聊天那一套
- 共享 composer / 快速编辑面板新增 `promptInputSlot` 注入位,宿主控件替换默认 textarea,不传时 DOM 与行为逐字不变
- `ResourceReferenceInput` 新增 `showPolishAction`:快速编辑的润色仍由 ResourcePromptPolishSlot 承担,面板里不再出现第二个润色入口
- `ResourceReferenceInput` 的多行输入区把 Enter 留给换行,只有单行输入区才用 Enter 提交
- `@` 面板的搜索与筛选改用共享 `PlatformResourceFilterBar`,搜索 + 功能分类 + 标签三者在面板内叠加生效
- 新增 `resourceReferenceMatchesTagSelection` / `resourceReferenceTagLibrary`,标签口径(多标签 AND、计数与排序)落在共享标签库,两个页签各持一份
- 宿主新增 `quickEditReferences` 与 `applyResourceQuickEditDraft`,派生请求的 prompt 就是输入区回填的那份文本(`@显示名`),不引入第二种引用格式
- `useImageCanvasFloatingOptionDismiss` 新增 `isInsideExtraOverlay`:宿主 portal 到 body 的 `@` 选择器与候选菜单不再被判成点外部而收起快速编辑面板
- 资源画布快速编辑提示词输入区补样式(铺满 composer 一整行、高度上限),`@` 面板去掉手写搜索行样式
- 用例:新增标签库口径、标签筛选(多标签叠加 + 关键字 + 功能分类)、两页签独立标签、聊天与快速编辑草稿一致性、快速编辑内 `@` 出选择器且派生 payload 断言;扩展 AGC 资源工作台 V3 端到端验收用例
2026-09-12 16:44:55 +08:00

186 lines
7.0 KiB
TypeScript

import {
type CSSProperties,
type Dispatch,
type ReactNode,
type RefObject,
type SetStateAction,
} from 'react';
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
import type {
GenerateDialogState,
QuickEditPanelState,
UploadTarget,
} from './ImageCanvasEditorTypes';
import {
QUICK_EDIT_REFERENCE_LIMIT,
resolveExtraImageReferenceLimit,
} from './ImageCanvasGenerationModel';
export type ImageCanvasQuickEditPanelViewProps = {
panel: QuickEditPanelState;
style: CSSProperties;
referenceButtonRef?: RefObject<HTMLButtonElement | null>;
isReferenceMenuOpen?: boolean;
setIsReferenceMenuOpen?: Dispatch<SetStateAction<boolean>>;
setIsPickingQuickEditReferenceFromCanvas?: Dispatch<SetStateAction<boolean>>;
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
renderEditorPortal?: (node: ReactNode) => ReactNode;
buildPortalMenuStyle?: (
anchor: HTMLElement | null,
placement: 'above' | 'below',
) => CSSProperties;
onRequestUpload?: (target: UploadTarget) => void;
onRememberImageModel?: (model: string) => void;
hasPendingImageReferenceUploads?: boolean;
/** 提示词输入控件的主机注入位(AGC 用它把提示词换成能 `@` 引用素材的输入区)。 */
promptInputSlot?: ReactNode;
/** 提示词下方的主机注入位(AGC 用它放「AI 润色」);不传时 DOM 不变。 */
promptActionSlot?: ReactNode;
onSubmit: () => void;
};
function resetFailedPanelStatus<
T extends { status: string; errorMessage?: string },
>(panel: T) {
return {
...panel,
status: panel.status === 'failed' ? 'idle' : panel.status,
errorMessage: panel.status === 'failed' ? undefined : panel.errorMessage,
};
}
function createQuickEditDialog(
panel: QuickEditPanelState,
): GenerateDialogState {
return {
mode: 'quick-edit',
prompt: panel.prompt,
status: panel.status,
errorMessage: panel.errorMessage,
sourceLayerId: panel.sourceLayerId,
generationReferences: panel.quickEditReferences,
assetLabel: panel.assetLabel,
imageModel: panel.model,
aspectRatio: panel.aspectRatio,
imageSize: panel.imageSize,
};
}
function syncPanelFromDialogUpdate(
panel: QuickEditPanelState,
dialog: GenerateDialogState,
) {
const normalizedPanel = resetFailedPanelStatus(panel);
const normalizedDialog = resetFailedPanelStatus(dialog);
return {
...normalizedPanel,
size: panel.size,
prompt: dialog.prompt,
aspectRatio:
normalizedDialog.aspectRatio ?? normalizedPanel.aspectRatio ?? '1:1',
imageSize: normalizedDialog.imageSize ?? normalizedPanel.imageSize ?? '1K',
model: normalizedDialog.imageModel ?? normalizedPanel.model,
quickEditReferences: normalizedDialog.generationReferences,
assetLabel: normalizedDialog.assetLabel,
// 中文注释:dialog 由 createQuickEditDialog 从面板自身派生,组合器只会做
// failed→idle 的重置,不会写入 'pending-confirmation' 这类完美像素专属状态。
// 因此归一化后的 dialog 状态恒等于归一化后的面板状态,直接取面板侧即可,
// 也让面板的三态联合类型不必在这里做运行时收窄。
status: normalizedPanel.status,
errorMessage: normalizedDialog.errorMessage,
};
}
export function ImageCanvasQuickEditPanelView({
panel,
style,
referenceButtonRef,
isReferenceMenuOpen = false,
setIsReferenceMenuOpen,
setIsPickingQuickEditReferenceFromCanvas,
setQuickEditPanel,
renderEditorPortal = (node) => node,
buildPortalMenuStyle = () => ({}),
onRequestUpload,
onRememberImageModel = () => {},
hasPendingImageReferenceUploads = false,
promptInputSlot,
promptActionSlot,
onSubmit,
}: ImageCanvasQuickEditPanelViewProps) {
const referenceCount = panel.quickEditReferences?.length ?? 0;
const canAddReferences =
referenceCount <
resolveExtraImageReferenceLimit(panel.model, QUICK_EDIT_REFERENCE_LIMIT, 1);
const quickEditDialog = createQuickEditDialog(panel);
return (
<ImageCanvasBasicGenerationComposerView
dialog={quickEditDialog}
style={style}
setGenerateDialog={(valueOrUpdater) => {
setQuickEditPanel((currentPanel) => {
if (!currentPanel) {
return currentPanel;
}
const currentDialog = createQuickEditDialog(currentPanel);
const nextDialog =
typeof valueOrUpdater === 'function'
? valueOrUpdater(currentDialog)
: valueOrUpdater;
if (!nextDialog) {
return currentPanel;
}
return syncPanelFromDialogUpdate(currentPanel, nextDialog);
});
}}
generationReferenceButtonRef={referenceButtonRef}
isGenerationReferenceMenuOpen={isReferenceMenuOpen}
setIsGenerationReferenceMenuOpen={setIsReferenceMenuOpen}
setIsPickingGenerationReferenceFromCanvas={
setIsPickingQuickEditReferenceFromCanvas
}
onRequestUpload={onRequestUpload ?? (() => {})}
onPickReferenceFromCanvas={() =>
setIsPickingQuickEditReferenceFromCanvas?.(true)
}
onToggleReferenceMenu={() => setIsReferenceMenuOpen?.((open) => !open)}
onRememberImageModel={onRememberImageModel}
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
onSubmit={onSubmit}
dialogLabel="快速编辑图片"
includeDimensions={true}
includeModel={true}
includeReferences={false}
promptLabel="快速编辑提示词"
promptPlaceholder="你希望素材如何修改?"
optionLabelPrefix="快速编辑"
dimensionRatioAriaLabelPrefix="比例"
dimensionSizeAriaLabelPrefix="尺寸"
referenceUploadTarget="quick-edit-reference"
referenceButtonLabel="添加快速编辑参考图"
referenceButtonAriaLabel="添加快速编辑参考图"
referenceMenuLabel="快速编辑参考图来源"
referenceSlotTone="quick-edit"
referenceSlotDisabled={!canAddReferences}
showReferenceAddButton={false}
referenceLabelFormatter={(_, index) => `图${index + 1}`}
referenceAriaLabelFormatter={(_, index) => `图${index + 1}`}
referenceRemoveLabelFormatter={(_, index) => `删除图${index + 1}`}
panelClassName="image-canvas-editor__quick-edit-panel image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__generation-composer--quick-edit"
promptInputSlot={promptInputSlot}
promptActionSlot={promptActionSlot}
footerClassName="image-canvas-editor__generation-composer-footer image-canvas-editor__quick-edit-footer"
promptClassName="image-canvas-editor__generation-prompt image-canvas-editor__quick-edit-prompt"
referenceSlotClassName="image-canvas-editor__quick-edit-reference"
submitButtonClassName="image-canvas-editor__generation-submit image-canvas-editor__quick-edit-submit"
submitLabel="修改"
submitAriaLabel="修改"
submittingStatusLabel="修改中"
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
/>
);
}