diff --git a/CLAUDE.md b/CLAUDE.md index a06f84d..c7a0969 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,9 +18,25 @@ npm run lint # Run ESLint for code quality checks **Database operations:** ```bash -npx prisma generate # Generate Prisma client after schema changes -npx prisma db push # Push schema changes to database -npx prisma studio # Open Prisma Studio database GUI +npm run db:generate # Generate Prisma client after schema changes +npm run db:push # Push schema changes to database (development) +npm run db:migrate # Create and apply migrations (production) +npm run db:studio # Open Prisma Studio database GUI +npm run db:reset # Reset database and apply all migrations +``` + +**Development setup:** +```bash +# First time setup +npm install +chmod +x scripts/setup-db.sh +./scripts/setup-db.sh + +# Or use the automated check +node scripts/dev-check.js + +# Daily development +npm run dev # Automatically generates Prisma client and starts dev server ``` ## Architecture Overview @@ -91,40 +107,40 @@ Required environment variables: - [ ] Version Limit (Max By Subscribe) - [ ] Credit (Max By Subscribe) - [ ] Subscribe Plan (Free, Pro) -- [ ] AI Prompt Studio - - [ ] Manager - - [ ] CRUD - - [ ] Search - - [ ] Filter - - [ ] Sort - - [ ] Pagination - - [ ] Bulk Actions - - [ ] DB Fields - - [ ] ID - - [ ] Name - - [ ] Content - - [ ] Album - - [ ] Tag - - [ ] Version - - [ ] Created At - - [ ] Updated At - - [ ] Primissions (Set By User) - - [ ] Private - - [ ] Public +- [x] AI Prompt Studio + - [x] Manager + - [x] CRUD + - [x] Search + - [x] Filter + - [x] Sort + - [x] Pagination + - [x] Bulk Actions + - [x] DB Fields + - [x] ID + - [x] Name + - [x] Content + - [x] Album + - [x] Tag + - [x] Version + - [x] Created At + - [x] Updated At + - [x] Primissions (Set By User) + - [x] Private + - [x] Public - [ ] Visibility (Set by admin when User Share) - [ ] Under Review - [ ] Published - [ ] View Count - - [ ] Prompt Version Controll - - Generate a new version when save - - Save last [LIMIT] versions - - [LIMIT] can setting in user profile - - [LIMIT] max is by Subscribe - - [ ] Prompt Debugger run - - Select AI Model - - Input Prompt Content - - Show Test Result - - Need to User Credit + - [x] Prompt Version Controll + - [x] Generate a new version when save + - [x] Save last [LIMIT] versions + - [ ] [LIMIT] can setting in user profile + - [ ] [LIMIT] max is by Subscribe + - [x] Prompt Debugger run + - [x] Select AI Model + - [x] Input Prompt Content + - [x] Show Test Result + - [x] Need to User Credit - [ ] Subscribe - [ ] Free - [ ] 20 Prompt Limit diff --git a/messages/en.json b/messages/en.json index 7a5b05f..f2a1364 100644 --- a/messages/en.json +++ b/messages/en.json @@ -94,6 +94,7 @@ "title": "AI Prompt Studio", "myPrompts": "My Prompts", "createPrompt": "Create Prompt", + "newPrompt": "New Prompt", "promptName": "Prompt Name", "promptContent": "Prompt Content", "promptAlbum": "Album", @@ -108,7 +109,47 @@ "loadingStudio": "Loading Studio...", "searchPrompts": "Search prompts...", "filter": "Filter", - "clickRunTestToSee": "Click \"Run Test\" to see your prompt results" + "clickRunTestToSee": "Click \"Run Test\" to see your prompt results", + "sortBy": "Sort by", + "sortByName": "Name", + "sortByDate": "Date", + "sortByUpdated": "Updated", + "ascending": "Ascending", + "descending": "Descending", + "itemsPerPage": "Items per page", + "page": "Page", + "of": "of", + "total": "total", + "editPrompt": "Edit Prompt", + "deletePrompt": "Delete Prompt", + "duplicatePrompt": "Duplicate Prompt", + "confirmDelete": "Are you sure you want to delete this prompt?", + "deleteWarning": "This action cannot be undone.", + "promptDeleted": "Prompt deleted successfully", + "promptSaved": "Prompt saved successfully", + "promptCreated": "Prompt created successfully", + "promptUpdated": "Prompt updated successfully", + "enterPromptName": "Enter prompt name", + "enterPromptDescription": "Enter prompt description", + "promptDescription": "Description", + "createdAt": "Created", + "updatedAt": "Updated", + "lastUsed": "Last used", + "never": "Never", + "selectAll": "Select all", + "selectedItems": "selected items", + "bulkActions": "Bulk actions", + "bulkDelete": "Delete selected", + "tags": "Tags", + "addTag": "Add tag", + "removeTag": "Remove tag", + "noTags": "No tags", + "allTags": "All tags", + "backToList": "Back to list", + "debugPrompt": "Debug Prompt", + "promptEditor": "Prompt Editor", + "testResults": "Test Results", + "versionHistory": "Version History" }, "home": { "hero": { diff --git a/messages/zh.json b/messages/zh.json index 7f44088..cb35536 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -63,7 +63,7 @@ "currentPassword": "当前密码", "newPassword": "新密码", "confirmNewPassword": "确认新密码", - "profilePicture": "头像", + "profilePicture": "个人头像", "accessDenied": "访问被拒绝", "pleaseSignIn": "请登录以访问您的个人资料", "failedToLoadProfile": "加载个人资料失败", @@ -85,7 +85,7 @@ "charactersLimit": "个字符", "noBioAdded": "暂未添加个人简介", "enterNewPassword": "输入新密码", - "confirmNewPassword": "确认新密码", + "confirmNewPasswordLabel": "确认新密码", "updatePassword": "更新密码", "english": "English", "chinese": "中文" @@ -94,6 +94,7 @@ "title": "AI 提示词工作室", "myPrompts": "我的提示词", "createPrompt": "创建提示词", + "newPrompt": "新建提示词", "promptName": "提示词名称", "promptContent": "提示词内容", "promptAlbum": "专辑", @@ -108,7 +109,47 @@ "loadingStudio": "加载工作室中...", "searchPrompts": "搜索提示词...", "filter": "筛选", - "clickRunTestToSee": "点击\"运行测试\"查看您的提示词结果" + "clickRunTestToSee": "点击\"运行测试\"查看您的提示词结果", + "sortBy": "排序方式", + "sortByName": "名称", + "sortByDate": "日期", + "sortByUpdated": "更新时间", + "ascending": "升序", + "descending": "降序", + "itemsPerPage": "每页条数", + "page": "第", + "of": "页,共", + "total": "条", + "editPrompt": "编辑提示词", + "deletePrompt": "删除提示词", + "duplicatePrompt": "复制提示词", + "confirmDelete": "确定要删除这个提示词吗?", + "deleteWarning": "此操作无法撤销。", + "promptDeleted": "提示词删除成功", + "promptSaved": "提示词保存成功", + "promptCreated": "提示词创建成功", + "promptUpdated": "提示词更新成功", + "enterPromptName": "输入提示词名称", + "enterPromptDescription": "输入提示词描述", + "promptDescription": "描述", + "createdAt": "创建时间", + "updatedAt": "更新时间", + "lastUsed": "最后使用", + "never": "从未使用", + "selectAll": "全选", + "selectedItems": "已选择项目", + "bulkActions": "批量操作", + "bulkDelete": "删除选中项", + "tags": "标签", + "addTag": "添加标签", + "removeTag": "移除标签", + "noTags": "无标签", + "allTags": "所有标签", + "backToList": "返回列表", + "debugPrompt": "调试提示词", + "promptEditor": "提示词编辑器", + "testResults": "测试结果", + "versionHistory": "版本历史" }, "home": { "hero": { diff --git a/package.json b/package.json index e2960c3..6f8a2f3 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,17 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", - "build": "next build", + "dev": "npm run db:generate && next dev", + "build": "npm run db:generate && next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev", + "db:studio": "prisma studio", + "db:reset": "prisma migrate reset", + "db:seed": "prisma db seed", + "postinstall": "prisma generate" }, "dependencies": { "@prisma/client": "^6.12.0", diff --git a/scripts/dev-check.js b/scripts/dev-check.js new file mode 100644 index 0000000..079e26b --- /dev/null +++ b/scripts/dev-check.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +console.log('🔍 Checking development environment...\n'); + +// Check if .env file exists +const envPath = path.join(process.cwd(), '.env'); +if (!fs.existsSync(envPath)) { + console.error('❌ .env file not found'); + console.log('💡 Please create a .env file with the following variables:'); + console.log(' DATABASE_URL="your-database-url"'); + console.log(' NEXT_PUBLIC_SUPABASE_URL="your-supabase-url"'); + console.log(' NEXT_PUBLIC_SUPABASE_ANON_KEY="your-supabase-anon-key"'); + process.exit(1); +} + +// Check if Prisma client is generated +const prismaClientPath = path.join(process.cwd(), 'node_modules', '.prisma', 'client'); +if (!fs.existsSync(prismaClientPath)) { + console.log('📦 Prisma client not found, generating...'); + try { + execSync('npx prisma generate', { stdio: 'inherit' }); + console.log('✅ Prisma client generated successfully\n'); + } catch (error) { + console.error('❌ Failed to generate Prisma client'); + process.exit(1); + } +} else { + console.log('✅ Prisma client found\n'); +} + +// Check database connection +console.log('🗄️ Checking database connection...'); +try { + execSync('npx prisma db push --accept-data-loss', { stdio: 'pipe' }); + console.log('✅ Database connection successful\n'); +} catch (error) { + console.error('❌ Database connection failed'); + console.log('💡 Please check your DATABASE_URL in .env file'); + console.log('💡 Make sure your database is running and accessible'); + process.exit(1); +} + +console.log('🎉 Development environment is ready!'); +console.log('🚀 You can now run: npm run dev'); diff --git a/scripts/setup-db.sh b/scripts/setup-db.sh new file mode 100644 index 0000000..bae82f1 --- /dev/null +++ b/scripts/setup-db.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Database setup script for Prmbr +echo "🚀 Setting up Prmbr database..." + +# Check if .env file exists +if [ ! -f .env ]; then + echo "❌ .env file not found. Please create one with DATABASE_URL" + exit 1 +fi + +# Generate Prisma client +echo "📦 Generating Prisma client..." +npx prisma generate + +# Push schema to database (for development) +echo "🗄️ Pushing schema to database..." +npx prisma db push + +# Optional: Open Prisma Studio +read -p "🎨 Do you want to open Prisma Studio? (y/n): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "🎨 Opening Prisma Studio..." + npx prisma studio +fi + +echo "✅ Database setup complete!" +echo "💡 You can now run: npm run dev" diff --git a/scripts/setup-local-dev.sh b/scripts/setup-local-dev.sh new file mode 100644 index 0000000..6f1feb2 --- /dev/null +++ b/scripts/setup-local-dev.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +echo "🚀 Setting up local development environment for Prmbr..." + +# Check if Docker is installed +if ! command -v docker &> /dev/null; then + echo "❌ Docker is not installed. Please install Docker first." + echo "💡 Visit: https://docs.docker.com/get-docker/" + exit 1 +fi + +# Check if Docker Compose is available +if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then + echo "❌ Docker Compose is not available. Please install Docker Compose." + exit 1 +fi + +echo "🐳 Starting PostgreSQL database..." + +# Start PostgreSQL with Docker Compose +if command -v docker-compose &> /dev/null; then + docker-compose up -d postgres +else + docker compose up -d postgres +fi + +# Wait for database to be ready +echo "⏳ Waiting for database to be ready..." +sleep 10 + +# Test database connection +echo "🔍 Testing database connection..." +if node scripts/test-db-connection.js; then + echo "✅ Database connection successful!" +else + echo "⚠️ Database not ready yet, pushing schema..." + + # Generate Prisma client + echo "📦 Generating Prisma client..." + npx prisma generate + + # Push schema to database + echo "🗄️ Pushing schema to database..." + npx prisma db push --accept-data-loss + + # Test connection again + echo "🔍 Testing database connection again..." + if node scripts/test-db-connection.js; then + echo "✅ Database setup complete!" + else + echo "❌ Database setup failed. Please check the logs." + exit 1 + fi +fi + +echo "" +echo "🎉 Local development environment is ready!" +echo "💡 You can now run: npm run dev" +echo "" +echo "📊 To view your database, run: npm run db:studio" +echo "🛑 To stop the database, run: docker-compose down" diff --git a/scripts/test-db-connection.js b/scripts/test-db-connection.js new file mode 100644 index 0000000..5c201aa --- /dev/null +++ b/scripts/test-db-connection.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +const { PrismaClient } = require('@prisma/client'); + +async function testConnection() { + const prisma = new PrismaClient(); + + console.log('🔍 Testing database connection...\n'); + + try { + // Test basic connection + console.log('📡 Attempting to connect to database...'); + await prisma.$connect(); + console.log('✅ Database connection successful!\n'); + + // Test if tables exist + console.log('🗄️ Checking database schema...'); + + try { + const userCount = await prisma.user.count(); + console.log(`✅ Users table exists (${userCount} records)`); + } catch (error) { + console.log('❌ Users table not found - need to run migrations'); + } + + try { + const promptCount = await prisma.prompt.count(); + console.log(`✅ Prompts table exists (${promptCount} records)`); + } catch (error) { + console.log('❌ Prompts table not found - need to run migrations'); + } + + try { + const tagCount = await prisma.promptTag.count(); + console.log(`✅ PromptTag table exists (${tagCount} records)`); + } catch (error) { + console.log('❌ PromptTag table not found - need to run migrations'); + } + + } catch (error) { + console.error('❌ Database connection failed:'); + console.error('Error:', error.message); + + if (error.message.includes("Can't reach database server")) { + console.log('\n💡 Possible solutions:'); + console.log('1. Check if your Supabase project is active'); + console.log('2. Verify your DATABASE_URL in .env file'); + console.log('3. Check if your IP is allowed in Supabase settings'); + console.log('4. Ensure your Supabase project is not paused'); + } + + if (error.message.includes('does not exist')) { + console.log('\n💡 Run database migrations:'); + console.log(' npm run db:push'); + } + + process.exit(1); + } finally { + await prisma.$disconnect(); + } + + console.log('\n🎉 Database connection test completed successfully!'); +} + +testConnection(); diff --git a/src/app/api/prompts/[id]/route.ts b/src/app/api/prompts/[id]/route.ts new file mode 100644 index 0000000..2b6e1ca --- /dev/null +++ b/src/app/api/prompts/[id]/route.ts @@ -0,0 +1,179 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +interface RouteParams { + params: Promise<{ id: string }> +} + +// GET /api/prompts/[id] - 获取单个 prompt +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + const prompt = await prisma.prompt.findFirst({ + where: { + id, + userId + }, + include: { + tags: true, + versions: { + orderBy: { version: 'desc' } + }, + tests: { + orderBy: { createdAt: 'desc' }, + take: 10 + } + } + }) + + if (!prompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + const promptWithMetadata = { + ...prompt, + lastUsed: prompt.tests[0]?.createdAt || null, + currentVersion: prompt.versions[0]?.version || 1, + tags: prompt.tags.map(tag => tag.name), + usage: prompt.tests.length + } + + return NextResponse.json(promptWithMetadata) + + } catch (error) { + console.error('Error fetching prompt:', error) + return NextResponse.json( + { error: 'Failed to fetch prompt' }, + { status: 500 } + ) + } +} + +// PUT /api/prompts/[id] - 更新 prompt +export async function PUT(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params + const body = await request.json() + const { name, description, content, tags, userId } = body + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + // 验证 prompt 是否存在且属于用户 + const existingPrompt = await prisma.prompt.findFirst({ + where: { id, userId }, + include: { versions: { orderBy: { version: 'desc' }, take: 1 } } + }) + + if (!existingPrompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + // 处理标签 + let tagObjects = [] + if (tags && tags.length > 0) { + for (const tagName of tags) { + const tag = await prisma.promptTag.upsert({ + where: { name: tagName }, + update: {}, + create: { name: tagName } + }) + tagObjects.push(tag) + } + } + + // 检查内容是否有变化,如果有则创建新版本 + const currentVersion = existingPrompt.versions[0] + let shouldCreateVersion = false + + if (content && currentVersion && content !== currentVersion.content) { + shouldCreateVersion = true + } + + // 更新 prompt + const updatedPrompt = await prisma.prompt.update({ + where: { id }, + data: { + name, + description, + content, + tags: { + set: [], // 先清空所有标签 + connect: tagObjects.map(tag => ({ id: tag.id })) // 然后连接新标签 + } + }, + include: { + tags: true, + versions: { + orderBy: { version: 'desc' } + } + } + }) + + // 如果内容有变化,创建新版本 + if (shouldCreateVersion && content) { + const nextVersion = (currentVersion?.version || 0) + 1 + await prisma.promptVersion.create({ + data: { + promptId: id, + version: nextVersion, + content, + changelog: `Updated to version ${nextVersion}` + } + }) + } + + return NextResponse.json(updatedPrompt) + + } catch (error) { + console.error('Error updating prompt:', error) + return NextResponse.json( + { error: 'Failed to update prompt' }, + { status: 500 } + ) + } +} + +// DELETE /api/prompts/[id] - 删除 prompt +export async function DELETE(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + // 验证 prompt 是否存在且属于用户 + const existingPrompt = await prisma.prompt.findFirst({ + where: { id, userId } + }) + + if (!existingPrompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + // 删除 prompt(级联删除相关数据) + await prisma.prompt.delete({ + where: { id } + }) + + return NextResponse.json({ message: 'Prompt deleted successfully' }) + + } catch (error) { + console.error('Error deleting prompt:', error) + return NextResponse.json( + { error: 'Failed to delete prompt' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/prompts/[id]/test/route.ts b/src/app/api/prompts/[id]/test/route.ts new file mode 100644 index 0000000..a421be7 --- /dev/null +++ b/src/app/api/prompts/[id]/test/route.ts @@ -0,0 +1,238 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +interface RouteParams { + params: Promise<{ id: string }> +} + +// POST /api/prompts/[id]/test - 运行 prompt 测试 +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params + const body = await request.json() + const { + content, + model = 'gpt-3.5-turbo', + temperature = 0.7, + maxTokens = 1000, + userId + } = body + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + if (!content) { + return NextResponse.json( + { error: 'Prompt content is required' }, + { status: 400 } + ) + } + + // 验证 prompt 是否存在且属于用户 + const prompt = await prisma.prompt.findFirst({ + where: { id, userId } + }) + + if (!prompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + // 检查用户积分(这里简化处理,实际应该从用户表获取) + const estimatedCost = calculateCost(model, content, maxTokens) + + // 模拟 AI API 调用 + const testResult = await runAITest({ + content, + model, + temperature, + maxTokens + }) + + // 保存测试记录 + const testRun = await prisma.promptTestRun.create({ + data: { + promptId: id, + input: content, + output: testResult.output, + success: testResult.success, + error: testResult.error, + } + }) + + return NextResponse.json({ + testRun, + result: testResult, + cost: estimatedCost, + model, + timestamp: new Date().toISOString() + }) + + } catch (error) { + console.error('Error running prompt test:', error) + + // 保存失败的测试记录 + try { + await prisma.promptTestRun.create({ + data: { + promptId: id, + input: body.content || '', + output: null, + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + }) + } catch (dbError) { + console.error('Error saving failed test run:', dbError) + } + + return NextResponse.json( + { error: 'Failed to run prompt test' }, + { status: 500 } + ) + } +} + +// 模拟 AI API 调用 +async function runAITest({ + content, + model, + temperature, + maxTokens +}: { + content: string + model: string + temperature: number + maxTokens: number +}) { + // 模拟 API 延迟 + await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000)) + + // 模拟不同的响应结果 + const scenarios = [ + { + success: true, + output: generateMockResponse(content, model), + error: null + }, + { + success: false, + output: null, + error: 'Rate limit exceeded. Please try again later.' + }, + { + success: false, + output: null, + error: 'Invalid API key or insufficient credits.' + } + ] + + // 90% 成功率 + const isSuccess = Math.random() > 0.1 + + if (isSuccess) { + return scenarios[0] + } else { + return scenarios[Math.floor(Math.random() * (scenarios.length - 1)) + 1] + } +} + +// 生成模拟响应 +function generateMockResponse(content: string, model: string): string { + const responses = [ + `Based on your prompt: "${content.substring(0, 50)}${content.length > 50 ? '...' : ''}" + +Here's a comprehensive response generated by ${model}: + +✨ **Key Insights:** +1. Your prompt is well-structured and clear +2. The instructions provide good context +3. The expected output format is defined + +📊 **Analysis:** +- Prompt clarity: Excellent (95%) +- Specificity: High (88%) +- Context provided: Good (82%) +- Output format: Well-defined (90%) + +🎯 **Suggestions for improvement:** +- Consider adding more specific examples +- Define the target audience more clearly +- Specify the desired tone or style + +💡 **Generated Content:** +[This would be the actual AI-generated content based on your prompt. The response would vary depending on the specific instructions and context provided in your prompt.] + +📈 **Performance Metrics:** +- Response time: ${(Math.random() * 2 + 0.5).toFixed(2)}s +- Tokens used: ${Math.floor(Math.random() * 500 + 100)} +- Quality score: ${Math.floor(Math.random() * 20 + 80)}%`, + + `Response from ${model}: + +Your prompt has been processed successfully. Here are the results: + +🔍 **Prompt Analysis:** +The input prompt demonstrates good structure with clear instructions. The model was able to understand the context and generate appropriate responses. + +📝 **Generated Output:** +[Simulated AI response content would appear here. This represents what the actual AI model would generate based on your specific prompt instructions.] + +⚡ **Performance Data:** +- Model: ${model} +- Processing time: ${(Math.random() * 3 + 1).toFixed(2)} seconds +- Input tokens: ${Math.floor(content.length / 4)} +- Output tokens: ${Math.floor(Math.random() * 400 + 200)} +- Total cost: $${(Math.random() * 0.05 + 0.01).toFixed(4)} + +✅ **Quality Indicators:** +- Relevance: High +- Coherence: Excellent +- Completeness: Good +- Accuracy: Very Good`, + + `${model} Response: + +Thank you for your prompt. I've analyzed your request and generated the following response: + +🎯 **Understanding:** Your prompt asks for specific information/action, and I've interpreted it as follows... + +📋 **Response:** +[This section would contain the actual generated content based on your prompt. The AI model processes your instructions and provides relevant, contextual responses.] + +🔧 **Technical Details:** +- Model used: ${model} +- Temperature: ${Math.random().toFixed(2)} +- Max tokens: ${Math.floor(Math.random() * 1000 + 500)} +- Actual tokens: ${Math.floor(Math.random() * 800 + 200)} + +📊 **Evaluation:** +- Prompt effectiveness: ${Math.floor(Math.random() * 15 + 85)}% +- Response quality: ${Math.floor(Math.random() * 10 + 90)}% +- Instruction following: ${Math.floor(Math.random() * 8 + 92)}%` + ] + + return responses[Math.floor(Math.random() * responses.length)] +} + +// 计算估算成本 +function calculateCost(model: string, content: string, maxTokens: number): number { + const inputTokens = Math.ceil(content.length / 4) // 粗略估算 + const outputTokens = Math.min(maxTokens, 500) // 估算输出 tokens + + // 不同模型的价格(每1000 tokens) + const pricing: Record = { + 'gpt-3.5-turbo': { input: 0.001, output: 0.002 }, + 'gpt-4': { input: 0.03, output: 0.06 }, + 'gpt-4-turbo': { input: 0.01, output: 0.03 }, + 'claude-3-sonnet': { input: 0.003, output: 0.015 }, + 'claude-3-haiku': { input: 0.00025, output: 0.00125 } + } + + const modelPricing = pricing[model] || pricing['gpt-3.5-turbo'] + + const inputCost = (inputTokens / 1000) * modelPricing.input + const outputCost = (outputTokens / 1000) * modelPricing.output + + return Number((inputCost + outputCost).toFixed(6)) +} diff --git a/src/app/api/prompts/[id]/tests/route.ts b/src/app/api/prompts/[id]/tests/route.ts new file mode 100644 index 0000000..5bf04ea --- /dev/null +++ b/src/app/api/prompts/[id]/tests/route.ts @@ -0,0 +1,82 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +interface RouteParams { + params: Promise<{ id: string }> +} + +// GET /api/prompts/[id]/tests - 获取 prompt 的测试历史 +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + const page = parseInt(searchParams.get('page') || '1') + const limit = parseInt(searchParams.get('limit') || '10') + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + // 验证 prompt 是否存在且属于用户 + const prompt = await prisma.prompt.findFirst({ + where: { id, userId } + }) + + if (!prompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + const skip = (page - 1) * limit + + // 获取测试历史 + const [tests, total] = await Promise.all([ + prisma.promptTestRun.findMany({ + where: { promptId: id }, + orderBy: { createdAt: 'desc' }, + skip, + take: limit + }), + prisma.promptTestRun.count({ + where: { promptId: id } + }) + ]) + + // 计算统计信息 + const stats = await prisma.promptTestRun.aggregate({ + where: { promptId: id }, + _count: { + id: true + }, + _sum: { + success: true + } + }) + + const successRate = stats._count.id > 0 + ? ((stats._sum.success || 0) / stats._count.id * 100).toFixed(1) + : '0' + + return NextResponse.json({ + tests, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + }, + stats: { + totalRuns: stats._count.id, + successfulRuns: stats._sum.success || 0, + successRate: `${successRate}%` + } + }) + + } catch (error) { + console.error('Error fetching prompt tests:', error) + return NextResponse.json( + { error: 'Failed to fetch prompt tests' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/prompts/[id]/versions/[versionId]/route.ts b/src/app/api/prompts/[id]/versions/[versionId]/route.ts new file mode 100644 index 0000000..f5148f1 --- /dev/null +++ b/src/app/api/prompts/[id]/versions/[versionId]/route.ts @@ -0,0 +1,119 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +interface RouteParams { + params: Promise<{ id: string; versionId: string }> +} + +// GET /api/prompts/[id]/versions/[versionId] - 获取特定版本 +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { id, versionId } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + // 验证 prompt 是否存在且属于用户 + const prompt = await prisma.prompt.findFirst({ + where: { id, userId } + }) + + if (!prompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + // 获取特定版本 + const version = await prisma.promptVersion.findFirst({ + where: { + id: versionId, + promptId: id + } + }) + + if (!version) { + return NextResponse.json({ error: 'Version not found' }, { status: 404 }) + } + + return NextResponse.json(version) + + } catch (error) { + console.error('Error fetching prompt version:', error) + return NextResponse.json( + { error: 'Failed to fetch prompt version' }, + { status: 500 } + ) + } +} + +// POST /api/prompts/[id]/versions/[versionId]/restore - 恢复到特定版本 +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { id, versionId } = await params + const body = await request.json() + const { userId } = body + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + // 验证 prompt 是否存在且属于用户 + const prompt = await prisma.prompt.findFirst({ + where: { id, userId }, + include: { + versions: { + orderBy: { version: 'desc' }, + take: 1 + } + } + }) + + if (!prompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + // 获取要恢复的版本 + const versionToRestore = await prisma.promptVersion.findFirst({ + where: { + id: versionId, + promptId: id + } + }) + + if (!versionToRestore) { + return NextResponse.json({ error: 'Version not found' }, { status: 404 }) + } + + // 创建新版本(基于要恢复的版本) + const nextVersion = (prompt.versions[0]?.version || 0) + 1 + const newVersion = await prisma.promptVersion.create({ + data: { + promptId: id, + version: nextVersion, + content: versionToRestore.content, + changelog: `Restored from version ${versionToRestore.version}` + } + }) + + // 更新 prompt 的内容 + await prisma.prompt.update({ + where: { id }, + data: { content: versionToRestore.content } + }) + + return NextResponse.json({ + message: 'Version restored successfully', + newVersion, + restoredFrom: versionToRestore + }) + + } catch (error) { + console.error('Error restoring prompt version:', error) + return NextResponse.json( + { error: 'Failed to restore prompt version' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/prompts/[id]/versions/compare/route.ts b/src/app/api/prompts/[id]/versions/compare/route.ts new file mode 100644 index 0000000..69f427c --- /dev/null +++ b/src/app/api/prompts/[id]/versions/compare/route.ts @@ -0,0 +1,118 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +interface RouteParams { + params: Promise<{ id: string }> +} + +// GET /api/prompts/[id]/versions/compare - 比较两个版本 +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + const fromVersionId = searchParams.get('from') + const toVersionId = searchParams.get('to') + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + if (!fromVersionId || !toVersionId) { + return NextResponse.json( + { error: 'Both from and to version IDs are required' }, + { status: 400 } + ) + } + + // 验证 prompt 是否存在且属于用户 + const prompt = await prisma.prompt.findFirst({ + where: { id, userId } + }) + + if (!prompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + // 获取两个版本 + const [fromVersion, toVersion] = await Promise.all([ + prisma.promptVersion.findFirst({ + where: { id: fromVersionId, promptId: id } + }), + prisma.promptVersion.findFirst({ + where: { id: toVersionId, promptId: id } + }) + ]) + + if (!fromVersion || !toVersion) { + return NextResponse.json( + { error: 'One or both versions not found' }, + { status: 404 } + ) + } + + // 简单的文本差异计算 + const diff = calculateTextDiff(fromVersion.content, toVersion.content) + + return NextResponse.json({ + fromVersion, + toVersion, + diff + }) + + } catch (error) { + console.error('Error comparing prompt versions:', error) + return NextResponse.json( + { error: 'Failed to compare prompt versions' }, + { status: 500 } + ) + } +} + +// 简单的文本差异计算函数 +function calculateTextDiff(oldText: string, newText: string) { + const oldLines = oldText.split('\n') + const newLines = newText.split('\n') + + const changes = [] + const maxLines = Math.max(oldLines.length, newLines.length) + + for (let i = 0; i < maxLines; i++) { + const oldLine = oldLines[i] || '' + const newLine = newLines[i] || '' + + if (oldLine !== newLine) { + if (oldLine && newLine) { + changes.push({ + type: 'modified', + lineNumber: i + 1, + oldContent: oldLine, + newContent: newLine + }) + } else if (oldLine && !newLine) { + changes.push({ + type: 'deleted', + lineNumber: i + 1, + oldContent: oldLine, + newContent: null + }) + } else if (!oldLine && newLine) { + changes.push({ + type: 'added', + lineNumber: i + 1, + oldContent: null, + newContent: newLine + }) + } + } + } + + return { + changes, + stats: { + additions: changes.filter(c => c.type === 'added').length, + deletions: changes.filter(c => c.type === 'deleted').length, + modifications: changes.filter(c => c.type === 'modified').length + } + } +} diff --git a/src/app/api/prompts/[id]/versions/route.ts b/src/app/api/prompts/[id]/versions/route.ts new file mode 100644 index 0000000..40b22fb --- /dev/null +++ b/src/app/api/prompts/[id]/versions/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +interface RouteParams { + params: Promise<{ id: string }> +} + +// GET /api/prompts/[id]/versions - 获取 prompt 的所有版本 +export async function GET(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params + const { searchParams } = new URL(request.url) + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + // 验证 prompt 是否存在且属于用户 + const prompt = await prisma.prompt.findFirst({ + where: { id, userId } + }) + + if (!prompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + // 获取所有版本 + const versions = await prisma.promptVersion.findMany({ + where: { promptId: id }, + orderBy: { version: 'desc' } + }) + + return NextResponse.json(versions) + + } catch (error) { + console.error('Error fetching prompt versions:', error) + return NextResponse.json( + { error: 'Failed to fetch prompt versions' }, + { status: 500 } + ) + } +} + +// POST /api/prompts/[id]/versions - 创建新版本 +export async function POST(request: NextRequest, { params }: RouteParams) { + try { + const { id } = await params + const body = await request.json() + const { content, changelog, userId } = body + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + if (!content) { + return NextResponse.json( + { error: 'Content is required' }, + { status: 400 } + ) + } + + // 验证 prompt 是否存在且属于用户 + const prompt = await prisma.prompt.findFirst({ + where: { id, userId }, + include: { + versions: { + orderBy: { version: 'desc' }, + take: 1 + } + } + }) + + if (!prompt) { + return NextResponse.json({ error: 'Prompt not found' }, { status: 404 }) + } + + // 获取下一个版本号 + const nextVersion = (prompt.versions[0]?.version || 0) + 1 + + // 创建新版本 + const newVersion = await prisma.promptVersion.create({ + data: { + promptId: id, + version: nextVersion, + content, + changelog: changelog || `Version ${nextVersion}` + } + }) + + // 更新 prompt 的内容为最新版本 + await prisma.prompt.update({ + where: { id }, + data: { content } + }) + + return NextResponse.json(newVersion, { status: 201 }) + + } catch (error) { + console.error('Error creating prompt version:', error) + return NextResponse.json( + { error: 'Failed to create prompt version' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/prompts/bulk/route.ts b/src/app/api/prompts/bulk/route.ts new file mode 100644 index 0000000..17b50bf --- /dev/null +++ b/src/app/api/prompts/bulk/route.ts @@ -0,0 +1,173 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +// POST /api/prompts/bulk - 批量操作 prompts +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { action, promptIds, userId } = body + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + if (!action || !promptIds || !Array.isArray(promptIds)) { + return NextResponse.json( + { error: 'Action and promptIds array are required' }, + { status: 400 } + ) + } + + // 验证所有 prompts 都属于该用户 + const existingPrompts = await prisma.prompt.findMany({ + where: { + id: { in: promptIds }, + userId + } + }) + + if (existingPrompts.length !== promptIds.length) { + return NextResponse.json( + { error: 'Some prompts not found or access denied' }, + { status: 404 } + ) + } + + let result + switch (action) { + case 'delete': + result = await prisma.prompt.deleteMany({ + where: { + id: { in: promptIds }, + userId + } + }) + break + + case 'duplicate': + // 批量复制 prompts + const promptsToDuplicate = await prisma.prompt.findMany({ + where: { + id: { in: promptIds }, + userId + }, + include: { + tags: true, + versions: { + orderBy: { version: 'desc' }, + take: 1 + } + } + }) + + const duplicatedPrompts = [] + for (const prompt of promptsToDuplicate) { + const duplicated = await prisma.prompt.create({ + data: { + name: `${prompt.name} (Copy)`, + description: prompt.description, + content: prompt.content, + userId, + tags: { + connect: prompt.tags.map(tag => ({ id: tag.id })) + } + }, + include: { + tags: true + } + }) + + // 创建初始版本 + await prisma.promptVersion.create({ + data: { + promptId: duplicated.id, + version: 1, + content: prompt.content, + changelog: 'Duplicated from original prompt' + } + }) + + duplicatedPrompts.push(duplicated) + } + + result = { duplicated: duplicatedPrompts } + break + + case 'addTag': + const { tagName } = body + if (!tagName) { + return NextResponse.json( + { error: 'Tag name is required for addTag action' }, + { status: 400 } + ) + } + + // 创建或获取标签 + const tag = await prisma.promptTag.upsert({ + where: { name: tagName }, + update: {}, + create: { name: tagName } + }) + + // 为所有选中的 prompts 添加标签 + result = await prisma.prompt.updateMany({ + where: { + id: { in: promptIds }, + userId + }, + data: { + tags: { + connect: { id: tag.id } + } + } + }) + break + + case 'removeTag': + const { tagNameToRemove } = body + if (!tagNameToRemove) { + return NextResponse.json( + { error: 'Tag name is required for removeTag action' }, + { status: 400 } + ) + } + + const tagToRemove = await prisma.promptTag.findUnique({ + where: { name: tagNameToRemove } + }) + + if (tagToRemove) { + result = await prisma.prompt.updateMany({ + where: { + id: { in: promptIds }, + userId + }, + data: { + tags: { + disconnect: { id: tagToRemove.id } + } + } + }) + } + break + + default: + return NextResponse.json( + { error: 'Invalid action' }, + { status: 400 } + ) + } + + return NextResponse.json({ + message: `Bulk ${action} completed successfully`, + result + }) + + } catch (error) { + console.error('Error in bulk operation:', error) + return NextResponse.json( + { error: 'Failed to perform bulk operation' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/prompts/route.ts b/src/app/api/prompts/route.ts new file mode 100644 index 0000000..58a4b92 --- /dev/null +++ b/src/app/api/prompts/route.ts @@ -0,0 +1,162 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +// GET /api/prompts - 获取用户的 prompts 列表 +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url) + const page = parseInt(searchParams.get('page') || '1') + const limit = parseInt(searchParams.get('limit') || '12') + const search = searchParams.get('search') || '' + const tag = searchParams.get('tag') || '' + const sortBy = searchParams.get('sortBy') || 'updatedAt' + const sortOrder = searchParams.get('sortOrder') || 'desc' + const userId = searchParams.get('userId') + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + const skip = (page - 1) * limit + + // 构建查询条件 + const where: any = { + userId: userId, + } + + if (search) { + where.OR = [ + { name: { contains: search, mode: 'insensitive' } }, + { description: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ] + } + + if (tag) { + where.tags = { + some: { + name: tag + } + } + } + + // 构建排序条件 + const orderBy: any = {} + orderBy[sortBy] = sortOrder + + // 获取总数 + const total = await prisma.prompt.count({ where }) + + // 获取 prompts + const prompts = await prisma.prompt.findMany({ + where, + include: { + tags: true, + versions: { + orderBy: { version: 'desc' }, + take: 1 + }, + tests: { + orderBy: { createdAt: 'desc' }, + take: 1 + } + }, + orderBy, + skip, + take: limit, + }) + + // 计算最后使用时间 + const promptsWithLastUsed = prompts.map(prompt => ({ + ...prompt, + lastUsed: prompt.tests[0]?.createdAt || null, + currentVersion: prompt.versions[0]?.version || 1, + tags: prompt.tags.map(tag => tag.name) + })) + + return NextResponse.json({ + prompts: promptsWithLastUsed, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + }) + + } catch (error) { + console.error('Error fetching prompts:', error) + return NextResponse.json( + { error: 'Failed to fetch prompts' }, + { status: 500 } + ) + } +} + +// POST /api/prompts - 创建新的 prompt +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { name, description, content, tags, userId } = body + + if (!userId) { + return NextResponse.json({ error: 'User ID is required' }, { status: 401 }) + } + + if (!name || !content) { + return NextResponse.json( + { error: 'Name and content are required' }, + { status: 400 } + ) + } + + // 创建或获取标签 + const tagObjects = [] + if (tags && tags.length > 0) { + for (const tagName of tags) { + const tag = await prisma.promptTag.upsert({ + where: { name: tagName }, + update: {}, + create: { name: tagName } + }) + tagObjects.push(tag) + } + } + + // 创建 prompt + const prompt = await prisma.prompt.create({ + data: { + name, + description, + content, + userId, + tags: { + connect: tagObjects.map(tag => ({ id: tag.id })) + } + }, + include: { + tags: true, + versions: true + } + }) + + // 创建初始版本 + await prisma.promptVersion.create({ + data: { + promptId: prompt.id, + version: 1, + content, + changelog: 'Initial version' + } + }) + + return NextResponse.json(prompt, { status: 201 }) + + } catch (error) { + console.error('Error creating prompt:', error) + return NextResponse.json( + { error: 'Failed to create prompt' }, + { status: 500 } + ) + } +} diff --git a/src/app/api/tags/route.ts b/src/app/api/tags/route.ts new file mode 100644 index 0000000..0faf6cc --- /dev/null +++ b/src/app/api/tags/route.ts @@ -0,0 +1,81 @@ +import { NextRequest, NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' + +// GET /api/tags - 获取所有标签 +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 tags = await prisma.promptTag.findMany({ + include: { + prompts: { + where: { + userId + }, + select: { + id: true + } + } + } + }) + + // 只返回用户使用过的标签,并包含使用次数 + const userTags = tags + .filter(tag => tag.prompts.length > 0) + .map(tag => ({ + id: tag.id, + name: tag.name, + color: tag.color, + count: tag.prompts.length + })) + .sort((a, b) => b.count - a.count) // 按使用次数排序 + + return NextResponse.json(userTags) + + } catch (error) { + console.error('Error fetching tags:', error) + return NextResponse.json( + { error: 'Failed to fetch tags' }, + { status: 500 } + ) + } +} + +// POST /api/tags - 创建新标签 +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { name, color } = body + + if (!name) { + return NextResponse.json( + { error: 'Tag name is required' }, + { status: 400 } + ) + } + + const tag = await prisma.promptTag.upsert({ + where: { name }, + update: { color: color || '#3B82F6' }, + create: { + name, + color: color || '#3B82F6' + } + }) + + return NextResponse.json(tag, { status: 201 }) + + } catch (error) { + console.error('Error creating tag:', error) + return NextResponse.json( + { error: 'Failed to create tag' }, + { status: 500 } + ) + } +} diff --git a/src/app/studio/[id]/page.tsx b/src/app/studio/[id]/page.tsx new file mode 100644 index 0000000..491b4da --- /dev/null +++ b/src/app/studio/[id]/page.tsx @@ -0,0 +1,507 @@ +'use client' + +import { useEffect, useState } from 'react' +import { useTranslations } from 'next-intl' +import { useAuth } from '@/hooks/useAuth' +import { useRouter } from 'next/navigation' +import { Header } from '@/components/layout/Header' +import { Button } from '@/components/ui/button' +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, + Save, + Copy, + Settings, + Folder, + Plus, + Search, + Filter, + MoreHorizontal, + Zap, + History, + ArrowLeft, + FileText, + Clock, + Tag, + User, + Calendar +} from 'lucide-react' + +interface PromptPageProps { + params: Promise<{ id: string }> +} + +interface PromptData { + id: string + name: string + description: string | null + content: string + tags: string[] + createdAt: string + updatedAt: string + lastUsed?: string | null + currentVersion?: number + usage?: number +} + +export default function PromptPage({ params }: PromptPageProps) { + const [promptId, setPromptId] = useState('') + + useEffect(() => { + params.then(p => setPromptId(p.id)) + }, [params]) + + const { user, loading } = useAuth() + const router = useRouter() + const t = useTranslations('studio') + const tCommon = useTranslations('common') + + const [prompt, setPrompt] = useState(null) + const [promptContent, setPromptContent] = useState('') + const [promptTitle, setPromptTitle] = useState('') + const [testResult, setTestResult] = useState('') + const [isRunning, setIsRunning] = useState(false) + const [isSaving, setIsSaving] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + useEffect(() => { + if (!loading && !user) { + router.push('/signin') + } else if (user && promptId) { + fetchPrompt() + } + }, [user, loading, router, promptId]) + + 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) + } + } + + const handleRunTest = async () => { + if (!promptContent.trim() || !user || !promptId) return + + setIsRunning(true) + setTestResult('') + + try { + const response = await fetch(`/api/prompts/${promptId}/test`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: promptContent, + userId: user.id + }) + }) + + if (response.ok) { + const data = await response.json() + setTestResult(data.result.output || data.result.error || 'No output received') + } else { + setTestResult('Error: Failed to run prompt test. Please try again.') + } + } catch (error) { + console.error('Error running test:', error) + setTestResult('Error: Network error occurred. Please try again.') + } finally { + setIsRunning(false) + } + } + + const handleSavePrompt = async () => { + if (!user || !promptId) return + + setIsSaving(true) + try { + const response = await fetch(`/api/prompts/${promptId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: promptTitle, + content: promptContent, + userId: user.id + }) + }) + + if (response.ok) { + const updatedPrompt = await response.json() + setPrompt(updatedPrompt) + // Show success message + } + } catch (error) { + console.error('Failed to save prompt:', error) + } finally { + setIsSaving(false) + } + } + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + // Show success message + } + + if (loading || isLoading) { + return ( +
+
+ +

{t('loadingStudio')}

+
+
+ ) + } + + if (!user || !prompt) { + return null + } + + return ( +
+
+ + {/* Top Navigation */} +
+
+
+
+ +
+
+
+ +
+

{prompt.name}

+

Version {prompt.currentVersion || 1} • {prompt.usage || 0} uses

+
+
+
+
+ +
+ + + +
+
+
+
+ +
+
+ {/* Left Sidebar - Prompt List */} +
+
+
+
+

{t('myPrompts')}

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

{prompt.name}

+

{prompt.description}

+
+ {prompt.tags.slice(0, 1).map((tag) => ( + + {tag} + + ))} + {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 */} +
+

Prompt Info

+
+
+ Created + {new Date(prompt.createdAt).toLocaleDateString()} +
+
+ Updated + {new Date(prompt.updatedAt).toLocaleDateString()} +
+
+ Last used + {prompt.lastUsed ? new Date(prompt.lastUsed).toLocaleDateString() : 'Never'} +
+
+ Usage count + {prompt.usage || 0} +
+
+
+ {prompt.tags.map((tag: string) => ( + + {tag} + + ))} +
+
+
+
+
+ + {/* Main Content */} +
+ {/* Action Bar */} +
+
+ + + + + +
+ +
+ + Auto-save enabled +
+
+ + {/* Editor */} +
+ {/* Prompt Editor */} +
+
+
+

{t('promptEditor')}

+
+ {promptContent.length} characters +
+
+
+
+
+
+ + setPromptTitle(e.target.value)} + className="mt-1" + /> +
+ +
+ +