'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { useTranslations } from 'next-intl' import { useBetterAuth } from '@/hooks/useBetterAuth' import { useRouter } from 'next/navigation' import { Header } from '@/components/layout/Header' import { Footer } from '@/components/layout/Footer' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' import { LoadingSpinner } from '@/components/ui/loading-spinner' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' import { ArrowLeft, Play, Copy, RefreshCw, Clock, CheckCircle2, XCircle, Zap, Eye, FileText, Settings, BarChart3, DollarSign, Edit, Save, X, Share2, Globe } from 'lucide-react' import Link from 'next/link' import { formatDistanceToNow } from 'date-fns' import { zhCN, enUS } from 'date-fns/locale' import { useLocale } from 'next-intl' import { getPromptContent } from '@/lib/simulator-utils' import { DebugPanel } from '@/components/simulator/DebugPanel' import { Base64Image } from '@/components/ui/Base64Image' interface SimulatorRun { id: string name: string status: string userInput: string promptContent?: string | null output?: string error?: string outputType?: string permissions: string visibility?: string | null createdAt: string completedAt?: string temperature?: number maxTokens?: number topP?: number frequencyPenalty?: number presencePenalty?: number inputTokens?: number outputTokens?: number totalCost?: number duration?: number debugRequest?: Record | null debugResponse?: Record | null generatedFilePath?: string | null prompt: { id: string name: string content: string } model: { id: string name: string provider: string modelId: string outputType?: string description?: string maxTokens?: number } } interface Model { id: string modelId: string name: string provider: string outputType?: string description?: string maxTokens?: number inputCostPer1k?: number outputCostPer1k?: number } export default function SimulatorRunPage({ params }: { params: Promise<{ id: string }> }) { const { user, loading: authLoading } = useBetterAuth() const router = useRouter() const t = useTranslations('simulator') const locale = useLocale() const [run, setRun] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isExecuting, setIsExecuting] = useState(false) const [streamOutput, setStreamOutput] = useState('') const [runId, setRunId] = useState(null) const [isDuplicating, setIsDuplicating] = useState(false) const [isEditing, setIsEditing] = useState(false) const [isSaving, setIsSaving] = useState(false) const [models, setModels] = useState([]) const [generatedImageUrl, setGeneratedImageUrl] = useState(null) const [isLoadingImage, setIsLoadingImage] = useState(false) const [imageLoadError, setImageLoadError] = useState(false) const [isTogglingShare, setIsTogglingShare] = useState(false) const [editForm, setEditForm] = useState({ name: '', userInput: '', promptContent: '', modelId: '', temperature: 0.7, maxTokens: undefined as number | undefined, topP: 1, frequencyPenalty: 0, presencePenalty: 0, }) const outputRef = useRef(null) useEffect(() => { const getParams = async () => { const resolvedParams = await params setRunId(resolvedParams.id) } getParams() }, [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 () => { if (!runId) return try { setIsLoading(true) const response = await fetch(`/api/simulator/${runId}`) if (response.ok) { const data = await response.json() setRun(data) // 如果是图片类型,优先使用 generatedFilePath 获取临时URL if (data.model?.outputType === 'image') { if (data.generatedFilePath) { // 有文件路径,获取临时URL fetchImageUrl(runId) } else if (data.output) { // 从输出中提取图像URL(兼容旧数据) const urlMatch = data.output.match(/https?:\/\/[^\s<>"']*\.(?:png|jpg|jpeg|gif|webp|bmp|svg)(?:\?[^\s<>"']*)?/i) if (urlMatch) { setGeneratedImageUrl(urlMatch[0]) } } setStreamOutput(data.output || '') } else { // 只有非图片类型才设置文本输出 setStreamOutput(data.output || '') } } else if (response.status === 404) { router.push('/simulator') } } catch (error) { console.error('Error fetching simulator run:', error) } finally { setIsLoading(false) } }, [runId, router]) const fetchModels = useCallback(async () => { try { const response = await fetch('/api/simulator/models') if (response.ok) { const data = await response.json() setModels(data.models || []) } } catch (error) { console.error('Error fetching models:', error) } }, []) const handleStartEdit = () => { if (!run || run.status !== 'pending') { return } const promptContent = getPromptContent(run) setEditForm({ name: run.name, userInput: run.userInput, promptContent, modelId: run.model.id, temperature: run.temperature || 0.7, maxTokens: run.maxTokens, topP: run.topP || 1, frequencyPenalty: run.frequencyPenalty || 0, presencePenalty: run.presencePenalty || 0, }) setIsEditing(true) // 获取模型列表 fetchModels() } const handleCancelEdit = () => { setIsEditing(false) setEditForm({ name: '', userInput: '', promptContent: '', modelId: '', temperature: 0.7, maxTokens: undefined, topP: 1, frequencyPenalty: 0, presencePenalty: 0, }) } const handleSaveEdit = async () => { if (!run) return setIsSaving(true) try { const response = await fetch(`/api/simulator/${run.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: editForm.name, userInput: editForm.userInput, promptContent: editForm.promptContent, modelId: editForm.modelId, temperature: editForm.temperature, maxTokens: editForm.maxTokens, topP: editForm.topP, frequencyPenalty: editForm.frequencyPenalty, presencePenalty: editForm.presencePenalty, }), }) if (response.ok) { const updatedRun = await response.json() setRun(updatedRun) setIsEditing(false) console.log(t('runUpdated')) } else { console.error('Error updating run:', await response.text()) } } catch (error) { console.error('Error updating run:', error) } finally { setIsSaving(false) } } useEffect(() => { if (!authLoading && user && runId) { fetchRunCallback() } }, [user, authLoading, runId, fetchRunCallback]) const executeRun = async () => { if (!run || run.status !== 'pending' || !runId) return setIsExecuting(true) setStreamOutput('') try { const response = await fetch(`/api/simulator/${runId}/execute`, { method: 'POST', }) if (!response.ok) { throw new Error('Failed to start execution') } const reader = response.body?.getReader() if (!reader) { throw new Error('No response body') } let buffer = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += new TextDecoder().decode(value) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6) if (data === '[DONE]') { // Execution completed, refresh data await fetchRunCallback() return } try { const parsed = JSON.parse(data) if (parsed.choices?.[0]?.delta?.content) { const content = parsed.choices[0].delta.content setStreamOutput(prev => prev + content) // 对于图像生成模型,尝试从内容中提取图像URL if (run?.model?.outputType === 'image') { // 匹配各种图像URL格式 const urlMatch = content.match(/https?:\/\/[^\s<>"']*\.(?:png|jpg|jpeg|gif|webp|bmp|svg)(?:\?[^\s<>"']*)?/i) if (urlMatch) { setGeneratedImageUrl(urlMatch[0]) } } // Auto scroll to bottom if (outputRef.current) { outputRef.current.scrollTop = outputRef.current.scrollHeight } } } catch { // Ignore parsing errors } } } } } catch (error) { console.error('Error executing run:', error) // Refresh data to get error status await fetchRunCallback() } finally { setIsExecuting(false) } } const handleDuplicateRun = async () => { if (!run || !confirm(t('duplicateRunConfirm'))) { return } setIsDuplicating(true) try { const response = await fetch(`/api/simulator/${run.id}/duplicate`, { method: 'POST', }) if (response.ok) { const newRun = await response.json() // 跳转到新创建的运行页面 router.push(`/simulator/${newRun.id}`) } else { console.error('Error duplicating run:', await response.text()) } } catch (error) { console.error('Error duplicating run:', error) } finally { setIsDuplicating(false) } } const handleToggleShare = async () => { if (!run) return setIsTogglingShare(true) try { const newPermissions = run.permissions === 'private' ? 'public' : 'private' const response = await fetch(`/api/simulator/${run.id}/share`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ permissions: newPermissions, }), }) if (response.ok) { const updatedRun = await response.json() setRun(updatedRun) } else { console.error('Error toggling share status:', await response.text()) } } catch (error) { console.error('Error toggling share status:', error) } finally { setIsTogglingShare(false) } } const getStatusIcon = (status: string) => { switch (status) { case 'pending': return case 'running': return case 'completed': return case 'failed': return default: return } } const getStatusColor = (status: string) => { switch (status) { case 'pending': return 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800' case 'running': return 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800' case 'completed': return 'bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800' case 'failed': return 'bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800' default: return 'bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-900/20 dark:text-gray-400 dark:border-gray-800' } } if (authLoading || isLoading) { return (

Loading run details...

) } if (!user || !run) { return null } const promptContent = getPromptContent(run) // 对于图片类型,不显示 output 作为文本内容 const currentOutput = (isExecuting && run.status === 'pending') ? streamOutput : (run?.model?.outputType === 'image' ? '' : run.output) // 获取图片数据(使用临时URL或提取的URL) const imageData = generatedImageUrl // 调试:在开发环境中显示图片数据信息 if (process.env.NODE_ENV === 'development' && run?.model?.outputType === 'image') { console.log('Image data info:', { hasGeneratedFilePath: !!run?.generatedFilePath, generatedFilePath: run?.generatedFilePath, hasGeneratedImageUrl: !!generatedImageUrl, finalImageData: imageData?.substring(0, 100) + '...' }) } return (
{/* Header */}
{isEditing ? (
setEditForm({...editForm, name: e.target.value})} className="text-2xl font-bold border-2 border-primary" placeholder={t('runName')} />
) : (

{run.name}

)}
{getStatusIcon(run.status)} {t(`status.${run.status}`)}
{run.model.provider} {run.model.name} {formatDistanceToNow(new Date(run.createdAt), { addSuffix: true, locale: locale === 'zh' ? zhCN : enUS })}
{/* 共享按钮 - 只有已完成的 run 才能共享 */} {run.status === 'completed' && ( )} {/* 编辑按钮 - 只有pending状态才显示 */} {run.status === 'pending' && !isEditing && ( )} {/* 编辑模式下的保存和取消按钮 */} {isEditing && ( <> )} {run.status === 'pending' && !isEditing && ( )}
{/* Left Side - Configuration */}
{/* Prompt Content */}

{t('promptContent')}

{!isEditing && ( )}
{isEditing ? (