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,18 +93,40 @@ export async function POST(request: NextRequest) {
topP, topP,
frequencyPenalty, frequencyPenalty,
presencePenalty, presencePenalty,
// 用于创建新prompt的字段
createNewPrompt,
newPromptName,
newPromptContent,
} = body; } = body;
// 验证用户是否拥有该prompt let finalPromptId = promptId;
const prompt = await prisma.prompt.findFirst({
where: {
id: promptId,
userId: user.id,
},
});
if (!prompt) { // 如果是创建新prompt模式
return NextResponse.json({ error: "Prompt not found" }, { status: 404 }); 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
const prompt = await prisma.prompt.findFirst({
where: {
id: promptId,
userId: user.id,
},
});
if (!prompt) {
return NextResponse.json({ error: "Prompt not found" }, { status: 404 });
}
} else {
return NextResponse.json({ error: "Either promptId or newPromptContent is required" }, { status: 400 });
} }
// 验证模型是否可用 // 验证模型是否可用
@ -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 requestBody: {
name: string;
modelId: string;
userInput: string;
temperature: number;
maxTokens?: number;
topP: number;
frequencyPenalty: number;
presencePenalty: number;
createNewPrompt?: boolean;
newPromptName?: string;
newPromptContent?: string;
promptId?: string;
promptVersionId?: string;
promptContent?: string;
} = {
name: simulatorName,
modelId: selectedModelId,
userInput,
temperature: parseFloat(temperature),
maxTokens: maxTokens ? parseInt(maxTokens) : undefined,
topP: parseFloat(topP),
frequencyPenalty: parseFloat(frequencyPenalty),
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', { const response = await fetch('/api/simulator', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify(requestBody),
promptId: selectedPromptId,
promptVersionId: selectedVersionId || undefined,
modelId: selectedModelId,
userInput,
promptContent: getCustomPromptContent(),
temperature: parseFloat(temperature),
maxTokens: maxTokens ? parseInt(maxTokens) : undefined,
topP: parseFloat(topP),
frequencyPenalty: parseFloat(frequencyPenalty),
presencePenalty: parseFloat(presencePenalty),
}),
}) })
if (response.ok) { if (response.ok) {
@ -267,104 +315,178 @@ 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="space-y-4"> {/* 模式切换 */}
<div> <div className="mb-6">
<Label htmlFor="prompt" className="text-sm font-medium">{t('prompt')}</Label> <div className="flex space-x-1 p-1 bg-muted rounded-lg">
<select <button
id="prompt" type="button"
value={selectedPromptId} className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
onChange={(e) => handlePromptChange(e.target.value)} promptInputMode === 'select'
className="w-full mt-1 bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring" ? 'bg-background text-foreground shadow-sm'
required : 'text-muted-foreground hover:text-foreground'
}`}
onClick={() => {
setPromptInputMode('select')
setEditablePromptContent('')
}}
> >
<option value="">{t('selectPromptPlaceholder')}</option>
{prompts.map(prompt => ( </button>
<option key={prompt.id} value={prompt.id}> <button
{prompt.name} type="button"
</option> className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
))} promptInputMode === 'create'
</select> ? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
onClick={() => {
setPromptInputMode('create')
setSelectedPromptId('')
setSelectedVersionId('')
if (!simulatorName) {
setSimulatorName('新模拟运行')
}
}}
>
</button>
</div> </div>
</div>
{selectedPrompt && selectedPrompt.versions.length > 0 && (
<div> <div className="space-y-4">
<Label htmlFor="version" className="text-sm font-medium">{t('version')}</Label> {promptInputMode === 'select' && (
<select <>
id="version" <div>
value={selectedVersionId} <Label htmlFor="prompt" className="text-sm font-medium">{t('prompt')}</Label>
onChange={(e) => handleVersionChange(e.target.value)} <select
className="w-full mt-1 bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring" id="prompt"
> value={selectedPromptId}
<option value="">{t('useLatestVersion')}</option> onChange={(e) => handlePromptChange(e.target.value)}
{selectedPrompt.versions className="w-full mt-1 bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring"
.sort((a, b) => b.version - a.version) required
.map(version => ( >
<option key={version.id} value={version.id}> <option value="">{t('selectPromptPlaceholder')}</option>
v{version.version} {prompts.map(prompt => (
<option key={prompt.id} value={prompt.id}>
{prompt.name}
</option> </option>
))} ))}
</select> </select>
</div>
{selectedPrompt && selectedPrompt.versions.length > 0 && (
<div>
<Label htmlFor="version" className="text-sm font-medium">{t('version')}</Label>
<select
id="version"
value={selectedVersionId}
onChange={(e) => handleVersionChange(e.target.value)}
className="w-full mt-1 bg-background border border-input rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-ring"
>
<option value="">{t('useLatestVersion')}</option>
{selectedPrompt.versions
.sort((a, b) => b.version - a.version)
.map(version => (
<option key={version.id} value={version.id}>
v{version.version}
</option>
))}
</select>
</div>
)}
</>
)}
{/* 名称输入框 - 在提示词选择下方 */}
<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> </div>
)} )}
{editablePromptContent && ( {/* 提示词内容显示/编辑 */}
{(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">
<div className="flex items-center space-x-2"> {promptInputMode === 'create' ? '提示词内容' : t('promptContent')}
{!isEditingPrompt && ( </Label>
<Button {promptInputMode === 'select' && (
type="button" <div className="flex items-center space-x-2">
variant="outline" {!isEditingPrompt && (
size="sm"
onClick={handleEditPrompt}
>
<Edit className="h-3 w-3 mr-1" />
{t('edit')}
</Button>
)}
{isEditingPrompt && (
<>
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleCancelPromptEdit} onClick={handleEditPrompt}
> >
{t('cancel')} <Edit className="h-3 w-3 mr-1" />
{t('edit')}
</Button> </Button>
<Button )}
type="button" {isEditingPrompt && (
variant="default" <>
size="sm" <Button
onClick={handleSavePromptEdit} type="button"
> variant="outline"
{t('save')} size="sm"
</Button> onClick={handleCancelPromptEdit}
</> >
)} {t('cancel')}
</div> </Button>
<Button
type="button"
variant="default"
size="sm"
onClick={handleSavePromptEdit}
>
{t('save')}
</Button>
</>
)}
</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)}