fix prompt edit

This commit is contained in:
songtianlun 2025-07-30 14:46:42 +08:00
parent 2b2e311c58
commit 2707a510b5
12 changed files with 909 additions and 768 deletions

1072
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) => (

View File

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