feat(ai-render): bind render tasks to sku
This commit is contained in:
parent
fce3f46a04
commit
c2f0898784
@ -24,6 +24,7 @@ import { UvSettingsSubmenu } from '@/app/asset-center/PreviewSubmenus'
|
||||
import { RenderInspector } from './RenderInspector'
|
||||
import type {
|
||||
AiRenderNodeMaterial,
|
||||
AiRenderTaskState,
|
||||
AiRenderSku,
|
||||
AiRenderSpu,
|
||||
AiRenderUvEditorFormState,
|
||||
@ -56,6 +57,16 @@ const CURVE_DETAIL_ENHANCEMENT_PROMPT =
|
||||
const CURVE_DETAIL_ENHANCEMENT_NEGATIVE_PROMPT =
|
||||
'changed product structure, changed dimensions, extra holes, missing holes, missing slots, distorted straight edges, melted corners, warped panels, altered material grain direction, changed part boundaries, blurry product'
|
||||
|
||||
const AI_RENDER_TASK_PROGRESS_CAP = 88
|
||||
const AI_RENDER_TASK_PROGRESS_STAGES = [
|
||||
{ progress: 12, stage: '正在抓取当前 3D 参考图' },
|
||||
{ progress: 24, stage: '正在提交 AI 生图请求' },
|
||||
{ progress: 42, stage: 'AI 正在锁定产品结构' },
|
||||
{ progress: 58, stage: '正在生成图像细节' },
|
||||
{ progress: 72, stage: '正在增强曲面和材质表现' },
|
||||
{ progress: 84, stage: '等待 AI 返回结果' },
|
||||
] as const
|
||||
|
||||
function mergeNegativePrompt(...parts: Array<string | null | undefined>): string | null {
|
||||
const normalized = parts
|
||||
.map((part) => part?.trim())
|
||||
@ -64,6 +75,19 @@ function mergeNegativePrompt(...parts: Array<string | null | undefined>): string
|
||||
return normalized.length > 0 ? normalized.join(', ') : null
|
||||
}
|
||||
|
||||
function getTaskModeLabel(mode: LocalRenderRecord['mode']): string {
|
||||
return mode === 'white-background' ? '白底图' : '场景效果图'
|
||||
}
|
||||
|
||||
function buildTaskStartedMessage(params: {
|
||||
mode: LocalRenderRecord['mode']
|
||||
skuName: string
|
||||
imageCount: number
|
||||
}) {
|
||||
const modeLabel = getTaskModeLabel(params.mode)
|
||||
return `${modeLabel}已进入后台生成:${params.skuName},共 ${params.imageCount} 张。你可以继续切换 SKU 或编辑参数。`
|
||||
}
|
||||
|
||||
type StoredWorkbenchState = {
|
||||
spus: AiRenderSpu[]
|
||||
selectedSpuId: string | null
|
||||
@ -262,6 +286,7 @@ function normalizeRecords(value: unknown): LocalRenderRecord[] {
|
||||
|
||||
return {
|
||||
id: typeof record.id === 'string' && record.id.trim() ? record.id : createId('render'),
|
||||
skuId: typeof record.skuId === 'string' ? record.skuId : '',
|
||||
skuName: typeof record.skuName === 'string' ? record.skuName : '未命名 SKU',
|
||||
mode,
|
||||
imageUrl: typeof record.imageUrl === 'string' ? record.imageUrl : '',
|
||||
@ -271,7 +296,6 @@ function normalizeRecords(value: unknown): LocalRenderRecord[] {
|
||||
}
|
||||
})
|
||||
.filter((record) => record.imageUrl.trim())
|
||||
.slice(0, 18)
|
||||
}
|
||||
|
||||
function readStoredWorkbenchState(): StoredWorkbenchState | null {
|
||||
@ -503,7 +527,7 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
const [imageCount, setImageCount] = useState(storedWorkbenchState?.imageCount ?? 1)
|
||||
const [records, setRecords] = useState<LocalRenderRecord[]>(() => storedWorkbenchState?.records ?? [])
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
const [generating, setGenerating] = useState(false)
|
||||
const [renderTask, setRenderTask] = useState<AiRenderTaskState | null>(null)
|
||||
const [optimizing, setOptimizing] = useState(false)
|
||||
const [optimExplanation, setOptimExplanation] = useState('')
|
||||
const [uploadingSkuId, setUploadingSkuId] = useState<string | null>(null)
|
||||
@ -531,6 +555,11 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
() => buildSkuMaterialAssignments(selectedSku, modelNodeNames),
|
||||
[modelNodeNames, selectedSku],
|
||||
)
|
||||
const selectedSkuRecords = useMemo(() => {
|
||||
if (!selectedSku) return []
|
||||
return records.filter((record) => record.skuId === selectedSku.id)
|
||||
}, [records, selectedSku])
|
||||
const generating = renderTask?.status === 'running'
|
||||
const materialPanelTargetNodeNames = useMemo(() => {
|
||||
if (!materialPanel) return []
|
||||
const targetNodeNames = materialPanel.scope === 'all'
|
||||
@ -619,6 +648,39 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
setSpus((current) => (isLegacySampleOnly(current) ? [] : current))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!renderTask || renderTask.status !== 'running') return
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
setRenderTask((current) => {
|
||||
if (!current || current.id !== renderTask.id || current.status !== 'running') return current
|
||||
|
||||
const elapsedMs = Date.now() - new Date(current.startedAt).getTime()
|
||||
const stage = [...AI_RENDER_TASK_PROGRESS_STAGES]
|
||||
.reverse()
|
||||
.find((item) => elapsedMs >= item.progress * 320)
|
||||
?? AI_RENDER_TASK_PROGRESS_STAGES[0]
|
||||
const drift = Math.floor(elapsedMs / 1800)
|
||||
const nextProgress = Math.min(
|
||||
AI_RENDER_TASK_PROGRESS_CAP,
|
||||
Math.max(current.progress, stage.progress + drift),
|
||||
)
|
||||
|
||||
if (nextProgress === current.progress && stage.stage === current.stage) {
|
||||
return current
|
||||
}
|
||||
|
||||
return {
|
||||
...current,
|
||||
progress: nextProgress,
|
||||
stage: stage.stage,
|
||||
}
|
||||
})
|
||||
}, 700)
|
||||
|
||||
return () => window.clearInterval(intervalId)
|
||||
}, [renderTask])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
@ -1041,10 +1103,13 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
const appendRenderedRecords = useCallback((params: {
|
||||
mode: LocalRenderRecord['mode']
|
||||
images: Array<{ originalUrl?: string | null; thumbnailUrl?: string | null }>
|
||||
skuId: string
|
||||
skuName?: string
|
||||
prompt?: string
|
||||
status?: LocalRenderRecord['status']
|
||||
}) => {
|
||||
if (!selectedSku) return 0
|
||||
const skuId = params.skuId
|
||||
const skuName = params.skuName ?? selectedSku?.name ?? '未命名 SKU'
|
||||
const createdAt = new Date().toISOString()
|
||||
const nextRecords = params.images
|
||||
.map((image) => {
|
||||
@ -1057,7 +1122,8 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
if (!imageUrl) return null
|
||||
return {
|
||||
id: createId('render'),
|
||||
skuName: selectedSku.name || '未命名 SKU',
|
||||
skuId,
|
||||
skuName,
|
||||
mode: params.mode,
|
||||
imageUrl,
|
||||
prompt: params.prompt ?? prompt,
|
||||
@ -1069,13 +1135,56 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
|
||||
if (nextRecords.length === 0) return 0
|
||||
|
||||
setRecords((current) => [...nextRecords, ...current].slice(0, 18))
|
||||
setRecords((current) => {
|
||||
const sameSkuRecords = [...nextRecords, ...current.filter((record) => record.skuId === skuId)].slice(0, 18)
|
||||
const otherSkuRecords = current.filter((record) => record.skuId !== skuId)
|
||||
return [...sameSkuRecords, ...otherSkuRecords]
|
||||
})
|
||||
return nextRecords.length
|
||||
}, [prompt, selectedSku])
|
||||
const startRenderTask = useCallback((params: {
|
||||
mode: LocalRenderRecord['mode']
|
||||
skuId: string
|
||||
skuName: string
|
||||
imageCount: number
|
||||
}) => {
|
||||
const task: AiRenderTaskState = {
|
||||
id: createId('render-task'),
|
||||
skuId: params.skuId,
|
||||
skuName: params.skuName,
|
||||
mode: params.mode,
|
||||
imageCount: params.imageCount,
|
||||
progress: 12,
|
||||
stage: AI_RENDER_TASK_PROGRESS_STAGES[0].stage,
|
||||
status: 'running',
|
||||
startedAt: new Date().toISOString(),
|
||||
}
|
||||
setRenderTask(task)
|
||||
return task
|
||||
}, [])
|
||||
|
||||
const settleRenderTask = useCallback((taskId: string, nextStatus: 'succeeded' | 'failed', stage: string) => {
|
||||
setRenderTask((current) =>
|
||||
current?.id === taskId
|
||||
? {
|
||||
...current,
|
||||
status: nextStatus,
|
||||
progress: 100,
|
||||
stage,
|
||||
}
|
||||
: current,
|
||||
)
|
||||
|
||||
window.setTimeout(() => {
|
||||
setRenderTask((current) => (current?.id === taskId ? null : current))
|
||||
}, 1400)
|
||||
}, [])
|
||||
|
||||
const handleCaptureWhiteBackground = useCallback(async () => {
|
||||
if (generating) return
|
||||
if (!selectedSku || !selectedSku.modelUrl) {
|
||||
const taskSku = selectedSku
|
||||
const taskSpu = selectedSpu
|
||||
if (!taskSku || !taskSku.modelUrl) {
|
||||
setMessage('请先选择并配置一个 SKU 模型文件。')
|
||||
return
|
||||
}
|
||||
@ -1089,53 +1198,65 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
setMessage('当前 3D 预览还没准备好,稍等模型加载完成再试。')
|
||||
return
|
||||
}
|
||||
const taskImageCount = imageCount
|
||||
const taskAspectRatio = aspectRatio
|
||||
const taskImageSize = imageSize
|
||||
const taskNegativePrompt = negativePrompt
|
||||
const taskNodeNames = [...modelNodeNames]
|
||||
const lockedPrompt = buildWhiteBackgroundPrompt({
|
||||
spu: selectedSpu,
|
||||
sku: selectedSku,
|
||||
spu: taskSpu,
|
||||
sku: taskSku,
|
||||
userPrompt: prompt,
|
||||
aspectRatio,
|
||||
aspectRatio: taskAspectRatio,
|
||||
})
|
||||
|
||||
setGenerating(true)
|
||||
setMessage('正在基于当前 3D 参考图生成白底图,并增强低分段圆弧细节...')
|
||||
const task = startRenderTask({
|
||||
mode: 'white-background',
|
||||
skuId: taskSku.id,
|
||||
skuName: taskSku.name || '未命名 SKU',
|
||||
imageCount: taskImageCount,
|
||||
})
|
||||
setMessage(buildTaskStartedMessage(task))
|
||||
try {
|
||||
const render = await createAssistedAssemblyAiRender({
|
||||
prompt: lockedPrompt,
|
||||
negativePrompt: mergeNegativePrompt(negativePrompt, CURVE_DETAIL_ENHANCEMENT_NEGATIVE_PROMPT),
|
||||
imageCount,
|
||||
aspectRatio,
|
||||
imageSize,
|
||||
negativePrompt: mergeNegativePrompt(taskNegativePrompt, CURVE_DETAIL_ENHANCEMENT_NEGATIVE_PROMPT),
|
||||
imageCount: taskImageCount,
|
||||
aspectRatio: taskAspectRatio,
|
||||
imageSize: taskImageSize,
|
||||
referenceImageDataUrl: dataUrl,
|
||||
workspaceSnapshot: buildAiRenderWorkspaceSnapshot({
|
||||
spu: selectedSpu,
|
||||
sku: selectedSku,
|
||||
spu: taskSpu,
|
||||
sku: taskSku,
|
||||
prompt: lockedPrompt,
|
||||
mode: 'white-background',
|
||||
}),
|
||||
assetSnapshot: buildAiRenderAssetSnapshot({
|
||||
spu: selectedSpu,
|
||||
sku: selectedSku,
|
||||
nodeNames: modelNodeNames,
|
||||
spu: taskSpu,
|
||||
sku: taskSku,
|
||||
nodeNames: taskNodeNames,
|
||||
}),
|
||||
})
|
||||
const savedCount = appendRenderedRecords({
|
||||
mode: 'white-background',
|
||||
images: render.resultImages,
|
||||
skuId: task.skuId,
|
||||
skuName: task.skuName,
|
||||
prompt: lockedPrompt,
|
||||
})
|
||||
if (render.status === 'SUCCEEDED' && savedCount > 0) {
|
||||
setMessage(
|
||||
savedCount > 1
|
||||
? `精准白底图已生成,共 ${savedCount} 张,并按当前模型参考增强了低分段圆弧的视觉细节。`
|
||||
: '精准白底图已生成,并按当前模型参考增强了低分段圆弧的视觉细节。',
|
||||
? `白底图已生成,共 ${savedCount} 张,并按当前模型参考增强了低分段圆弧的视觉细节。`
|
||||
: '白底图已生成,并按当前模型参考增强了低分段圆弧的视觉细节。',
|
||||
)
|
||||
settleRenderTask(task.id, 'succeeded', `已完成,生成 ${savedCount} 张图片`)
|
||||
} else {
|
||||
setMessage(render.errorMessage || 'AI 白底图请求已返回,但没有可用结果图。')
|
||||
settleRenderTask(task.id, 'failed', '生成结束,但没有返回可用图片')
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : 'AI 白底图生成失败')
|
||||
} finally {
|
||||
setGenerating(false)
|
||||
settleRenderTask(task.id, 'failed', '生成失败,请稍后重试')
|
||||
}
|
||||
}, [
|
||||
appendRenderedRecords,
|
||||
@ -1147,8 +1268,10 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
modelNodeNames,
|
||||
negativePrompt,
|
||||
prompt,
|
||||
settleRenderTask,
|
||||
selectedSku,
|
||||
selectedSpu,
|
||||
startRenderTask,
|
||||
])
|
||||
|
||||
const handleOptimizePrompt = useCallback(async () => {
|
||||
@ -1203,7 +1326,10 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
}, [aspectRatio, capturePreview, optimizing, prompt])
|
||||
|
||||
const handleGenerateScene = useCallback(async () => {
|
||||
if (!selectedSku || !selectedSku.modelUrl) {
|
||||
if (generating) return
|
||||
const taskSku = selectedSku
|
||||
const taskSpu = selectedSpu
|
||||
if (!taskSku || !taskSku.modelUrl) {
|
||||
setMessage('请先选择并配置一个 SKU 模型文件。')
|
||||
return
|
||||
}
|
||||
@ -1222,61 +1348,76 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
setMessage('请先填写场景效果图提示词。')
|
||||
return
|
||||
}
|
||||
|
||||
setGenerating(true)
|
||||
setMessage('正在请求 AI 生图服务...')
|
||||
const taskImageCount = imageCount
|
||||
const taskAspectRatio = aspectRatio
|
||||
const taskImageSize = imageSize
|
||||
const taskNegativePrompt = negativePrompt
|
||||
const taskNodeNames = [...modelNodeNames]
|
||||
const lockedScenePrompt = buildLockedScenePrompt({
|
||||
spu: taskSpu,
|
||||
sku: taskSku,
|
||||
userPrompt: normalizedPrompt,
|
||||
aspectRatio: taskAspectRatio,
|
||||
})
|
||||
const task = startRenderTask({
|
||||
mode: 'scene',
|
||||
skuId: taskSku.id,
|
||||
skuName: taskSku.name || '未命名 SKU',
|
||||
imageCount: taskImageCount,
|
||||
})
|
||||
setMessage(buildTaskStartedMessage(task))
|
||||
try {
|
||||
const lockedScenePrompt = buildLockedScenePrompt({
|
||||
spu: selectedSpu,
|
||||
sku: selectedSku,
|
||||
userPrompt: normalizedPrompt,
|
||||
aspectRatio,
|
||||
})
|
||||
const render = await createAssistedAssemblyAiRender({
|
||||
prompt: lockedScenePrompt,
|
||||
negativePrompt: mergeNegativePrompt(negativePrompt, CURVE_DETAIL_ENHANCEMENT_NEGATIVE_PROMPT),
|
||||
imageCount,
|
||||
aspectRatio,
|
||||
imageSize,
|
||||
negativePrompt: mergeNegativePrompt(taskNegativePrompt, CURVE_DETAIL_ENHANCEMENT_NEGATIVE_PROMPT),
|
||||
imageCount: taskImageCount,
|
||||
aspectRatio: taskAspectRatio,
|
||||
imageSize: taskImageSize,
|
||||
referenceImageDataUrl,
|
||||
workspaceSnapshot: buildAiRenderWorkspaceSnapshot({
|
||||
spu: selectedSpu,
|
||||
sku: selectedSku,
|
||||
spu: taskSpu,
|
||||
sku: taskSku,
|
||||
prompt: lockedScenePrompt,
|
||||
mode: 'scene',
|
||||
}),
|
||||
assetSnapshot: buildAiRenderAssetSnapshot({
|
||||
spu: selectedSpu,
|
||||
sku: selectedSku,
|
||||
nodeNames: modelNodeNames,
|
||||
spu: taskSpu,
|
||||
sku: taskSku,
|
||||
nodeNames: taskNodeNames,
|
||||
}),
|
||||
})
|
||||
const savedCount = appendRenderedRecords({
|
||||
mode: 'scene',
|
||||
images: render.resultImages,
|
||||
skuId: task.skuId,
|
||||
skuName: task.skuName,
|
||||
prompt: lockedScenePrompt,
|
||||
})
|
||||
if (render.status === 'SUCCEEDED' && savedCount > 0) {
|
||||
setMessage(savedCount > 1 ? `场景效果图已生成,共 ${savedCount} 张。` : '场景效果图已生成。')
|
||||
settleRenderTask(task.id, 'succeeded', `已完成,生成 ${savedCount} 张图片`)
|
||||
} else {
|
||||
setMessage(render.errorMessage || 'AI 生图请求已返回,但没有可用结果图。')
|
||||
settleRenderTask(task.id, 'failed', '生成结束,但没有返回可用图片')
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage(error instanceof Error ? error.message : 'AI 生图失败')
|
||||
} finally {
|
||||
setGenerating(false)
|
||||
settleRenderTask(task.id, 'failed', '生成失败,请稍后重试')
|
||||
}
|
||||
}, [
|
||||
appendRenderedRecords,
|
||||
aspectRatio,
|
||||
capturePreview,
|
||||
generating,
|
||||
imageCount,
|
||||
imageSize,
|
||||
modelNodeNames,
|
||||
negativePrompt,
|
||||
prompt,
|
||||
settleRenderTask,
|
||||
selectedSku,
|
||||
selectedSpu,
|
||||
startRenderTask,
|
||||
])
|
||||
|
||||
const handleDeleteRecord = useCallback((recordId: string) => {
|
||||
@ -1419,7 +1560,7 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
>
|
||||
<AlertCircle size={14} strokeWidth={1.8} />
|
||||
{selectedSku?.modelUrl
|
||||
? '精准白底图会以当前 3D 视角为参考做 AI 曲面细节增强,场景图也会锁定产品结构。'
|
||||
? '白底图会以当前 3D 视角为参考做 AI 曲面细节增强,场景图也会锁定产品结构。'
|
||||
: '当前 SKU 还没有模型文件,先上传或粘贴模型 URL。'}
|
||||
</div>
|
||||
}
|
||||
@ -1492,10 +1633,11 @@ export default function AiRenderWorkbenchClient({ finishes }: AiRenderWorkbenchC
|
||||
aspectRatio={aspectRatio}
|
||||
imageCount={imageCount}
|
||||
generating={generating}
|
||||
renderTask={renderTask}
|
||||
optimizing={optimizing}
|
||||
optimExplanation={optimExplanation}
|
||||
message={message}
|
||||
records={records}
|
||||
records={selectedSkuRecords}
|
||||
onSelectSpu={handleSelectSpu}
|
||||
onCreateSpu={handleCreateSpu}
|
||||
onRenameSpu={(spuId, name) => updateSpu(spuId, { name })}
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { AiRenderSku, AiRenderSpu, LocalRenderRecord } from './types'
|
||||
import type { AiRenderSku, AiRenderSpu, AiRenderTaskState, LocalRenderRecord } from './types'
|
||||
|
||||
interface RenderInspectorProps {
|
||||
spus: AiRenderSpu[]
|
||||
@ -36,6 +36,7 @@ interface RenderInspectorProps {
|
||||
aspectRatio: string
|
||||
imageCount: number
|
||||
generating: boolean
|
||||
renderTask: AiRenderTaskState | null
|
||||
optimizing: boolean
|
||||
optimExplanation: string
|
||||
message: string | null
|
||||
@ -144,6 +145,9 @@ function formatRecordTime(value: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function getRenderTaskModeLabel(mode: LocalRenderRecord['mode']) {
|
||||
return mode === 'white-background' ? '白底图' : '场景效果图'
|
||||
}
|
||||
export function RenderInspector({
|
||||
spus,
|
||||
selectedSpuId,
|
||||
@ -156,6 +160,7 @@ export function RenderInspector({
|
||||
aspectRatio,
|
||||
imageCount,
|
||||
generating,
|
||||
renderTask,
|
||||
optimizing,
|
||||
optimExplanation,
|
||||
message,
|
||||
@ -539,7 +544,6 @@ export function RenderInspector({
|
||||
<textarea
|
||||
value={prompt}
|
||||
onChange={(event) => onPromptChange(event.target.value)}
|
||||
disabled={generating}
|
||||
placeholder="描述你想要的效果,如:保留产品结构,生成真实白底产品摄影或真实室内场景,材质自然,灯光柔和"
|
||||
className="min-h-[60px] w-full resize-none rounded-[4px] bg-white/70 px-2.5 py-1.5 text-[11px] font-medium leading-[18px] text-[#2d3335] outline-none ring-1 ring-black/[0.04] transition placeholder:text-[#5a6062]/35 focus:bg-white focus:ring-[#47646b]/20 disabled:opacity-50"
|
||||
/>
|
||||
@ -552,7 +556,6 @@ export function RenderInspector({
|
||||
<select
|
||||
value={imageCount}
|
||||
onChange={(event) => onImageCountChange(Number(event.target.value))}
|
||||
disabled={generating}
|
||||
className="h-7 rounded-[5px] bg-white/70 px-2 text-[10px] font-bold text-[#2d3335] outline-none ring-1 ring-black/[0.04]"
|
||||
>
|
||||
{[1, 2, 3, 4].map((c) => <option key={c} value={c}>{c} 张</option>)}
|
||||
@ -560,7 +563,6 @@ export function RenderInspector({
|
||||
<select
|
||||
value={aspectRatio}
|
||||
onChange={(event) => onAspectRatioChange(event.target.value)}
|
||||
disabled={generating}
|
||||
className="h-7 rounded-[5px] bg-white/70 px-2 text-[10px] font-bold text-[#2d3335] outline-none ring-1 ring-black/[0.04]"
|
||||
>
|
||||
{['16:9', '4:3', '1:1', '3:4'].map((r) => <option key={r} value={r}>{r}</option>)}
|
||||
@ -603,7 +605,6 @@ export function RenderInspector({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRefreshRecords}
|
||||
disabled={generating}
|
||||
className="flex h-7 shrink-0 items-center rounded-[5px] bg-white/70 px-1.5 text-[#5a6062]/55 ring-1 ring-black/[0.04] transition hover:text-[#2d3335]"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
@ -688,7 +689,6 @@ export function RenderInspector({
|
||||
value={negativePrompt}
|
||||
onChange={(event) => onNegativePromptChange(event.target.value)}
|
||||
rows={2}
|
||||
disabled={generating}
|
||||
placeholder="不想出现的内容,可选"
|
||||
className="w-full resize-none rounded-[4px] bg-white/70 px-2.5 py-1.5 text-[11px] font-medium leading-[18px] text-[#5a6062] outline-none ring-1 ring-black/[0.04] transition placeholder:text-[#5a6062]/35 focus:bg-white focus:ring-[#47646b]/20 disabled:opacity-50"
|
||||
/>
|
||||
@ -702,12 +702,58 @@ export function RenderInspector({
|
||||
<span className="line-clamp-2">{message}</span>
|
||||
</div>
|
||||
) : null}
|
||||
{renderTask ? (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-[8px] px-3 py-2.5 ring-1',
|
||||
renderTask.status === 'failed'
|
||||
? 'bg-[#b75b5b]/7 ring-[#b75b5b]/12'
|
||||
: 'bg-[#47646b]/6 ring-[#47646b]/10',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 text-[#47646b]',
|
||||
renderTask.status === 'running' && 'animate-spin',
|
||||
renderTask.status === 'failed' && 'text-[#9f4f4f]',
|
||||
)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5 text-[10px] font-black text-[#2d3335]">
|
||||
<span>{getRenderTaskModeLabel(renderTask.mode)}</span>
|
||||
<span className="rounded-[4px] bg-white/60 px-1.5 py-0.5 text-[8px] text-[#5a6062]/60">
|
||||
{renderTask.imageCount} 张
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-[10px] font-medium text-[#5a6062]/70">
|
||||
{renderTask.skuName}
|
||||
</div>
|
||||
</div>
|
||||
<span className="font-mono text-[11px] font-black text-[#47646b]">
|
||||
{Math.round(renderTask.progress)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 h-1.5 overflow-hidden rounded-full bg-white/70">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-[width] duration-500 ease-out',
|
||||
renderTask.status === 'failed' ? 'bg-[#9f4f4f]' : 'bg-[#47646b]',
|
||||
)}
|
||||
style={{ width: `${renderTask.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1.5 text-[10px] font-medium text-[#5a6062]/70">
|
||||
{renderTask.stage}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOptimizePrompt}
|
||||
disabled={optimizing || generating || !prompt.trim()}
|
||||
disabled={optimizing || !prompt.trim()}
|
||||
className={cn(
|
||||
'flex h-9 shrink-0 items-center gap-1.5 rounded-[6px] px-3 text-[11px] font-bold transition ring-1',
|
||||
optimizing
|
||||
@ -730,12 +776,12 @@ export function RenderInspector({
|
||||
) : (
|
||||
<ImagePlus className="mr-1.5 h-3.5 w-3.5" />
|
||||
)}
|
||||
{generating ? '生成中...' : '生成图片'}
|
||||
{generating ? '后台生成中' : '生成图片'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="mb-2 text-[12px] font-black text-[#2d3335]">本地生图记录</div>
|
||||
<div className="mb-2 text-[12px] font-black text-[#2d3335]">当前 SKU 生图记录</div>
|
||||
{records.length === 0 ? (
|
||||
<div className="py-8 text-center text-[10px] font-medium text-[#5a6062]/35">
|
||||
暂无记录
|
||||
@ -769,7 +815,7 @@ export function RenderInspector({
|
||||
{record.status === 'SUCCEEDED' ? '已完成' : '失败'}
|
||||
</span>
|
||||
<span className="shrink-0 rounded-[4px] bg-black/[0.035] px-1.5 py-0.5 text-[8px] font-black text-[#5a6062]/55">
|
||||
{record.mode === 'white-background' ? '精准白底图' : '场景图'}
|
||||
{record.mode === 'white-background' ? '白底图' : '场景图'}
|
||||
</span>
|
||||
<span className="text-[8px] text-[#5a6062]/35">
|
||||
{formatRecordTime(record.createdAt)}
|
||||
@ -824,7 +870,7 @@ export function RenderInspector({
|
||||
className="flex h-10 items-center justify-center gap-1.5 rounded-[6px] bg-black/[0.035] text-[11px] font-black text-[#2d3335] transition hover:bg-black/[0.06] disabled:opacity-40"
|
||||
>
|
||||
<ImageIcon className="h-3.5 w-3.5" />
|
||||
精准白底图
|
||||
白底图
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -833,7 +879,7 @@ export function RenderInspector({
|
||||
className="flex h-10 items-center justify-center gap-1.5 rounded-[6px] bg-[#2d3335] text-[11px] font-black text-white transition hover:opacity-90 disabled:opacity-40"
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
{generating ? '生成中...' : '场景效果图'}
|
||||
{generating ? '后台生成中' : '场景效果图'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -55,6 +55,7 @@ export type AiRenderOutputMode = 'white-background' | 'scene'
|
||||
|
||||
export interface LocalRenderRecord {
|
||||
id: string
|
||||
skuId: string
|
||||
skuName: string
|
||||
mode: AiRenderOutputMode
|
||||
imageUrl: string
|
||||
@ -63,6 +64,18 @@ export interface LocalRenderRecord {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AiRenderTaskState {
|
||||
id: string
|
||||
skuId: string
|
||||
skuName: string
|
||||
mode: AiRenderOutputMode
|
||||
imageCount: number
|
||||
progress: number
|
||||
stage: string
|
||||
status: 'running' | 'succeeded' | 'failed'
|
||||
startedAt: string
|
||||
}
|
||||
|
||||
export interface AiRenderSkuMaterialAssignment {
|
||||
nodeName: string
|
||||
material: MaterialConfig
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user