Add bulk add

This commit is contained in:
songtianlun 2025-08-01 23:52:43 +08:00
parent a4f92b2da8
commit bea205d0ec
3 changed files with 461 additions and 13 deletions

View File

@ -1,16 +1,28 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase'
import { prisma } from '@/lib/prisma'
// POST /api/prompts/bulk - 批量操作 prompts
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { action, promptIds, userId } = body
// Get user from Supabase
const supabase = createClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (!userId) {
return NextResponse.json({ error: 'User ID is required' }, { status: 401 })
if (authError || !user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Handle bulk create action
if (body.prompts && Array.isArray(body.prompts)) {
return handleBulkCreate(body.prompts, user.id)
}
// Handle other bulk actions
const { action, promptIds } = body
if (!action || !promptIds || !Array.isArray(promptIds)) {
return NextResponse.json(
{ error: 'Action and promptIds array are required' },
@ -22,7 +34,7 @@ export async function POST(request: NextRequest) {
const existingPrompts = await prisma.prompt.findMany({
where: {
id: { in: promptIds },
userId
userId: user.id
}
})
@ -39,7 +51,7 @@ export async function POST(request: NextRequest) {
result = await prisma.prompt.deleteMany({
where: {
id: { in: promptIds },
userId
userId: user.id
}
})
break
@ -49,7 +61,7 @@ export async function POST(request: NextRequest) {
const promptsToDuplicate = await prisma.prompt.findMany({
where: {
id: { in: promptIds },
userId
userId: user.id
},
include: {
tags: true,
@ -67,7 +79,7 @@ export async function POST(request: NextRequest) {
name: `${prompt.name} (Copy)`,
description: prompt.description,
content: prompt.content,
userId,
userId: user.id,
tags: {
connect: prompt.tags.map(tag => ({ id: tag.id }))
}
@ -175,3 +187,108 @@ export async function POST(request: NextRequest) {
)
}
}
interface BulkPromptData {
name: string
description?: string
content: string
tags?: string[]
}
async function handleBulkCreate(prompts: BulkPromptData[], userId: string) {
try {
// Validate each prompt
for (const prompt of prompts) {
if (!prompt.name || !prompt.content) {
return NextResponse.json({
error: 'Each prompt must have name and content'
}, { status: 400 })
}
}
// Create prompts in transaction
const createdPrompts = await prisma.$transaction(async (tx) => {
const results = []
for (const promptData of prompts) {
// Create the prompt
const prompt = await tx.prompt.create({
data: {
name: promptData.name.trim(),
description: promptData.description?.trim() || null,
content: promptData.content.trim(),
userId: userId,
},
})
// Create the initial version
const version = await tx.promptVersion.create({
data: {
promptId: prompt.id,
versionNumber: 1,
content: promptData.content.trim(),
changelog: 'Initial version',
userId: userId,
},
})
// Update prompt with current version
const updatedPrompt = await tx.prompt.update({
where: { id: prompt.id },
data: { currentVersionId: version.id },
})
// Handle tags if provided
if (promptData.tags && Array.isArray(promptData.tags) && promptData.tags.length > 0) {
for (const tagName of promptData.tags) {
if (tagName.trim()) {
// Find or create tag
let tag = await tx.promptTag.findUnique({
where: {
name_userId: {
name: tagName.trim(),
userId: userId,
},
},
})
if (!tag) {
tag = await tx.promptTag.create({
data: {
name: tagName.trim(),
userId: userId,
},
})
}
// Create prompt-tag relationship
await tx.promptTagRelation.create({
data: {
promptId: prompt.id,
tagId: tag.id,
},
})
}
}
}
results.push(updatedPrompt)
}
return results
})
return NextResponse.json({
success: true,
count: createdPrompts.length,
prompts: createdPrompts
})
} catch (error) {
console.error('Error creating bulk prompts:', error)
return NextResponse.json(
{ error: 'Failed to create prompts' },
{ status: 500 }
)
}
}

View File

@ -10,17 +10,18 @@ import { Input } from '@/components/ui/input'
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
import { EditPromptModal } from '@/components/studio/EditPromptModal'
import { PromptDetailModal } from '@/components/studio/PromptDetailModal'
import { BulkAddPromptModal } from '@/components/studio/BulkAddPromptModal'
import {
Plus,
Search,
MoreHorizontal,
Edit,
Play,
FileText,
Calendar,
ChevronDown,
Grid,
List
List,
FolderPlus
} from 'lucide-react'
interface Prompt {
@ -70,6 +71,9 @@ export default function StudioPage() {
const [detailPrompt, setDetailPrompt] = useState<Prompt | null>(null)
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false)
// Bulk Add Modal
const [isBulkAddModalOpen, setIsBulkAddModalOpen] = useState(false)
// Pagination
const [currentPage, setCurrentPage] = useState(1)
const itemsPerPage = 12
@ -188,6 +192,46 @@ export default function StudioPage() {
router.push(`/studio/${id}`)
}
const handleBulkAdd = async (bulkPrompts: Array<{
id: string
name: string
description: string
tags: string
content: string
}>) => {
try {
// Process each prompt
const promptsToCreate = bulkPrompts.map(bulkPrompt => ({
name: bulkPrompt.name.trim(),
description: bulkPrompt.description.trim() || null,
content: bulkPrompt.content.trim(),
tags: bulkPrompt.tags
.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0),
userId: user?.id || ''
}))
// Use the bulk create API endpoint
const response = await fetch('/api/prompts/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompts: promptsToCreate })
})
if (!response.ok) {
throw new Error('Failed to create prompts')
}
// Refresh the prompts list
await fetchPrompts()
setIsBulkAddModalOpen(false)
} catch (error) {
console.error('Error bulk adding prompts:', error)
throw error
}
}
const formatDate = (dateString: string) => {
@ -221,10 +265,20 @@ export default function StudioPage() {
<h1 className="text-3xl font-bold text-foreground">{t('title')}</h1>
<p className="text-muted-foreground mt-1">{t('myPrompts')}</p>
</div>
<Button onClick={handleCreatePrompt} className="flex items-center space-x-2">
<Plus className="h-4 w-4" />
<span>{t('createPrompt')}</span>
</Button>
<div className="flex items-center space-x-3">
<Button
variant="outline"
onClick={() => setIsBulkAddModalOpen(true)}
className="flex items-center space-x-2"
>
<FolderPlus className="h-4 w-4" />
<span>Bulk Add</span>
</Button>
<Button onClick={handleCreatePrompt} className="flex items-center space-x-2">
<Plus className="h-4 w-4" />
<span>{t('createPrompt')}</span>
</Button>
</div>
</div>
{/* Search and Filters */}
@ -647,6 +701,14 @@ export default function StudioPage() {
onEdit={handleEditPrompt}
onDebug={handleDebugPrompt}
/>
{/* Bulk Add Modal */}
<BulkAddPromptModal
isOpen={isBulkAddModalOpen}
onClose={() => setIsBulkAddModalOpen(false)}
onSave={handleBulkAdd}
userId={user?.id || ''}
/>
</div>
)
}

View File

@ -0,0 +1,269 @@
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { LoadingSpinner } from '@/components/ui/loading-spinner'
import { X, Plus, Trash2, FileText, Tag, Type, MessageSquare } from 'lucide-react'
interface BulkPromptItem {
id: string
name: string
description: string
tags: string
content: string
}
interface BulkAddPromptModalProps {
isOpen: boolean
onClose: () => void
onSave: (prompts: BulkPromptItem[]) => Promise<void>
userId: string
}
export function BulkAddPromptModal({ isOpen, onClose, onSave }: BulkAddPromptModalProps) {
const [prompts, setPrompts] = useState<BulkPromptItem[]>([
{ id: '1', name: '', description: '', tags: '', content: '' }
])
const [isLoading, setIsLoading] = useState(false)
const [errors, setErrors] = useState<{[key: string]: string}>({})
const addPrompt = () => {
const newId = (prompts.length + 1).toString()
setPrompts(prev => [...prev, { id: newId, name: '', description: '', tags: '', content: '' }])
}
const removePrompt = (id: string) => {
if (prompts.length > 1) {
setPrompts(prev => prev.filter(p => p.id !== id))
// Clear errors for removed prompt
setErrors(prev => {
const newErrors = { ...prev }
delete newErrors[`${id}-name`]
delete newErrors[`${id}-content`]
return newErrors
})
}
}
const updatePrompt = (id: string, field: keyof BulkPromptItem, value: string) => {
setPrompts(prev => prev.map(p => p.id === id ? { ...p, [field]: value } : p))
// Clear error for this field when user starts typing
const errorKey = `${id}-${field}`
if (errors[errorKey]) {
setErrors(prev => {
const newErrors = { ...prev }
delete newErrors[errorKey]
return newErrors
})
}
}
const validatePrompts = () => {
const newErrors: {[key: string]: string} = {}
let isValid = true
prompts.forEach(prompt => {
if (!prompt.name.trim()) {
newErrors[`${prompt.id}-name`] = 'Name is required'
isValid = false
}
if (!prompt.content.trim()) {
newErrors[`${prompt.id}-content`] = 'Content is required'
isValid = false
}
})
setErrors(newErrors)
return isValid
}
const handleSave = async () => {
if (!validatePrompts()) {
return
}
try {
setIsLoading(true)
await onSave(prompts)
onClose()
// Reset form
setPrompts([{ id: '1', name: '', description: '', tags: '', content: '' }])
setErrors({})
} catch (error) {
console.error('Error saving prompts:', error)
} finally {
setIsLoading(false)
}
}
const handleClose = () => {
onClose()
// Reset form
setPrompts([{ id: '1', name: '', description: '', tags: '', content: '' }])
setErrors({})
}
if (!isOpen) return null
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-background rounded-xl border border-border max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border">
<div className="flex items-center space-x-3">
<div className="p-2 bg-primary/10 rounded-lg">
<FileText className="h-5 w-5 text-primary" />
</div>
<div>
<h2 className="text-xl font-semibold text-foreground">Bulk Add Prompts</h2>
<p className="text-sm text-muted-foreground">Add multiple prompts at once. Use commas to separate tags.</p>
</div>
</div>
<Button variant="ghost" size="sm" onClick={handleClose} disabled={isLoading}>
<X className="h-4 w-4" />
</Button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{prompts.map((prompt, index) => (
<div key={prompt.id} className="bg-card rounded-lg border border-border p-4 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-muted-foreground bg-muted px-2 py-1 rounded-full">
#{index + 1}
</span>
<span className="text-sm text-muted-foreground">Prompt</span>
</div>
{prompts.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => removePrompt(prompt.id)}
className="text-destructive hover:text-destructive hover:bg-destructive/10"
disabled={isLoading}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
{/* Form Fields */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Name */}
<div className="space-y-2">
<Label htmlFor={`name-${prompt.id}`} className="flex items-center space-x-2">
<Type className="h-4 w-4 text-muted-foreground" />
<span>Name *</span>
</Label>
<Input
id={`name-${prompt.id}`}
value={prompt.name}
onChange={(e) => updatePrompt(prompt.id, 'name', e.target.value)}
placeholder="Enter prompt name"
className={errors[`${prompt.id}-name`] ? 'border-destructive' : ''}
disabled={isLoading}
/>
{errors[`${prompt.id}-name`] && (
<p className="text-xs text-destructive">{errors[`${prompt.id}-name`]}</p>
)}
</div>
{/* Tags */}
<div className="space-y-2">
<Label htmlFor={`tags-${prompt.id}`} className="flex items-center space-x-2">
<Tag className="h-4 w-4 text-muted-foreground" />
<span>Tags</span>
</Label>
<Input
id={`tags-${prompt.id}`}
value={prompt.tags}
onChange={(e) => updatePrompt(prompt.id, 'tags', e.target.value)}
placeholder="tag1, tag2, tag3"
disabled={isLoading}
/>
<p className="text-xs text-muted-foreground">Separate tags with commas</p>
</div>
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor={`description-${prompt.id}`} className="flex items-center space-x-2">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
<span>Description</span>
</Label>
<Textarea
id={`description-${prompt.id}`}
value={prompt.description}
onChange={(e) => updatePrompt(prompt.id, 'description', e.target.value)}
placeholder="Brief description of this prompt"
className="min-h-[60px]"
disabled={isLoading}
/>
</div>
{/* Content */}
<div className="space-y-2">
<Label htmlFor={`content-${prompt.id}`} className="flex items-center space-x-2">
<FileText className="h-4 w-4 text-muted-foreground" />
<span>Prompt Content *</span>
</Label>
<Textarea
id={`content-${prompt.id}`}
value={prompt.content}
onChange={(e) => updatePrompt(prompt.id, 'content', e.target.value)}
placeholder="Enter your prompt content here..."
className={`min-h-[120px] ${errors[`${prompt.id}-content`] ? 'border-destructive' : ''}`}
disabled={isLoading}
/>
{errors[`${prompt.id}-content`] && (
<p className="text-xs text-destructive">{errors[`${prompt.id}-content`]}</p>
)}
</div>
</div>
))}
{/* Add More Button */}
<Button
variant="outline"
onClick={addPrompt}
className="w-full border-dashed border-2 hover:border-primary/50 hover:bg-primary/5 py-8"
disabled={isLoading}
>
<Plus className="h-5 w-5 mr-2" />
Add Another Prompt
</Button>
</div>
{/* Footer */}
<div className="flex items-center justify-between p-6 border-t border-border bg-muted/30">
<div className="text-sm text-muted-foreground">
{prompts.length} prompt{prompts.length !== 1 ? 's' : ''} ready to add
</div>
<div className="flex items-center space-x-3">
<Button variant="outline" onClick={handleClose} disabled={isLoading}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isLoading}>
{isLoading ? (
<>
<LoadingSpinner size="sm" className="mr-2" />
Adding Prompts...
</>
) : (
<>
<Plus className="h-4 w-4 mr-2" />
Add {prompts.length} Prompt{prompts.length !== 1 ? 's' : ''}
</>
)}
</Button>
</div>
</div>
</div>
</div>
)
}