fix image simulator
This commit is contained in:
parent
ad428e0761
commit
13d4027559
@ -45,6 +45,8 @@ interface SimulatorRun {
|
|||||||
promptContent?: string | null
|
promptContent?: string | null
|
||||||
output?: string
|
output?: string
|
||||||
error?: string
|
error?: string
|
||||||
|
outputType?: string
|
||||||
|
generatedFilePath?: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
completedAt?: string
|
completedAt?: string
|
||||||
temperature?: number
|
temperature?: number
|
||||||
@ -97,6 +99,9 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
|
|||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [models, setModels] = useState<Model[]>([])
|
const [models, setModels] = useState<Model[]>([])
|
||||||
|
const [generatedImageUrl, setGeneratedImageUrl] = useState<string | null>(null)
|
||||||
|
const [isLoadingImage, setIsLoadingImage] = useState(false)
|
||||||
|
const [imageLoadError, setImageLoadError] = useState(false)
|
||||||
const [editForm, setEditForm] = useState({
|
const [editForm, setEditForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
userInput: '',
|
userInput: '',
|
||||||
@ -118,6 +123,32 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
|
|||||||
getParams()
|
getParams()
|
||||||
}, [params])
|
}, [params])
|
||||||
|
|
||||||
|
const fetchImageUrl = useCallback(async (runId: string) => {
|
||||||
|
try {
|
||||||
|
setIsLoadingImage(true)
|
||||||
|
const response = await fetch(`/api/simulator/${runId}/image`)
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
if (data.fileUrl) {
|
||||||
|
setGeneratedImageUrl(data.fileUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching image URL:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoadingImage(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const copyToClipboard = useCallback(async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy to clipboard:', error)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const fetchRunCallback = useCallback(async () => {
|
const fetchRunCallback = useCallback(async () => {
|
||||||
if (!runId) return
|
if (!runId) return
|
||||||
try {
|
try {
|
||||||
@ -126,7 +157,14 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setRun(data)
|
setRun(data)
|
||||||
setStreamOutput(data.output || '')
|
|
||||||
|
// 如果是图片类型且有生成的文件路径,获取临时 URL
|
||||||
|
if (data.outputType === 'image' && data.generatedFilePath) {
|
||||||
|
fetchImageUrl(runId)
|
||||||
|
} else {
|
||||||
|
// 只有非图片类型才设置文本输出
|
||||||
|
setStreamOutput(data.output || '')
|
||||||
|
}
|
||||||
} else if (response.status === 404) {
|
} else if (response.status === 404) {
|
||||||
router.push('/simulator')
|
router.push('/simulator')
|
||||||
}
|
}
|
||||||
@ -135,7 +173,7 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [runId, router])
|
}, [runId, router, fetchImageUrl])
|
||||||
|
|
||||||
const fetchModels = useCallback(async () => {
|
const fetchModels = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@ -297,14 +335,6 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
// You could add a toast notification here
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error copying to clipboard:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDuplicateRun = async () => {
|
const handleDuplicateRun = async () => {
|
||||||
if (!run || !confirm(t('duplicateRunConfirm'))) {
|
if (!run || !confirm(t('duplicateRunConfirm'))) {
|
||||||
@ -847,17 +877,154 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : currentOutput ? (
|
) : (run.outputType === 'image' && generatedImageUrl) || (run.outputType !== 'image' && currentOutput) ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center space-x-2 mb-4 text-green-600 dark:text-green-400">
|
<div className="flex items-center space-x-2 mb-4 text-green-600 dark:text-green-400">
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
<span className="text-sm font-medium">{t('completed')}</span>
|
<span className="text-sm font-medium">{t('completed')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted rounded-md p-4 border min-h-32 max-h-96 overflow-y-auto">
|
|
||||||
<div className="text-sm text-foreground whitespace-pre-wrap">
|
{/* 图片输出 */}
|
||||||
{currentOutput}
|
{run.outputType === 'image' && generatedImageUrl && (
|
||||||
|
<div className="bg-gradient-to-br from-slate-50 via-background to-slate-50 dark:from-slate-900 dark:via-background dark:to-slate-900 rounded-2xl p-8 border shadow-lg">
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<div className="inline-flex items-center space-x-2 px-3 py-1.5 bg-primary/10 text-primary rounded-full text-sm font-medium mb-4">
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||||
|
<polyline points="21,15 16,10 5,21"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span>生成的图片</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className="relative group cursor-pointer">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-xl blur-xl opacity-20 group-hover:opacity-30 transition-opacity duration-300"></div>
|
||||||
|
<img
|
||||||
|
src={generatedImageUrl}
|
||||||
|
alt="Generated image"
|
||||||
|
className="relative max-w-full max-h-[600px] rounded-xl shadow-2xl border border-white/20 transition-all duration-300 group-hover:scale-[1.02] group-hover:shadow-3xl"
|
||||||
|
onError={() => {
|
||||||
|
console.error('Failed to load image')
|
||||||
|
setImageLoadError(true)
|
||||||
|
}}
|
||||||
|
onLoad={() => {
|
||||||
|
setImageLoadError(false)
|
||||||
|
}}
|
||||||
|
onClick={() => window.open(generatedImageUrl, '_blank')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={() => window.open(generatedImageUrl, '_blank')}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-primary/10 hover:bg-primary/20 text-primary rounded-lg transition-all duration-200 hover:scale-105 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
<span>查看大图</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(generatedImageUrl)
|
||||||
|
const blob = await response.blob()
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.style.display = 'none'
|
||||||
|
a.href = url
|
||||||
|
a.download = `generated-image-${run.name || 'untitled'}-${Date.now()}.webp`
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
document.body.removeChild(a)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Download failed:', error)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-green-50 hover:bg-green-100 dark:bg-green-900/30 dark:hover:bg-green-900/50 text-green-600 dark:text-green-400 rounded-lg transition-all duration-200 hover:scale-105 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
||||||
|
</svg>
|
||||||
|
<span>下载图片</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => copyToClipboard(generatedImageUrl)}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 bg-blue-50 hover:bg-blue-100 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 text-blue-600 dark:text-blue-400 rounded-lg transition-all duration-200 hover:scale-105 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
<span>复制链接</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
{/* 文本输出 - 只在非图片类型时显示 */}
|
||||||
|
{run.outputType !== 'image' && currentOutput && (
|
||||||
|
<div className="bg-muted rounded-md p-4 border min-h-32 max-h-96 overflow-y-auto">
|
||||||
|
<div className="text-sm text-foreground whitespace-pre-wrap">
|
||||||
|
{currentOutput}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 图片错误状态 */}
|
||||||
|
{run.outputType === 'image' && generatedImageUrl && imageLoadError && (
|
||||||
|
<div className="bg-gradient-to-br from-red-50 to-red-100 dark:from-red-950/50 dark:to-red-900/50 rounded-xl p-8 border border-red-200 dark:border-red-800">
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-4">
|
||||||
|
<div className="p-3 bg-red-100 dark:bg-red-900 rounded-full">
|
||||||
|
<svg className="h-8 w-8 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.19 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-medium text-red-800 dark:text-red-200">图片加载失败</p>
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400 mt-1">图片可能已过期或无法访问</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setImageLoadError(false)
|
||||||
|
if (runId) {
|
||||||
|
fetchImageUrl(runId)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-xs rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
重新加载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 图片加载中状态 */}
|
||||||
|
{run.outputType === 'image' && !generatedImageUrl && isLoadingImage && (
|
||||||
|
<div className="bg-gradient-to-br from-slate-50 via-background to-slate-50 dark:from-slate-900 dark:via-background dark:to-slate-900 rounded-2xl p-12 border shadow-lg">
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-6">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 rounded-full blur-md opacity-30 animate-pulse"></div>
|
||||||
|
<div className="relative p-4 bg-background rounded-full border shadow-sm">
|
||||||
|
<RefreshCw className="h-8 w-8 animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<p className="text-lg font-semibold text-foreground">正在获取图片</p>
|
||||||
|
<p className="text-sm text-muted-foreground">正在从云端加载您生成的图片...</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-32 h-1 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div className="h-full bg-gradient-to-r from-primary to-primary/50 rounded-full animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : run.outputType === 'image' && !generatedImageUrl && !isLoadingImage ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Eye className="h-16 w-16 text-muted-foreground mx-auto mb-4" />
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
等待图片生成完成...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
|
Loading…
Reference in New Issue
Block a user