add simulator duplicate

This commit is contained in:
songtianlun 2025-08-09 11:51:52 +08:00
parent a63bfd6783
commit ff0c2017af
5 changed files with 183 additions and 9 deletions

View File

@ -386,6 +386,10 @@
"creating": "Creating...",
"runSimulation": "Run Simulation",
"createRun": "Create Run",
"duplicateRun": "Duplicate Run",
"duplicateRunConfirm": "Are you sure you want to create a copy of this run?",
"duplicateRunSuccess": "Duplicate created successfully",
"copyOf": " Copy",
"execute": "Execute",
"executing": "Executing...",
"generating": "Generating...",

View File

@ -386,6 +386,10 @@
"creating": "创建中...",
"runSimulation": "运行模拟",
"createRun": "创建运行",
"duplicateRun": "创建副本",
"duplicateRunConfirm": "确定要创建此运行的副本吗?",
"duplicateRunSuccess": "副本创建成功",
"copyOf": "的副本",
"execute": "执行",
"executing": "执行中...",
"generating": "生成中...",

View File

@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from "next/server";
import { createServerSupabaseClient } from "@/lib/supabase-server";
import { prisma } from "@/lib/prisma";
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const supabase = await createServerSupabaseClient();
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 获取原始运行记录
const originalRun = await prisma.simulatorRun.findFirst({
where: {
id,
userId: user.id,
},
include: {
prompt: true,
promptVersion: true,
model: true,
}
});
if (!originalRun) {
return NextResponse.json({ error: "Run not found" }, { status: 404 });
}
// 创建副本名称
const copyName = `${originalRun.name}${originalRun.name.includes('Copy') || originalRun.name.includes('副本') ? '' : ' Copy'}`;
// 创建新的运行记录(副本)
const duplicateRun = await prisma.simulatorRun.create({
data: {
userId: user.id,
name: copyName,
promptId: originalRun.promptId,
promptVersionId: originalRun.promptVersionId,
modelId: originalRun.modelId,
userInput: originalRun.userInput,
promptContent: originalRun.promptContent,
temperature: originalRun.temperature,
maxTokens: originalRun.maxTokens,
topP: originalRun.topP,
frequencyPenalty: originalRun.frequencyPenalty,
presencePenalty: originalRun.presencePenalty,
status: "pending", // 副本状态为pending可以重新运行
// 不复制输出相关字段output, error, inputTokens, outputTokens, totalCost, duration, completedAt
},
select: {
id: true,
name: true,
status: true,
userInput: true,
createdAt: true,
prompt: {
select: { id: true, name: true }
},
model: {
select: { id: true, name: true, provider: true }
}
}
});
return NextResponse.json(duplicateRun, { status: 201 });
} catch (error) {
console.error("Error duplicating simulator run:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@ -76,6 +76,7 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
const [isExecuting, setIsExecuting] = useState(false)
const [streamOutput, setStreamOutput] = useState('')
const [runId, setRunId] = useState<string | null>(null)
const [isDuplicating, setIsDuplicating] = useState(false)
const outputRef = useRef<HTMLDivElement>(null)
useEffect(() => {
@ -186,6 +187,31 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
}
}
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 getStatusIcon = (status: string) => {
switch (status) {
case 'pending':
@ -272,21 +298,41 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
</div>
</div>
{run.status === 'pending' && (
<Button onClick={executeRun} disabled={isExecuting}>
{isExecuting ? (
<div className="flex items-center space-x-2">
<Button
onClick={handleDuplicateRun}
disabled={isDuplicating}
variant="outline"
>
{isDuplicating ? (
<>
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
{t('executing')}
<LoadingSpinner size="sm" />
<span className="ml-2">{t('creating')}</span>
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
{t('execute')}
<Copy className="h-4 w-4 mr-2" />
{t('duplicateRun')}
</>
)}
</Button>
)}
{run.status === 'pending' && (
<Button onClick={executeRun} disabled={isExecuting}>
{isExecuting ? (
<>
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
{t('executing')}
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
{t('execute')}
</>
)}
</Button>
)}
</div>
</div>
</div>

View File

@ -20,7 +20,8 @@ import {
Eye,
RefreshCw,
Zap,
DollarSign
DollarSign,
Copy
} from 'lucide-react'
import Link from 'next/link'
import { formatDistanceToNow } from 'date-fns'
@ -72,6 +73,7 @@ export default function SimulatorPage() {
})
const [isLoading, setIsLoading] = useState(true)
const [statusFilter, setStatusFilter] = useState<string>('')
const [duplicatingRunId, setDuplicatingRunId] = useState<string | null>(null)
const fetchRuns = useCallback(async () => {
try {
@ -101,6 +103,32 @@ export default function SimulatorPage() {
}
}, [user, authLoading, pagination.page, statusFilter, fetchRuns])
const handleDuplicateRun = async (runId: string) => {
if (!confirm(t('duplicateRunConfirm'))) {
return
}
setDuplicatingRunId(runId)
try {
const response = await fetch(`/api/simulator/${runId}/duplicate`, {
method: 'POST',
})
if (response.ok) {
// 刷新列表
await fetchRuns()
// 可以添加一个成功提示
console.log(t('duplicateRunSuccess'))
} else {
console.error('Error duplicating run:', await response.text())
}
} catch (error) {
console.error('Error duplicating run:', error)
} finally {
setDuplicatingRunId(null)
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case 'pending':
@ -391,6 +419,19 @@ export default function SimulatorPage() {
</div>
<div className="flex items-center space-x-2 ml-6">
<Button
size="sm"
variant="outline"
onClick={() => handleDuplicateRun(run.id)}
disabled={duplicatingRunId === run.id}
>
{duplicatingRunId === run.id ? (
<LoadingSpinner size="sm" />
) : (
<Copy className="h-4 w-4 mr-1" />
)}
{t('duplicateRun')}
</Button>
<Link href={`/simulator/${run.id}`}>
<Button size="sm" variant="outline">
<Eye className="h-4 w-4 mr-1" />