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",
|
"name": "prmbr",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0",
|
||||||
|
"npm": ">=10.0.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run db:generate && next dev",
|
"dev": "npm run db:generate && next dev",
|
||||||
"build": "npm run db:generate && next build",
|
"build": "npm run db:generate && next build",
|
||||||
@ -41,4 +45,4 @@
|
|||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,10 +14,9 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(cuid())
|
id String @id // 使用Supabase用户ID,不再自动生成
|
||||||
email String @unique
|
email String @unique
|
||||||
username String @unique
|
username String? @unique // 允许为空,因为有些用户可能没有设置用户名
|
||||||
password String
|
|
||||||
avatar String?
|
avatar String?
|
||||||
bio String?
|
bio String?
|
||||||
language String @default("en")
|
language String @default("en")
|
||||||
|
@ -93,7 +93,7 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
|||||||
// 检查内容是否有变化,如果有则创建新版本
|
// 检查内容是否有变化,如果有则创建新版本
|
||||||
const currentVersion = existingPrompt.versions[0]
|
const currentVersion = existingPrompt.versions[0]
|
||||||
let shouldCreateVersion = false
|
let shouldCreateVersion = false
|
||||||
|
|
||||||
if (content && currentVersion && content !== currentVersion.content) {
|
if (content && currentVersion && content !== currentVersion.content) {
|
||||||
shouldCreateVersion = true
|
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) {
|
} catch (error) {
|
||||||
console.error('Error updating prompt:', 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) {
|
} catch (error) {
|
||||||
console.error('Error creating prompt:', 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,
|
new: false,
|
||||||
confirm: 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 [isLoading, setIsLoading] = useState(false)
|
||||||
const [profileLoading, setProfileLoading] = useState(true)
|
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 [avatarUploading, setAvatarUploading] = useState(false)
|
||||||
|
|
||||||
const supabase = createClient()
|
const supabase = createClient()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
|
||||||
loadProfile()
|
|
||||||
}
|
|
||||||
}, [user]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
const loadProfile = async () => {
|
|
||||||
if (!user) return
|
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 = {
|
const loadProfile = async () => {
|
||||||
id: userData?.id || '',
|
setProfileLoading(true)
|
||||||
email: userData?.email || '',
|
try {
|
||||||
username: userData?.user_metadata?.username || userData?.user_metadata?.full_name || '',
|
// Get user metadata and profile data
|
||||||
bio: userData?.user_metadata?.bio || '',
|
const { data: { user: userData }, error: userError } = await supabase.auth.getUser()
|
||||||
avatar_url: userData?.user_metadata?.avatar_url || '',
|
|
||||||
language: userData?.user_metadata?.language || 'en'
|
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) => {
|
const updateProfile = async (field: string, value: string) => {
|
||||||
if (!user) return
|
if (!user) return
|
||||||
|
|
||||||
setFieldLoading(prev => ({ ...prev, [field]: true }))
|
setFieldLoading(prev => ({ ...prev, [field]: true }))
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setSaveStatus({type: null, message: ''})
|
setSaveStatus({ type: null, message: '' })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updates: Record<string, string> = {}
|
const updates: Record<string, string> = {}
|
||||||
|
|
||||||
if (field === 'email') {
|
if (field === 'email') {
|
||||||
const { error } = await supabase.auth.updateUser({ email: value })
|
const { error } = await supabase.auth.updateUser({ email: value })
|
||||||
if (error) throw error
|
if (error) throw error
|
||||||
setSaveStatus({type: 'success', message: t('checkEmailToConfirm')})
|
setSaveStatus({ type: 'success', message: t('checkEmailToConfirm') })
|
||||||
} else {
|
} else {
|
||||||
updates[field] = value
|
updates[field] = value
|
||||||
const { error } = await supabase.auth.updateUser({
|
const { error } = await supabase.auth.updateUser({
|
||||||
data: updates
|
data: updates
|
||||||
})
|
})
|
||||||
if (error) throw error
|
if (error) throw error
|
||||||
setSaveStatus({type: 'success', message: `${field} ${t('updatedSuccessfully')}`})
|
setSaveStatus({ type: 'success', message: `${field} ${t('updatedSuccessfully')}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload profile
|
// Reload profile
|
||||||
await loadProfile()
|
await loadProfile()
|
||||||
|
|
||||||
// Reset editing state
|
// Reset editing state
|
||||||
setIsEditing(prev => ({ ...prev, [field]: false }))
|
setIsEditing(prev => ({ ...prev, [field]: false }))
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} 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 {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setFieldLoading(prev => ({ ...prev, [field]: false }))
|
setFieldLoading(prev => ({ ...prev, [field]: false }))
|
||||||
@ -139,27 +137,27 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
const updatePassword = async () => {
|
const updatePassword = async () => {
|
||||||
if (!formData.newPassword || formData.newPassword !== formData.confirmPassword) {
|
if (!formData.newPassword || formData.newPassword !== formData.confirmPassword) {
|
||||||
setSaveStatus({type: 'error', message: t('passwordsNotMatch')})
|
setSaveStatus({ type: 'error', message: t('passwordsNotMatch') })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.newPassword.length < 6) {
|
if (formData.newPassword.length < 6) {
|
||||||
setSaveStatus({type: 'error', message: t('passwordMinLength')})
|
setSaveStatus({ type: 'error', message: t('passwordMinLength') })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setFieldLoading(prev => ({ ...prev, password: true }))
|
setFieldLoading(prev => ({ ...prev, password: true }))
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setSaveStatus({type: null, message: ''})
|
setSaveStatus({ type: null, message: '' })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase.auth.updateUser({
|
const { error } = await supabase.auth.updateUser({
|
||||||
password: formData.newPassword
|
password: formData.newPassword
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) throw error
|
if (error) throw error
|
||||||
|
|
||||||
setSaveStatus({type: 'success', message: t('passwordUpdatedSuccessfully')})
|
setSaveStatus({ type: 'success', message: t('passwordUpdatedSuccessfully') })
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
currentPassword: '',
|
currentPassword: '',
|
||||||
@ -167,9 +165,9 @@ export default function ProfilePage() {
|
|||||||
confirmPassword: ''
|
confirmPassword: ''
|
||||||
}))
|
}))
|
||||||
setIsEditing(prev => ({ ...prev, password: false }))
|
setIsEditing(prev => ({ ...prev, password: false }))
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} 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 {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setFieldLoading(prev => ({ ...prev, password: false }))
|
setFieldLoading(prev => ({ ...prev, password: false }))
|
||||||
@ -182,34 +180,34 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
setAvatarUploading(true)
|
setAvatarUploading(true)
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setSaveStatus({type: null, message: ''})
|
setSaveStatus({ type: null, message: '' })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a unique filename for future use with actual file storage
|
// Create a unique filename for future use with actual file storage
|
||||||
// const fileExt = file.name.split('.').pop()
|
// const fileExt = file.name.split('.').pop()
|
||||||
// const fileName = `${user.id}-${Date.now()}.${fileExt}`
|
// const fileName = `${user.id}-${Date.now()}.${fileExt}`
|
||||||
|
|
||||||
// For now, we'll use a placeholder upload since we need to configure storage
|
// 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
|
// In a real implementation, you would upload to Supabase Storage or Cloudflare R2
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = async (e) => {
|
reader.onload = async (e) => {
|
||||||
const dataUrl = e.target?.result as string
|
const dataUrl = e.target?.result as string
|
||||||
|
|
||||||
const { error } = await supabase.auth.updateUser({
|
const { error } = await supabase.auth.updateUser({
|
||||||
data: { avatar_url: dataUrl }
|
data: { avatar_url: dataUrl }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) throw error
|
if (error) throw error
|
||||||
|
|
||||||
await loadProfile()
|
await loadProfile()
|
||||||
setSaveStatus({type: 'success', message: t('avatarUpdatedSuccessfully')})
|
setSaveStatus({ type: 'success', message: t('avatarUpdatedSuccessfully') })
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setAvatarUploading(false)
|
setAvatarUploading(false)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} 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)
|
setIsLoading(false)
|
||||||
setAvatarUploading(false)
|
setAvatarUploading(false)
|
||||||
}
|
}
|
||||||
@ -240,7 +238,7 @@ export default function ProfilePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-foreground mb-2">{t('title')}</h1>
|
<h1 className="text-3xl font-bold text-foreground mb-2">{t('title')}</h1>
|
||||||
@ -248,11 +246,10 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{saveStatus.type && (
|
{saveStatus.type && (
|
||||||
<div className={`mb-6 p-4 rounded-lg border ${
|
<div className={`mb-6 p-4 rounded-lg border ${saveStatus.type === 'success'
|
||||||
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-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'
|
: 'bg-destructive/10 border-destructive/20 text-destructive'
|
||||||
}`}>
|
}`}>
|
||||||
{saveStatus.message}
|
{saveStatus.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -268,8 +265,8 @@ export default function ProfilePage() {
|
|||||||
<h2 className="text-xl font-semibold text-foreground mb-4">{t('profilePicture')}</h2>
|
<h2 className="text-xl font-semibold text-foreground mb-4">{t('profilePicture')}</h2>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={profile?.avatar_url}
|
src={profile?.avatar_url}
|
||||||
alt="Profile Avatar"
|
alt="Profile Avatar"
|
||||||
size={96}
|
size={96}
|
||||||
className="w-24 h-24"
|
className="w-24 h-24"
|
||||||
@ -319,7 +316,7 @@ export default function ProfilePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditing.username ? (
|
{isEditing.username ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
@ -380,7 +377,7 @@ export default function ProfilePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditing.email ? (
|
{isEditing.email ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
@ -442,7 +439,7 @@ export default function ProfilePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditing.bio ? (
|
{isEditing.bio ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Textarea
|
<Textarea
|
||||||
@ -503,7 +500,7 @@ export default function ProfilePage() {
|
|||||||
<Globe className="w-5 h-5 text-muted-foreground" />
|
<Globe className="w-5 h-5 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={formData.language}
|
value={formData.language}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@ -540,7 +537,7 @@ export default function ProfilePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditing.password ? (
|
{isEditing.password ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@ -565,7 +562,7 @@ export default function ProfilePage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="confirmPassword">{t('confirmNewPassword')}</Label>
|
<Label htmlFor="confirmPassword">{t('confirmNewPassword')}</Label>
|
||||||
<div className="relative mt-1">
|
<div className="relative mt-1">
|
||||||
@ -588,7 +585,7 @@ export default function ProfilePage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -10,8 +10,8 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
||||||
import {
|
import {
|
||||||
Play,
|
Play,
|
||||||
Save,
|
Save,
|
||||||
Copy,
|
Copy,
|
||||||
Settings,
|
Settings,
|
||||||
@ -63,35 +63,43 @@ export default function PromptPage({ params }: PromptPageProps) {
|
|||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
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(() => {
|
useEffect(() => {
|
||||||
if (!loading && !user) {
|
if (!loading && !user) {
|
||||||
router.push('/signin')
|
router.push('/signin')
|
||||||
} else if (user && promptId) {
|
return
|
||||||
fetchPrompt()
|
|
||||||
}
|
}
|
||||||
}, [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 (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
{/* Top Navigation */}
|
{/* Top Navigation */}
|
||||||
<div className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
<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">
|
<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">
|
<div className="flex items-center space-x-2">
|
||||||
<FileText className="h-5 w-5 text-muted-foreground" />
|
<FileText className="h-5 w-5 text-muted-foreground" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-semibold text-foreground">{prompt.name}</h1>
|
<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>
|
<p className="text-xs text-muted-foreground">Version {prompt?.currentVersion || 1} • {prompt?.usage || 0} uses</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button variant="outline" size="sm" className="flex items-center space-x-1">
|
<Button variant="outline" size="sm" className="flex items-center space-x-1">
|
||||||
<History className="h-4 w-4" />
|
<History className="h-4 w-4" />
|
||||||
@ -232,7 +240,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
|||||||
<Plus className="h-3 w-3" />
|
<Plus className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<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="flex items-start space-x-3">
|
||||||
<div className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0"></div>
|
<div className="w-2 h-2 rounded-full bg-primary mt-2 flex-shrink-0"></div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-medium text-foreground text-sm truncate">{prompt.name}</h3>
|
<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>
|
<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">
|
<div className="flex items-center mt-2 space-x-2">
|
||||||
{prompt.tags.slice(0, 1).map((tag) => (
|
{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">
|
<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">
|
||||||
{tag}
|
{typeof tag === 'string' ? tag : (tag as any)?.name || ''}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{prompt.tags.length > 1 && (
|
{prompt?.tags && prompt.tags.length > 1 && (
|
||||||
<span className="text-xs text-muted-foreground">+{prompt.tags.length - 1}</span>
|
<span className="text-xs text-muted-foreground">+{prompt.tags.length - 1}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -325,30 +333,33 @@ export default function PromptPage({ params }: PromptPageProps) {
|
|||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-muted-foreground">Created</span>
|
<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>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-muted-foreground">Updated</span>
|
<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>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-muted-foreground">Last used</span>
|
<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>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-muted-foreground">Usage count</span>
|
<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>
|
||||||
<div className="pt-2 border-t border-border">
|
<div className="pt-2 border-t border-border">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{prompt.tags.map((tag: string) => (
|
{prompt?.tags?.map((tag: any) => {
|
||||||
<span
|
const tagName = typeof tag === 'string' ? tag : tag?.name || '';
|
||||||
key={tag}
|
return (
|
||||||
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded-full"
|
<span
|
||||||
>
|
key={tagName}
|
||||||
{tag}
|
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded-full"
|
||||||
</span>
|
>
|
||||||
))}
|
{tagName}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -372,7 +383,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
|||||||
)}
|
)}
|
||||||
{tCommon('save')}
|
{tCommon('save')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleRunTest}
|
onClick={handleRunTest}
|
||||||
@ -428,7 +439,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
|||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="promptContent" className="text-sm font-medium">
|
<Label htmlFor="promptContent" className="text-sm font-medium">
|
||||||
{t('promptContent')}
|
{t('promptContent')}
|
||||||
@ -463,7 +474,7 @@ export default function PromptPage({ params }: PromptPageProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="bg-muted rounded-lg min-h-[400px] p-4">
|
<div className="bg-muted rounded-lg min-h-[400px] p-4">
|
||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
|
@ -10,7 +10,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
import { LoadingSpinner } from '@/components/ui/loading-spinner'
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Save,
|
Save,
|
||||||
Tag,
|
Tag,
|
||||||
@ -31,27 +31,28 @@ export default function NewPromptPage() {
|
|||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [availableTags, setAvailableTags] = useState<string[]>([])
|
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(() => {
|
useEffect(() => {
|
||||||
if (!loading && !user) {
|
if (!loading && !user) {
|
||||||
router.push('/signin')
|
router.push('/signin')
|
||||||
} else if (user) {
|
return
|
||||||
fetchAvailableTags()
|
|
||||||
}
|
}
|
||||||
}, [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 (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<div className="border-b">
|
<div className="border-b">
|
||||||
<div className="max-w-4xl mx-auto px-4 py-4">
|
<div className="max-w-4xl mx-auto px-4 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<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>
|
<p className="text-sm text-muted-foreground">Create a new AI prompt</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -152,7 +153,7 @@ export default function NewPromptPage() {
|
|||||||
>
|
>
|
||||||
{tCommon('cancel')}
|
{tCommon('cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
@ -176,7 +177,7 @@ export default function NewPromptPage() {
|
|||||||
{/* Basic Information */}
|
{/* Basic Information */}
|
||||||
<div className="bg-card rounded-lg border border-border p-6">
|
<div className="bg-card rounded-lg border border-border p-6">
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-4">Basic Information</h2>
|
<h2 className="text-lg font-semibold text-foreground mb-4">Basic Information</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="promptName">{t('promptName')} *</Label>
|
<Label htmlFor="promptName">{t('promptName')} *</Label>
|
||||||
@ -189,7 +190,7 @@ export default function NewPromptPage() {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="promptDescription">{t('promptDescription')}</Label>
|
<Label htmlFor="promptDescription">{t('promptDescription')}</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -206,7 +207,7 @@ export default function NewPromptPage() {
|
|||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div className="bg-card rounded-lg border border-border p-6">
|
<div className="bg-card rounded-lg border border-border p-6">
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-4">{t('tags')}</h2>
|
<h2 className="text-lg font-semibold text-foreground mb-4">{t('tags')}</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
@ -225,7 +226,7 @@ export default function NewPromptPage() {
|
|||||||
{t('addTag')}
|
{t('addTag')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tags.length > 0 && (
|
{tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
@ -273,7 +274,7 @@ export default function NewPromptPage() {
|
|||||||
{/* Prompt Content */}
|
{/* Prompt Content */}
|
||||||
<div className="bg-card rounded-lg border border-border p-6">
|
<div className="bg-card rounded-lg border border-border p-6">
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-4">{t('promptContent')}</h2>
|
<h2 className="text-lg font-semibold text-foreground mb-4">{t('promptContent')}</h2>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="promptContent">Content</Label>
|
<Label htmlFor="promptContent">Content</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
|
@ -78,16 +78,6 @@ export default function StudioPage() {
|
|||||||
totalPages: 0
|
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
|
// Fetch prompts from API
|
||||||
const fetchPrompts = async () => {
|
const fetchPrompts = async () => {
|
||||||
if (!user) return
|
if (!user) return
|
||||||
@ -117,27 +107,62 @@ export default function StudioPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch tags from API
|
// Initial load effect
|
||||||
const fetchTags = async () => {
|
useEffect(() => {
|
||||||
|
if (!loading && !user) {
|
||||||
|
router.push('/signin')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) return
|
if (!user) return
|
||||||
|
|
||||||
try {
|
const fetchTags = async () => {
|
||||||
const response = await fetch(`/api/tags?userId=${user.id}`)
|
try {
|
||||||
if (response.ok) {
|
const response = await fetch(`/api/tags?userId=${user.id}`)
|
||||||
const tags = await response.json()
|
if (response.ok) {
|
||||||
setAllTags(tags.map((tag: { name: string }) => tag.name))
|
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
|
// Refetch when filters change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (!user) return
|
||||||
fetchPrompts()
|
|
||||||
|
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])
|
}, [currentPage, itemsPerPage, searchQuery, selectedTag, sortField, sortOrder, user])
|
||||||
|
|
||||||
// Since filtering and sorting is now done on the server,
|
// Since filtering and sorting is now done on the server,
|
||||||
@ -235,8 +260,8 @@ export default function StudioPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectPrompt = (id: string) => {
|
const handleSelectPrompt = (id: string) => {
|
||||||
setSelectedPrompts(prev =>
|
setSelectedPrompts(prev =>
|
||||||
prev.includes(id)
|
prev.includes(id)
|
||||||
? prev.filter(pId => pId !== id)
|
? prev.filter(pId => pId !== id)
|
||||||
: [...prev, id]
|
: [...prev, id]
|
||||||
)
|
)
|
||||||
@ -247,7 +272,7 @@ export default function StudioPage() {
|
|||||||
(currentPage - 1) * itemsPerPage,
|
(currentPage - 1) * itemsPerPage,
|
||||||
currentPage * itemsPerPage
|
currentPage * itemsPerPage
|
||||||
)
|
)
|
||||||
|
|
||||||
if (selectedPrompts.length === currentPagePrompts.length) {
|
if (selectedPrompts.length === currentPagePrompts.length) {
|
||||||
setSelectedPrompts([])
|
setSelectedPrompts([])
|
||||||
} else {
|
} else {
|
||||||
@ -284,7 +309,7 @@ export default function StudioPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-4 py-8">
|
<div className="max-w-7xl mx-auto px-4 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
@ -411,9 +436,8 @@ export default function StudioPage() {
|
|||||||
{currentPrompts.map((prompt) => (
|
{currentPrompts.map((prompt) => (
|
||||||
<div
|
<div
|
||||||
key={prompt.id}
|
key={prompt.id}
|
||||||
className={`bg-card rounded-lg border border-border p-4 hover:shadow-md transition-shadow cursor-pointer ${
|
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' : ''
|
||||||
selectedPrompts.includes(prompt.id) ? 'ring-2 ring-primary' : ''
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => handleSelectPrompt(prompt.id)}
|
onClick={() => handleSelectPrompt(prompt.id)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-3">
|
<div className="flex items-start justify-between mb-3">
|
||||||
@ -442,15 +466,18 @@ export default function StudioPage() {
|
|||||||
|
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
<div className="flex flex-wrap gap-1 mb-3">
|
<div className="flex flex-wrap gap-1 mb-3">
|
||||||
{prompt.tags.slice(0, 3).map((tag) => (
|
{prompt.tags?.slice(0, 3).map((tag: any) => {
|
||||||
<span
|
const tagName = typeof tag === 'string' ? tag : tag?.name || '';
|
||||||
key={tag}
|
return (
|
||||||
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded-full"
|
<span
|
||||||
>
|
key={tagName}
|
||||||
{tag}
|
className="inline-flex items-center px-2 py-1 text-xs font-medium bg-muted text-muted-foreground rounded-full"
|
||||||
</span>
|
>
|
||||||
))}
|
{tagName}
|
||||||
{prompt.tags.length > 3 && (
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{prompt.tags && prompt.tags.length > 3 && (
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
+{prompt.tags.length - 3}
|
+{prompt.tags.length - 3}
|
||||||
</span>
|
</span>
|
||||||
@ -526,9 +553,8 @@ export default function StudioPage() {
|
|||||||
{currentPrompts.map((prompt) => (
|
{currentPrompts.map((prompt) => (
|
||||||
<div
|
<div
|
||||||
key={prompt.id}
|
key={prompt.id}
|
||||||
className={`flex items-center p-3 bg-card rounded-lg border border-border hover:shadow-sm transition-shadow ${
|
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' : ''
|
||||||
selectedPrompts.includes(prompt.id) ? 'ring-2 ring-primary' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="w-8">
|
<div className="w-8">
|
||||||
<input
|
<input
|
||||||
|
@ -32,16 +32,16 @@ interface EditPromptModalProps {
|
|||||||
userId: string
|
userId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditPromptModal({
|
export function EditPromptModal({
|
||||||
prompt,
|
prompt,
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
userId
|
userId
|
||||||
}: EditPromptModalProps) {
|
}: EditPromptModalProps) {
|
||||||
const t = useTranslations('studio')
|
const t = useTranslations('studio')
|
||||||
const tCommon = useTranslations('common')
|
const tCommon = useTranslations('common')
|
||||||
|
|
||||||
const [name, setName] = useState(prompt.name)
|
const [name, setName] = useState(prompt.name)
|
||||||
const [description, setDescription] = useState(prompt.description || '')
|
const [description, setDescription] = useState(prompt.description || '')
|
||||||
const [tags, setTags] = useState<string[]>(prompt.tags)
|
const [tags, setTags] = useState<string[]>(prompt.tags)
|
||||||
@ -53,7 +53,11 @@ export function EditPromptModal({
|
|||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
setName(prompt.name)
|
setName(prompt.name)
|
||||||
setDescription(prompt.description || '')
|
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()
|
fetchAvailableTags()
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -123,11 +127,11 @@ export function EditPromptModal({
|
|||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* 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">
|
<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 */}
|
{/* Header */}
|
||||||
@ -184,7 +188,7 @@ export function EditPromptModal({
|
|||||||
<Tag className="h-3 w-3" />
|
<Tag className="h-3 w-3" />
|
||||||
<span>{t('tags')}</span>
|
<span>{t('tags')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{/* Current Tags */}
|
{/* Current Tags */}
|
||||||
<div className="mt-2 flex flex-wrap gap-2">
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
|
@ -10,16 +10,35 @@ export function useAuth() {
|
|||||||
const supabase = createClient()
|
const supabase = createClient()
|
||||||
|
|
||||||
useEffect(() => {
|
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 getUser = async () => {
|
||||||
const { data: { user } } = await supabase.auth.getUser()
|
const { data: { user: userData } } = await supabase.auth.getUser()
|
||||||
setUser(user)
|
if (userData) {
|
||||||
|
await syncUser(userData)
|
||||||
|
}
|
||||||
|
setUser(userData)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser()
|
getUser()
|
||||||
|
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
|
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
||||||
setUser(session?.user ?? null)
|
const userData = session?.user ?? null
|
||||||
|
if (userData && (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED')) {
|
||||||
|
await syncUser(userData)
|
||||||
|
}
|
||||||
|
setUser(userData)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user