From 94432e04d33f291d420db9d80e94d3153b7253bd Mon Sep 17 00:00:00 2001 From: WuYongJie Date: Mon, 15 Jun 2026 19:18:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(intake):=20=E5=B0=86=20AI=20=E7=99=BD?= =?UTF-8?q?=E5=BA=95=E5=9B=BE=E5=8D=87=E7=BA=A7=E4=B8=BA=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E5=AE=9E=E6=8B=8D=E5=B7=A5=E4=BD=9C=E5=8C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NonIntegratedDetailClient.test.tsx | 18 +- .../NonIntegratedDetailClient.tsx | 61 +- .../NonIntegratedAiWhiteBackgroundModal.tsx | 564 +----------------- ...onIntegratedAiWhiteBackgroundWorkspace.tsx | 562 +++++++++++++++++ .../documents/ProductPhotoHeaderActions.tsx | 13 +- .../documents/ProductPhotoWorkspace.tsx | 257 ++++---- .../documents/documentWorkspaceHelpers.ts | 1 + .../documents/documentWorkspaceRenderers.tsx | 10 +- .../shared/DetailOverlayDialogs.tsx | 7 - .../DocumentWorkspaceHeaderActionsSlot.tsx | 6 + 10 files changed, 791 insertions(+), 708 deletions(-) create mode 100644 furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundWorkspace.tsx diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx index 718d866c..bf6f50d1 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.test.tsx @@ -10587,6 +10587,9 @@ describe("NonIntegratedDetailClient", () => { await actor.click( screen.getByTestId("product-photo-open-ai-white-background"), ); + expect( + screen.getByTestId("product-photo-ai-white-background-workspace"), + ).toBeInTheDocument(); expect(await screen.findByTestId("ai-white-background-grid")).toBeInTheDocument(); expect( screen.getByTestId("ai-white-background-control-card"), @@ -10765,19 +10768,22 @@ describe("NonIntegratedDetailClient", () => { screen.getByTestId("product-photo-open-ai-white-background"), ); - const aiWhiteBackgroundDialog = await screen.findByRole("dialog", { - name: "AI 白底图", - }); + const aiWhiteBackgroundWorkspace = await screen.findByTestId( + "product-photo-ai-white-background-workspace", + ); expect( - within(aiWhiteBackgroundDialog).getByAltText("front-ai-white.png"), + within(aiWhiteBackgroundWorkspace).getByAltText("front-ai-white.png"), ).toBeInTheDocument(); expect( - within(aiWhiteBackgroundDialog).queryByAltText("正视图源图.png"), + within(aiWhiteBackgroundWorkspace).queryByAltText("正视图源图.png"), ).not.toBeInTheDocument(); expect( - within(aiWhiteBackgroundDialog).getByAltText("后视图源图.png"), + within(aiWhiteBackgroundWorkspace).getByAltText("后视图源图.png"), ).toBeInTheDocument(); + expect( + screen.queryByRole("dialog", { name: "AI 白底图" }), + ).not.toBeInTheDocument(); }); it("generates only currently uploaded product-photo source views", async () => { diff --git a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx index d51fca7c..94cb8f73 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/NonIntegratedDetailClient.tsx @@ -141,6 +141,9 @@ import { import { markNonIntegratedAiWhiteBackgroundNeedsReview, } from "./workspaces/ai-white-background/nonIntegratedAiWhiteBackground"; +import { + NonIntegratedAiWhiteBackgroundWorkspace, +} from "./workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundWorkspace"; import { buildProductPayload } from "./nonIntegratedProductPayload"; import { useNonIntegratedBomLibraryOptions } from "./useNonIntegratedBomLibraryOptions"; import { useNonIntegratedComponentSkuRows } from "./useNonIntegratedComponentSkuRows"; @@ -226,6 +229,7 @@ import { type ProductDataGroup, type ProductPhotoGroup, type ProductPhotoSelection, + type ProductPhotoWorkspaceMode, type UploadCategoryKey, type WorkflowNodeCategory, } from "./workspaces/documents/documentWorkspaceHelpers"; @@ -652,8 +656,8 @@ export default function NonIntegratedDetailClient({ }); const [selectedProductPhotoStyleKey, setSelectedProductPhotoStyleKey] = useState(""); - const [isAiWhiteBackgroundModalOpen, setIsAiWhiteBackgroundModalOpen] = - useState(false); + const [activeProductPhotoMode, setActiveProductPhotoMode] = + useState("source"); const [selectedInstallationStyleKey, setSelectedInstallationStyleKey] = useState(""); const [activeInstallationAssetType, setActiveInstallationAssetType] = @@ -4026,6 +4030,7 @@ export default function NonIntegratedDetailClient({ activeStyleEntry: activeProductPhotoStyleEntry, workspaceData: productPhotoWorkspaceData, activePanel: activeProductPhotoPanel, + activeMode: activeProductPhotoMode, activePanelFiles: activeProductPhotoPanelFiles, activePanelTitle: activeProductPhotoPanelTitle, selectedTarget: selectedProductPhotoTarget, @@ -4033,8 +4038,33 @@ export default function NonIntegratedDetailClient({ isReadOnly, previewTitle: workspacePreviewTitle, previewDescription: workspacePreviewDescription, + renderAiWhiteBackgroundContent: () => ( + { + setSaveMessage(message); + setSaveMessageType(type); + }} + /> + ), renderPreviewContent: renderDocumentWorkspacePreviewContent, onSelectStyle: handleSelectProductPhotoStyle, + onSwitchMode: setActiveProductPhotoMode, onSwitchPanel: handleSwitchProductPhotoPanel, onOpenCurrentUpload: openProductPhotoCurrentUpload, onSelectSlot: handleSelectProductPhotoSlot, @@ -4772,6 +4802,7 @@ export default function NonIntegratedDetailClient({ activeInstallationAssetFiles, activeProductDataGroupFiles, activeProductPhotoDownloadFiles, + activeProductPhotoMode, activeProductPhotoPanel, canDeleteSelectedReviewRound, canDownloadWorkspaceFiles, @@ -4875,7 +4906,11 @@ export default function NonIntegratedDetailClient({ onImportProductLimitWorkbookChange: handleImportProductLimitWorkbookChange, onOpenAiWhiteBackground: () => - setIsAiWhiteBackgroundModalOpen(true), + setActiveProductPhotoMode((current) => + current === "ai-white-background" + ? "source" + : "ai-white-background", + ), onOpenProductPhotoCurrentUpload: openProductPhotoCurrentUpload, onOpenProductPhotoPanelUpload: openProductPhotoPanelUpload, @@ -4943,26 +4978,6 @@ export default function NonIntegratedDetailClient({ )} setIsAiWhiteBackgroundModalOpen(false), - productId: productId ?? null, - isReadOnly, - canUseAiWhiteBackground, - currentProductPhotoStyleKey, - currentProductPhotoStyleLabel, - displayedSkuItems, - skuItems, - materialLibraryFinishes, - uploadFiles, - productMetadata, - sourceSlotFiles: productPhotoWorkspaceData.slotFiles, - onProductSynced: syncAiWhiteBackgroundSavedProduct, - onNotify: (message, type) => { - setSaveMessage(message); - setSaveMessageType(type); - }, - }} barcodeScannerSkuId={barcodeScannerSkuId} bomImagePreview={bomImagePreview} canGoToNextWorkspaceImage={canGoToNextWorkspaceImage} diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundModal.tsx b/furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundModal.tsx index 3a58e015..d49fc9af 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundModal.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundModal.tsx @@ -1,573 +1,17 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; import { X } from "lucide-react"; import Modal from "@/components/ui/Modal"; import { - AI_WHITE_BACKGROUND_SOURCE_SLOTS, - AI_WHITE_BACKGROUND_SOURCE_SLOT_LABELS, - AI_WHITE_BACKGROUND_VIEW_KEYS, - AI_WHITE_BACKGROUND_VIEW_KEY_TO_SOURCE_SLOT, - AI_WHITE_BACKGROUND_VIEW_LABELS, - getAiWhiteBackgroundSessionGeneratedFiles, - getLatestAiWhiteBackgroundSession, - hasAnyAiWhiteBackgroundGeneratedView, - hasCompleteAiWhiteBackgroundSession, - readNonIntegratedAiWhiteBackgroundMetadata, - type AiWhiteBackgroundTargetFinishOption, - type AiWhiteBackgroundSourceSlot, - type AiWhiteBackgroundViewKey, -} from "./nonIntegratedAiWhiteBackground"; -import type { - NonIntegratedProductRecord, - NonIntegratedSkuItem, - NonIntegratedUploadFile, -} from "../../nonIntegratedStorage"; -import { - cleanupSharedNonIntegratedAiWhiteBackgroundManifest, - fetchSharedNonIntegratedAiWhiteBackgroundCapabilities, - generateSharedNonIntegratedAiWhiteBackgroundSession, - uploadSharedNonIntegratedAiWhiteBackgroundSession, -} from "../../nonIntegratedApi"; -import NonIntegratedAiWhiteBackgroundPanel from "./NonIntegratedAiWhiteBackgroundPanel"; - -type AiWhiteBackgroundBarcodeOption = { - barcode: string; - label: string; - batchNo: number; - color: string; -}; + NonIntegratedAiWhiteBackgroundWorkspace, + type NonIntegratedAiWhiteBackgroundWorkspaceProps, +} from "./NonIntegratedAiWhiteBackgroundWorkspace"; type NonIntegratedAiWhiteBackgroundModalProps = { isOpen: boolean; onClose: () => void; } & NonIntegratedAiWhiteBackgroundWorkspaceProps; -export type NonIntegratedAiWhiteBackgroundWorkspaceProps = { - productId: string | null; - isActive?: boolean; - isReadOnly: boolean; - canUseAiWhiteBackground: boolean; - currentProductPhotoStyleKey: string; - currentProductPhotoStyleLabel: string; - displayedSkuItems: NonIntegratedSkuItem[]; - skuItems: NonIntegratedSkuItem[]; - materialLibraryFinishes: AiWhiteBackgroundTargetFinishOption[]; - uploadFiles: NonIntegratedUploadFile[]; - productMetadata: NonIntegratedProductRecord["metadata"] | undefined; - sourceSlotFiles: Record< - AiWhiteBackgroundSourceSlot, - NonIntegratedUploadFile | null - >; - onProductSynced: (product: NonIntegratedProductRecord) => void; - onNotify: (message: string, type: "success" | "error") => void; -}; - -function getStyleKeyFromModel(model: string) { - const match = model - .trim() - .toUpperCase() - .match(/K\d{2}$/); - return match?.[0] ?? ""; -} - -function buildClientAiWhiteBackgroundSessionId() { - const suffix = - typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" - ? crypto.randomUUID().slice(0, 8) - : Math.random().toString(36).slice(2, 10); - return `ai-wbg-session-${Date.now()}-${suffix}`; -} - -export function NonIntegratedAiWhiteBackgroundWorkspace({ - productId, - isActive = true, - isReadOnly, - canUseAiWhiteBackground, - currentProductPhotoStyleKey, - currentProductPhotoStyleLabel, - displayedSkuItems, - skuItems, - materialLibraryFinishes, - uploadFiles, - productMetadata, - sourceSlotFiles, - onProductSynced, - onNotify, -}: NonIntegratedAiWhiteBackgroundWorkspaceProps) { - const [selectedBarcode, setSelectedBarcode] = useState(""); - const [targetFinishId, setTargetFinishId] = useState(""); - const [pendingSessionId, setPendingSessionId] = useState(null); - const [generatingViewKeys, setGeneratingViewKeys] = useState< - AiWhiteBackgroundViewKey[] - >([]); - const [isBulkGenerating, setIsBulkGenerating] = useState(false); - const [isUploading, setIsUploading] = useState(false); - const [cleaningManifestId, setCleaningManifestId] = useState( - null, - ); - const [materialLibraryCapabilities, setMaterialLibraryCapabilities] = useState<{ - isLoading: boolean; - canUploadToMaterialLibrary: boolean; - uploadDisabledReason: string | null; - }>({ - isLoading: false, - canUploadToMaterialLibrary: false, - uploadDisabledReason: null, - }); - - const aiWhiteBackgroundMetadata = useMemo( - () => readNonIntegratedAiWhiteBackgroundMetadata(productMetadata), - [productMetadata], - ); - - const barcodeOptions = useMemo(() => { - const withBarcode = (items: NonIntegratedSkuItem[]) => - items.filter((item) => item.barcode.trim()); - const currentStyleItems = withBarcode( - displayedSkuItems.filter( - (item) => getStyleKeyFromModel(item.model) === currentProductPhotoStyleKey, - ), - ); - const displayedItems = withBarcode(displayedSkuItems); - const fallbackStyleItems = withBarcode( - skuItems.filter( - (item) => getStyleKeyFromModel(item.model) === currentProductPhotoStyleKey, - ), - ); - const sourceItems = - currentStyleItems.length > 0 - ? currentStyleItems - : displayedItems.length > 0 - ? displayedItems - : fallbackStyleItems.length > 0 - ? fallbackStyleItems - : withBarcode(skuItems); - const optionMap = new Map(); - - sourceItems.forEach((item) => { - const barcode = item.barcode.trim().toUpperCase(); - if (!barcode || optionMap.has(barcode)) { - return; - } - - const itemLabel = item.model.trim() || item.skuCode.trim() || item.id; - const batchNo = item.batchNo || 1; - optionMap.set(barcode, { - barcode, - label: `${itemLabel} / ${barcode} / 批次${batchNo}`, - batchNo, - color: item.color.trim(), - }); - }); - - return Array.from(optionMap.values()); - }, [currentProductPhotoStyleKey, displayedSkuItems, skuItems]); - - useEffect(() => { - setSelectedBarcode((current) => - barcodeOptions.some((option) => option.barcode === current) - ? current - : (barcodeOptions[0]?.barcode ?? ""), - ); - }, [barcodeOptions]); - - const selectedBarcodeOption = useMemo( - () => barcodeOptions.find((option) => option.barcode === selectedBarcode) ?? null, - [barcodeOptions, selectedBarcode], - ); - - const targetFinishOptions = useMemo( - () => materialLibraryFinishes.filter((finish) => finish.isActive !== false), - [materialLibraryFinishes], - ); - const selectedTargetFinish = useMemo( - () => - targetFinishOptions.find((finish) => finish.id === targetFinishId) ?? null, - [targetFinishId, targetFinishOptions], - ); - const targetColor = selectedTargetFinish?.name.trim() ?? ""; - - useEffect(() => { - if ( - targetFinishId && - !targetFinishOptions.some((finish) => finish.id === targetFinishId) - ) { - setTargetFinishId(""); - } - }, [targetFinishId, targetFinishOptions]); - - useEffect(() => { - setTargetFinishId(""); - }, [currentProductPhotoStyleKey, productId, selectedBarcode]); - - useEffect(() => { - if (!isActive || !productId || !canUseAiWhiteBackground) { - setMaterialLibraryCapabilities({ - isLoading: false, - canUploadToMaterialLibrary: false, - uploadDisabledReason: null, - }); - return; - } - - let disposed = false; - setMaterialLibraryCapabilities((current) => ({ - ...current, - isLoading: true, - })); - - void fetchSharedNonIntegratedAiWhiteBackgroundCapabilities(productId) - .then((result) => { - if (disposed) return; - setMaterialLibraryCapabilities({ - isLoading: false, - canUploadToMaterialLibrary: result.canUploadToMaterialLibrary, - uploadDisabledReason: result.uploadDisabledReason, - }); - }) - .catch((error) => { - if (disposed) return; - setMaterialLibraryCapabilities({ - isLoading: false, - canUploadToMaterialLibrary: false, - uploadDisabledReason: - error instanceof Error - ? error.message - : "素材库上传能力获取失败。", - }); - }); - - return () => { - disposed = true; - }; - }, [canUseAiWhiteBackground, isActive, productId]); - - const currentSession = useMemo( - () => - selectedBarcode - ? getLatestAiWhiteBackgroundSession(productMetadata, { - styleKey: currentProductPhotoStyleKey, - barcode: selectedBarcode, - targetColor, - targetFinishId, - }) - : null, - [currentProductPhotoStyleKey, productMetadata, selectedBarcode, targetColor, targetFinishId], - ); - - const generatedFiles = useMemo( - () => - currentSession - ? getAiWhiteBackgroundSessionGeneratedFiles(uploadFiles, currentSession.id) - : (Object.fromEntries( - AI_WHITE_BACKGROUND_VIEW_KEYS.map((viewKey) => [viewKey, null]), - ) as Record), - [currentSession, uploadFiles], - ); - - const currentManifest = useMemo(() => { - if (!currentSession) { - return null; - } - - const lastManifestId = currentSession.lastManifestId?.trim(); - if (lastManifestId) { - const matchedManifest = aiWhiteBackgroundMetadata.uploadManifests.find( - (manifest) => manifest.manifestId === lastManifestId, - ); - if (matchedManifest) { - return matchedManifest; - } - } - - return ( - aiWhiteBackgroundMetadata.uploadManifests - .filter((manifest) => manifest.sessionId === currentSession.id) - .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? - null - ); - }, [aiWhiteBackgroundMetadata.uploadManifests, currentSession]); - - const sourceReadyByView = useMemo( - () => - Object.fromEntries( - AI_WHITE_BACKGROUND_VIEW_KEYS.map((viewKey) => [ - viewKey, - Boolean(sourceSlotFiles[AI_WHITE_BACKGROUND_VIEW_KEY_TO_SOURCE_SLOT[viewKey]]), - ]), - ) as Record, - [sourceSlotFiles], - ); - const sourceFilesByView = useMemo( - () => - Object.fromEntries( - AI_WHITE_BACKGROUND_VIEW_KEYS.map((viewKey) => [ - viewKey, - sourceSlotFiles[AI_WHITE_BACKGROUND_VIEW_KEY_TO_SOURCE_SLOT[viewKey]] ?? null, - ]), - ) as Record, - [sourceSlotFiles], - ); - - const missingSourceSlotLabels = useMemo( - () => - AI_WHITE_BACKGROUND_SOURCE_SLOTS.filter((slot) => !sourceSlotFiles[slot]).map( - (slot) => AI_WHITE_BACKGROUND_SOURCE_SLOT_LABELS[slot], - ), - [sourceSlotFiles], - ); - - const availableSourceViewKeys = useMemo( - () => - AI_WHITE_BACKGROUND_VIEW_KEYS.filter((viewKey) => sourceReadyByView[viewKey]), - [sourceReadyByView], - ); - const availableSourceViewCount = availableSourceViewKeys.length; - const generatingViewKeySet = useMemo( - () => new Set(generatingViewKeys), - [generatingViewKeys], - ); - const isGenerating = generatingViewKeys.length > 0; - const canUploadToMaterialLibrary = - materialLibraryCapabilities.canUploadToMaterialLibrary; - const uploadDisabledReason = materialLibraryCapabilities.isLoading - ? "正在检查素材库上传接口配置…" - : materialLibraryCapabilities.uploadDisabledReason; - const generateProgressText = useMemo(() => { - if (isBulkGenerating && generatingViewKeys.length > 0) { - return `正在生成 ${generatingViewKeys.length} 个角度的白底图…`; - } - if (generatingViewKeys.length === 1) { - return `正在生成 ${AI_WHITE_BACKGROUND_VIEW_LABELS[generatingViewKeys[0]]} 白底图…`; - } - if (generatingViewKeys.length > 1) { - return `正在并发生成 ${generatingViewKeys.length} 个角度的白底图…`; - } - return ""; - }, [generatingViewKeys, isBulkGenerating]); - const sessionStatus = currentSession?.status ?? null; - const canUpload = - canUseAiWhiteBackground && - Boolean(selectedBarcode) && - hasAnyAiWhiteBackgroundGeneratedView(currentSession) && - canUploadToMaterialLibrary; - const statusText = !canUseAiWhiteBackground - ? "当前环境未启用服务端白底图能力" - : isGenerating && generateProgressText - ? generateProgressText - : currentManifest - ? `上传清单:${currentManifest.status}` - : currentSession - ? `当前会话:${currentSession.status}` - : "还没有生成白底图会话"; - - useEffect(() => { - if (!isActive) { - setPendingSessionId(null); - setGeneratingViewKeys([]); - setIsBulkGenerating(false); - } - }, [isActive]); - - useEffect(() => { - setPendingSessionId(null); - setGeneratingViewKeys([]); - setIsBulkGenerating(false); - }, [currentProductPhotoStyleKey, productId, selectedBarcode, targetFinishId]); - - useEffect(() => { - if (currentSession?.id) { - setPendingSessionId((current) => - current === currentSession.id ? null : current, - ); - } - }, [currentSession?.id]); - - useEffect(() => { - if (generatingViewKeys.length === 0 && !currentSession?.id) { - setPendingSessionId(null); - } - }, [currentSession?.id, generatingViewKeys.length]); - - async function handleGenerate(requestedViewKeys?: AiWhiteBackgroundViewKey[]) { - if ( - isReadOnly || - !productId || - !canUseAiWhiteBackground || - !selectedBarcode || - availableSourceViewCount === 0 || - (requestedViewKeys == null && generatingViewKeys.length > 0) - ) { - return; - } - - const effectiveRequestedViewKeys = - requestedViewKeys && requestedViewKeys.length > 0 - ? requestedViewKeys - : availableSourceViewKeys; - const filteredRequestedViewKeys = effectiveRequestedViewKeys.filter( - (viewKey) => - sourceReadyByView[viewKey] && - !generatingViewKeySet.has(viewKey) && - !isBulkGenerating, - ); - if (filteredRequestedViewKeys.length === 0) { - return; - } - - const requestSessionId = - currentSession?.id ?? - pendingSessionId ?? - buildClientAiWhiteBackgroundSessionId(); - const isSingleViewRequest = filteredRequestedViewKeys.length === 1; - - try { - if (!currentSession?.id && !pendingSessionId) { - setPendingSessionId(requestSessionId); - } - if (!isSingleViewRequest) { - setIsBulkGenerating(true); - } - setGeneratingViewKeys((current) => - Array.from(new Set([...current, ...filteredRequestedViewKeys])), - ); - - const result = await generateSharedNonIntegratedAiWhiteBackgroundSession( - productId, - { - sessionId: requestSessionId, - styleKey: currentProductPhotoStyleKey, - barcode: selectedBarcode, - targetColor: targetColor.trim() || null, - targetFinishId: selectedTargetFinish?.id ?? null, - requestedViewKeys: filteredRequestedViewKeys, - }, - ); - onProductSynced(result.product); - - onNotify( - isSingleViewRequest - ? `${AI_WHITE_BACKGROUND_VIEW_LABELS[filteredRequestedViewKeys[0]]} 白底图已更新` - : hasCompleteAiWhiteBackgroundSession(result.session) - ? "8 个固定角度白底图已生成完成" - : `已生成 ${filteredRequestedViewKeys.length} 个已上传角度的白底图,可先确认上传已完成的视角`, - "success", - ); - } catch (error) { - onNotify( - error instanceof Error ? error.message : "AI 白底图生成失败,请稍后重试", - "error", - ); - } finally { - setGeneratingViewKeys((current) => - current.filter((viewKey) => !filteredRequestedViewKeys.includes(viewKey)), - ); - if (!isSingleViewRequest) { - setIsBulkGenerating(false); - } - } - } - - async function handleUpload() { - if ( - isReadOnly || - !productId || - !canUseAiWhiteBackground || - !currentSession || - !canUploadToMaterialLibrary || - !canUpload || - isUploading - ) { - return; - } - - try { - setIsUploading(true); - const result = await uploadSharedNonIntegratedAiWhiteBackgroundSession( - productId, - currentSession.id, - ); - - onProductSynced(result.product); - onNotify("白底图已确认上传到素材库", "success"); - } catch (error) { - onNotify( - error instanceof Error ? error.message : "白底图上传素材库失败,请稍后重试", - "error", - ); - } finally { - setIsUploading(false); - } - } - - async function handleCleanup() { - if ( - isReadOnly || - !productId || - !canUseAiWhiteBackground || - !currentManifest || - cleaningManifestId === currentManifest.manifestId - ) { - return; - } - - try { - setCleaningManifestId(currentManifest.manifestId); - const result = await cleanupSharedNonIntegratedAiWhiteBackgroundManifest( - productId, - currentManifest.manifestId, - "manual-review-cleanup", - ); - - onProductSynced(result.product); - onNotify("当前白底图上传清单已完成清理", "success"); - } catch (error) { - onNotify( - error instanceof Error ? error.message : "白底图清理失败,请稍后重试", - "error", - ); - } finally { - setCleaningManifestId(null); - } - } - - return ( - void handleCleanup()} - /> - ); -} - export default function NonIntegratedAiWhiteBackgroundModal({ isOpen, onClose, @@ -589,7 +33,7 @@ export default function NonIntegratedAiWhiteBackgroundModal({
-

+

AI 白底图

diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundWorkspace.tsx b/furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundWorkspace.tsx new file mode 100644 index 00000000..1586442f --- /dev/null +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/ai-white-background/NonIntegratedAiWhiteBackgroundWorkspace.tsx @@ -0,0 +1,562 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + AI_WHITE_BACKGROUND_SOURCE_SLOTS, + AI_WHITE_BACKGROUND_SOURCE_SLOT_LABELS, + AI_WHITE_BACKGROUND_VIEW_KEYS, + AI_WHITE_BACKGROUND_VIEW_KEY_TO_SOURCE_SLOT, + AI_WHITE_BACKGROUND_VIEW_LABELS, + getAiWhiteBackgroundSessionGeneratedFiles, + getLatestAiWhiteBackgroundSession, + hasAnyAiWhiteBackgroundGeneratedView, + hasCompleteAiWhiteBackgroundSession, + readNonIntegratedAiWhiteBackgroundMetadata, + type AiWhiteBackgroundTargetFinishOption, + type AiWhiteBackgroundSourceSlot, + type AiWhiteBackgroundViewKey, +} from "./nonIntegratedAiWhiteBackground"; +import type { + NonIntegratedProductRecord, + NonIntegratedSkuItem, + NonIntegratedUploadFile, +} from "../../nonIntegratedStorage"; +import { + cleanupSharedNonIntegratedAiWhiteBackgroundManifest, + fetchSharedNonIntegratedAiWhiteBackgroundCapabilities, + generateSharedNonIntegratedAiWhiteBackgroundSession, + uploadSharedNonIntegratedAiWhiteBackgroundSession, +} from "../../nonIntegratedApi"; +import NonIntegratedAiWhiteBackgroundPanel from "./NonIntegratedAiWhiteBackgroundPanel"; + +type AiWhiteBackgroundBarcodeOption = { + barcode: string; + label: string; + batchNo: number; + color: string; +}; + +export type NonIntegratedAiWhiteBackgroundWorkspaceProps = { + productId: string | null; + isActive?: boolean; + isReadOnly: boolean; + canUseAiWhiteBackground: boolean; + currentProductPhotoStyleKey: string; + currentProductPhotoStyleLabel: string; + displayedSkuItems: NonIntegratedSkuItem[]; + skuItems: NonIntegratedSkuItem[]; + materialLibraryFinishes: AiWhiteBackgroundTargetFinishOption[]; + uploadFiles: NonIntegratedUploadFile[]; + productMetadata: NonIntegratedProductRecord["metadata"] | undefined; + sourceSlotFiles: Record< + AiWhiteBackgroundSourceSlot, + NonIntegratedUploadFile | null + >; + onProductSynced: (product: NonIntegratedProductRecord) => void; + onNotify: (message: string, type: "success" | "error") => void; +}; + +function getStyleKeyFromModel(model: string) { + const match = model + .trim() + .toUpperCase() + .match(/K\d{2}$/); + return match?.[0] ?? ""; +} + +function buildClientAiWhiteBackgroundSessionId() { + const suffix = + typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" + ? crypto.randomUUID().slice(0, 8) + : Math.random().toString(36).slice(2, 10); + return `ai-wbg-session-${Date.now()}-${suffix}`; +} + +export function NonIntegratedAiWhiteBackgroundWorkspace({ + productId, + isActive = true, + isReadOnly, + canUseAiWhiteBackground, + currentProductPhotoStyleKey, + currentProductPhotoStyleLabel, + displayedSkuItems, + skuItems, + materialLibraryFinishes, + uploadFiles, + productMetadata, + sourceSlotFiles, + onProductSynced, + onNotify, +}: NonIntegratedAiWhiteBackgroundWorkspaceProps) { + const [selectedBarcode, setSelectedBarcode] = useState(""); + const [targetFinishId, setTargetFinishId] = useState(""); + const [pendingSessionId, setPendingSessionId] = useState(null); + const [generatingViewKeys, setGeneratingViewKeys] = useState< + AiWhiteBackgroundViewKey[] + >([]); + const [isBulkGenerating, setIsBulkGenerating] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [cleaningManifestId, setCleaningManifestId] = useState( + null, + ); + const [materialLibraryCapabilities, setMaterialLibraryCapabilities] = useState<{ + isLoading: boolean; + canUploadToMaterialLibrary: boolean; + uploadDisabledReason: string | null; + }>({ + isLoading: false, + canUploadToMaterialLibrary: false, + uploadDisabledReason: null, + }); + + const aiWhiteBackgroundMetadata = useMemo( + () => readNonIntegratedAiWhiteBackgroundMetadata(productMetadata), + [productMetadata], + ); + + const barcodeOptions = useMemo(() => { + const withBarcode = (items: NonIntegratedSkuItem[]) => + items.filter((item) => item.barcode.trim()); + const currentStyleItems = withBarcode( + displayedSkuItems.filter( + (item) => getStyleKeyFromModel(item.model) === currentProductPhotoStyleKey, + ), + ); + const displayedItems = withBarcode(displayedSkuItems); + const fallbackStyleItems = withBarcode( + skuItems.filter( + (item) => getStyleKeyFromModel(item.model) === currentProductPhotoStyleKey, + ), + ); + const sourceItems = + currentStyleItems.length > 0 + ? currentStyleItems + : displayedItems.length > 0 + ? displayedItems + : fallbackStyleItems.length > 0 + ? fallbackStyleItems + : withBarcode(skuItems); + const optionMap = new Map(); + + sourceItems.forEach((item) => { + const barcode = item.barcode.trim().toUpperCase(); + if (!barcode || optionMap.has(barcode)) { + return; + } + + const itemLabel = item.model.trim() || item.skuCode.trim() || item.id; + const batchNo = item.batchNo || 1; + optionMap.set(barcode, { + barcode, + label: `${itemLabel} / ${barcode} / 批次${batchNo}`, + batchNo, + color: item.color.trim(), + }); + }); + + return Array.from(optionMap.values()); + }, [currentProductPhotoStyleKey, displayedSkuItems, skuItems]); + + useEffect(() => { + setSelectedBarcode((current) => + barcodeOptions.some((option) => option.barcode === current) + ? current + : (barcodeOptions[0]?.barcode ?? ""), + ); + }, [barcodeOptions]); + + const selectedBarcodeOption = useMemo( + () => barcodeOptions.find((option) => option.barcode === selectedBarcode) ?? null, + [barcodeOptions, selectedBarcode], + ); + + const targetFinishOptions = useMemo( + () => materialLibraryFinishes.filter((finish) => finish.isActive !== false), + [materialLibraryFinishes], + ); + const selectedTargetFinish = useMemo( + () => + targetFinishOptions.find((finish) => finish.id === targetFinishId) ?? null, + [targetFinishId, targetFinishOptions], + ); + const targetColor = selectedTargetFinish?.name.trim() ?? ""; + + useEffect(() => { + if ( + targetFinishId && + !targetFinishOptions.some((finish) => finish.id === targetFinishId) + ) { + setTargetFinishId(""); + } + }, [targetFinishId, targetFinishOptions]); + + useEffect(() => { + setTargetFinishId(""); + }, [currentProductPhotoStyleKey, productId, selectedBarcode]); + + useEffect(() => { + if (!isActive || !productId || !canUseAiWhiteBackground) { + setMaterialLibraryCapabilities({ + isLoading: false, + canUploadToMaterialLibrary: false, + uploadDisabledReason: null, + }); + return; + } + + let disposed = false; + setMaterialLibraryCapabilities((current) => ({ + ...current, + isLoading: true, + })); + + void fetchSharedNonIntegratedAiWhiteBackgroundCapabilities(productId) + .then((result) => { + if (disposed) return; + setMaterialLibraryCapabilities({ + isLoading: false, + canUploadToMaterialLibrary: result.canUploadToMaterialLibrary, + uploadDisabledReason: result.uploadDisabledReason, + }); + }) + .catch((error) => { + if (disposed) return; + setMaterialLibraryCapabilities({ + isLoading: false, + canUploadToMaterialLibrary: false, + uploadDisabledReason: + error instanceof Error + ? error.message + : "素材库上传能力获取失败。", + }); + }); + + return () => { + disposed = true; + }; + }, [canUseAiWhiteBackground, isActive, productId]); + + const currentSession = useMemo( + () => + selectedBarcode + ? getLatestAiWhiteBackgroundSession(productMetadata, { + styleKey: currentProductPhotoStyleKey, + barcode: selectedBarcode, + targetColor, + targetFinishId, + }) + : null, + [currentProductPhotoStyleKey, productMetadata, selectedBarcode, targetColor, targetFinishId], + ); + + const generatedFiles = useMemo( + () => + currentSession + ? getAiWhiteBackgroundSessionGeneratedFiles(uploadFiles, currentSession.id) + : (Object.fromEntries( + AI_WHITE_BACKGROUND_VIEW_KEYS.map((viewKey) => [viewKey, null]), + ) as Record), + [currentSession, uploadFiles], + ); + + const currentManifest = useMemo(() => { + if (!currentSession) { + return null; + } + + const lastManifestId = currentSession.lastManifestId?.trim(); + if (lastManifestId) { + const matchedManifest = aiWhiteBackgroundMetadata.uploadManifests.find( + (manifest) => manifest.manifestId === lastManifestId, + ); + if (matchedManifest) { + return matchedManifest; + } + } + + return ( + aiWhiteBackgroundMetadata.uploadManifests + .filter((manifest) => manifest.sessionId === currentSession.id) + .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? + null + ); + }, [aiWhiteBackgroundMetadata.uploadManifests, currentSession]); + + const sourceReadyByView = useMemo( + () => + Object.fromEntries( + AI_WHITE_BACKGROUND_VIEW_KEYS.map((viewKey) => [ + viewKey, + Boolean(sourceSlotFiles[AI_WHITE_BACKGROUND_VIEW_KEY_TO_SOURCE_SLOT[viewKey]]), + ]), + ) as Record, + [sourceSlotFiles], + ); + const sourceFilesByView = useMemo( + () => + Object.fromEntries( + AI_WHITE_BACKGROUND_VIEW_KEYS.map((viewKey) => [ + viewKey, + sourceSlotFiles[AI_WHITE_BACKGROUND_VIEW_KEY_TO_SOURCE_SLOT[viewKey]] ?? null, + ]), + ) as Record, + [sourceSlotFiles], + ); + + const missingSourceSlotLabels = useMemo( + () => + AI_WHITE_BACKGROUND_SOURCE_SLOTS.filter((slot) => !sourceSlotFiles[slot]).map( + (slot) => AI_WHITE_BACKGROUND_SOURCE_SLOT_LABELS[slot], + ), + [sourceSlotFiles], + ); + + const availableSourceViewKeys = useMemo( + () => + AI_WHITE_BACKGROUND_VIEW_KEYS.filter((viewKey) => sourceReadyByView[viewKey]), + [sourceReadyByView], + ); + const availableSourceViewCount = availableSourceViewKeys.length; + const generatingViewKeySet = useMemo( + () => new Set(generatingViewKeys), + [generatingViewKeys], + ); + const isGenerating = generatingViewKeys.length > 0; + const canUploadToMaterialLibrary = + materialLibraryCapabilities.canUploadToMaterialLibrary; + const uploadDisabledReason = materialLibraryCapabilities.isLoading + ? "正在检查素材库上传接口配置…" + : materialLibraryCapabilities.uploadDisabledReason; + const generateProgressText = useMemo(() => { + if (isBulkGenerating && generatingViewKeys.length > 0) { + return `正在生成 ${generatingViewKeys.length} 个角度的白底图…`; + } + if (generatingViewKeys.length === 1) { + return `正在生成 ${AI_WHITE_BACKGROUND_VIEW_LABELS[generatingViewKeys[0]]} 白底图…`; + } + if (generatingViewKeys.length > 1) { + return `正在并发生成 ${generatingViewKeys.length} 个角度的白底图…`; + } + return ""; + }, [generatingViewKeys, isBulkGenerating]); + const sessionStatus = currentSession?.status ?? null; + const canUpload = + canUseAiWhiteBackground && + Boolean(selectedBarcode) && + hasAnyAiWhiteBackgroundGeneratedView(currentSession) && + canUploadToMaterialLibrary; + const statusText = !canUseAiWhiteBackground + ? "当前环境未启用服务端白底图能力" + : isGenerating && generateProgressText + ? generateProgressText + : currentManifest + ? `上传清单:${currentManifest.status}` + : currentSession + ? `当前会话:${currentSession.status}` + : "还没有生成白底图会话"; + + useEffect(() => { + if (!isActive) { + setPendingSessionId(null); + setGeneratingViewKeys([]); + setIsBulkGenerating(false); + } + }, [isActive]); + + useEffect(() => { + setPendingSessionId(null); + setGeneratingViewKeys([]); + setIsBulkGenerating(false); + }, [currentProductPhotoStyleKey, productId, selectedBarcode, targetFinishId]); + + useEffect(() => { + if (currentSession?.id) { + setPendingSessionId((current) => + current === currentSession.id ? null : current, + ); + } + }, [currentSession?.id]); + + useEffect(() => { + if (generatingViewKeys.length === 0 && !currentSession?.id) { + setPendingSessionId(null); + } + }, [currentSession?.id, generatingViewKeys.length]); + + async function handleGenerate(requestedViewKeys?: AiWhiteBackgroundViewKey[]) { + if ( + isReadOnly || + !productId || + !canUseAiWhiteBackground || + !selectedBarcode || + availableSourceViewCount === 0 || + (requestedViewKeys == null && generatingViewKeys.length > 0) + ) { + return; + } + + const effectiveRequestedViewKeys = + requestedViewKeys && requestedViewKeys.length > 0 + ? requestedViewKeys + : availableSourceViewKeys; + const filteredRequestedViewKeys = effectiveRequestedViewKeys.filter( + (viewKey) => + sourceReadyByView[viewKey] && + !generatingViewKeySet.has(viewKey) && + !isBulkGenerating, + ); + if (filteredRequestedViewKeys.length === 0) { + return; + } + + const requestSessionId = + currentSession?.id ?? + pendingSessionId ?? + buildClientAiWhiteBackgroundSessionId(); + const isSingleViewRequest = filteredRequestedViewKeys.length === 1; + + try { + if (!currentSession?.id && !pendingSessionId) { + setPendingSessionId(requestSessionId); + } + if (!isSingleViewRequest) { + setIsBulkGenerating(true); + } + setGeneratingViewKeys((current) => + Array.from(new Set([...current, ...filteredRequestedViewKeys])), + ); + + const result = await generateSharedNonIntegratedAiWhiteBackgroundSession( + productId, + { + sessionId: requestSessionId, + styleKey: currentProductPhotoStyleKey, + barcode: selectedBarcode, + targetColor: targetColor.trim() || null, + targetFinishId: selectedTargetFinish?.id ?? null, + requestedViewKeys: filteredRequestedViewKeys, + }, + ); + onProductSynced(result.product); + + onNotify( + isSingleViewRequest + ? `${AI_WHITE_BACKGROUND_VIEW_LABELS[filteredRequestedViewKeys[0]]} 白底图已更新` + : hasCompleteAiWhiteBackgroundSession(result.session) + ? "8 个固定角度白底图已生成完成" + : `已生成 ${filteredRequestedViewKeys.length} 个已上传角度的白底图,可先确认上传已完成的视角`, + "success", + ); + } catch (error) { + onNotify( + error instanceof Error ? error.message : "AI 白底图生成失败,请稍后重试", + "error", + ); + } finally { + setGeneratingViewKeys((current) => + current.filter((viewKey) => !filteredRequestedViewKeys.includes(viewKey)), + ); + if (!isSingleViewRequest) { + setIsBulkGenerating(false); + } + } + } + + async function handleUpload() { + if ( + isReadOnly || + !productId || + !canUseAiWhiteBackground || + !currentSession || + !canUploadToMaterialLibrary || + !canUpload || + isUploading + ) { + return; + } + + try { + setIsUploading(true); + const result = await uploadSharedNonIntegratedAiWhiteBackgroundSession( + productId, + currentSession.id, + ); + + onProductSynced(result.product); + onNotify("白底图已确认上传到素材库", "success"); + } catch (error) { + onNotify( + error instanceof Error ? error.message : "白底图上传素材库失败,请稍后重试", + "error", + ); + } finally { + setIsUploading(false); + } + } + + async function handleCleanup() { + if ( + isReadOnly || + !productId || + !canUseAiWhiteBackground || + !currentManifest || + cleaningManifestId === currentManifest.manifestId + ) { + return; + } + + try { + setCleaningManifestId(currentManifest.manifestId); + const result = await cleanupSharedNonIntegratedAiWhiteBackgroundManifest( + productId, + currentManifest.manifestId, + "manual-review-cleanup", + ); + + onProductSynced(result.product); + onNotify("当前白底图上传清单已完成清理", "success"); + } catch (error) { + onNotify( + error instanceof Error ? error.message : "白底图清理失败,请稍后重试", + "error", + ); + } finally { + setCleaningManifestId(null); + } + } + + return ( + void handleCleanup()} + /> + ); +} diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/ProductPhotoHeaderActions.tsx b/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/ProductPhotoHeaderActions.tsx index 1ebde63b..a269b4a6 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/ProductPhotoHeaderActions.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/ProductPhotoHeaderActions.tsx @@ -4,6 +4,7 @@ import type { NonIntegratedUploadFile } from "../../nonIntegratedStorage"; import type { ProductPhotoGroup, ProductPhotoSelection, + ProductPhotoWorkspaceMode, } from "./documentWorkspaceHelpers"; type ProductPhotoHeaderActionsProps = { @@ -15,6 +16,7 @@ type ProductPhotoHeaderActionsProps = { currentUploadInputRef: RefObject; detailUploadKey: string; activePanel: ProductPhotoGroup; + activeMode: ProductPhotoWorkspaceMode; selectedTarget: ProductPhotoSelection; groupUploadLabel: string; currentUploadLabel: string; @@ -41,6 +43,7 @@ export function ProductPhotoHeaderActions({ currentUploadInputRef, detailUploadKey, activePanel, + activeMode, selectedTarget, groupUploadLabel, currentUploadLabel, @@ -60,15 +63,15 @@ export function ProductPhotoHeaderActions({ return (
- {isReadOnly ? null : ( + {isReadOnly || activeMode === "ai-white-background" ? null : ( <> + ); + })} +
+ +
+

款式分类 @@ -110,62 +145,71 @@ export function ProductPhotoWorkspace({ })}

-
- -
-
-
- {PRODUCT_PHOTO_PANEL_ORDER.map((panelKey) => { - const countLabel = - panelKey === "nine-view" - ? `${workspaceData.filledSlotCount} / 9` - : `${workspaceData.groupFiles[panelKey].length} 张`; - const isActive = activePanel === panelKey; - - return ( - - ); - })} -
-
- {activePanel === "nine-view" ? ( -
-
- {PRODUCT_PHOTO_NINE_VIEW_SLOTS.map((slot) => { - const slotFile = workspaceData.slotFiles[slot.key]; - const isActive = - selectedTarget.group === "nine-view" && - selectedTarget.slot === slot.key; + {activeMode === "ai-white-background" ? ( +
+ {aiWhiteBackgroundContent} +
+ ) : ( + <> +
+
+
+ {PRODUCT_PHOTO_PANEL_ORDER.map((panelKey) => { + const countLabel = + panelKey === "nine-view" + ? `${workspaceData.filledSlotCount} / 9` + : `${workspaceData.groupFiles[panelKey].length} 张`; + const isActive = activePanel === panelKey; + + return ( + + ); + })} +
+
+ +
+ {activePanel === "nine-view" ? ( +
+
+ {PRODUCT_PHOTO_NINE_VIEW_SLOTS.map((slot) => { + const slotFile = workspaceData.slotFiles[slot.key]; + const isActive = + selectedTarget.group === "nine-view" && + selectedTarget.slot === slot.key; return ( ); - })} -
-
- ) : ( -
- {activePanelFiles.length === 0 ? ( - - ) : ( -
- {activePanelFiles.map((file) => { - const isActive = - selectedTarget.group === activePanel && - selectedTarget.fileId === file.id; + })} +
+
+ ) : ( +
+ {activePanelFiles.length === 0 ? ( + + ) : ( +
+ {activePanelFiles.map((file) => { + const isActive = + selectedTarget.group === activePanel && + selectedTarget.fileId === file.id; return ( ); - })} -
- )} + })} +
+ )} +
+ )} +
- )} -
-
-
-
-

- {previewTitle} -

-

- {previewDescription} -

-
-
- {previewContent} -
+
+
+

+ {previewTitle} +

+

+ {previewDescription} +

+
+
+ {previewContent} +
+
+ + )}
); diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/documentWorkspaceHelpers.ts b/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/documentWorkspaceHelpers.ts index 7f7850be..8ba26e36 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/documentWorkspaceHelpers.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/documentWorkspaceHelpers.ts @@ -52,6 +52,7 @@ export type ProductPhotoFileGroup = | "competitor-sample" | "competitor-disassembly"; export type ProductPhotoGroup = "nine-view" | ProductPhotoFileGroup; +export type ProductPhotoWorkspaceMode = "source" | "ai-white-background"; export type ProductPhotoNineViewSlot = | "perspective" | "top" diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/documentWorkspaceRenderers.tsx b/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/documentWorkspaceRenderers.tsx index 0296ff5c..51968bd7 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/documentWorkspaceRenderers.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/documents/documentWorkspaceRenderers.tsx @@ -28,8 +28,9 @@ type ReviewSheetWorkspaceProps = ComponentProps; type ProductPhotoWorkspaceRendererProps = Omit< ProductPhotoWorkspaceProps, - "previewContent" + "aiWhiteBackgroundContent" | "previewContent" > & { + renderAiWhiteBackgroundContent: () => ReactNode; renderPreviewContent: () => ReactNode; }; @@ -65,7 +66,11 @@ export function buildDocumentWorkspaceRenderers({ detection, reviewSheet, }: DocumentWorkspaceRendererOptions) { - const { renderPreviewContent, ...productPhotoProps } = productPhoto; + const { + renderAiWhiteBackgroundContent, + renderPreviewContent, + ...productPhotoProps + } = productPhoto; function renderQualityInspectionWorkspace() { return ; @@ -79,6 +84,7 @@ export function buildDocumentWorkspaceRenderers({ return ( ); diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/shared/DetailOverlayDialogs.tsx b/furniture-3d-system/src/app/intake/non-integrated/workspaces/shared/DetailOverlayDialogs.tsx index 8363f748..4c39990e 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/shared/DetailOverlayDialogs.tsx +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/shared/DetailOverlayDialogs.tsx @@ -1,7 +1,6 @@ import type { ComponentProps } from "react"; import CameraScannerModal from "@/components/mobile/CameraScannerModal"; -import NonIntegratedAiWhiteBackgroundModal from "../ai-white-background/NonIntegratedAiWhiteBackgroundModal"; import { ImagePreviewDialog, SeriesComponentDetailsDialog, @@ -36,15 +35,11 @@ type WorkspaceImagePreviewState = | { kind: "workspace-gallery" } | null; -type AiWhiteBackgroundModalProps = ComponentProps< - typeof NonIntegratedAiWhiteBackgroundModal ->; type CameraScannerDetectedHandler = ComponentProps< typeof CameraScannerModal >["onDetected"]; export function DetailOverlayDialogs({ - aiWhiteBackgroundProps, barcodeScannerSkuId, bomImagePreview, canGoToNextWorkspaceImage, @@ -69,7 +64,6 @@ export function DetailOverlayDialogs({ workspacePreviewDialogLabel, workspacePreviewDialogSrc, }: { - aiWhiteBackgroundProps: AiWhiteBackgroundModalProps; barcodeScannerSkuId: string | null; bomImagePreview: ImagePreviewState; canGoToNextWorkspaceImage: boolean; @@ -149,7 +143,6 @@ export function DetailOverlayDialogs({ navigationLabel={workspacePreviewDialogLabel} /> ) : null} - {selectedSeriesComponentSku ? ( ["reopenField"]; type ProductPhotoPanel = ComponentProps["activePanel"]; +type ProductPhotoWorkspaceMode = ComponentProps< + typeof ProductPhotoHeaderActions +>["activeMode"]; type ProductPhotoSelection = ComponentProps< typeof ProductPhotoHeaderActions >["selectedTarget"]; @@ -31,6 +34,7 @@ export function DocumentWorkspaceHeaderActionsSlot({ activeInstallationAssetFiles, activeProductDataGroupFiles, activeProductPhotoDownloadFiles, + activeProductPhotoMode, activeProductPhotoPanel, canDeleteSelectedReviewRound, canDownloadWorkspaceFiles, @@ -135,6 +139,7 @@ export function DocumentWorkspaceHeaderActionsSlot({ activeInstallationAssetFiles: NonIntegratedUploadFile[]; activeProductDataGroupFiles: NonIntegratedUploadFile[]; activeProductPhotoDownloadFiles: NonIntegratedUploadFile[]; + activeProductPhotoMode: ProductPhotoWorkspaceMode; activeProductPhotoPanel: ProductPhotoPanel; canDeleteSelectedReviewRound: boolean; canDownloadWorkspaceFiles: boolean; @@ -352,6 +357,7 @@ export function DocumentWorkspaceHeaderActionsSlot({ currentUploadInputRef={productPhotoCurrentUploadInputRef} detailUploadKey={detailUploadKey} activePanel={activeProductPhotoPanel} + activeMode={activeProductPhotoMode} selectedTarget={selectedProductPhotoTarget} groupUploadLabel={currentProductPhotoGroupUploadLabel} currentUploadLabel={currentProductPhotoUploadLabel}