Persist image submissions with fixed idempotency keys, leased execution, provider identity, reference snapshots and staged results. Integrate DIY, assisted assembly and non-integrated white-background sessions without holding generation in a browser request. Recover ambiguous submissions through owner-scoped reads and stop a new eight-view batch when restoring an earlier request. Add the NativeImageGenerationJob migration and a dedicated worker to both release entry points. Protect job inputs/results from storage GC. Existing production configuration and business data are not changed by this commit. Validation: 256 focused Jest tests, isolated PostgreSQL migration/lease/transaction checks (8), deployment guard tests (9), type-check, production build and desktop/ mobile browser component scenarios. Live native 2K reference-image request worked; reusing its idempotency key returned the same task. Build uses local placeholders for otherwise absent Drawing System configuration. Three unrelated material tests also fail with the original mainline test file; they remain outside this change. Architecture/visual/root-cause review: passed with warnings. Independent review completed for the integration and shared input/storage paths; worker additionally reviewed by the primary agent. Production merge/deployment requires authorization.
47 lines
3.5 KiB
TypeScript
47 lines
3.5 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { Download } from 'lucide-react'
|
||
import Button from '@/components/ui/Button'
|
||
import type { DiyAiRenderDTO } from '@/lib/diy/ai-render'
|
||
import type { DiyAiRenderController } from '../hooks/useDiyAiRender'
|
||
import { RenderRecordGalleryLightbox } from '@/app/ai-render-workbench/RenderRecordGalleryLightbox'
|
||
|
||
export default function DiyAiResults({ records, ai }: { records: DiyAiRenderDTO[]; ai: DiyAiRenderController }) {
|
||
const [large,setLarge] = useState<string|null>(null)
|
||
useEffect(() => {
|
||
if (!large) return
|
||
const close = (event:KeyboardEvent) => { if(event.key==='Escape')setLarge(null) }
|
||
window.addEventListener('keydown',close); return () => window.removeEventListener('keydown',close)
|
||
},[large])
|
||
return <div className="space-y-3" aria-label="DIY 出图结果">
|
||
{large && <RenderRecordGalleryLightbox imageUrl={large} onClose={() => setLarge(null)}/>}
|
||
{records.map(record => <section key={record.id} className="space-y-2 border-b border-black/[0.04] pb-3">
|
||
<div className="flex flex-wrap items-center justify-between gap-1 text-[10px] text-[#5a6062]/60">
|
||
<span className="font-semibold text-[#2d3335]">{record.name} · {record.mode==='scene'?'效果图':'白底图'}</span>
|
||
<span>{new Date(record.createdAt).toLocaleString('zh-CN',{timeZone:'Asia/Shanghai'})}</span>
|
||
</div>
|
||
<p className="text-[10px] text-[#5a6062]/60">{record.snapshot.items.length} 柜体 · {Object.keys(record.snapshot.doors).length} 柜门{record.schemeCode ? ' · 已保存方案' : ' · 生成时方案'}</p>
|
||
{record.status==='PENDING' && !record.errorMessage && <p role="status" className="text-[11px] text-zinc-500">正在生成…</p>}
|
||
{(record.status==='FAILED' || record.errorMessage) && <p className="text-[11px] text-red-600">{record.errorMessage || '生成失败'}</p>}
|
||
<div className="grid grid-cols-1 gap-2.5">
|
||
{record.images.map((image,index) => {
|
||
const filename = `${record.name}-${record.mode==='scene'?'效果图':'白底图'}-${index+1}.png`
|
||
return <div key={image.storageKey} className="overflow-hidden rounded-[7px] border border-black/[0.04] bg-white">
|
||
<button type="button" className="block w-full cursor-zoom-in bg-[#eef2f2]" aria-label={`查看${record.mode==='scene'?'效果图':'白底图'} ${index+1}`} onClick={() => setLarge(image.originalUrl)}>
|
||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||
<img src={image.thumbnailUrl} alt={`${record.name} ${index+1}`} loading="lazy" className="aspect-[16/10] w-full object-contain"/>
|
||
</button>
|
||
<div className="flex items-center justify-between px-2 py-1.5 text-xs text-zinc-400"><span>{image.width} × {image.height}</span>
|
||
{ai.access.canDownload && <Button size="small" variant="plain" loading={ai.downloading===image.originalUrl} disabled={Boolean(ai.downloading)}
|
||
onClick={() => void ai.download(image.originalUrl,filename)}><Download size={13}/>下载</Button>}
|
||
</div>
|
||
</div>
|
||
})}
|
||
</div>
|
||
{record.referenceImageUrl && <button type="button" className="text-[10px] text-[#5a6062]/60 hover:text-[#2d3335]" onClick={() => setLarge(record.referenceImageUrl!)}>查看生成时参考图</button>}
|
||
</section>)}
|
||
{!records.length && <p className="py-8 text-center text-[11px] text-[#5a6062]/45">还没有{ai.mode==='scene'?'效果图':'白底图'}记录</p>}
|
||
</div>
|
||
}
|