diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index b2378a9..da97c2e 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -305,6 +305,20 @@ interface CanvasNode { y: number; } +interface RecordCanvasGroup { + id: string; + mode: string; + outputLabel: string; + settingLabel: string; + sourceImages: CloneImageItem[]; + results: CloneResult[]; + createdAt: number; + turnIndex: number; + status: EcommerceHistoryStatus; + x: number; + y: number; +} + interface PreviewTouchPoint { id: number; x: number; @@ -4695,6 +4709,65 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { isOneClickVideoTool || isVideoWorkspaceVisible || Boolean(activeHistoryRecordId); + const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isQuickSetTool && !isCopywritingTool && !isOneClickVideoTool; + const isRecordDetailWorkspace = isMainCloneWorkspace && Boolean(activeHistoryRecordId); + const activeHistoryRecord = activeHistoryRecordId ? ecommerceHistoryRecords.find((record) => record.id === activeHistoryRecordId) : null; + const activeConversationTurns = activeHistoryRecord + ? activeHistoryRecord.turns?.length + ? activeHistoryRecord.turns + : [buildHistoryTurnFromRecord(activeHistoryRecord)] + : []; + const activeRecallTurn = activeConversationTurns[activeConversationTurns.length - 1]; + const commandRecallPrompt = + requirement.trim() || + activeRecallTurn?.requirement?.trim() || + activeHistoryRecord?.requirement?.trim() || + "展开完整对话,继续补充生成需求。"; + const commandRecallAsset = productImages[0] || activeRecallTurn?.productImages?.[0] || activeHistoryRecord?.productImages?.[0]; + const commandRecallStatus = + status === "generating" + ? "生成中" + : activeRecallTurn?.status === "failed" || status === "failed" + ? "可恢复" + : "继续生成"; + const hasGeneratedCloneWork = status === "done" || canvasNodes.length > 0; + const shouldUseCompactComposer = isCommandComposerCompact && hasGeneratedCloneWork && !(isRecordDetailWorkspace && !isCloneConversationCollapsed); + const shouldShowScenarioScrollHint = !isRecordDetailWorkspace; + const getHistoryTurnSettingLabel = (turn: EcommerceHistoryTurn) => { + if (turn.settingLabel) return turn.settingLabel; + if (turn.output === "set" && turn.results?.length && !turn.setResultImages?.length) { + return `单图 ${turn.results.length}张`; + } + if (turn.output === "set") { + const total = cloneSetCountKeys.reduce((sum, key) => sum + (turn.setCounts?.[key] ?? 0), 0); + return `套图 ${total || 1}张`; + } + if (turn.output === "detail") return `详情 ${turn.detailModules?.length || 1}项`; + if (turn.output === "model") return `模特 ${turn.modelScenes?.length || 1}景`; + return cloneOutputOptions.find((option) => option.key === turn.output)?.label || selectedCloneOutput.label; + }; + const recordCanvasGroups: RecordCanvasGroup[] = isRecordDetailWorkspace + ? activeConversationTurns.reduce((groups, turn, index) => { + const turnResults = getTurnResults(turn); + if (!turnResults.length && turn.status !== "generating") return groups; + const canvasNode = canvasNodes.find((node) => node.id === turn.id); + const outputLabel = turn.modeLabel || cloneOutputOptions.find((option) => option.key === turn.output)?.label || selectedCloneOutput.label; + groups.push({ + id: turn.id, + mode: turn.output, + outputLabel, + settingLabel: getHistoryTurnSettingLabel(turn), + sourceImages: turn.productImages, + results: turnResults, + createdAt: turn.createdAt, + turnIndex: index + 1, + status: turn.status, + x: canvasNode?.x ?? groups.length * 420, + y: canvasNode?.y ?? (groups.length % 2 === 0 ? 0 : 160), + }); + return groups; + }, []) + : []; useEffect(() => { onWorkspaceChromeChange?.({ isToolPage: isWorkspaceToolPage }); @@ -6109,93 +6182,211 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { ) ) : ( <> - {status === "done" || canvasNodes.length > 0 ? ( + {status === "done" || canvasNodes.length > 0 || (isRecordDetailWorkspace && recordCanvasGroups.length > 0) ? (
-
- {canvasNodes.map((node) => ( -
startCanvasNodeDrag(event, node)} - onPointerMove={(event) => moveCanvasNodeDrag(event, node.id)} - onPointerUp={(event) => stopCanvasNodeDrag(event, node.id)} - onPointerCancel={(event) => stopCanvasNodeDrag(event, node.id)} - > -
{ - if (e.button !== 0) return; - e.stopPropagation(); - e.currentTarget.setPointerCapture(e.pointerId); - nodeDragRef.current = { active: true, nodeId: node.id, startX: e.clientX, startY: e.clientY, originX: node.x, originY: node.y }; - }} - onPointerMove={(e) => { - const drag = nodeDragRef.current; - if (!drag.active || drag.nodeId !== node.id) return; - const zoom = previewZoomRef.current; - const dx = (e.clientX - drag.startX) / zoom; - const dy = (e.clientY - drag.startY) / zoom; - setCanvasNodes((prev) => prev.map((n) => n.id === node.id ? { ...n, x: drag.originX + dx, y: drag.originY + dy } : n)); - }} - onPointerUp={(e) => { - if (nodeDragRef.current.nodeId === node.id) { - nodeDragRef.current = { ...nodeDragRef.current, active: false }; - e.currentTarget.releasePointerCapture(e.pointerId); - } - }} - /> -
- - -
-
+ ); + })} + {status === "generating" && !recordCanvasGroups.some((group) => group.status === "generating") ? ( +
+
+
+ 继续生成 + {selectedCloneOutput.label} +
+ AI 正在整理新一轮结果 +
+
+ + 正在生成{selectedCloneOutput.label}... +
+
+ ) : null} +
+ ) : ( +
+ {canvasNodes.map((node) => ( +
startCanvasNodeDrag(event, node)} + onPointerMove={(event) => moveCanvasNodeDrag(event, node.id)} + onPointerUp={(event) => stopCanvasNodeDrag(event, node.id)} + onPointerCancel={(event) => stopCanvasNodeDrag(event, node.id)} + > +
{ + if (e.button !== 0) return; + e.stopPropagation(); + e.currentTarget.setPointerCapture(e.pointerId); + nodeDragRef.current = { active: true, nodeId: node.id, startX: e.clientX, startY: e.clientY, originX: node.x, originY: node.y }; + }} + onPointerMove={(e) => { + const drag = nodeDragRef.current; + if (!drag.active || drag.nodeId !== node.id) return; + const zoom = previewZoomRef.current; + const dx = (e.clientX - drag.startX) / zoom; + const dy = (e.clientY - drag.startY) / zoom; + setCanvasNodes((prev) => prev.map((n) => n.id === node.id ? { ...n, x: drag.originX + dx, y: drag.originY + dy } : n)); + }} + onPointerUp={(e) => { + if (nodeDragRef.current.nodeId === node.id) { + nodeDragRef.current = { ...nodeDragRef.current, active: false }; + e.currentTarget.releasePointerCapture(e.pointerId); + } + }} + /> +
+ - ))} -
-
-
- ))} - {status === "generating" ? ( -
- - 正在生成{selectedCloneOutput.label}… -
- ) : null} -
+ +
+ ) : status === "idle" || status === "ready" ? null : (
@@ -6221,13 +6412,71 @@ function ProductClonePage(_props: ProductClonePageProps = {}) {
0 ? " has-generated" : " is-before-generate"}${isCommandComposerCompact && (status === "done" || canvasNodes.length > 0) ? " is-compact" : ""}`} - aria-label="生成指令" + className={`clone-ai-bottom-input ecom-command-composer-wrap${hasGeneratedCloneWork ? " has-generated" : " is-before-generate"}${shouldUseCompactComposer ? " is-compact" : ""}${isRecordDetailWorkspace && isCloneConversationCollapsed ? " is-recall-entry" : ""}`} + aria-label={isRecordDetailWorkspace && isCloneConversationCollapsed ? "展开完整生成指令" : "生成指令"} + role={isRecordDetailWorkspace && isCloneConversationCollapsed ? "button" : undefined} + tabIndex={isRecordDetailWorkspace && isCloneConversationCollapsed ? 0 : undefined} + onClickCapture={(event) => { + if (isRecordDetailWorkspace && isCloneConversationCollapsed) { + if ((event.target as HTMLElement).closest(".ecom-command-recall__home")) return; + event.preventDefault(); + event.stopPropagation(); + setIsCloneConversationCollapsed(false); + window.setTimeout(() => requirementTextareaRef.current?.focus(), 180); + } + }} onClick={() => { + if (isRecordDetailWorkspace && isCloneConversationCollapsed) { + setIsCloneConversationCollapsed(false); + window.setTimeout(() => requirementTextareaRef.current?.focus(), 180); + return; + } if (isCommandComposerCompact) setIsCommandComposerCompact(false); }} + onKeyDown={(event) => { + if (!isRecordDetailWorkspace || !isCloneConversationCollapsed) return; + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + setIsCloneConversationCollapsed(false); + window.setTimeout(() => requirementTextareaRef.current?.focus(), 180); + }} > -

0 ? " is-after-generate" : ""}`}> + {isRecordDetailWorkspace && isCloneConversationCollapsed ? ( +
+ + + + {commandRecallStatus} + + {commandRecallPrompt} + + + +
+ ) : null} +

{typewriterText}

@@ -6287,9 +6536,11 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { - + {shouldShowScenarioScrollHint ? ( + + ) : null}
{productImages.length ? (
@@ -8268,28 +8519,7 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { ? oneClickVideoPreview : clonePreview : placeholderPreview; - const isMainCloneWorkspace = isCloneTool && !isSmartCutoutTool && !isQuickDetailTool && !isWatermarkTool && !isTranslateTool && !isImageEditTool && !isQuickSetTool && !isCopywritingTool && !isOneClickVideoTool; - const isRecordDetailWorkspace = isMainCloneWorkspace && Boolean(activeHistoryRecordId); const currentResultCount = canvasNodes.reduce((count, node) => count + node.results.length, 0); - const activeHistoryRecord = activeHistoryRecordId ? ecommerceHistoryRecords.find((record) => record.id === activeHistoryRecordId) : null; - const activeConversationTurns = activeHistoryRecord - ? activeHistoryRecord.turns?.length - ? activeHistoryRecord.turns - : [buildHistoryTurnFromRecord(activeHistoryRecord)] - : []; - const getHistoryTurnSettingLabel = (turn: EcommerceHistoryTurn) => { - if (turn.settingLabel) return turn.settingLabel; - if (turn.output === "set" && turn.results?.length && !turn.setResultImages?.length) { - return `单图 ${turn.results.length}张`; - } - if (turn.output === "set") { - const total = cloneSetCountKeys.reduce((sum, key) => sum + (turn.setCounts?.[key] ?? 0), 0); - return `套图 ${total || 1}张`; - } - if (turn.output === "detail") return `详情 ${turn.detailModules?.length || 1}项`; - if (turn.output === "model") return `模特 ${turn.modelScenes?.length || 1}景`; - return cloneOutputOptions.find((option) => option.key === turn.output)?.label || selectedCloneOutput.label; - }; const restoreHistoryTurnInputs = (turn: EcommerceHistoryTurn) => { setCloneOutput(turn.output); setPlatform(turn.platform); @@ -8350,18 +8580,30 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { <> - ) : null} diff --git a/src/features/ecommerce/panels/CommandHistorySidebar.tsx b/src/features/ecommerce/panels/CommandHistorySidebar.tsx index 03c341d..ed0a2eb 100644 --- a/src/features/ecommerce/panels/CommandHistorySidebar.tsx +++ b/src/features/ecommerce/panels/CommandHistorySidebar.tsx @@ -75,7 +75,7 @@ export default function CommandHistorySidebar({
生成记录 - {records.length} 条 + {records.length} 条
{refreshMessage ? (

@@ -86,13 +86,25 @@ export default function CommandHistorySidebar({ {records.length ? ( records.map((record) => { const outputLabel = outputLabels.find((option) => option.key === record.output)?.label || "生成记录"; + const statusKey = record.status === "generating" ? "generating" : record.status === "failed" ? "failed" : "done"; const statusLabel = record.status === "generating" ? "生成中" : record.status === "failed" ? "失败" : formatHistoryTime(record.createdAt); return ( -

-