diff --git a/CLAUDE.md b/CLAUDE.md index 567ac84..3429eee 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,13 +132,12 @@ Required environment variables: - [ ] Published - [ ] View Count - [ ] Prompt Debugger - - [ ] Main Version - - [x] Prompt Version Controll - [x] Generate a new version when save - [x] Save last [LIMIT] versions - - [ ] [LIMIT] can setting in user profile + - [ ] [LIMIT] can setting in use profile - [ ] [LIMIT] max is by Subscribe + - [ ] Delete Version Button - [x] Prompt Debugger run - [x] Select AI Model - [x] Input Prompt Content diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c97da52..def4488 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,14 +14,17 @@ datasource db { } model User { - id String @id // 使用Supabase用户ID,不再自动生成 - email String @unique - username String? @unique // 允许为空,因为有些用户可能没有设置用户名 - avatar String? - bio String? - language String @default("en") - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id // 使用Supabase用户ID,不再自动生成 + email String @unique + username String? @unique // 允许为空,因为有些用户可能没有设置用户名 + avatar String? + bio String? + language String @default("en") + versionLimit Int @default(3) // 版本数量限制,可在用户配置中设置 + subscribePlan String @default("free") // 订阅计划: "free", "pro" + maxVersionLimit Int @default(3) // 基于订阅的最大版本限制 + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt prompts Prompt[] diff --git a/src/app/api/prompts/[id]/route.ts b/src/app/api/prompts/[id]/route.ts index 6e40953..5cf4ceb 100644 --- a/src/app/api/prompts/[id]/route.ts +++ b/src/app/api/prompts/[id]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { prisma } from '@/lib/prisma' +import { getVersionsToDelete } from '@/lib/subscription' interface RouteParams { params: Promise<{ id: string }> @@ -67,10 +68,13 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) } - // 验证 prompt 是否存在且属于用户 + // 验证 prompt 是否存在且属于用户,并获取用户信息 const existingPrompt = await prisma.prompt.findFirst({ where: { id, userId }, - include: { versions: { orderBy: { version: 'desc' }, take: 1 } } + include: { + versions: { orderBy: { version: 'desc' } }, + user: true + } }) if (!existingPrompt) { @@ -96,6 +100,9 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { if (content && currentVersion && content !== currentVersion.content) { shouldCreateVersion = true + } else if (content && !currentVersion) { + // 如果没有版本历史,创建第一个版本 + shouldCreateVersion = true } // 更新 prompt @@ -118,8 +125,27 @@ export async function PUT(request: NextRequest, { params }: RouteParams) { } }) - // 如果内容有变化,创建新版本 + // 如果内容有变化,创建新版本并处理版本限制 if (shouldCreateVersion && content) { + const user = existingPrompt.user + const currentVersionCount = existingPrompt.versions.length + + // 检查是否需要删除旧版本以遵守版本限制 + const versionsToDelete = getVersionsToDelete(currentVersionCount, user) + + if (versionsToDelete > 0) { + // 删除最旧的版本 + const oldestVersions = existingPrompt.versions + .slice(-versionsToDelete) + .map(v => v.id) + + await prisma.promptVersion.deleteMany({ + where: { + id: { in: oldestVersions } + } + }) + } + const nextVersion = (currentVersion?.version || 0) + 1 await prisma.promptVersion.create({ data: { diff --git a/src/app/api/users/profile/route.ts b/src/app/api/users/profile/route.ts new file mode 100644 index 0000000..138b50f --- /dev/null +++ b/src/app/api/users/profile/route.ts @@ -0,0 +1,124 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { getMaxVersionLimit } from '@/lib/subscription' + +// GET /api/users/profile - 获取用户配置 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + email: true, + username: true, + avatar: true, + bio: true, + language: true, + versionLimit: true, + subscribePlan: true, + maxVersionLimit: true, + createdAt: true, + updatedAt: true + } + }) + + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + return NextResponse.json(user) + + } catch (error) { + console.error('Error fetching user profile:', error) + return NextResponse.json( + { error: 'Failed to fetch user profile' }, + { status: 500 } + ) + } +} + +// PUT /api/users/profile - 更新用户配置 +export async function PUT(request: NextRequest) { + try { + const body = await request.json() + const { + userId, + username, + avatar, + bio, + language, + versionLimit + } = body + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + // 获取用户当前信息以验证版本限制 + const currentUser = await prisma.user.findUnique({ + where: { id: userId }, + select: { subscribePlan: true, maxVersionLimit: true } + }) + + if (!currentUser) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }) + } + + // 验证版本限制不能超过订阅计划的最大限制 + let finalVersionLimit = versionLimit + if (versionLimit !== undefined) { + const maxVersionLimit = getMaxVersionLimit(currentUser.subscribePlan) + if (versionLimit > maxVersionLimit) { + return NextResponse.json( + { + error: 'Version limit exceeds subscription plan maximum', + maxAllowed: maxVersionLimit + }, + { status: 400 } + ) + } + finalVersionLimit = Math.max(1, Math.min(versionLimit, maxVersionLimit)) + } + + const updateData: Record = {} + if (username !== undefined) updateData.username = username + if (avatar !== undefined) updateData.avatar = avatar + if (bio !== undefined) updateData.bio = bio + if (language !== undefined) updateData.language = language + if (finalVersionLimit !== undefined) updateData.versionLimit = finalVersionLimit + + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: updateData, + select: { + id: true, + email: true, + username: true, + avatar: true, + bio: true, + language: true, + versionLimit: true, + subscribePlan: true, + maxVersionLimit: true, + createdAt: true, + updatedAt: true + } + }) + + return NextResponse.json(updatedUser) + + } catch (error) { + console.error('Error updating user profile:', error) + return NextResponse.json( + { error: 'Failed to update user profile' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 27cc6cf..0857cb7 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -19,8 +19,13 @@ interface UserProfile { email: string username?: string bio?: string - avatar_url?: string + avatar?: string language?: 'en' | 'zh' + versionLimit: number + subscribePlan: string + maxVersionLimit: number + createdAt?: string + updatedAt?: string } export default function ProfilePage() { @@ -33,7 +38,8 @@ export default function ProfilePage() { username: false, email: false, bio: false, - password: false + password: false, + versionLimit: false }) const [formData, setFormData] = useState({ username: '', @@ -42,7 +48,8 @@ export default function ProfilePage() { currentPassword: '', newPassword: '', confirmPassword: '', - language: 'en' as 'en' | 'zh' + language: 'en' as 'en' | 'zh', + versionLimit: 3 }) const [showPasswords, setShowPasswords] = useState({ current: false, @@ -58,21 +65,17 @@ export default function ProfilePage() { const supabase = createClient() const loadProfile = useCallback(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' + // Get profile data from our API + const response = await fetch(`/api/users/profile?userId=${user.id}`) + if (!response.ok) { + throw new Error('Failed to fetch profile') } + + const profileData: UserProfile = await response.json() setProfile(profileData) setFormData({ @@ -82,7 +85,8 @@ export default function ProfilePage() { currentPassword: '', newPassword: '', confirmPassword: '', - language: profileData.language || 'en' + language: profileData.language || 'en', + versionLimit: profileData.versionLimit }) } catch (error) { console.error('Error loading profile:', error) @@ -90,14 +94,14 @@ export default function ProfilePage() { } finally { setProfileLoading(false) } - }, [supabase, t]) + }, [user, t]) useEffect(() => { if (!user) return loadProfile() }, [user, loadProfile]) - const updateProfile = async (field: string, value: string) => { + const updateProfile = async (field: string, value: string | number) => { if (!user) return setFieldLoading(prev => ({ ...prev, [field]: true })) @@ -105,18 +109,26 @@ export default function ProfilePage() { setSaveStatus({ type: null, message: '' }) try { - const updates: Record = {} - if (field === 'email') { - const { error } = await supabase.auth.updateUser({ email: value }) + const { error } = await supabase.auth.updateUser({ email: value as string }) if (error) throw error setSaveStatus({ type: 'success', message: t('checkEmailToConfirm') }) } else { - updates[field] = value - const { error } = await supabase.auth.updateUser({ - data: updates + // Use our API for other fields + const updateData: Record = { userId: user.id } + updateData[field] = value + + const response = await fetch('/api/users/profile', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updateData) }) - if (error) throw error + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to update profile') + } + setSaveStatus({ type: 'success', message: `${field} ${t('updatedSuccessfully')}` }) } @@ -192,11 +204,20 @@ export default function ProfilePage() { reader.onload = async (e) => { const dataUrl = e.target?.result as string - const { error } = await supabase.auth.updateUser({ - data: { avatar_url: dataUrl } + // Update avatar using our API + const response = await fetch('/api/users/profile', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user.id, + avatar: dataUrl + }) }) - if (error) throw error + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || 'Failed to update avatar') + } await loadProfile() setSaveStatus({ type: 'success', message: t('avatarUpdatedSuccessfully') }) @@ -265,7 +286,7 @@ export default function ProfilePage() {
)} + + {/* Version Limit Settings */} + {profileLoading ? ( + + ) : ( + +
+
+
+

Version Limit

+

+ Maximum number of versions to keep for each prompt +

+
+ {!isEditing.versionLimit && ( + + )} +
+ + {isEditing.versionLimit ? ( +
+
+
+ setFormData(prev => ({ + ...prev, + versionLimit: Math.max(1, Math.min(parseInt(e.target.value) || 1, profile?.maxVersionLimit || 3)) + }))} + className="w-24" + disabled={fieldLoading.versionLimit} + /> + + versions (max: {profile?.maxVersionLimit || 3} for {profile?.subscribePlan || 'free'} plan) + +
+

+ When this limit is reached, oldest versions will be automatically deleted when new versions are created. +

+
+
+ + +
+
+ ) : ( +
+

+ Current limit: {profile?.versionLimit || 3} versions +

+
+

Subscription: {profile?.subscribePlan || 'free'}

+

Maximum allowed: {profile?.maxVersionLimit || 3} versions

+
+
+ )} +
+
+ )}
diff --git a/src/app/studio/[id]/page.tsx b/src/app/studio/[id]/page.tsx index c8742fc..8f59edf 100644 --- a/src/app/studio/[id]/page.tsx +++ b/src/app/studio/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useState, useRef } from 'react' import { useTranslations } from 'next-intl' import { useAuth } from '@/hooks/useAuth' import { useRouter } from 'next/navigation' @@ -10,15 +10,12 @@ 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 { VersionTimeline, VersionTimelineRef } from '@/components/studio/VersionTimeline' import { Play, Save, Copy, Settings, - Plus, - Search, - Filter, - MoreHorizontal, Zap, History, ArrowLeft, @@ -43,6 +40,14 @@ interface PromptData { usage?: number } +interface PromptVersion { + id: string + version: number + content: string + changelog: string + createdAt: string +} + export default function PromptPage({ params }: PromptPageProps) { const [promptId, setPromptId] = useState('') @@ -62,6 +67,11 @@ export default function PromptPage({ params }: PromptPageProps) { const [isRunning, setIsRunning] = useState(false) const [isSaving, setIsSaving] = useState(false) const [isLoading, setIsLoading] = useState(true) + const [currentVersion, setCurrentVersion] = useState(null) + const [originalContent, setOriginalContent] = useState('') + const [originalTitle, setOriginalTitle] = useState('') + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + const versionTimelineRef = useRef(null) useEffect(() => { if (!loading && !user) { @@ -87,6 +97,17 @@ export default function PromptPage({ params }: PromptPageProps) { setPrompt(processedData) setPromptContent(data.content) setPromptTitle(data.name) + setOriginalContent(data.content) + setOriginalTitle(data.name) + + // 获取当前版本信息 + const versionResponse = await fetch(`/api/prompts/${promptId}/versions?userId=${user.id}&limit=1`) + if (versionResponse.ok) { + const versionData = await versionResponse.json() + if (versionData.length > 0) { + setCurrentVersion(versionData[0]) + } + } } else { router.push('/studio') } @@ -101,6 +122,12 @@ export default function PromptPage({ params }: PromptPageProps) { fetchPrompt() }, [user, loading, router, promptId]) + // 检测未保存的更改 + useEffect(() => { + const hasChanges = promptContent !== originalContent || promptTitle !== originalTitle + setHasUnsavedChanges(hasChanges) + }, [promptContent, promptTitle, originalContent, originalTitle]) + const handleRunTest = async () => { @@ -151,7 +178,22 @@ export default function PromptPage({ params }: PromptPageProps) { if (response.ok) { const updatedPrompt = await response.json() setPrompt(updatedPrompt) - // Show success message + setOriginalContent(promptContent) + setOriginalTitle(promptTitle) + + // 获取新的版本信息 + const versionResponse = await fetch(`/api/prompts/${promptId}/versions?userId=${user.id}&limit=1`) + if (versionResponse.ok) { + const versionData = await versionResponse.json() + if (versionData.length > 0) { + setCurrentVersion(versionData[0]) + } + } + + // 刷新版本历史列表 + if (versionTimelineRef.current) { + await versionTimelineRef.current.refreshVersions() + } } } catch (error) { console.error('Failed to save prompt:', error) @@ -165,6 +207,45 @@ export default function PromptPage({ params }: PromptPageProps) { // Show success message } + const handleVersionSelect = (version: PromptVersion) => { + setPromptContent(version.content) + setOriginalContent(version.content) + setCurrentVersion(version) + setTestResult('') // 清除测试结果 + + // 重置保存状态 + setHasUnsavedChanges(false) + } + + const handleVersionRestore = async (version: PromptVersion) => { + if (!user || !promptId) return + + try { + const response = await fetch(`/api/prompts/${promptId}/versions/${version.id}/restore`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId: user.id }) + }) + + if (response.ok) { + const updatedPrompt = await response.json() + setPrompt(updatedPrompt) + setPromptContent(version.content) + setOriginalContent(version.content) + setCurrentVersion(version) + setTestResult('') + + // 刷新版本历史列表 + if (versionTimelineRef.current) { + await versionTimelineRef.current.refreshVersions() + } + } + } catch (error) { + console.error('Error restoring version:', error) + throw error + } + } + if (loading || isLoading) { return (
@@ -230,101 +311,18 @@ export default function PromptPage({ params }: PromptPageProps) {
- {/* Left Sidebar - Prompt List */} + {/* Left Sidebar - Version Timeline */}
-
-
-
-

{t('myPrompts')}

- -
- - {/* Search */} -
- - -
-
- - {/* Filters */} -
-
- - 3 prompts -
-
- - {/* Prompt List */} -
- {/* Active Prompt */} -
-
-
-
-

{prompt?.name || 'Loading...'}

-

{prompt?.description || ''}

-
- {prompt?.tags?.slice(0, 1).map((tag) => ( - - {typeof tag === 'string' ? tag : (tag as { name: string })?.name || ''} - - ))} - {prompt?.tags && prompt.tags.length > 1 && ( - +{prompt.tags.length - 1} - )} -
-
- -
-
- - {/* Other Prompts */} -
-
-
-
-

Code Review Assistant

-

Help review code for best practices and potential issues

-
- - development - -
-
- -
-
- -
-
-
-
-

Content Summarizer

-

Summarize long articles into key points

-
- - content - -
-
- -
-
-
+
+
{/* Prompt Info */} @@ -408,9 +406,18 @@ export default function PromptPage({ params }: PromptPageProps) {
-
- - Auto-save enabled +
+ {hasUnsavedChanges ? ( + <> +
+ Unsaved changes + + ) : ( + <> + + All changes saved + + )}
diff --git a/src/components/studio/VersionTimeline.tsx b/src/components/studio/VersionTimeline.tsx new file mode 100644 index 0000000..190bef5 --- /dev/null +++ b/src/components/studio/VersionTimeline.tsx @@ -0,0 +1,364 @@ +'use client' + +import { useState, useEffect, useImperativeHandle, forwardRef, useCallback } from 'react' +import { useTranslations } from 'next-intl' +import { Button } from '@/components/ui/button' +import { LoadingSpinner } from '@/components/ui/loading-spinner' +import { + History, + Eye, + RotateCcw, + AlertTriangle +} from 'lucide-react' + +interface PromptVersion { + id: string + version: number + content: string + changelog: string + createdAt: string +} + +interface VersionTimelineProps { + promptId: string + userId: string + currentVersion?: PromptVersion | null + hasUnsavedChanges?: boolean + onVersionSelect: (version: PromptVersion) => void + onVersionRestore: (version: PromptVersion) => void +} + +interface UserLimits { + versionLimit: number + maxVersionLimit: number + subscribePlan: string +} + +export interface VersionTimelineRef { + refreshVersions: () => Promise +} + +export const VersionTimeline = forwardRef(({ + promptId, + userId, + currentVersion, + hasUnsavedChanges = false, + onVersionSelect, + onVersionRestore +}, ref) => { + const t = useTranslations('studio') + const [versions, setVersions] = useState([]) + const [loading, setLoading] = useState(true) + const [selectedVersionId, setSelectedVersionId] = useState(null) + const [restoring, setRestoring] = useState(null) + const [showWarning, setShowWarning] = useState(false) + const [pendingVersionId, setPendingVersionId] = useState(null) + const [userLimits, setUserLimits] = useState(null) + + useEffect(() => { + fetchVersions() + fetchUserLimits() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [promptId, userId]) + + useEffect(() => { + if (currentVersion) { + setSelectedVersionId(currentVersion.id) + } + }, [currentVersion]) + + const fetchVersions = useCallback(async () => { + try { + setLoading(true) + const response = await fetch(`/api/prompts/${promptId}/versions?userId=${userId}`) + if (response.ok) { + const data = await response.json() + setVersions(data) + if (data.length > 0 && !selectedVersionId) { + setSelectedVersionId(data[0].id) + } + } + } catch (error) { + console.error('Error fetching versions:', error) + } finally { + setLoading(false) + } + }, [promptId, userId, selectedVersionId]) + + // 暴露刷新函数给父组件 + useImperativeHandle(ref, () => ({ + refreshVersions: fetchVersions + }), [fetchVersions]) + + const fetchUserLimits = async () => { + try { + const response = await fetch(`/api/users/profile?userId=${userId}`) + if (response.ok) { + const userData = await response.json() + setUserLimits({ + versionLimit: userData.versionLimit, + maxVersionLimit: userData.maxVersionLimit, + subscribePlan: userData.subscribePlan + }) + } + } catch (error) { + console.error('Error fetching user limits:', error) + } + } + + const handleVersionSelect = (version: PromptVersion) => { + if (hasUnsavedChanges && version.id !== selectedVersionId) { + setShowWarning(true) + setPendingVersionId(version.id) + return + } + + setSelectedVersionId(version.id) + onVersionSelect(version) + } + + const confirmVersionSwitch = () => { + if (pendingVersionId) { + const version = versions.find(v => v.id === pendingVersionId) + if (version) { + setSelectedVersionId(version.id) + onVersionSelect(version) + } + } + setShowWarning(false) + setPendingVersionId(null) + } + + const cancelVersionSwitch = () => { + setShowWarning(false) + setPendingVersionId(null) + } + + const handleRestoreVersion = async (version: PromptVersion) => { + if (restoring) return + + try { + setRestoring(version.id) + await onVersionRestore(version) + await fetchVersions() + setSelectedVersionId(version.id) + } catch (error) { + console.error('Error restoring version:', error) + } finally { + setRestoring(null) + } + } + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + const now = new Date() + const diff = now.getTime() - date.getTime() + const hours = Math.floor(diff / (1000 * 60 * 60)) + const days = Math.floor(hours / 24) + + if (days === 0) { + if (hours === 0) { + const minutes = Math.floor(diff / (1000 * 60)) + return minutes <= 0 ? 'Just now' : `${minutes}m ago` + } + return `${hours}h ago` + } else if (days < 7) { + return `${days}d ago` + } else { + return new Intl.DateTimeFormat('default', { + month: 'short', + day: 'numeric' + }).format(date) + } + } + + if (loading) { + return ( +
+ +
+ ) + } + + return ( + <> +
+ {/* Header */} +
+
+ +

{t('versionHistory')}

+ + ({versions.length}) + +
+ {userLimits && ( +
+
+ Limit: {userLimits.versionLimit}/{userLimits.maxVersionLimit} + {userLimits.subscribePlan} +
+
+
= userLimits.versionLimit ? 'bg-orange-500' : 'bg-primary' + }`} + style={{ width: `${Math.min(100, (versions.length / userLimits.versionLimit) * 100)}%` }} + >
+
+
+ )} +
+ + {/* Timeline */} +
+ {/* Timeline line */} +
+ +
+ {versions.map((version, index) => { + const isSelected = selectedVersionId === version.id + const isLatest = index === 0 + const isCurrentVersion = currentVersion?.id === version.id + + return ( +
handleVersionSelect(version)} + > + {/* Timeline dot */} +
+ {isCurrentVersion && hasUnsavedChanges && ( +
+ )} +
+ +
+
+
+ + v{version.version} + + {isLatest && ( + + Latest + + )} + {isCurrentVersion && hasUnsavedChanges && ( + + Modified + + )} +
+ + {formatDate(version.createdAt)} + +
+ +

+ {version.changelog} +

+ + {/* Action buttons - only show on hover or when selected */} + {isSelected && ( +
+ + {!isLatest && ( + + )} +
+ )} +
+
+ ) + })} +
+
+ + {versions.length === 0 && ( +
+ +

No versions yet

+
+ )} +
+ + {/* Warning Modal */} + {showWarning && ( +
+
+
+ +
+

+ Unsaved Changes +

+

+ You have unsaved changes that will be lost if you switch versions. Are you sure you want to continue? +

+
+ + +
+
+
+
+
+ )} + + ) +}) + +VersionTimeline.displayName = 'VersionTimeline' \ No newline at end of file diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index f01f0ae..7006c8b 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -3,7 +3,7 @@ import { cn } from '@/lib/utils' interface ButtonProps extends ButtonHTMLAttributes { variant?: 'default' | 'outline' | 'ghost' | 'destructive' - size?: 'sm' | 'md' | 'lg' + size?: 'xs' | 'sm' | 'md' | 'lg' } const Button = forwardRef( @@ -18,6 +18,7 @@ const Button = forwardRef( } const sizes = { + xs: 'h-6 px-2 text-xs', sm: 'h-8 px-3 text-sm', md: 'h-10 px-4 text-sm', lg: 'h-12 px-6 text-base' diff --git a/src/lib/subscription.ts b/src/lib/subscription.ts new file mode 100644 index 0000000..751c9ad --- /dev/null +++ b/src/lib/subscription.ts @@ -0,0 +1,69 @@ +// 订阅计划配置 +export const SUBSCRIPTION_PLANS = { + free: { + name: 'Free', + maxVersionLimit: 3, + promptLimit: 20, + credit: 5 + }, + pro: { + name: 'Pro', + maxVersionLimit: 10, + promptLimit: 500, + credit: 20 + } +} as const + +export type SubscriptionPlan = keyof typeof SUBSCRIPTION_PLANS + +// 获取用户的版本限制 +export function getUserVersionLimit(user: { + versionLimit: number + subscribePlan: string + maxVersionLimit: number +}): number { + const plan = user.subscribePlan as SubscriptionPlan + const planConfig = SUBSCRIPTION_PLANS[plan] + + if (!planConfig) { + // 如果订阅计划无效,使用免费计划的限制 + return Math.min(user.versionLimit, SUBSCRIPTION_PLANS.free.maxVersionLimit) + } + + // 用户设置的限制不能超过订阅计划的最大限制 + return Math.min(user.versionLimit, planConfig.maxVersionLimit) +} + +// 获取用户的最大版本限制(基于订阅) +export function getMaxVersionLimit(subscribePlan: string): number { + const plan = subscribePlan as SubscriptionPlan + const planConfig = SUBSCRIPTION_PLANS[plan] + + return planConfig?.maxVersionLimit || SUBSCRIPTION_PLANS.free.maxVersionLimit +} + +// 检查用户是否可以创建新版本 +export function canCreateNewVersion( + currentVersionCount: number, + user: { + versionLimit: number + subscribePlan: string + maxVersionLimit: number + } +): boolean { + const versionLimit = getUserVersionLimit(user) + return currentVersionCount < versionLimit +} + +// 计算需要删除的版本数量 +export function getVersionsToDelete( + currentVersionCount: number, + user: { + versionLimit: number + subscribePlan: string + maxVersionLimit: number + } +): number { + const versionLimit = getUserVersionLimit(user) + return Math.max(0, currentVersionCount - versionLimit + 1) // +1 因为要创建新版本 +} \ No newline at end of file