diff --git a/messages/en.json b/messages/en.json index b03bd04..5912268 100644 --- a/messages/en.json +++ b/messages/en.json @@ -366,8 +366,8 @@ "planModelsCount": "models available" }, "plaza": { - "title": "Prompt Plaza", - "subtitle": "Discover and explore prompts shared by the community", + "title": "Plaza", + "subtitle": "Discover and explore shared prompts and simulation results", "searchPlaceholder": "Search prompts by name or description...", "filterByTag": "Filter by tag", "allTags": "All Tags", @@ -472,7 +472,10 @@ "running": "Running", "completed": "Completed", "failed": "Failed" - } + }, + "shareRun": "Share Run", + "shared": "Shared", + "updating": "Updating..." }, "errors": { "generic": "Something went wrong. Please try again.", diff --git a/messages/zh.json b/messages/zh.json index 291ca30..20c9cbc 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -367,8 +367,8 @@ "planModelsCount": "个可用模型" }, "plaza": { - "title": "提示词广场", - "subtitle": "发现并探索社区分享的提示词", + "title": "广场", + "subtitle": "发现并探索社区分享的提示词和运行效果", "searchPlaceholder": "按名称或描述搜索提示词...", "filterByTag": "按标签筛选", "allTags": "所有标签", @@ -472,7 +472,10 @@ "running": "运行中", "completed": "已完成", "failed": "失败" - } + }, + "shareRun": "分享运行", + "shared": "已分享", + "updating": "更新中..." }, "errors": { "generic": "出现错误,请重试。", diff --git a/src/app/admin/review/page.tsx b/src/app/admin/review/page.tsx index bcb49a3..b6c6a23 100644 --- a/src/app/admin/review/page.tsx +++ b/src/app/admin/review/page.tsx @@ -5,7 +5,7 @@ import { useTranslations } from 'next-intl' import { Card } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' -import { CheckCircle, XCircle, Eye, Calendar, User as UserIcon } from 'lucide-react' +import { CheckCircle, XCircle, Eye, Calendar, User as UserIcon, FileText, Play, Zap } from 'lucide-react' import { formatDistanceToNow } from 'date-fns' interface ReviewPrompt { @@ -22,9 +22,34 @@ interface ReviewPrompt { } } +interface ReviewSimulatorRun { + id: string + name: string + status: string + userInput: string + output?: string | null + visibility: string | null + createdAt: string + user: { + username: string | null + email: string + } + prompt: { + name: string + content: string + } + model: { + name: string + provider: string + outputType?: string + } +} + export default function AdminReviewPage() { const t = useTranslations('admin') + const [activeTab, setActiveTab] = useState<'prompts' | 'simulators'>('prompts') const [prompts, setPrompts] = useState([]) + const [simulatorRuns, setSimulatorRuns] = useState([]) const [loading, setLoading] = useState(true) const fetchPendingPrompts = async () => { @@ -36,13 +61,29 @@ export default function AdminReviewPage() { } } catch (error) { console.error('Failed to fetch pending prompts:', error) - } finally { - setLoading(false) } } + const fetchPendingSimulators = async () => { + try { + const response = await fetch('/api/admin/simulators/pending') + if (response.ok) { + const data = await response.json() + setSimulatorRuns(data.simulatorRuns) + } + } catch (error) { + console.error('Failed to fetch pending simulator runs:', error) + } + } + + const fetchData = async () => { + setLoading(true) + await Promise.all([fetchPendingPrompts(), fetchPendingSimulators()]) + setLoading(false) + } + useEffect(() => { - fetchPendingPrompts() + fetchData() }, []) const handleApprove = async (promptId: string) => { @@ -71,6 +112,40 @@ export default function AdminReviewPage() { } } + const handleApproveSimulator = async (simulatorId: string) => { + try { + const response = await fetch(`/api/admin/simulators/${simulatorId}/approve`, { + method: 'POST' + }) + if (response.ok) { + setSimulatorRuns(simulatorRuns.filter(s => s.id !== simulatorId)) + } + } catch (error) { + console.error('Failed to approve simulator run:', error) + } + } + + const handleRejectSimulator = async (simulatorId: string) => { + try { + const response = await fetch(`/api/admin/simulators/${simulatorId}/reject`, { + method: 'POST' + }) + if (response.ok) { + setSimulatorRuns(simulatorRuns.filter(s => s.id !== simulatorId)) + } + } catch (error) { + console.error('Failed to reject simulator run:', error) + } + } + + const currentItems = activeTab === 'prompts' ? prompts : simulatorRuns + const pendingCount = activeTab === 'prompts' + ? prompts.filter(p => !p.visibility || p.visibility === 'under_review').length + : simulatorRuns.filter(s => !s.visibility || s.visibility === 'under_review').length + const publishedCount = activeTab === 'prompts' + ? prompts.filter(p => p.visibility === 'published').length + : simulatorRuns.filter(s => s.visibility === 'published').length + if (loading) { return (
@@ -79,10 +154,10 @@ export default function AdminReviewPage() {

- {t('allPrompts')} + 内容审核

- {t('reviewPromptsDesc')} + 审核用户提交的提示词和运行结果

@@ -92,7 +167,7 @@ export default function AdminReviewPage() {
-

{t('loadingPrompts')}

+

加载中...

@@ -106,37 +181,63 @@ export default function AdminReviewPage() {

- {t('allPrompts')} + 内容审核

- {t('reviewPromptsDesc')} + 审核用户提交的提示词和运行结果

- {prompts.filter(p => !p.visibility || p.visibility === 'under_review').length} {t('pending')} + {pendingCount} 待审核 - {prompts.filter(p => p.visibility === 'published').length} {t('published')} + {publishedCount} 已发布
+ + {/* Tabs */} +
+ + +
{/* Content */} - {prompts.length === 0 ? ( + {currentItems.length === 0 ? (

- {t('noPromptsPending')} + 暂无待审核内容

- {t('allPromptsReviewed')} + 所有{activeTab === 'prompts' ? '提示词' : '运行结果'}都已审核完毕

- ) : ( + ) : activeTab === 'prompts' ? (
{prompts.map((prompt) => ( @@ -162,7 +263,7 @@ export default function AdminReviewPage() { : '' }`} > - {prompt.visibility === 'published' ? t('published') : t('underReview')} + {prompt.visibility === 'published' ? '已发布' : '待审核'}
@@ -186,7 +287,7 @@ export default function AdminReviewPage() {
- {t('promptContent')} + 提示词内容
@@ -206,7 +307,7 @@ export default function AdminReviewPage() { size="sm" > - {t('approve')} + 通过 )} +
+ + + ))} + + ) : ( +
+ {simulatorRuns.map((run) => ( + +
+ {/* Header */} +
+
+

+ {run.name} +

+

+ 模型:{run.model.provider} - {run.model.name} + {run.model.outputType && run.model.outputType !== 'text' && ( + + {run.model.outputType} + + )} +

+
+
+ + {run.visibility === 'published' ? '已发布' : '待审核'} + +
+
+ + {/* Metadata */} +
+
+ + {run.user.username || run.user.email} +
+
+ + + {formatDistanceToNow(new Date(run.createdAt), { addSuffix: true })} + +
+
+ + {/* Prompt and Input Preview */} +
+
+
+ + + 提示词:{run.prompt.name} + +
+
+ {run.prompt.content.length > 200 + ? `${run.prompt.content.slice(0, 200)}...` + : run.prompt.content + } +
+
+ +
+
+ + + 用户输入 + +
+
+ {run.userInput.length > 200 + ? `${run.userInput.slice(0, 200)}...` + : run.userInput + } +
+
+ + {run.output && ( +
+
+ + + 生成结果 + +
+
+ {run.model.outputType === 'image' ? ( + 图片内容(点击查看完整运行结果) + ) : run.output.length > 300 ? ( + `${run.output.slice(0, 300)}...` + ) : run.output} +
+
+ )} +
+ + {/* Actions */} +
+ + {run.visibility !== 'published' && ( + + )} +
diff --git a/src/app/api/admin/simulators/[id]/approve/route.ts b/src/app/api/admin/simulators/[id]/approve/route.ts new file mode 100644 index 0000000..ddaa1a6 --- /dev/null +++ b/src/app/api/admin/simulators/[id]/approve/route.ts @@ -0,0 +1,65 @@ +import { NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { auth } from '@/lib/auth' +import { headers } from 'next/headers' + +export async function POST( + request: Request, + context: { params: Promise<{ id: string }> } +) { + try { + const params = await context.params + const session = await auth.api.getSession({ + headers: await headers() + }) + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check if user is admin + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { isAdmin: true } + }) + + if (!user?.isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + // Check if simulator run exists and is public + const simulatorRun = await prisma.simulatorRun.findFirst({ + where: { + id: params.id, + permissions: 'public' + } + }) + + if (!simulatorRun) { + return NextResponse.json( + { error: 'Simulator run not found or not public' }, + { status: 404 } + ) + } + + // Approve the simulator run + const approvedRun = await prisma.simulatorRun.update({ + where: { id: params.id }, + data: { + visibility: 'published' + } + }) + + return NextResponse.json({ + success: true, + simulatorRun: approvedRun + }) + + } catch (error) { + console.error('Error approving simulator run:', error) + return NextResponse.json( + { error: 'Failed to approve simulator run' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/api/admin/simulators/[id]/reject/route.ts b/src/app/api/admin/simulators/[id]/reject/route.ts new file mode 100644 index 0000000..ad004b6 --- /dev/null +++ b/src/app/api/admin/simulators/[id]/reject/route.ts @@ -0,0 +1,66 @@ +import { NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { auth } from '@/lib/auth' +import { headers } from 'next/headers' + +export async function POST( + request: Request, + context: { params: Promise<{ id: string }> } +) { + try { + const params = await context.params + const session = await auth.api.getSession({ + headers: await headers() + }) + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check if user is admin + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { isAdmin: true } + }) + + if (!user?.isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + // Check if simulator run exists and is public + const simulatorRun = await prisma.simulatorRun.findFirst({ + where: { + id: params.id, + permissions: 'public' + } + }) + + if (!simulatorRun) { + return NextResponse.json( + { error: 'Simulator run not found or not public' }, + { status: 404 } + ) + } + + // Reject the simulator run by setting it back to private + const rejectedRun = await prisma.simulatorRun.update({ + where: { id: params.id }, + data: { + permissions: 'private', + visibility: null + } + }) + + return NextResponse.json({ + success: true, + simulatorRun: rejectedRun + }) + + } catch (error) { + console.error('Error rejecting simulator run:', error) + return NextResponse.json( + { error: 'Failed to reject simulator run' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/api/admin/simulators/pending/route.ts b/src/app/api/admin/simulators/pending/route.ts new file mode 100644 index 0000000..11d95c2 --- /dev/null +++ b/src/app/api/admin/simulators/pending/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { auth } from '@/lib/auth' +import { headers } from 'next/headers' + +export async function GET() { + try { + const session = await auth.api.getSession({ + headers: await headers() + }) + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + // Check if user is admin + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + select: { isAdmin: true } + }) + + if (!user?.isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + // Get simulator runs that are public (including pending and published ones for review) + const simulatorRuns = await prisma.simulatorRun.findMany({ + where: { + permissions: 'public', + OR: [ + { visibility: null }, + { visibility: 'under_review' }, + { visibility: 'published' } + ] + }, + include: { + user: { + select: { + username: true, + email: true + } + }, + prompt: { + select: { + name: true, + content: true + } + }, + model: { + select: { + name: true, + provider: true, + outputType: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }) + + return NextResponse.json({ simulatorRuns }) + + } catch (error) { + console.error('Error fetching pending simulator runs:', error) + return NextResponse.json( + { error: 'Failed to fetch simulator runs' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/api/plaza/route.ts b/src/app/api/plaza/route.ts index 7e32698..6ce6cc6 100644 --- a/src/app/api/plaza/route.ts +++ b/src/app/api/plaza/route.ts @@ -8,6 +8,7 @@ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const search = searchParams.get('search') || '' const tag = searchParams.get('tag') || '' + const type = searchParams.get('type') || 'prompts' // 'prompts' | 'simulators' | 'all' const page = parseInt(searchParams.get('page') || '1') const limit = parseInt(searchParams.get('limit') || '12') const sortBy = searchParams.get('sortBy') || 'createdAt' @@ -15,97 +16,203 @@ export async function GET(request: NextRequest) { const skip = (page - 1) * limit - // 构建where条件 - const where: Record = { - permissions: 'public', - visibility: 'published', - } - - // 搜索条件:根据名称和描述匹配 - if (search) { - where.OR = [ - { name: { contains: search, mode: 'insensitive' } }, - { description: { contains: search, mode: 'insensitive' } }, - ] - } - - // 标签过滤 - if (tag) { - where.tags = { - some: { - name: tag - } + if (type === 'prompts') { + // 获取提示词数据 + const where: Record = { + permissions: 'public', + visibility: 'published', } - } - // 排序条件 - const orderBy: Record = {} - if (sortBy === 'name') { - orderBy.name = sortOrder - } else { - orderBy.createdAt = sortOrder - } + // 搜索条件:根据名称和描述匹配 + if (search) { + where.OR = [ + { name: { contains: search, mode: 'insensitive' } }, + { description: { contains: search, mode: 'insensitive' } }, + ] + } - // 获取总数和数据 - const [total, prompts] = await Promise.all([ - prisma.prompt.count({ where }), - prisma.prompt.findMany({ - where, - skip, - take: limit, - orderBy, - include: { - user: { - select: { - id: true, - username: true, - image: true, - } - }, - tags: { - select: { - id: true, - name: true, - color: true, - } - }, - versions: { - orderBy: { - version: 'desc' - }, - take: 1, - select: { - content: true, - version: true, - createdAt: true, - } - }, - _count: { - select: { - versions: true, - } + // 标签过滤 + if (tag) { + where.tags = { + some: { + name: tag } } - }) - ]) - - const totalPages = Math.ceil(total / limit) - - return NextResponse.json({ - prompts, - pagination: { - page, - limit, - total, - totalPages, - hasNext: page < totalPages, - hasPrev: page > 1, } - }) + + // 排序条件 + const orderBy: Record = {} + if (sortBy === 'name') { + orderBy.name = sortOrder + } else { + orderBy.createdAt = sortOrder + } + + // 获取总数和数据 + const [total, prompts] = await Promise.all([ + prisma.prompt.count({ where }), + prisma.prompt.findMany({ + where, + skip, + take: limit, + orderBy, + include: { + user: { + select: { + id: true, + username: true, + image: true, + } + }, + tags: { + select: { + id: true, + name: true, + color: true, + } + }, + versions: { + orderBy: { + version: 'desc' + }, + take: 1, + select: { + content: true, + version: true, + createdAt: true, + } + }, + stats: { + select: { + viewCount: true, + likeCount: true, + rating: true, + ratingCount: true, + } + }, + _count: { + select: { + versions: true, + } + } + } + }) + ]) + + const totalPages = Math.ceil(total / limit) + + return NextResponse.json({ + prompts, + type: 'prompts', + pagination: { + page, + limit, + total, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1, + } + }) + + } else if (type === 'simulators') { + // 获取运行结果数据 + const where: Record = { + permissions: 'public', + visibility: 'published', + } + + // 搜索条件 + if (search) { + where.OR = [ + { name: { contains: search, mode: 'insensitive' } }, + { userInput: { contains: search, mode: 'insensitive' } }, + ] + } + + // 排序条件 + const orderBy: Record = {} + if (sortBy === 'name') { + orderBy.name = sortOrder + } else { + orderBy.createdAt = sortOrder + } + + // 获取总数和数据 + const [total, simulatorRuns] = await Promise.all([ + prisma.simulatorRun.count({ where }), + prisma.simulatorRun.findMany({ + where, + skip, + take: limit, + orderBy, + include: { + user: { + select: { + id: true, + username: true, + image: true, + } + }, + prompt: { + select: { + id: true, + name: true, + content: true, + tags: { + select: { + id: true, + name: true, + color: true, + } + } + } + }, + model: { + select: { + name: true, + provider: true, + outputType: true, + description: true, + } + } + } + }) + ]) + + const totalPages = Math.ceil(total / limit) + + return NextResponse.json({ + simulatorRuns, + type: 'simulators', + pagination: { + page, + limit, + total, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1, + } + }) + } else { + // 返回混合内容 (暂时返回空,可以后续实现) + return NextResponse.json({ + prompts: [], + simulatorRuns: [], + type: 'all', + pagination: { + page, + limit, + total: 0, + totalPages: 0, + hasNext: false, + hasPrev: false, + } + }) + } } catch (error) { - console.error('Error fetching plaza prompts:', error) + console.error('Error fetching plaza content:', error) return NextResponse.json( - { error: 'Failed to fetch prompts' }, + { error: 'Failed to fetch content' }, { status: 500 } ) } diff --git a/src/app/api/simulator/[id]/route.ts b/src/app/api/simulator/[id]/route.ts index 0186785..7e25a8c 100644 --- a/src/app/api/simulator/[id]/route.ts +++ b/src/app/api/simulator/[id]/route.ts @@ -32,6 +32,8 @@ export async function GET( promptContent: true, output: true, error: true, + permissions: true, + visibility: true, createdAt: true, completedAt: true, temperature: true, diff --git a/src/app/api/simulator/[id]/share/route.ts b/src/app/api/simulator/[id]/share/route.ts new file mode 100644 index 0000000..907bb0e --- /dev/null +++ b/src/app/api/simulator/[id]/share/route.ts @@ -0,0 +1,91 @@ +import { NextResponse } from 'next/server' +import { prisma } from '@/lib/prisma' +import { auth } from '@/lib/auth' +import { headers } from 'next/headers' + +export async function POST( + request: Request, + context: { params: Promise<{ id: string }> } +) { + try { + const params = await context.params + const session = await auth.api.getSession({ + headers: await headers() + }) + + if (!session?.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { permissions } = await request.json() + + if (!permissions || !['private', 'public'].includes(permissions)) { + return NextResponse.json( + { error: 'Invalid permissions value' }, + { status: 400 } + ) + } + + // 检查 simulator run 是否存在且属于当前用户 + const run = await prisma.simulatorRun.findFirst({ + where: { + id: params.id, + userId: session.user.id + } + }) + + if (!run) { + return NextResponse.json( + { error: 'Simulator run not found' }, + { status: 404 } + ) + } + + // 只有已完成的 run 才能共享 + if (run.status !== 'completed') { + return NextResponse.json( + { error: 'Only completed simulator runs can be shared' }, + { status: 400 } + ) + } + + // 更新权限设置 + const updatedRun = await prisma.simulatorRun.update({ + where: { id: params.id }, + data: { + permissions, + // 如果设为 public,重置 visibility 为等待审核状态 + visibility: permissions === 'public' ? null : null + }, + include: { + prompt: { + select: { + id: true, + name: true, + content: true + } + }, + model: { + select: { + id: true, + name: true, + provider: true, + modelId: true, + outputType: true, + description: true, + maxTokens: true + } + } + } + }) + + return NextResponse.json(updatedRun) + + } catch (error) { + console.error('Error toggling simulator run share status:', error) + return NextResponse.json( + { error: 'Failed to update share status' }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/src/app/simulator/[id]/page.tsx b/src/app/simulator/[id]/page.tsx index 3f2ec8f..f7b857b 100644 --- a/src/app/simulator/[id]/page.tsx +++ b/src/app/simulator/[id]/page.tsx @@ -29,7 +29,9 @@ import { DollarSign, Edit, Save, - X + X, + Share2, + Globe } from 'lucide-react' import Link from 'next/link' import { formatDistanceToNow } from 'date-fns' @@ -48,6 +50,8 @@ interface SimulatorRun { output?: string error?: string outputType?: string + permissions: string + visibility?: string | null createdAt: string completedAt?: string temperature?: number @@ -108,6 +112,7 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str const [generatedImageUrl, setGeneratedImageUrl] = useState(null) const [isLoadingImage, setIsLoadingImage] = useState(false) const [imageLoadError, setImageLoadError] = useState(false) + const [isTogglingShare, setIsTogglingShare] = useState(false) const [editForm, setEditForm] = useState({ name: '', userInput: '', @@ -386,6 +391,35 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str } } + const handleToggleShare = async () => { + if (!run) return + + setIsTogglingShare(true) + try { + const newPermissions = run.permissions === 'private' ? 'public' : 'private' + const response = await fetch(`/api/simulator/${run.id}/share`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + permissions: newPermissions, + }), + }) + + if (response.ok) { + const updatedRun = await response.json() + setRun(updatedRun) + } else { + console.error('Error toggling share status:', await response.text()) + } + } catch (error) { + console.error('Error toggling share status:', error) + } finally { + setIsTogglingShare(false) + } + } + const getStatusIcon = (status: string) => { switch (status) { case 'pending': @@ -502,6 +536,32 @@ export default function SimulatorRunPage({ params }: { params: Promise<{ id: str
+ {/* 共享按钮 - 只有已完成的 run 才能共享 */} + {run.status === 'completed' && ( + + )} + + +
+ + {/* Filters */}

- {t('showingResults', { - current: prompts.length, - total: pagination.total - })} + 显示 {contentType === 'prompts' ? prompts.length : simulatorRuns.length} / {pagination.total} 个{contentType === 'prompts' ? '提示词' : '运行结果'}

@@ -201,19 +272,28 @@ export function PlazaClient() { {loading ? (
- {t('loadingPrompts')} + 加载中...
- ) : prompts.length > 0 ? ( + ) : (contentType === 'prompts' ? prompts.length > 0 : simulatorRuns.length > 0) ? ( <> - {/* Prompt Grid */} + {/* Content Grid */}
- {prompts.map((prompt) => ( - - ))} + {contentType === 'prompts' + ? prompts.map((prompt) => ( + + )) + : simulatorRuns.map((simulatorRun) => ( + + )) + }
{/* Load More */} @@ -228,10 +308,10 @@ export function PlazaClient() { {loadingMore ? ( <> - {t('loadingPrompts')} + 加载中... ) : ( - t('loadMore') + '加载更多' )} @@ -240,13 +320,13 @@ export function PlazaClient() { ) : (

- {t('noPromptsFound')} + 暂无{contentType === 'prompts' ? '提示词' : '运行结果'}

- {t('noPromptsMessage')} + 尝试调整筛选条件或清除筛选器

)} diff --git a/src/components/plaza/SimulatorCard.tsx b/src/components/plaza/SimulatorCard.tsx new file mode 100644 index 0000000..7bf2649 --- /dev/null +++ b/src/components/plaza/SimulatorCard.tsx @@ -0,0 +1,235 @@ +'use client' + +import { useState } from 'react' +import { useTranslations } from 'next-intl' +import { Card } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Play, + User as UserIcon, + Calendar, + Eye, + FileText, + Zap, + ExternalLink, + Copy +} from 'lucide-react' +import Link from 'next/link' +import { formatDistanceToNow } from 'date-fns' +import { zhCN, enUS } from 'date-fns/locale' +import { useLocale } from 'next-intl' +import { Base64Image } from '@/components/ui/Base64Image' + +interface SimulatorRun { + id: string + name: string + status: string + userInput: string + output?: string | null + createdAt: string + user: { + id: string + username: string | null + image: string | null + } + prompt: { + id: string + name: string + content: string + tags: Array<{ + id: string + name: string + color: string + }> + } + model: { + name: string + provider: string + outputType?: string + description?: string + } +} + +interface SimulatorCardProps { + simulatorRun: SimulatorRun + onViewIncrement?: (id: string) => void +} + +export function SimulatorCard({ simulatorRun, onViewIncrement }: SimulatorCardProps) { + const t = useTranslations('plaza') + const locale = useLocale() + const [imageError, setImageError] = useState(false) + + const handleViewClick = () => { + if (onViewIncrement) { + onViewIncrement(simulatorRun.id) + } + } + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text) + } catch (error) { + console.error('Failed to copy to clipboard:', error) + } + } + + // 检查是否是图片输出 + const isImageOutput = simulatorRun.model.outputType === 'image' + const hasImageUrl = simulatorRun.output && ( + simulatorRun.output.includes('http') && + simulatorRun.output.match(/\.(png|jpg|jpeg|gif|webp)/i) + ) + + return ( + +
+ {/* Header */} +
+
+
+

+ {simulatorRun.name} +

+
+ {simulatorRun.model.provider} + - + {simulatorRun.model.name} + {simulatorRun.model.outputType && simulatorRun.model.outputType !== 'text' && ( + + {simulatorRun.model.outputType} + + )} +
+
+
+ + {/* Tags from prompt */} + {simulatorRun.prompt.tags.length > 0 && ( +
+ {simulatorRun.prompt.tags.slice(0, 3).map((tag) => ( + + {tag.name} + + ))} + {simulatorRun.prompt.tags.length > 3 && ( + + +{simulatorRun.prompt.tags.length - 3} + + )} +
+ )} +
+ + {/* Prompt Info */} +
+
+ + 基于提示词:{simulatorRun.prompt.name} +
+
+
用户输入:
+
+ {simulatorRun.userInput} +
+
+
+ + {/* Output Preview */} + {simulatorRun.output && ( +
+
+ + 生成结果 +
+ + {isImageOutput && hasImageUrl ? ( +
+ {!imageError ? ( +
+ setImageError(true)} + onClick={() => window.open(simulatorRun.output!, '_blank')} + /> +
+ +
+
+ ) : ( +
+
+ 图片预览不可用,点击查看详情 +
+
+ )} +
+ ) : ( +
+
+ {isImageOutput ? + "🎨 图片已生成,点击查看详情" : + simulatorRun.output + } +
+
+ )} +
+ )} + + {/* Meta Info */} +
+
+ + + {simulatorRun.user.username || simulatorRun.user.id.slice(0, 8)} + +
+
+ + + {formatDistanceToNow(new Date(simulatorRun.createdAt), { + addSuffix: true, + locale: locale === 'zh' ? zhCN : enUS + })} + +
+
+ + {/* Actions */} +
+ + + + +
+
+
+ ) +} \ No newline at end of file diff --git a/src/lib/simulator-utils.ts b/src/lib/simulator-utils.ts index e7e633b..c9e8ca5 100644 --- a/src/lib/simulator-utils.ts +++ b/src/lib/simulator-utils.ts @@ -53,7 +53,7 @@ export function calculateCost( inputCostPer1k?: number | null outputCostPer1k?: number | null }, - costMultiplier?: number + // costMultiplier?: number ): number { const inputCostPer1k = model.inputCostPer1k || 0 const outputCostPer1k = model.outputCostPer1k || 0 @@ -65,7 +65,8 @@ export function calculateCost( // 获取环境变量中的费用倍率,默认为 10 倍 const defaultMultiplier = parseFloat(process.env.FEE_CALCULATION_MULTIPLIER || '10.0') - const finalMultiplier = costMultiplier ?? defaultMultiplier + // const finalMultiplier = costMultiplier ?? defaultMultiplier + const finalMultiplier = defaultMultiplier // 应用费用倍率 return baseCost * finalMultiplier