add trans prompt to public, fix build bug

This commit is contained in:
songtianlun 2025-08-02 00:09:25 +08:00
parent bea205d0ec
commit 774f0ecb33
6 changed files with 394 additions and 53 deletions

View File

@ -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

View File

@ -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: {

View File

@ -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

View File

@ -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>

View File

@ -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 */}

View 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>
)
}