add trans prompt to public, fix build bug
This commit is contained in:
parent
bea205d0ec
commit
774f0ecb33
@ -39,7 +39,9 @@ model Prompt {
|
||||
name String
|
||||
content String
|
||||
description String?
|
||||
isPublic Boolean @default(false)
|
||||
isPublic Boolean @default(false) // 保留用于向后兼容
|
||||
permissions String @default("private") // "private" | "public"
|
||||
visibility String? // "under_review" | "published" | null
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
@ -62,7 +62,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { name, description, content, tags, userId } = body
|
||||
const { name, description, content, tags, userId, permissions } = body
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'User ID is required' }, { status: 401 })
|
||||
@ -105,18 +105,44 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
shouldCreateVersion = true
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
const updateData: {
|
||||
name?: string
|
||||
description?: string | null
|
||||
content?: string
|
||||
permissions?: 'private' | 'public'
|
||||
visibility?: 'under_review' | 'published' | null
|
||||
tags?: {
|
||||
set: never[]
|
||||
connect: { id: string }[]
|
||||
}
|
||||
} = {}
|
||||
|
||||
if (name !== undefined) updateData.name = name
|
||||
if (description !== undefined) updateData.description = description
|
||||
if (content !== undefined) updateData.content = content
|
||||
if (permissions !== undefined) {
|
||||
updateData.permissions = permissions
|
||||
// 如果设置为公开,自动设置审核状态
|
||||
if (permissions === 'public') {
|
||||
updateData.visibility = 'under_review'
|
||||
} else if (permissions === 'private') {
|
||||
updateData.visibility = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理标签更新
|
||||
if (tags !== undefined) {
|
||||
updateData.tags = {
|
||||
set: [], // 先清空所有标签
|
||||
connect: tagObjects.map(tag => ({ id: tag.id })) // 然后连接新标签
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 prompt
|
||||
const updatedPrompt = await prisma.prompt.update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
tags: {
|
||||
set: [], // 先清空所有标签
|
||||
connect: tagObjects.map(tag => ({ id: tag.id })) // 然后连接新标签
|
||||
}
|
||||
},
|
||||
data: updateData,
|
||||
include: {
|
||||
tags: true,
|
||||
versions: {
|
||||
|
@ -222,57 +222,44 @@ async function handleBulkCreate(prompts: BulkPromptData[], userId: string) {
|
||||
})
|
||||
|
||||
// Create the initial version
|
||||
const version = await tx.promptVersion.create({
|
||||
await tx.promptVersion.create({
|
||||
data: {
|
||||
promptId: prompt.id,
|
||||
versionNumber: 1,
|
||||
version: 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) {
|
||||
const tagObjects = []
|
||||
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,
|
||||
},
|
||||
const tag = await tx.promptTag.upsert({
|
||||
where: { name: tagName.trim() },
|
||||
update: {},
|
||||
create: { name: tagName.trim() }
|
||||
})
|
||||
tagObjects.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect tags to prompt
|
||||
if (tagObjects.length > 0) {
|
||||
await tx.prompt.update({
|
||||
where: { id: prompt.id },
|
||||
data: {
|
||||
tags: {
|
||||
connect: tagObjects.map(tag => ({ id: tag.id }))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
results.push(updatedPrompt)
|
||||
results.push(prompt)
|
||||
}
|
||||
|
||||
return results
|
||||
|
@ -12,6 +12,7 @@ import { Textarea } from '@/components/ui/textarea'
|
||||
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
||||
import { FullScreenLoading } from '@/components/ui/full-screen-loading'
|
||||
import { VersionTimeline, VersionTimelineRef } from '@/components/studio/VersionTimeline'
|
||||
import { PermissionToggle } from '@/components/studio/PermissionToggle'
|
||||
import {
|
||||
Save,
|
||||
ArrowLeft,
|
||||
@ -36,6 +37,8 @@ interface PromptData {
|
||||
lastUsed?: string | null
|
||||
currentVersion?: number
|
||||
usage?: number
|
||||
permissions?: 'private' | 'public'
|
||||
visibility?: 'under_review' | 'published' | null
|
||||
}
|
||||
|
||||
interface PromptVersion {
|
||||
@ -132,6 +135,31 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
|
||||
|
||||
|
||||
const handleUpdatePermissions = async (newPermissions: 'private' | 'public') => {
|
||||
if (!user || !promptId) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/prompts/${promptId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
permissions: newPermissions,
|
||||
userId: user.id
|
||||
})
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const updatedPrompt = await response.json()
|
||||
setPrompt(updatedPrompt)
|
||||
} else {
|
||||
throw new Error('Failed to update permissions')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating permissions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const handleSavePrompt = async () => {
|
||||
if (!user || !promptId) return
|
||||
|
||||
@ -394,6 +422,16 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Permissions (Mobile) */}
|
||||
<PermissionToggle
|
||||
permissions={prompt?.permissions || 'private'}
|
||||
visibility={prompt?.visibility}
|
||||
onUpdate={handleUpdatePermissions}
|
||||
disabled={isLoading}
|
||||
size="md"
|
||||
variant="card"
|
||||
/>
|
||||
|
||||
{/* Action Bar (Mobile) */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
@ -633,6 +671,16 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Permissions (Desktop) */}
|
||||
<PermissionToggle
|
||||
permissions={prompt?.permissions || 'private'}
|
||||
visibility={prompt?.visibility}
|
||||
onUpdate={handleUpdatePermissions}
|
||||
disabled={isLoading}
|
||||
size="md"
|
||||
variant="card"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,6 +11,7 @@ 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 { PermissionToggle } from '@/components/studio/PermissionToggle'
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
@ -35,6 +36,8 @@ interface Prompt {
|
||||
lastUsed?: string | null
|
||||
currentVersion?: number
|
||||
usage?: number
|
||||
permissions?: 'private' | 'public'
|
||||
visibility?: 'under_review' | 'published' | null
|
||||
}
|
||||
|
||||
interface PaginationInfo {
|
||||
@ -192,6 +195,33 @@ export default function StudioPage() {
|
||||
router.push(`/studio/${id}`)
|
||||
}
|
||||
|
||||
const handleUpdatePermissions = async (promptId: string, newPermissions: 'private' | 'public') => {
|
||||
if (!user) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/prompts/${promptId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
permissions: newPermissions,
|
||||
userId: user.id
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update permissions')
|
||||
}
|
||||
|
||||
// Update the prompt in local state
|
||||
const updatedPrompt = await response.json()
|
||||
setPrompts(prev => prev.map(p => p.id === promptId ? { ...p, permissions: updatedPrompt.permissions, visibility: updatedPrompt.visibility } : p))
|
||||
setFilteredPrompts(prev => prev.map(p => p.id === promptId ? { ...p, permissions: updatedPrompt.permissions, visibility: updatedPrompt.visibility } : p))
|
||||
} catch (error) {
|
||||
console.error('Error updating permissions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const handleBulkAdd = async (bulkPrompts: Array<{
|
||||
id: string
|
||||
name: string
|
||||
@ -474,6 +504,18 @@ export default function StudioPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Permissions */}
|
||||
<div className="mb-3" onClick={(e) => e.stopPropagation()}>
|
||||
<PermissionToggle
|
||||
permissions={prompt.permissions || 'private'}
|
||||
visibility={prompt.visibility}
|
||||
onUpdate={(newPermissions) => handleUpdatePermissions(prompt.id, newPermissions)}
|
||||
size="sm"
|
||||
variant="compact"
|
||||
className="w-full justify-center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
@ -511,7 +553,8 @@ export default function StudioPage() {
|
||||
<div className="hidden md:flex items-center px-4 py-3 bg-muted/50 rounded-lg text-sm font-semibold text-muted-foreground border border-border/50">
|
||||
<div className="flex-1 min-w-0">{t('promptName')}</div>
|
||||
<div className="w-28 text-center hidden lg:block">{t('updatedAt')}</div>
|
||||
<div className="w-32 text-center">{t('tags')}</div>
|
||||
<div className="w-24 text-center">{t('tags')}</div>
|
||||
<div className="w-24 text-center">Privacy</div>
|
||||
<div className="w-20 text-center">Actions</div>
|
||||
</div>
|
||||
|
||||
@ -576,11 +619,11 @@ export default function StudioPage() {
|
||||
<div className="w-28 text-sm text-muted-foreground text-center hidden lg:block">
|
||||
{formatDate(prompt.updatedAt)}
|
||||
</div>
|
||||
<div className="w-32 flex justify-center">
|
||||
<div className="w-24 flex justify-center">
|
||||
<div className="flex flex-wrap gap-1 items-center justify-center">
|
||||
{prompt.tags?.slice(0, 2).map((tag: string | { name: string }) => {
|
||||
{prompt.tags?.slice(0, 1).map((tag: string | { name: string }) => {
|
||||
const tagName = typeof tag === 'string' ? tag : tag?.name || '';
|
||||
const displayTag = tagName.length > 8 ? tagName.slice(0, 8) + '...' : tagName;
|
||||
const displayTag = tagName.length > 6 ? tagName.slice(0, 6) + '...' : tagName;
|
||||
return (
|
||||
<span
|
||||
key={tagName}
|
||||
@ -591,16 +634,25 @@ export default function StudioPage() {
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{prompt.tags && prompt.tags.length > 2 && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-muted text-muted-foreground rounded-full" title={`${prompt.tags.length - 2} more tags`}>
|
||||
+{prompt.tags.length - 2}
|
||||
{prompt.tags && prompt.tags.length > 1 && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 text-xs font-medium bg-muted text-muted-foreground rounded-full" title={`${prompt.tags.length - 1} more tags`}>
|
||||
+{prompt.tags.length - 1}
|
||||
</span>
|
||||
)}
|
||||
{(!prompt.tags || prompt.tags.length === 0) && (
|
||||
<span className="text-xs text-muted-foreground/50 italic">No tags</span>
|
||||
<span className="text-xs text-muted-foreground/50 italic">-</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-24 flex justify-center" onClick={(e) => e.stopPropagation()}>
|
||||
<PermissionToggle
|
||||
permissions={prompt.permissions || 'private'}
|
||||
visibility={prompt.visibility}
|
||||
onUpdate={(newPermissions) => handleUpdatePermissions(prompt.id, newPermissions)}
|
||||
size="sm"
|
||||
variant="compact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
|
226
src/components/studio/PermissionToggle.tsx
Normal file
226
src/components/studio/PermissionToggle.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
||||
import { Lock, Globe, Eye, AlertCircle, CheckCircle2 } from 'lucide-react'
|
||||
|
||||
interface PermissionToggleProps {
|
||||
permissions: 'private' | 'public'
|
||||
visibility?: 'under_review' | 'published' | null
|
||||
onUpdate: (permissions: 'private' | 'public') => Promise<void>
|
||||
disabled?: boolean
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
variant?: 'card' | 'inline' | 'compact'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PermissionToggle({
|
||||
permissions,
|
||||
visibility,
|
||||
onUpdate,
|
||||
disabled = false,
|
||||
size = 'md',
|
||||
variant = 'card',
|
||||
className = ''
|
||||
}: PermissionToggleProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (disabled || isLoading) return
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const newPermissions = permissions === 'private' ? 'public' : 'private'
|
||||
await onUpdate(newPermissions)
|
||||
} catch (error) {
|
||||
console.error('Failed to update permissions:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusInfo = () => {
|
||||
if (permissions === 'private') {
|
||||
return {
|
||||
icon: Lock,
|
||||
status: 'Private',
|
||||
description: 'Only you can see this prompt',
|
||||
color: 'text-slate-600 dark:text-slate-400',
|
||||
bgColor: 'bg-slate-100 dark:bg-slate-800'
|
||||
}
|
||||
}
|
||||
|
||||
if (permissions === 'public') {
|
||||
if (visibility === 'published') {
|
||||
return {
|
||||
icon: Globe,
|
||||
status: 'Published',
|
||||
description: 'Visible in the prompt gallery',
|
||||
color: 'text-green-600 dark:text-green-400',
|
||||
bgColor: 'bg-green-100 dark:bg-green-900/20'
|
||||
}
|
||||
} else if (visibility === 'under_review') {
|
||||
return {
|
||||
icon: Eye,
|
||||
status: 'Under Review',
|
||||
description: 'Pending approval for the gallery',
|
||||
color: 'text-amber-600 dark:text-amber-400',
|
||||
bgColor: 'bg-amber-100 dark:bg-amber-900/20'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
icon: AlertCircle,
|
||||
status: 'Public',
|
||||
description: 'Will be reviewed before appearing in gallery',
|
||||
color: 'text-blue-600 dark:text-blue-400',
|
||||
bgColor: 'bg-blue-100 dark:bg-blue-900/20'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
icon: Lock,
|
||||
status: 'Private',
|
||||
description: 'Only you can see this prompt',
|
||||
color: 'text-slate-600 dark:text-slate-400',
|
||||
bgColor: 'bg-slate-100 dark:bg-slate-800'
|
||||
}
|
||||
}
|
||||
|
||||
const statusInfo = getStatusInfo()
|
||||
const StatusIcon = statusInfo.icon
|
||||
|
||||
if (variant === 'compact') {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleToggle}
|
||||
disabled={disabled || isLoading}
|
||||
className={`flex items-center space-x-2 h-8 px-3 ${statusInfo.bgColor} ${statusInfo.color} hover:opacity-80 ${className}`}
|
||||
title={`${statusInfo.status}: ${statusInfo.description}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingSpinner size="sm" />
|
||||
) : (
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
)}
|
||||
<span className="text-xs font-medium">{statusInfo.status}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
if (variant === 'inline') {
|
||||
return (
|
||||
<div className={`flex items-center justify-between ${className}`}>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-full ${statusInfo.bgColor}`}>
|
||||
<StatusIcon className={`h-4 w-4 ${statusInfo.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-medium text-foreground">{statusInfo.status}</span>
|
||||
{visibility === 'published' && (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{statusInfo.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleToggle}
|
||||
disabled={disabled || isLoading}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingSpinner size="sm" />
|
||||
) : (
|
||||
<>
|
||||
{permissions === 'private' ? (
|
||||
<Globe className="h-4 w-4" />
|
||||
) : (
|
||||
<Lock className="h-4 w-4" />
|
||||
)}
|
||||
<span>Make {permissions === 'private' ? 'Public' : 'Private'}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Card variant (default)
|
||||
return (
|
||||
<div className={`bg-card rounded-lg border border-border p-4 ${className}`}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`p-2 rounded-full ${statusInfo.bgColor}`}>
|
||||
<StatusIcon className={`h-5 w-5 ${statusInfo.color}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-semibold text-foreground">{statusInfo.status}</h3>
|
||||
{visibility === 'published' && (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{statusInfo.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{permissions === 'public' && !visibility && (
|
||||
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start space-x-2">
|
||||
<AlertCircle className="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-sm">
|
||||
<p className="text-blue-800 dark:text-blue-200 font-medium mb-1">Awaiting Review</p>
|
||||
<p className="text-blue-700 dark:text-blue-300">
|
||||
Public prompts are reviewed by our team before appearing in the prompt gallery.
|
||||
This ensures quality and appropriate content for all users.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={permissions === 'private' ? 'default' : 'outline'}
|
||||
size={size}
|
||||
onClick={handleToggle}
|
||||
disabled={disabled || isLoading}
|
||||
className="w-full flex items-center justify-center space-x-2"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<LoadingSpinner size="sm" />
|
||||
<span>Updating...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{permissions === 'private' ? (
|
||||
<>
|
||||
<Globe className="h-4 w-4" />
|
||||
<span>Make Public</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lock className="h-4 w-4" />
|
||||
<span>Make Private</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{permissions === 'private' && (
|
||||
<p className="text-xs text-muted-foreground mt-2 text-center">
|
||||
Making this prompt public will submit it for review
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user