fix prompt edit
This commit is contained in:
parent
2b2e311c58
commit
2707a510b5
1072
package-lock.json
generated
1072
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,10 @@
|
||||
"name": "prmbr",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run db:generate && next dev",
|
||||
"build": "npm run db:generate && next build",
|
||||
@ -41,4 +45,4 @@
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
@ -14,10 +14,9 @@ datasource db {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
id String @id // 使用Supabase用户ID,不再自动生成
|
||||
email String @unique
|
||||
username String @unique
|
||||
password String
|
||||
username String? @unique // 允许为空,因为有些用户可能没有设置用户名
|
||||
avatar String?
|
||||
bio String?
|
||||
language String @default("en")
|
||||
|
@ -93,7 +93,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
// 检查内容是否有变化,如果有则创建新版本
|
||||
const currentVersion = existingPrompt.versions[0]
|
||||
let shouldCreateVersion = false
|
||||
|
||||
|
||||
if (content && currentVersion && content !== currentVersion.content) {
|
||||
shouldCreateVersion = true
|
||||
}
|
||||
@ -131,7 +131,13 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(updatedPrompt)
|
||||
// 处理返回数据格式,确保tags是字符串数组
|
||||
const promptWithMetadata = {
|
||||
...updatedPrompt,
|
||||
tags: updatedPrompt.tags.map(tag => tag.name)
|
||||
}
|
||||
|
||||
return NextResponse.json(promptWithMetadata)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating prompt:', error)
|
||||
|
@ -162,7 +162,13 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json(prompt, { status: 201 })
|
||||
// 处理返回数据格式,确保tags是字符串数组
|
||||
const promptWithMetadata = {
|
||||
...prompt,
|
||||
tags: prompt.tags.map(tag => tag.name)
|
||||
}
|
||||
|
||||
return NextResponse.json(promptWithMetadata, { status: 201 })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating prompt:', error)
|
||||
|
94
src/app/api/users/sync/route.ts
Normal file
94
src/app/api/users/sync/route.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { createServerSupabaseClient } from '@/lib/supabase-server'
|
||||
|
||||
// POST /api/users/sync - 同步Supabase用户到Prisma数据库
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const supabase = await createServerSupabaseClient()
|
||||
const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser()
|
||||
|
||||
if (authError || !supabaseUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 检查用户是否已存在于Prisma数据库中
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { id: supabaseUser.id }
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
// 用户已存在,更新信息
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: supabaseUser.id },
|
||||
data: {
|
||||
email: supabaseUser.email!,
|
||||
username: supabaseUser.user_metadata?.username || supabaseUser.user_metadata?.full_name || null,
|
||||
avatar: supabaseUser.user_metadata?.avatar_url || null,
|
||||
bio: supabaseUser.user_metadata?.bio || null,
|
||||
language: supabaseUser.user_metadata?.language || 'en',
|
||||
updatedAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'User updated successfully',
|
||||
user: updatedUser
|
||||
})
|
||||
} else {
|
||||
// 用户不存在,创建新用户
|
||||
const newUser = await prisma.user.create({
|
||||
data: {
|
||||
id: supabaseUser.id,
|
||||
email: supabaseUser.email!,
|
||||
username: supabaseUser.user_metadata?.username || supabaseUser.user_metadata?.full_name || null,
|
||||
avatar: supabaseUser.user_metadata?.avatar_url || null,
|
||||
bio: supabaseUser.user_metadata?.bio || null,
|
||||
language: supabaseUser.user_metadata?.language || 'en'
|
||||
}
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'User created successfully',
|
||||
user: newUser
|
||||
}, { status: 201 })
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error syncing user:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to sync user' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/users/sync - 获取当前用户信息
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const supabase = await createServerSupabaseClient()
|
||||
const { data: { user: supabaseUser }, error: authError } = await supabase.auth.getUser()
|
||||
|
||||
if (authError || !supabaseUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 从Prisma数据库获取用户信息
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: supabaseUser.id }
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'User not found in database' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ user })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch user' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
@ -49,88 +49,86 @@ export default function ProfilePage() {
|
||||
new: false,
|
||||
confirm: false
|
||||
})
|
||||
const [saveStatus, setSaveStatus] = useState<{type: 'success' | 'error' | null, message: string}>({type: null, message: ''})
|
||||
const [saveStatus, setSaveStatus] = useState<{ type: 'success' | 'error' | null, message: string }>({ type: null, message: '' })
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [profileLoading, setProfileLoading] = useState(true)
|
||||
const [fieldLoading, setFieldLoading] = useState<{[key: string]: boolean}>({})
|
||||
const [fieldLoading, setFieldLoading] = useState<{ [key: string]: boolean }>({})
|
||||
const [avatarUploading, setAvatarUploading] = useState(false)
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
loadProfile()
|
||||
}
|
||||
}, [user]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const loadProfile = async () => {
|
||||
if (!user) return
|
||||
|
||||
setProfileLoading(true)
|
||||
try {
|
||||
// Get user metadata and profile data
|
||||
const { data: { user: userData }, error: userError } = await supabase.auth.getUser()
|
||||
|
||||
if (userError) throw userError
|
||||
|
||||
const profileData: UserProfile = {
|
||||
id: userData?.id || '',
|
||||
email: userData?.email || '',
|
||||
username: userData?.user_metadata?.username || userData?.user_metadata?.full_name || '',
|
||||
bio: userData?.user_metadata?.bio || '',
|
||||
avatar_url: userData?.user_metadata?.avatar_url || '',
|
||||
language: userData?.user_metadata?.language || 'en'
|
||||
const loadProfile = async () => {
|
||||
setProfileLoading(true)
|
||||
try {
|
||||
// Get user metadata and profile data
|
||||
const { data: { user: userData }, error: userError } = await supabase.auth.getUser()
|
||||
|
||||
if (userError) throw userError
|
||||
|
||||
const profileData: UserProfile = {
|
||||
id: userData?.id || '',
|
||||
email: userData?.email || '',
|
||||
username: userData?.user_metadata?.username || userData?.user_metadata?.full_name || '',
|
||||
bio: userData?.user_metadata?.bio || '',
|
||||
avatar_url: userData?.user_metadata?.avatar_url || '',
|
||||
language: userData?.user_metadata?.language || 'en'
|
||||
}
|
||||
|
||||
setProfile(profileData)
|
||||
setFormData({
|
||||
username: profileData.username || '',
|
||||
email: profileData.email,
|
||||
bio: profileData.bio || '',
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
language: profileData.language || 'en'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error loading profile:', error)
|
||||
setSaveStatus({ type: 'error', message: t('failedToLoadProfile') })
|
||||
} finally {
|
||||
setProfileLoading(false)
|
||||
}
|
||||
|
||||
setProfile(profileData)
|
||||
setFormData({
|
||||
username: profileData.username || '',
|
||||
email: profileData.email,
|
||||
bio: profileData.bio || '',
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
language: profileData.language || 'en'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error loading profile:', error)
|
||||
setSaveStatus({type: 'error', message: t('failedToLoadProfile')})
|
||||
} finally {
|
||||
setProfileLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
loadProfile()
|
||||
}, [user, supabase, t])
|
||||
|
||||
const updateProfile = async (field: string, value: string) => {
|
||||
if (!user) return
|
||||
|
||||
|
||||
setFieldLoading(prev => ({ ...prev, [field]: true }))
|
||||
setIsLoading(true)
|
||||
setSaveStatus({type: null, message: ''})
|
||||
setSaveStatus({ type: null, message: '' })
|
||||
|
||||
try {
|
||||
const updates: Record<string, string> = {}
|
||||
|
||||
|
||||
if (field === 'email') {
|
||||
const { error } = await supabase.auth.updateUser({ email: value })
|
||||
if (error) throw error
|
||||
setSaveStatus({type: 'success', message: t('checkEmailToConfirm')})
|
||||
setSaveStatus({ type: 'success', message: t('checkEmailToConfirm') })
|
||||
} else {
|
||||
updates[field] = value
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
data: updates
|
||||
})
|
||||
if (error) throw error
|
||||
setSaveStatus({type: 'success', message: `${field} ${t('updatedSuccessfully')}`})
|
||||
setSaveStatus({ type: 'success', message: `${field} ${t('updatedSuccessfully')}` })
|
||||
}
|
||||
|
||||
// Reload profile
|
||||
await loadProfile()
|
||||
|
||||
|
||||
// Reset editing state
|
||||
setIsEditing(prev => ({ ...prev, [field]: false }))
|
||||
|
||||
|
||||
} catch (error: unknown) {
|
||||
setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || `${t('failedToUpdate')} ${field}`})
|
||||
setSaveStatus({ type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || `${t('failedToUpdate')} ${field}` })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setFieldLoading(prev => ({ ...prev, [field]: false }))
|
||||
@ -139,27 +137,27 @@ export default function ProfilePage() {
|
||||
|
||||
const updatePassword = async () => {
|
||||
if (!formData.newPassword || formData.newPassword !== formData.confirmPassword) {
|
||||
setSaveStatus({type: 'error', message: t('passwordsNotMatch')})
|
||||
setSaveStatus({ type: 'error', message: t('passwordsNotMatch') })
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.newPassword.length < 6) {
|
||||
setSaveStatus({type: 'error', message: t('passwordMinLength')})
|
||||
setSaveStatus({ type: 'error', message: t('passwordMinLength') })
|
||||
return
|
||||
}
|
||||
|
||||
setFieldLoading(prev => ({ ...prev, password: true }))
|
||||
setIsLoading(true)
|
||||
setSaveStatus({type: null, message: ''})
|
||||
setSaveStatus({ type: null, message: '' })
|
||||
|
||||
try {
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
password: formData.newPassword
|
||||
})
|
||||
|
||||
|
||||
if (error) throw error
|
||||
|
||||
setSaveStatus({type: 'success', message: t('passwordUpdatedSuccessfully')})
|
||||
|
||||
setSaveStatus({ type: 'success', message: t('passwordUpdatedSuccessfully') })
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
currentPassword: '',
|
||||
@ -167,9 +165,9 @@ export default function ProfilePage() {
|
||||
confirmPassword: ''
|
||||
}))
|
||||
setIsEditing(prev => ({ ...prev, password: false }))
|
||||
|
||||
|
||||
} catch (error: unknown) {
|
||||
setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || t('failedToUpdatePassword')})
|
||||
setSaveStatus({ type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || t('failedToUpdatePassword') })
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setFieldLoading(prev => ({ ...prev, password: false }))
|
||||
@ -182,34 +180,34 @@ export default function ProfilePage() {
|
||||
|
||||
setAvatarUploading(true)
|
||||
setIsLoading(true)
|
||||
setSaveStatus({type: null, message: ''})
|
||||
setSaveStatus({ type: null, message: '' })
|
||||
|
||||
try {
|
||||
// Create a unique filename for future use with actual file storage
|
||||
// const fileExt = file.name.split('.').pop()
|
||||
// const fileName = `${user.id}-${Date.now()}.${fileExt}`
|
||||
|
||||
|
||||
// For now, we'll use a placeholder upload since we need to configure storage
|
||||
// In a real implementation, you would upload to Supabase Storage or Cloudflare R2
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e) => {
|
||||
const dataUrl = e.target?.result as string
|
||||
|
||||
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
data: { avatar_url: dataUrl }
|
||||
})
|
||||
|
||||
|
||||
if (error) throw error
|
||||
|
||||
|
||||
await loadProfile()
|
||||
setSaveStatus({type: 'success', message: t('avatarUpdatedSuccessfully')})
|
||||
setSaveStatus({ type: 'success', message: t('avatarUpdatedSuccessfully') })
|
||||
setIsLoading(false)
|
||||
setAvatarUploading(false)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
|
||||
} catch (error: unknown) {
|
||||
setSaveStatus({type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || t('failedToUploadAvatar')})
|
||||
setSaveStatus({ type: 'error', message: (error instanceof Error ? error.message : 'Unknown error') || t('failedToUploadAvatar') })
|
||||
setIsLoading(false)
|
||||
setAvatarUploading(false)
|
||||
}
|
||||
@ -240,7 +238,7 @@ export default function ProfilePage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">{t('title')}</h1>
|
||||
@ -248,11 +246,10 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
|
||||
{saveStatus.type && (
|
||||
<div className={`mb-6 p-4 rounded-lg border ${
|
||||
saveStatus.type === 'success'
|
||||
<div className={`mb-6 p-4 rounded-lg border ${saveStatus.type === 'success'
|
||||
? 'bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-300'
|
||||
: 'bg-destructive/10 border-destructive/20 text-destructive'
|
||||
}`}>
|
||||
}`}>
|
||||
{saveStatus.message}
|
||||
</div>
|
||||
)}
|
||||
@ -268,8 +265,8 @@ export default function ProfilePage() {
|
||||
<h2 className="text-xl font-semibold text-foreground mb-4">{t('profilePicture')}</h2>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
src={profile?.avatar_url}
|
||||
<Avatar
|
||||
src={profile?.avatar_url}
|
||||
alt="Profile Avatar"
|
||||
size={96}
|
||||
className="w-24 h-24"
|
||||
@ -319,7 +316,7 @@ export default function ProfilePage() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{isEditing.username ? (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
@ -380,7 +377,7 @@ export default function ProfilePage() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{isEditing.email ? (
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
@ -442,7 +439,7 @@ export default function ProfilePage() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{isEditing.bio ? (
|
||||
<div className="space-y-4">
|
||||
<Textarea
|
||||
@ -503,7 +500,7 @@ export default function ProfilePage() {
|
||||
<Globe className="w-5 h-5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<select
|
||||
value={formData.language}
|
||||
onChange={(e) => {
|
||||
@ -540,7 +537,7 @@ export default function ProfilePage() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{isEditing.password ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@ -565,7 +562,7 @@ export default function ProfilePage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Label htmlFor="confirmPassword">{t('confirmNewPassword')}</Label>
|
||||
<div className="relative mt-1">
|
||||
@ -588,7 +585,7 @@ export default function ProfilePage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
|
@ -10,8 +10,8 @@ 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 {
|
||||
Play,
|
||||
import {
|
||||
Play,
|
||||
Save,
|
||||
Copy,
|
||||
Settings,
|
||||
@ -63,35 +63,43 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const fetchPrompt = async () => {
|
||||
if (!user || !promptId) return
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const response = await fetch(`/api/prompts/${promptId}?userId=${user.id}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setPrompt(data)
|
||||
setPromptContent(data.content)
|
||||
setPromptTitle(data.name)
|
||||
} else {
|
||||
router.push('/studio')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching prompt:', error)
|
||||
router.push('/studio')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
router.push('/signin')
|
||||
} else if (user && promptId) {
|
||||
fetchPrompt()
|
||||
return
|
||||
}
|
||||
}, [user, loading, router, promptId, fetchPrompt])
|
||||
|
||||
if (!user || !promptId) return
|
||||
|
||||
const fetchPrompt = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const response = await fetch(`/api/prompts/${promptId}?userId=${user.id}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// 确保tags是字符串数组
|
||||
const processedData = {
|
||||
...data,
|
||||
tags: Array.isArray(data.tags)
|
||||
? data.tags.map((tag: any) => typeof tag === 'string' ? tag : tag.name || '')
|
||||
: []
|
||||
}
|
||||
setPrompt(processedData)
|
||||
setPromptContent(data.content)
|
||||
setPromptTitle(data.name)
|
||||
} else {
|
||||
router.push('/studio')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching prompt:', error)
|
||||
router.push('/studio')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchPrompt()
|
||||
}, [user, loading, router, promptId])
|
||||
|
||||
|
||||
|
||||
@ -175,7 +183,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
|
||||
|
||||
{/* Top Navigation */}
|
||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
@ -195,13 +203,13 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
<div className="flex items-center space-x-2">
|
||||
<FileText className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 className="font-semibold text-foreground">{prompt.name}</h1>
|
||||
<p className="text-xs text-muted-foreground">Version {prompt.currentVersion || 1} • {prompt.usage || 0} uses</p>
|
||||
<h1 className="font-semibold text-foreground">{prompt?.name || 'Loading...'}</h1>
|
||||
<p className="text-xs text-muted-foreground">Version {prompt?.currentVersion || 1} • {prompt?.usage || 0} uses</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm" className="flex items-center space-x-1">
|
||||
<History className="h-4 w-4" />
|
||||
@ -232,7 +240,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
<Plus className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
@ -261,15 +269,15 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0"></div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-foreground text-sm truncate">{prompt.name}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">{prompt.description}</p>
|
||||
<h3 className="font-medium text-foreground text-sm truncate">{prompt?.name || 'Loading...'}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">{prompt?.description || ''}</p>
|
||||
<div className="flex items-center mt-2 space-x-2">
|
||||
{prompt.tags.slice(0, 1).map((tag) => (
|
||||
<span key={tag} className="inline-flex items-center px-1.5 py-0.5 text-xs font-medium bg-primary/10 text-primary rounded">
|
||||
{tag}
|
||||
{prompt?.tags?.slice(0, 1).map((tag) => (
|
||||
<span key={typeof tag === 'string' ? tag : (tag as any)?.name || ''} className="inline-flex items-center px-1.5 py-0.5 text-xs font-medium bg-primary/10 text-primary rounded">
|
||||
{typeof tag === 'string' ? tag : (tag as any)?.name || ''}
|
||||
</span>
|
||||
))}
|
||||
{prompt.tags.length > 1 && (
|
||||
{prompt?.tags && prompt.tags.length > 1 && (
|
||||
<span className="text-xs text-muted-foreground">+{prompt.tags.length - 1}</span>
|
||||
)}
|
||||
</div>
|
||||
@ -325,30 +333,33 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Created</span>
|
||||
<span className="text-foreground">{new Date(prompt.createdAt).toLocaleDateString()}</span>
|
||||
<span className="text-foreground">{prompt?.createdAt ? new Date(prompt.createdAt).toLocaleDateString() : '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Updated</span>
|
||||
<span className="text-foreground">{new Date(prompt.updatedAt).toLocaleDateString()}</span>
|
||||
<span className="text-foreground">{prompt?.updatedAt ? new Date(prompt.updatedAt).toLocaleDateString() : '-'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Last used</span>
|
||||
<span className="text-foreground">{prompt.lastUsed ? new Date(prompt.lastUsed).toLocaleDateString() : 'Never'}</span>
|
||||
<span className="text-foreground">{prompt?.lastUsed ? new Date(prompt.lastUsed).toLocaleDateString() : 'Never'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Usage count</span>
|
||||
<span className="text-foreground">{prompt.usage || 0}</span>
|
||||
<span className="text-foreground">{prompt?.usage || 0}</span>
|
||||
</div>
|
||||
<div className="pt-2 border-t border-border">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{prompt.tags.map((tag: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{prompt?.tags?.map((tag: any) => {
|
||||
const tagName = typeof tag === 'string' ? tag : tag?.name || '';
|
||||
return (
|
||||
<span
|
||||
key={tagName}
|
||||
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded-full"
|
||||
>
|
||||
{tagName}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -372,7 +383,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
)}
|
||||
{tCommon('save')}
|
||||
</Button>
|
||||
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleRunTest}
|
||||
@ -428,7 +439,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Label htmlFor="promptContent" className="text-sm font-medium">
|
||||
{t('promptContent')}
|
||||
@ -463,7 +474,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="p-4">
|
||||
<div className="bg-muted rounded-lg min-h-[400px] p-4">
|
||||
{isRunning ? (
|
||||
|
@ -10,7 +10,7 @@ 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 {
|
||||
import {
|
||||
ArrowLeft,
|
||||
Save,
|
||||
Tag,
|
||||
@ -31,27 +31,28 @@ export default function NewPromptPage() {
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [availableTags, setAvailableTags] = useState<string[]>([])
|
||||
|
||||
const fetchAvailableTags = async () => {
|
||||
if (!user) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tags?userId=${user.id}`)
|
||||
if (response.ok) {
|
||||
const tagsData = await response.json()
|
||||
setAvailableTags(tagsData.map((tag: { name: string }) => tag.name))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tags:', error)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
router.push('/signin')
|
||||
} else if (user) {
|
||||
fetchAvailableTags()
|
||||
return
|
||||
}
|
||||
}, [user, loading, router, fetchAvailableTags])
|
||||
|
||||
if (!user) return
|
||||
|
||||
const fetchAvailableTags = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/tags?userId=${user.id}`)
|
||||
if (response.ok) {
|
||||
const tagsData = await response.json()
|
||||
setAvailableTags(tagsData.map((tag: { name: string }) => tag.name))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tags:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchAvailableTags()
|
||||
}, [user, loading, router])
|
||||
|
||||
|
||||
|
||||
@ -123,7 +124,7 @@ export default function NewPromptPage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
|
||||
<div className="border-b">
|
||||
<div className="max-w-4xl mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -142,7 +143,7 @@ export default function NewPromptPage() {
|
||||
<p className="text-sm text-muted-foreground">Create a new AI prompt</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -152,7 +153,7 @@ export default function NewPromptPage() {
|
||||
>
|
||||
{tCommon('cancel')}
|
||||
</Button>
|
||||
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
@ -176,7 +177,7 @@ export default function NewPromptPage() {
|
||||
{/* Basic Information */}
|
||||
<div className="bg-card rounded-lg border border-border p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">Basic Information</h2>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="promptName">{t('promptName')} *</Label>
|
||||
@ -189,7 +190,7 @@ export default function NewPromptPage() {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Label htmlFor="promptDescription">{t('promptDescription')}</Label>
|
||||
<Input
|
||||
@ -206,7 +207,7 @@ export default function NewPromptPage() {
|
||||
{/* Tags */}
|
||||
<div className="bg-card rounded-lg border border-border p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">{t('tags')}</h2>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
@ -225,7 +226,7 @@ export default function NewPromptPage() {
|
||||
{t('addTag')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
@ -273,7 +274,7 @@ export default function NewPromptPage() {
|
||||
{/* Prompt Content */}
|
||||
<div className="bg-card rounded-lg border border-border p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">{t('promptContent')}</h2>
|
||||
|
||||
|
||||
<div>
|
||||
<Label htmlFor="promptContent">Content</Label>
|
||||
<Textarea
|
||||
|
@ -78,16 +78,6 @@ export default function StudioPage() {
|
||||
totalPages: 0
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
router.push('/signin')
|
||||
} else if (user) {
|
||||
fetchPrompts()
|
||||
fetchTags()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user, loading, router])
|
||||
|
||||
// Fetch prompts from API
|
||||
const fetchPrompts = async () => {
|
||||
if (!user) return
|
||||
@ -117,27 +107,62 @@ export default function StudioPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch tags from API
|
||||
const fetchTags = async () => {
|
||||
// Initial load effect
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
router.push('/signin')
|
||||
return
|
||||
}
|
||||
|
||||
if (!user) return
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/tags?userId=${user.id}`)
|
||||
if (response.ok) {
|
||||
const tags = await response.json()
|
||||
setAllTags(tags.map((tag: { name: string }) => tag.name))
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/tags?userId=${user.id}`)
|
||||
if (response.ok) {
|
||||
const tags = await response.json()
|
||||
setAllTags(tags.map((tag: { name: string }) => tag.name))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tags:', error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching tags:', error)
|
||||
}
|
||||
}
|
||||
|
||||
fetchPrompts()
|
||||
fetchTags()
|
||||
}, [user, loading, router])
|
||||
|
||||
// Refetch when filters change
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchPrompts()
|
||||
if (!user) return
|
||||
|
||||
const fetchPromptsWithFilters = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const params = new URLSearchParams({
|
||||
userId: user.id,
|
||||
page: currentPage.toString(),
|
||||
limit: itemsPerPage.toString(),
|
||||
search: searchQuery,
|
||||
tag: selectedTag,
|
||||
sortBy: sortField,
|
||||
sortOrder: sortOrder
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/prompts?${params}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setPrompts(data.prompts)
|
||||
setPagination(data.pagination)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching prompts:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
fetchPromptsWithFilters()
|
||||
}, [currentPage, itemsPerPage, searchQuery, selectedTag, sortField, sortOrder, user])
|
||||
|
||||
// Since filtering and sorting is now done on the server,
|
||||
@ -235,8 +260,8 @@ export default function StudioPage() {
|
||||
}
|
||||
|
||||
const handleSelectPrompt = (id: string) => {
|
||||
setSelectedPrompts(prev =>
|
||||
prev.includes(id)
|
||||
setSelectedPrompts(prev =>
|
||||
prev.includes(id)
|
||||
? prev.filter(pId => pId !== id)
|
||||
: [...prev, id]
|
||||
)
|
||||
@ -247,7 +272,7 @@ export default function StudioPage() {
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
)
|
||||
|
||||
|
||||
if (selectedPrompts.length === currentPagePrompts.length) {
|
||||
setSelectedPrompts([])
|
||||
} else {
|
||||
@ -284,7 +309,7 @@ export default function StudioPage() {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
@ -411,9 +436,8 @@ export default function StudioPage() {
|
||||
{currentPrompts.map((prompt) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
className={`bg-card rounded-lg border border-border p-4 hover:shadow-md transition-shadow cursor-pointer ${
|
||||
selectedPrompts.includes(prompt.id) ? 'ring-2 ring-primary' : ''
|
||||
}`}
|
||||
className={`bg-card rounded-lg border border-border p-4 hover:shadow-md transition-shadow cursor-pointer ${selectedPrompts.includes(prompt.id) ? 'ring-2 ring-primary' : ''
|
||||
}`}
|
||||
onClick={() => handleSelectPrompt(prompt.id)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
@ -442,15 +466,18 @@ export default function StudioPage() {
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{prompt.tags.slice(0, 3).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{prompt.tags.length > 3 && (
|
||||
{prompt.tags?.slice(0, 3).map((tag: any) => {
|
||||
const tagName = typeof tag === 'string' ? tag : tag?.name || '';
|
||||
return (
|
||||
<span
|
||||
key={tagName}
|
||||
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded-full"
|
||||
>
|
||||
{tagName}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{prompt.tags && prompt.tags.length > 3 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
+{prompt.tags.length - 3}
|
||||
</span>
|
||||
@ -526,9 +553,8 @@ export default function StudioPage() {
|
||||
{currentPrompts.map((prompt) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
className={`flex items-center p-3 bg-card rounded-lg border border-border hover:shadow-sm transition-shadow ${
|
||||
selectedPrompts.includes(prompt.id) ? 'ring-2 ring-primary' : ''
|
||||
}`}
|
||||
className={`flex items-center p-3 bg-card rounded-lg border border-border hover:shadow-sm transition-shadow ${selectedPrompts.includes(prompt.id) ? 'ring-2 ring-primary' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="w-8">
|
||||
<input
|
||||
|
@ -32,16 +32,16 @@ interface EditPromptModalProps {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export function EditPromptModal({
|
||||
prompt,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
userId
|
||||
export function EditPromptModal({
|
||||
prompt,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave,
|
||||
userId
|
||||
}: EditPromptModalProps) {
|
||||
const t = useTranslations('studio')
|
||||
const tCommon = useTranslations('common')
|
||||
|
||||
|
||||
const [name, setName] = useState(prompt.name)
|
||||
const [description, setDescription] = useState(prompt.description || '')
|
||||
const [tags, setTags] = useState<string[]>(prompt.tags)
|
||||
@ -53,7 +53,11 @@ export function EditPromptModal({
|
||||
if (isOpen) {
|
||||
setName(prompt.name)
|
||||
setDescription(prompt.description || '')
|
||||
setTags(prompt.tags)
|
||||
// 确保tags是字符串数组
|
||||
const processedTags = Array.isArray(prompt.tags)
|
||||
? prompt.tags.map((tag: any) => typeof tag === 'string' ? tag : tag.name || '')
|
||||
: []
|
||||
setTags(processedTags)
|
||||
fetchAvailableTags()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -123,11 +127,11 @@ export function EditPromptModal({
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-card border border-border rounded-lg shadow-lg w-full max-w-md mx-4 max-h-[90vh] overflow-hidden sm:mx-auto">
|
||||
{/* Header */}
|
||||
@ -184,7 +188,7 @@ export function EditPromptModal({
|
||||
<Tag className="h-3 w-3" />
|
||||
<span>{t('tags')}</span>
|
||||
</Label>
|
||||
|
||||
|
||||
{/* Current Tags */}
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
|
@ -10,16 +10,35 @@ export function useAuth() {
|
||||
const supabase = createClient()
|
||||
|
||||
useEffect(() => {
|
||||
// 同步用户到Prisma数据库
|
||||
const syncUser = async (_userData: User) => {
|
||||
try {
|
||||
await fetch('/api/users/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to sync user:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getUser = async () => {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
setUser(user)
|
||||
const { data: { user: userData } } = await supabase.auth.getUser()
|
||||
if (userData) {
|
||||
await syncUser(userData)
|
||||
}
|
||||
setUser(userData)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
getUser()
|
||||
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
|
||||
setUser(session?.user ?? null)
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||
const userData = session?.user ?? null
|
||||
if (userData && (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED')) {
|
||||
await syncUser(userData)
|
||||
}
|
||||
setUser(userData)
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user