add simulator edit

This commit is contained in:
songtianlun 2025-08-09 13:00:51 +08:00
parent ff0c2017af
commit 559f8fc878
4 changed files with 483 additions and 66 deletions

View File

@ -390,6 +390,13 @@
"duplicateRunConfirm": "Are you sure you want to create a copy of this run?", "duplicateRunConfirm": "Are you sure you want to create a copy of this run?",
"duplicateRunSuccess": "Duplicate created successfully", "duplicateRunSuccess": "Duplicate created successfully",
"copyOf": " Copy", "copyOf": " Copy",
"editRun": "Edit Run",
"saveChanges": "Save Changes",
"cancelEdit": "Cancel Edit",
"runName": "Run Name",
"cannotEditExecutedRun": "Cannot edit executed runs",
"runUpdated": "Run updated successfully",
"selectModel": "Select Model",
"execute": "Execute", "execute": "Execute",
"executing": "Executing...", "executing": "Executing...",
"generating": "Generating...", "generating": "Generating...",

View File

@ -390,6 +390,13 @@
"duplicateRunConfirm": "确定要创建此运行的副本吗?", "duplicateRunConfirm": "确定要创建此运行的副本吗?",
"duplicateRunSuccess": "副本创建成功", "duplicateRunSuccess": "副本创建成功",
"copyOf": "的副本", "copyOf": "的副本",
"editRun": "编辑运行",
"saveChanges": "保存更改",
"cancelEdit": "取消编辑",
"runName": "运行名称",
"cannotEditExecutedRun": "已运行的记录无法编辑",
"runUpdated": "运行记录更新成功",
"selectModel": "选择模型",
"execute": "执行", "execute": "执行",
"executing": "执行中...", "executing": "执行中...",
"generating": "生成中...", "generating": "生成中...",

View File

@ -128,6 +128,112 @@ export async function PATCH(
} }
}); });
return NextResponse.json(updatedRun);
} catch (error) {
console.error("Error updating simulator run:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function PUT(
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 body = await request.json();
const {
name,
userInput,
promptContent,
modelId,
temperature,
maxTokens,
topP,
frequencyPenalty,
presencePenalty,
} = body;
// 检查运行是否存在且属于用户
const existingRun = await prisma.simulatorRun.findFirst({
where: {
id,
userId: user.id,
},
});
if (!existingRun) {
return NextResponse.json({ error: "Run not found" }, { status: 404 });
}
// 只允许编辑pending状态的运行
if (existingRun.status !== "pending") {
return NextResponse.json({ error: "Cannot edit executed runs" }, { status: 400 });
}
// 如果更改了模型,验证新模型是否可用
if (modelId && modelId !== existingRun.modelId) {
const model = await prisma.model.findUnique({
where: { id: modelId },
});
if (!model || !model.isActive) {
return NextResponse.json({ error: "Model not available" }, { status: 400 });
}
}
// 更新运行记录
const updatedRun = await prisma.simulatorRun.update({
where: { id },
data: {
...(name && { name }),
...(userInput && { userInput }),
...(promptContent !== undefined && { promptContent }),
...(modelId && { modelId }),
...(temperature !== undefined && { temperature }),
...(maxTokens !== undefined && { maxTokens }),
...(topP !== undefined && { topP }),
...(frequencyPenalty !== undefined && { frequencyPenalty }),
...(presencePenalty !== undefined && { presencePenalty }),
},
select: {
id: true,
name: true,
status: true,
userInput: true,
promptContent: true,
createdAt: true,
temperature: true,
maxTokens: true,
topP: true,
frequencyPenalty: true,
presencePenalty: true,
prompt: {
select: { id: true, name: true, content: true }
},
model: {
select: {
id: true,
name: true,
provider: true,
modelId: true,
description: true,
maxTokens: true
}
}
}
});
return NextResponse.json(updatedRun); return NextResponse.json(updatedRun);
} catch (error) { } catch (error) {
console.error("Error updating simulator run:", error); console.error("Error updating simulator run:", error);

View File

@ -10,6 +10,9 @@ import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { LoadingSpinner } from '@/components/ui/loading-spinner' import { LoadingSpinner } from '@/components/ui/loading-spinner'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import { import {
ArrowLeft, ArrowLeft,
Play, Play,
@ -23,7 +26,10 @@ import {
FileText, FileText,
Settings, Settings,
BarChart3, BarChart3,
DollarSign DollarSign,
Edit,
Save,
X
} from 'lucide-react' } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import { formatDistanceToNow } from 'date-fns' import { formatDistanceToNow } from 'date-fns'
@ -65,6 +71,17 @@ interface SimulatorRun {
} }
} }
interface Model {
id: string
modelId: string
name: string
provider: string
description?: string
maxTokens?: number
inputCostPer1k?: number
outputCostPer1k?: number
}
export default function SimulatorRunPage({ params }: { params: Promise<{ id: string }> }) { export default function SimulatorRunPage({ params }: { params: Promise<{ id: string }> }) {
const { user, loading: authLoading } = useAuthUser() const { user, loading: authLoading } = useAuthUser()
const router = useRouter() const router = useRouter()
@ -77,6 +94,20 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
const [streamOutput, setStreamOutput] = useState('') const [streamOutput, setStreamOutput] = useState('')
const [runId, setRunId] = useState<string | null>(null) const [runId, setRunId] = useState<string | null>(null)
const [isDuplicating, setIsDuplicating] = useState(false) const [isDuplicating, setIsDuplicating] = useState(false)
const [isEditing, setIsEditing] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [models, setModels] = useState<Model[]>([])
const [editForm, setEditForm] = useState({
name: '',
userInput: '',
promptContent: '',
modelId: '',
temperature: 0.7,
maxTokens: undefined as number | undefined,
topP: 1,
frequencyPenalty: 0,
presencePenalty: 0,
})
const outputRef = useRef<HTMLDivElement>(null) const outputRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
@ -106,6 +137,94 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
} }
}, [runId, router]) }, [runId, router])
const fetchModels = useCallback(async () => {
try {
const response = await fetch('/api/simulator/models')
if (response.ok) {
const data = await response.json()
setModels(data.models || [])
}
} catch (error) {
console.error('Error fetching models:', error)
}
}, [])
const handleStartEdit = () => {
if (!run || run.status !== 'pending') {
return
}
const promptContent = getPromptContent(run)
setEditForm({
name: run.name,
userInput: run.userInput,
promptContent,
modelId: run.model.id,
temperature: run.temperature || 0.7,
maxTokens: run.maxTokens,
topP: run.topP || 1,
frequencyPenalty: run.frequencyPenalty || 0,
presencePenalty: run.presencePenalty || 0,
})
setIsEditing(true)
// 获取模型列表
fetchModels()
}
const handleCancelEdit = () => {
setIsEditing(false)
setEditForm({
name: '',
userInput: '',
promptContent: '',
modelId: '',
temperature: 0.7,
maxTokens: undefined,
topP: 1,
frequencyPenalty: 0,
presencePenalty: 0,
})
}
const handleSaveEdit = async () => {
if (!run) return
setIsSaving(true)
try {
const response = await fetch(`/api/simulator/${run.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: editForm.name,
userInput: editForm.userInput,
promptContent: editForm.promptContent,
modelId: editForm.modelId,
temperature: editForm.temperature,
maxTokens: editForm.maxTokens,
topP: editForm.topP,
frequencyPenalty: editForm.frequencyPenalty,
presencePenalty: editForm.presencePenalty,
}),
})
if (response.ok) {
const updatedRun = await response.json()
setRun(updatedRun)
setIsEditing(false)
console.log(t('runUpdated'))
} else {
console.error('Error updating run:', await response.text())
}
} catch (error) {
console.error('Error updating run:', error)
} finally {
setIsSaving(false)
}
}
useEffect(() => { useEffect(() => {
if (!authLoading && user && runId) { if (!authLoading && user && runId) {
fetchRunCallback() fetchRunCallback()
@ -278,9 +397,22 @@ 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"> {isEditing ? (
{run.name} <div className="mb-2">
</h1> <Label htmlFor="runName" className="text-sm font-medium">{t('runName')}</Label>
<Input
id="runName"
value={editForm.name}
onChange={(e) => setEditForm({...editForm, name: e.target.value})}
className="text-2xl font-bold border-2 border-primary"
placeholder={t('runName')}
/>
</div>
) : (
<h1 className="text-3xl font-bold text-foreground mb-2">
{run.name}
</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)}`}>
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
@ -317,7 +449,47 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
)} )}
</Button> </Button>
{run.status === 'pending' && ( {/* 编辑按钮 - 只有pending状态才显示 */}
{run.status === 'pending' && !isEditing && (
<Button
onClick={handleStartEdit}
variant="outline"
>
<Edit className="h-4 w-4 mr-2" />
{t('editRun')}
</Button>
)}
{/* 编辑模式下的保存和取消按钮 */}
{isEditing && (
<>
<Button
onClick={handleCancelEdit}
variant="outline"
>
<X className="h-4 w-4 mr-2" />
{t('cancelEdit')}
</Button>
<Button
onClick={handleSaveEdit}
disabled={isSaving}
>
{isSaving ? (
<>
<LoadingSpinner size="sm" />
<span className="ml-2">{t('creating')}</span>
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
{t('saveChanges')}
</>
)}
</Button>
</>
)}
{run.status === 'pending' && !isEditing && (
<Button onClick={executeRun} disabled={isExecuting}> <Button onClick={executeRun} disabled={isExecuting}>
{isExecuting ? ( {isExecuting ? (
<> <>
@ -346,38 +518,60 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
<FileText className="h-5 w-5 mr-2" /> <FileText className="h-5 w-5 mr-2" />
{t('promptContent')} {t('promptContent')}
</h2> </h2>
<Button {!isEditing && (
variant="outline" <Button
size="sm" variant="outline"
onClick={() => copyToClipboard(promptContent)} size="sm"
> onClick={() => copyToClipboard(promptContent)}
<Copy className="h-4 w-4" /> >
</Button> <Copy className="h-4 w-4" />
</div> </Button>
<div className="bg-muted rounded-md p-4 border max-h-64 overflow-y-auto"> )}
<pre className="text-sm text-foreground whitespace-pre-wrap font-mono">
{promptContent}
</pre>
</div> </div>
{isEditing ? (
<Textarea
value={editForm.promptContent}
onChange={(e) => setEditForm({...editForm, promptContent: e.target.value})}
className="min-h-32 font-mono text-sm"
placeholder={t('promptContentPlaceholder')}
/>
) : (
<div className="bg-muted rounded-md p-4 border max-h-64 overflow-y-auto">
<pre className="text-sm text-foreground whitespace-pre-wrap font-mono">
{promptContent}
</pre>
</div>
)}
</Card> </Card>
{/* User Input */} {/* User Input */}
<Card className="p-6"> <Card className="p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold">{t('userInput')}</h2> <h2 className="text-xl font-semibold">{t('userInput')}</h2>
<Button {!isEditing && (
variant="outline" <Button
size="sm" variant="outline"
onClick={() => copyToClipboard(run.userInput)} size="sm"
> onClick={() => copyToClipboard(run.userInput)}
<Copy className="h-4 w-4" /> >
</Button> <Copy className="h-4 w-4" />
</Button>
)}
</div> </div>
<div className="bg-muted rounded-md p-4 border"> {isEditing ? (
<div className="text-sm text-foreground whitespace-pre-wrap"> <Textarea
{run.userInput} value={editForm.userInput}
onChange={(e) => setEditForm({...editForm, userInput: e.target.value})}
className="min-h-32"
placeholder={t('userInputPlaceholder')}
/>
) : (
<div className="bg-muted rounded-md p-4 border">
<div className="text-sm text-foreground whitespace-pre-wrap">
{run.userInput}
</div>
</div> </div>
</div> )}
</Card> </Card>
{/* Configuration */} {/* Configuration */}
@ -386,56 +580,159 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
<Settings className="h-5 w-5 mr-2" /> <Settings className="h-5 w-5 mr-2" />
{t('configuration')} {t('configuration')}
</h2> </h2>
<div className="grid grid-cols-2 gap-4 text-sm"> {isEditing ? (
<div> <div className="space-y-4">
<span className="font-medium text-foreground"> {/* Model Selection */}
{t('temperature')}: <div>
</span> <Label htmlFor="modelSelect" className="text-sm font-medium">{t('selectModel')}</Label>
<span className="ml-2 text-muted-foreground"> <select
{run.temperature || 0.7} id="modelSelect"
</span> value={editForm.modelId}
onChange={(e) => setEditForm({...editForm, modelId: e.target.value})}
className="w-full mt-1 px-3 py-2 border border-input bg-background rounded-md text-sm"
>
{models.map((model) => (
<option key={model.id} value={model.id}>
{model.provider} - {model.name}
</option>
))}
</select>
</div>
{/* Temperature */}
<div>
<Label htmlFor="temperature" className="text-sm font-medium">{t('temperature')}</Label>
<Input
id="temperature"
type="number"
min="0"
max="2"
step="0.1"
value={editForm.temperature}
onChange={(e) => setEditForm({...editForm, temperature: parseFloat(e.target.value) || 0.7})}
className="mt-1"
/>
</div>
{/* Max Tokens */}
<div>
<Label htmlFor="maxTokens" className="text-sm font-medium">{t('maxTokens')}</Label>
<Input
id="maxTokens"
type="number"
min="1"
value={editForm.maxTokens || ''}
onChange={(e) => setEditForm({...editForm, maxTokens: e.target.value ? parseInt(e.target.value) : undefined})}
placeholder="Leave empty for model default"
className="mt-1"
/>
</div>
{/* Top P */}
<div>
<Label htmlFor="topP" className="text-sm font-medium">{t('topP')}</Label>
<Input
id="topP"
type="number"
min="0"
max="1"
step="0.01"
value={editForm.topP}
onChange={(e) => setEditForm({...editForm, topP: parseFloat(e.target.value) || 1})}
className="mt-1"
/>
</div>
{/* Frequency Penalty */}
<div>
<Label htmlFor="frequencyPenalty" className="text-sm font-medium">{t('frequencyPenalty')}</Label>
<Input
id="frequencyPenalty"
type="number"
min="-2"
max="2"
step="0.1"
value={editForm.frequencyPenalty}
onChange={(e) => setEditForm({...editForm, frequencyPenalty: parseFloat(e.target.value) || 0})}
className="mt-1"
/>
</div>
{/* Presence Penalty */}
<div>
<Label htmlFor="presencePenalty" className="text-sm font-medium">{t('presencePenalty')}</Label>
<Input
id="presencePenalty"
type="number"
min="-2"
max="2"
step="0.1"
value={editForm.presencePenalty}
onChange={(e) => setEditForm({...editForm, presencePenalty: parseFloat(e.target.value) || 0})}
className="mt-1"
/>
</div>
</div> </div>
{run.maxTokens && ( ) : (
<div> <div className="grid grid-cols-2 gap-4 text-sm">
<div className="col-span-2">
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">
{t('maxTokens')}: {t('selectModel')}:
</span> </span>
<span className="ml-2 text-muted-foreground"> <span className="ml-2 text-muted-foreground">
{run.maxTokens.toLocaleString()} {run.model.provider} - {run.model.name}
</span> </span>
</div> </div>
)}
{run.topP && (
<div> <div>
<span className="font-medium text-foreground"> <span className="font-medium text-foreground">
{t('topP')}: {t('temperature')}:
</span> </span>
<span className="ml-2 text-muted-foreground"> <span className="ml-2 text-muted-foreground">
{run.topP} {run.temperature || 0.7}
</span> </span>
</div> </div>
)} {run.maxTokens && (
{run.frequencyPenalty !== undefined && ( <div>
<div> <span className="font-medium text-foreground">
<span className="font-medium text-foreground"> {t('maxTokens')}:
{t('frequencyPenalty')}: </span>
</span> <span className="ml-2 text-muted-foreground">
<span className="ml-2 text-muted-foreground"> {run.maxTokens.toLocaleString()}
{run.frequencyPenalty} </span>
</span> </div>
</div> )}
)} {run.topP && (
{run.presencePenalty !== undefined && ( <div>
<div> <span className="font-medium text-foreground">
<span className="font-medium text-foreground"> {t('topP')}:
{t('presencePenalty')}: </span>
</span> <span className="ml-2 text-muted-foreground">
<span className="ml-2 text-muted-foreground"> {run.topP}
{run.presencePenalty} </span>
</span> </div>
</div> )}
)} {run.frequencyPenalty !== undefined && (
</div> <div>
<span className="font-medium text-foreground">
{t('frequencyPenalty')}:
</span>
<span className="ml-2 text-muted-foreground">
{run.frequencyPenalty}
</span>
</div>
)}
{run.presencePenalty !== undefined && (
<div>
<span className="font-medium text-foreground">
{t('presencePenalty')}:
</span>
<span className="ml-2 text-muted-foreground">
{run.presencePenalty}
</span>
</div>
)}
</div>
)}
</Card> </Card>
{/* Statistics */} {/* Statistics */}