allow create new simulator with new prompt

This commit is contained in:
songtianlun 2025-08-09 11:38:51 +08:00
parent 12293a0bcb
commit c7f33047ef
6 changed files with 292 additions and 96 deletions

View File

@ -242,6 +242,7 @@ model Model {
model SimulatorRun { model SimulatorRun {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
name String @default("Simulation Run") // 运行名称
promptId String promptId String
promptVersionId String? // 可选,如果选择了特定版本 promptVersionId String? // 可选,如果选择了特定版本
modelId String modelId String

View File

@ -20,7 +20,25 @@ export async function GET(
id, id,
userId: user.id, userId: user.id,
}, },
include: { select: {
id: true,
name: true,
status: true,
userInput: true,
promptContent: true,
output: true,
error: true,
createdAt: true,
completedAt: true,
temperature: true,
maxTokens: true,
topP: true,
frequencyPenalty: true,
presencePenalty: true,
inputTokens: true,
outputTokens: true,
totalCost: true,
duration: true,
prompt: { prompt: {
select: { id: true, name: true, content: true } select: { id: true, name: true, content: true }
}, },
@ -90,7 +108,17 @@ export async function PATCH(
...(duration !== undefined && { duration }), ...(duration !== undefined && { duration }),
...(status === "completed" && { completedAt: new Date() }), ...(status === "completed" && { completedAt: new Date() }),
}, },
include: { select: {
id: true,
name: true,
status: true,
output: true,
error: true,
inputTokens: true,
outputTokens: true,
totalCost: true,
duration: true,
completedAt: true,
prompt: { prompt: {
select: { id: true, name: true } select: { id: true, name: true }
}, },

View File

@ -26,7 +26,19 @@ export async function GET(request: NextRequest) {
const [runs, total] = await Promise.all([ const [runs, total] = await Promise.all([
prisma.simulatorRun.findMany({ prisma.simulatorRun.findMany({
where, where,
include: { select: {
id: true,
name: true,
status: true,
userInput: true,
output: true,
error: true,
createdAt: true,
completedAt: true,
inputTokens: true,
outputTokens: true,
totalCost: true,
duration: true,
prompt: { prompt: {
select: { id: true, name: true } select: { id: true, name: true }
}, },
@ -70,6 +82,7 @@ export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const { const {
name,
promptId, promptId,
promptVersionId, promptVersionId,
modelId, modelId,
@ -80,8 +93,27 @@ export async function POST(request: NextRequest) {
topP, topP,
frequencyPenalty, frequencyPenalty,
presencePenalty, presencePenalty,
// 用于创建新prompt的字段
createNewPrompt,
newPromptName,
newPromptContent,
} = body; } = body;
let finalPromptId = promptId;
// 如果是创建新prompt模式
if (createNewPrompt && newPromptContent) {
// 创建新的prompt
const newPrompt = await prisma.prompt.create({
data: {
userId: user.id,
name: newPromptName || name || "New Prompt",
content: newPromptContent,
visibility: "private",
},
});
finalPromptId = newPrompt.id;
} else if (promptId) {
// 验证用户是否拥有该prompt // 验证用户是否拥有该prompt
const prompt = await prisma.prompt.findFirst({ const prompt = await prisma.prompt.findFirst({
where: { where: {
@ -93,6 +125,9 @@ export async function POST(request: NextRequest) {
if (!prompt) { if (!prompt) {
return NextResponse.json({ error: "Prompt not found" }, { status: 404 }); return NextResponse.json({ error: "Prompt not found" }, { status: 404 });
} }
} else {
return NextResponse.json({ error: "Either promptId or newPromptContent is required" }, { status: 400 });
}
// 验证模型是否可用 // 验证模型是否可用
const model = await prisma.model.findUnique({ const model = await prisma.model.findUnique({
@ -108,7 +143,8 @@ export async function POST(request: NextRequest) {
const run = await prisma.simulatorRun.create({ const run = await prisma.simulatorRun.create({
data: { data: {
userId: user.id, userId: user.id,
promptId, name: name || "Simulation Run",
promptId: finalPromptId,
promptVersionId, promptVersionId,
modelId, modelId,
userInput, userInput,

View File

@ -33,6 +33,7 @@ import { getPromptContent } from '@/lib/simulator-utils'
interface SimulatorRun { interface SimulatorRun {
id: string id: string
name: string
status: string status: string
userInput: string userInput: string
promptContent?: string | null promptContent?: string | null
@ -252,7 +253,7 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-foreground mb-2"> <h1 className="text-3xl font-bold text-foreground mb-2">
{run.prompt.name} {run.name}
</h1> </h1>
<div className="flex items-center space-x-4 text-sm text-muted-foreground"> <div className="flex items-center space-x-4 text-sm text-muted-foreground">
<Badge className={`border ${getStatusColor(run.status)}`}> <Badge className={`border ${getStatusColor(run.status)}`}>

View File

@ -65,6 +65,10 @@ export default function NewSimulatorRunPage() {
const [editablePromptContent, setEditablePromptContent] = useState('') const [editablePromptContent, setEditablePromptContent] = useState('')
const [isEditingPrompt, setIsEditingPrompt] = useState(false) const [isEditingPrompt, setIsEditingPrompt] = useState(false)
// 新增状态
const [simulatorName, setSimulatorName] = useState('')
const [promptInputMode, setPromptInputMode] = useState<'select' | 'create'>('select') // 选择模式:选择现有提示词 或 创建新提示词
// Advanced settings // Advanced settings
const [temperature, setTemperature] = useState('0.7') const [temperature, setTemperature] = useState('0.7')
const [maxTokens, setMaxTokens] = useState('') const [maxTokens, setMaxTokens] = useState('')
@ -128,6 +132,11 @@ export default function NewSimulatorRunPage() {
} else if (prompt) { } else if (prompt) {
setEditablePromptContent(prompt.content) setEditablePromptContent(prompt.content)
} }
// 自动填充名称(如果当前名称为空)
if (!simulatorName && prompt) {
setSimulatorName(`${prompt.name} 模拟运行`)
}
} }
const handleVersionChange = (versionId: string) => { const handleVersionChange = (versionId: string) => {
@ -186,29 +195,68 @@ export default function NewSimulatorRunPage() {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!selectedPromptId || !selectedModelId || !userInput.trim()) {
// 验证必填字段
if (!selectedModelId || !userInput.trim() || !simulatorName.trim()) {
return
}
// 在创建模式下,需要有提示词内容
if (promptInputMode === 'create' && !editablePromptContent.trim()) {
return
}
// 在选择模式下,需要选择提示词
if (promptInputMode === 'select' && !selectedPromptId) {
return return
} }
setIsCreating(true) setIsCreating(true)
try { try {
const response = await fetch('/api/simulator', { const requestBody: {
method: 'POST', name: string;
headers: { modelId: string;
'Content-Type': 'application/json', userInput: string;
}, temperature: number;
body: JSON.stringify({ maxTokens?: number;
promptId: selectedPromptId, topP: number;
promptVersionId: selectedVersionId || undefined, frequencyPenalty: number;
presencePenalty: number;
createNewPrompt?: boolean;
newPromptName?: string;
newPromptContent?: string;
promptId?: string;
promptVersionId?: string;
promptContent?: string;
} = {
name: simulatorName,
modelId: selectedModelId, modelId: selectedModelId,
userInput, userInput,
promptContent: getCustomPromptContent(),
temperature: parseFloat(temperature), temperature: parseFloat(temperature),
maxTokens: maxTokens ? parseInt(maxTokens) : undefined, maxTokens: maxTokens ? parseInt(maxTokens) : undefined,
topP: parseFloat(topP), topP: parseFloat(topP),
frequencyPenalty: parseFloat(frequencyPenalty), frequencyPenalty: parseFloat(frequencyPenalty),
presencePenalty: parseFloat(presencePenalty), presencePenalty: parseFloat(presencePenalty),
}), }
if (promptInputMode === 'create') {
// 创建新提示词模式
requestBody.createNewPrompt = true
requestBody.newPromptName = simulatorName.replace(' 模拟运行', '')
requestBody.newPromptContent = editablePromptContent
} else {
// 选择现有提示词模式
requestBody.promptId = selectedPromptId
requestBody.promptVersionId = selectedVersionId || undefined
requestBody.promptContent = getCustomPromptContent()
}
const response = await fetch('/api/simulator', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
}) })
if (response.ok) { if (response.ok) {
@ -267,21 +315,61 @@ export default function NewSimulatorRunPage() {
</div> </div>
) : ( ) : (
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{/* Select Prompt */} {/* Prompt Configuration */}
<Card className="p-6"> <Card className="p-6">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div> <div>
<h2 className="text-xl font-semibold mb-2 flex items-center"> <h2 className="text-xl font-semibold mb-2 flex items-center">
<Zap className="h-5 w-5 mr-2" /> <Zap className="h-5 w-5 mr-2" />
{t('selectPrompt')}
</h2> </h2>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Choose the prompt you want to test with the AI model
</p> </p>
</div> </div>
</div> </div>
{/* 模式切换 */}
<div className="mb-6">
<div className="flex space-x-1 p-1 bg-muted rounded-lg">
<button
type="button"
className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
promptInputMode === 'select'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
onClick={() => {
setPromptInputMode('select')
setEditablePromptContent('')
}}
>
</button>
<button
type="button"
className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
promptInputMode === 'create'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
onClick={() => {
setPromptInputMode('create')
setSelectedPromptId('')
setSelectedVersionId('')
if (!simulatorName) {
setSimulatorName('新模拟运行')
}
}}
>
</button>
</div>
</div>
<div className="space-y-4"> <div className="space-y-4">
{promptInputMode === 'select' && (
<>
<div> <div>
<Label htmlFor="prompt" className="text-sm font-medium">{t('prompt')}</Label> <Label htmlFor="prompt" className="text-sm font-medium">{t('prompt')}</Label>
<select <select
@ -320,11 +408,42 @@ export default function NewSimulatorRunPage() {
</select> </select>
</div> </div>
)} )}
</>
)}
{editablePromptContent && ( {/* 名称输入框 - 在提示词选择下方 */}
<div>
<Label htmlFor="simulatorName" className="text-sm font-medium"></Label>
<Input
id="simulatorName"
type="text"
value={simulatorName}
onChange={(e) => setSimulatorName(e.target.value)}
placeholder="输入模拟运行的名称"
className="mt-1"
required
/>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
{promptInputMode === 'create' && (
<div>
<p className="text-xs text-muted-foreground mb-2">
💡
</p>
</div>
)}
{/* 提示词内容显示/编辑 */}
{(editablePromptContent || promptInputMode === 'create') && (
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Label className="text-sm font-medium">{t('promptContent')}</Label> <Label className="text-sm font-medium">
{promptInputMode === 'create' ? '提示词内容' : t('promptContent')}
</Label>
{promptInputMode === 'select' && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{!isEditingPrompt && ( {!isEditingPrompt && (
<Button <Button
@ -358,13 +477,16 @@ export default function NewSimulatorRunPage() {
</> </>
)} )}
</div> </div>
)}
</div> </div>
{isEditingPrompt ? (
{(isEditingPrompt || promptInputMode === 'create') ? (
<Textarea <Textarea
value={editablePromptContent} value={editablePromptContent}
onChange={(e) => setEditablePromptContent(e.target.value)} onChange={(e) => setEditablePromptContent(e.target.value)}
className="min-h-32 font-mono text-sm" className="min-h-32 font-mono text-sm"
placeholder={t('promptContentPlaceholder')} placeholder={promptInputMode === 'create' ? "请输入提示词内容..." : t('promptContentPlaceholder')}
required={promptInputMode === 'create'}
/> />
) : ( ) : (
<div className="mt-1 p-4 bg-muted rounded-md border max-h-48 overflow-y-auto"> <div className="mt-1 p-4 bg-muted rounded-md border max-h-48 overflow-y-auto">
@ -573,7 +695,14 @@ export default function NewSimulatorRunPage() {
</Link> </Link>
<Button <Button
type="submit" type="submit"
disabled={isCreating || !selectedPromptId || !selectedModelId || !userInput.trim()} disabled={
isCreating ||
!selectedModelId ||
!userInput.trim() ||
!simulatorName.trim() ||
(promptInputMode === 'select' && !selectedPromptId) ||
(promptInputMode === 'create' && !editablePromptContent.trim())
}
> >
{isCreating ? ( {isCreating ? (
<> <>

View File

@ -29,6 +29,7 @@ import { useLocale } from 'next-intl'
interface SimulatorRun { interface SimulatorRun {
id: string id: string
name: string
status: string status: string
userInput: string userInput: string
output?: string output?: string
@ -321,7 +322,7 @@ export default function SimulatorPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center space-x-3 mb-3"> <div className="flex items-center space-x-3 mb-3">
<h3 className="font-semibold text-foreground">{run.prompt.name}</h3> <h3 className="font-semibold text-foreground">{run.name}</h3>
<Badge className={`border ${getStatusColor(run.status)}`}> <Badge className={`border ${getStatusColor(run.status)}`}>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{getStatusIcon(run.status)} {getStatusIcon(run.status)}