allow create new simulator with new prompt
This commit is contained in:
parent
12293a0bcb
commit
c7f33047ef
@ -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
|
||||
|
@ -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 }
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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)}`}>
|
||||
|
@ -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 ? (
|
||||
<>
|
||||
|
@ -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)}
|
||||
|
Loading…
Reference in New Issue
Block a user