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 {
id String @id @default(cuid())
userId String
name String @default("Simulation Run") // 运行名称
promptId String
promptVersionId String? // 可选,如果选择了特定版本
modelId String

View File

@ -20,7 +20,25 @@ export async function GET(
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: {
select: { id: true, name: true, content: true }
},
@ -90,7 +108,17 @@ export async function PATCH(
...(duration !== undefined && { duration }),
...(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: {
select: { id: true, name: true }
},

View File

@ -26,7 +26,19 @@ export async function GET(request: NextRequest) {
const [runs, total] = await Promise.all([
prisma.simulatorRun.findMany({
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: {
select: { id: true, name: true }
},
@ -70,6 +82,7 @@ export async function POST(request: NextRequest) {
const body = await request.json();
const {
name,
promptId,
promptVersionId,
modelId,
@ -80,18 +93,40 @@ export async function POST(request: NextRequest) {
topP,
frequencyPenalty,
presencePenalty,
// 用于创建新prompt的字段
createNewPrompt,
newPromptName,
newPromptContent,
} = body;
// 验证用户是否拥有该prompt
const prompt = await prisma.prompt.findFirst({
where: {
id: promptId,
userId: user.id,
},
});
let finalPromptId = promptId;
if (!prompt) {
return NextResponse.json({ error: "Prompt not found" }, { status: 404 });
// 如果是创建新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
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({
data: {
userId: user.id,
promptId,
name: name || "Simulation Run",
promptId: finalPromptId,
promptVersionId,
modelId,
userInput,

View File

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

View File

@ -65,6 +65,10 @@ export default function NewSimulatorRunPage() {
const [editablePromptContent, setEditablePromptContent] = useState('')
const [isEditingPrompt, setIsEditingPrompt] = useState(false)
// 新增状态
const [simulatorName, setSimulatorName] = useState('')
const [promptInputMode, setPromptInputMode] = useState<'select' | 'create'>('select') // 选择模式:选择现有提示词 或 创建新提示词
// Advanced settings
const [temperature, setTemperature] = useState('0.7')
const [maxTokens, setMaxTokens] = useState('')
@ -128,6 +132,11 @@ export default function NewSimulatorRunPage() {
} else if (prompt) {
setEditablePromptContent(prompt.content)
}
// 自动填充名称(如果当前名称为空)
if (!simulatorName && prompt) {
setSimulatorName(`${prompt.name} 模拟运行`)
}
}
const handleVersionChange = (versionId: string) => {
@ -186,29 +195,68 @@ export default function NewSimulatorRunPage() {
const handleSubmit = async (e: React.FormEvent) => {
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
}
setIsCreating(true)
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', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
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),
}),
body: JSON.stringify(requestBody),
})
if (response.ok) {
@ -267,104 +315,178 @@ export default function NewSimulatorRunPage() {
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Select Prompt */}
{/* Prompt Configuration */}
<Card className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-semibold mb-2 flex items-center">
<Zap className="h-5 w-5 mr-2" />
{t('selectPrompt')}
</h2>
<p className="text-muted-foreground">
Choose the prompt you want to test with the AI model
</p>
</div>
</div>
<div className="space-y-4">
<div>
<Label htmlFor="prompt" className="text-sm font-medium">{t('prompt')}</Label>
<select
id="prompt"
value={selectedPromptId}
onChange={(e) => handlePromptChange(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"
required
{/* 模式切换 */}
<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('')
}}
>
<option value="">{t('selectPromptPlaceholder')}</option>
{prompts.map(prompt => (
<option key={prompt.id} value={prompt.id}>
{prompt.name}
</option>
))}
</select>
</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>
{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}
</div>
<div className="space-y-4">
{promptInputMode === 'select' && (
<>
<div>
<Label htmlFor="prompt" className="text-sm font-medium">{t('prompt')}</Label>
<select
id="prompt"
value={selectedPromptId}
onChange={(e) => handlePromptChange(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"
required
>
<option value="">{t('selectPromptPlaceholder')}</option>
{prompts.map(prompt => (
<option key={prompt.id} value={prompt.id}>
{prompt.name}
</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>
)}
{editablePromptContent && (
{/* 提示词内容显示/编辑 */}
{(editablePromptContent || promptInputMode === 'create') && (
<div>
<div className="flex items-center justify-between mb-2">
<Label className="text-sm font-medium">{t('promptContent')}</Label>
<div className="flex items-center space-x-2">
{!isEditingPrompt && (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleEditPrompt}
>
<Edit className="h-3 w-3 mr-1" />
{t('edit')}
</Button>
)}
{isEditingPrompt && (
<>
<Label className="text-sm font-medium">
{promptInputMode === 'create' ? '提示词内容' : t('promptContent')}
</Label>
{promptInputMode === 'select' && (
<div className="flex items-center space-x-2">
{!isEditingPrompt && (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleCancelPromptEdit}
onClick={handleEditPrompt}
>
{t('cancel')}
<Edit className="h-3 w-3 mr-1" />
{t('edit')}
</Button>
<Button
type="button"
variant="default"
size="sm"
onClick={handleSavePromptEdit}
>
{t('save')}
</Button>
</>
)}
</div>
)}
{isEditingPrompt && (
<>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleCancelPromptEdit}
>
{t('cancel')}
</Button>
<Button
type="button"
variant="default"
size="sm"
onClick={handleSavePromptEdit}
>
{t('save')}
</Button>
</>
)}
</div>
)}
</div>
{isEditingPrompt ? (
{(isEditingPrompt || promptInputMode === 'create') ? (
<Textarea
value={editablePromptContent}
onChange={(e) => setEditablePromptContent(e.target.value)}
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">
@ -573,7 +695,14 @@ export default function NewSimulatorRunPage() {
</Link>
<Button
type="submit"
disabled={isCreating || !selectedPromptId || !selectedModelId || !userInput.trim()}
disabled={
isCreating ||
!selectedModelId ||
!userInput.trim() ||
!simulatorName.trim() ||
(promptInputMode === 'select' && !selectedPromptId) ||
(promptInputMode === 'create' && !editablePromptContent.trim())
}
>
{isCreating ? (
<>

View File

@ -29,6 +29,7 @@ import { useLocale } from 'next-intl'
interface SimulatorRun {
id: string
name: string
status: string
userInput: string
output?: string
@ -321,7 +322,7 @@ export default function SimulatorPage() {
<div className="flex items-center justify-between">
<div className="flex-1">
<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)}`}>
<div className="flex items-center space-x-1">
{getStatusIcon(run.status)}