336 lines
9.6 KiB
TypeScript
336 lines
9.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import { Header } from '@/components/layout/Header'
|
|
import { PlazaFilters } from './PlazaFilters'
|
|
import { PromptCard } from './PromptCard'
|
|
import { SimulatorCard } from './SimulatorCard'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Loader2, FileText, Play } from 'lucide-react'
|
|
|
|
interface Prompt {
|
|
id: string
|
|
name: string
|
|
content: string
|
|
description: string | null
|
|
createdAt: string
|
|
user: {
|
|
id: string
|
|
username: string | null
|
|
avatar: string | null
|
|
}
|
|
tags: Array<{
|
|
id: string
|
|
name: string
|
|
color: string
|
|
}>
|
|
versions: Array<{
|
|
content: string
|
|
version: number
|
|
createdAt: string
|
|
}>
|
|
stats?: {
|
|
viewCount: number
|
|
likeCount: number
|
|
rating: number | null
|
|
ratingCount: number
|
|
} | null
|
|
_count: {
|
|
versions: number
|
|
}
|
|
}
|
|
|
|
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 PlazaResponse {
|
|
prompts?: Prompt[]
|
|
simulatorRuns?: SimulatorRun[]
|
|
type: 'prompts' | 'simulators' | 'all'
|
|
pagination: {
|
|
page: number
|
|
limit: number
|
|
total: number
|
|
totalPages: number
|
|
hasNext: boolean
|
|
hasPrev: boolean
|
|
}
|
|
}
|
|
|
|
interface Tag {
|
|
id: string
|
|
name: string
|
|
color: string
|
|
_count: {
|
|
prompts: number
|
|
}
|
|
}
|
|
|
|
export function PlazaClient() {
|
|
const t = useTranslations('plaza')
|
|
const [contentType, setContentType] = useState<'prompts' | 'simulators'>('prompts')
|
|
const [prompts, setPrompts] = useState<Prompt[]>([])
|
|
const [simulatorRuns, setSimulatorRuns] = useState<SimulatorRun[]>([])
|
|
const [tags, setTags] = useState<Tag[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [loadingMore, setLoadingMore] = useState(false)
|
|
const [search, setSearch] = useState('')
|
|
const [selectedTag, setSelectedTag] = useState('')
|
|
const [sortBy, setSortBy] = useState('createdAt')
|
|
const [sortOrder, setSortOrder] = useState('desc')
|
|
const [pagination, setPagination] = useState({
|
|
page: 1,
|
|
limit: 12,
|
|
total: 0,
|
|
totalPages: 0,
|
|
hasNext: false,
|
|
hasPrev: false,
|
|
})
|
|
|
|
const fetchContent = useCallback(async (page = 1, reset = false) => {
|
|
if (page === 1) setLoading(true)
|
|
else setLoadingMore(true)
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
type: contentType,
|
|
page: page.toString(),
|
|
limit: pagination.limit.toString(),
|
|
sortBy,
|
|
sortOrder,
|
|
})
|
|
|
|
if (search) params.append('search', search)
|
|
if (selectedTag && contentType === 'prompts') params.append('tag', selectedTag)
|
|
|
|
const response = await fetch(`/api/plaza?${params}`)
|
|
if (!response.ok) throw new Error('Failed to fetch')
|
|
|
|
const data: PlazaResponse = await response.json()
|
|
|
|
if (reset) {
|
|
if (data.type === 'prompts' && data.prompts) {
|
|
setPrompts(data.prompts)
|
|
setSimulatorRuns([])
|
|
} else if (data.type === 'simulators' && data.simulatorRuns) {
|
|
setSimulatorRuns(data.simulatorRuns)
|
|
setPrompts([])
|
|
}
|
|
} else {
|
|
if (data.type === 'prompts' && data.prompts) {
|
|
setPrompts(prev => [...prev, ...data.prompts!])
|
|
} else if (data.type === 'simulators' && data.simulatorRuns) {
|
|
setSimulatorRuns(prev => [...prev, ...data.simulatorRuns!])
|
|
}
|
|
}
|
|
|
|
setPagination(data.pagination)
|
|
} catch (error) {
|
|
console.error('Error fetching content:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
setLoadingMore(false)
|
|
}
|
|
}, [contentType, search, selectedTag, sortBy, sortOrder, pagination.limit])
|
|
|
|
const fetchTags = useCallback(async () => {
|
|
try {
|
|
const response = await fetch('/api/plaza/tags')
|
|
if (!response.ok) throw new Error('Failed to fetch tags')
|
|
|
|
const data = await response.json()
|
|
setTags(data.tags)
|
|
} catch (error) {
|
|
console.error('Error fetching tags:', error)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetchTags()
|
|
}, [fetchTags])
|
|
|
|
useEffect(() => {
|
|
fetchContent(1, true)
|
|
}, [contentType, search, selectedTag, sortBy, sortOrder, fetchContent])
|
|
|
|
const handleLoadMore = () => {
|
|
if (pagination.hasNext && !loadingMore) {
|
|
fetchContent(pagination.page + 1, false)
|
|
}
|
|
}
|
|
|
|
const handleClearFilters = () => {
|
|
setSearch('')
|
|
setSelectedTag('')
|
|
setSortBy('createdAt')
|
|
setSortOrder('desc')
|
|
}
|
|
|
|
const incrementViewCount = async (promptId: string) => {
|
|
try {
|
|
await fetch(`/api/plaza/${promptId}/view`, {
|
|
method: 'POST',
|
|
})
|
|
} catch (error) {
|
|
console.error('Error updating view count:', error)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-background">
|
|
<Header />
|
|
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Header */}
|
|
<div className="text-center mb-8">
|
|
<h1 className="text-4xl font-bold text-foreground mb-4">
|
|
广场
|
|
</h1>
|
|
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
发现并探索社区分享的提示词和运行效果
|
|
</p>
|
|
</div>
|
|
|
|
{/* Content Type Toggle */}
|
|
<div className="flex justify-center mb-8">
|
|
<div className="inline-flex rounded-lg border border-border p-1 bg-muted/50">
|
|
<button
|
|
onClick={() => setContentType('prompts')}
|
|
className={`inline-flex items-center px-4 py-2 text-sm font-medium rounded-md transition-all ${
|
|
contentType === 'prompts'
|
|
? 'bg-background text-foreground shadow-sm border border-border'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
提示词
|
|
</button>
|
|
<button
|
|
onClick={() => setContentType('simulators')}
|
|
className={`inline-flex items-center px-4 py-2 text-sm font-medium rounded-md transition-all ${
|
|
contentType === 'simulators'
|
|
? 'bg-background text-foreground shadow-sm border border-border'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
运行效果
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<PlazaFilters
|
|
search={search}
|
|
onSearchChange={setSearch}
|
|
selectedTag={selectedTag}
|
|
onTagChange={setSelectedTag}
|
|
tags={tags}
|
|
sortBy={sortBy}
|
|
onSortByChange={setSortBy}
|
|
sortOrder={sortOrder}
|
|
onSortOrderChange={setSortOrder}
|
|
onClearFilters={handleClearFilters}
|
|
/>
|
|
|
|
{/* Results */}
|
|
<div className="mb-6">
|
|
<p className="text-sm text-muted-foreground">
|
|
显示 {contentType === 'prompts' ? prompts.length : simulatorRuns.length} / {pagination.total} 个{contentType === 'prompts' ? '提示词' : '运行结果'}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="h-8 w-8 animate-spin" />
|
|
<span className="ml-2">加载中...</span>
|
|
</div>
|
|
) : (contentType === 'prompts' ? prompts.length > 0 : simulatorRuns.length > 0) ? (
|
|
<>
|
|
{/* Content Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
|
|
{contentType === 'prompts'
|
|
? prompts.map((prompt) => (
|
|
<PromptCard
|
|
key={prompt.id}
|
|
prompt={prompt}
|
|
onViewIncrement={incrementViewCount}
|
|
/>
|
|
))
|
|
: simulatorRuns.map((simulatorRun) => (
|
|
<SimulatorCard
|
|
key={simulatorRun.id}
|
|
simulatorRun={simulatorRun}
|
|
onViewIncrement={incrementViewCount}
|
|
/>
|
|
))
|
|
}
|
|
</div>
|
|
|
|
{/* Load More */}
|
|
{pagination.hasNext && (
|
|
<div className="text-center">
|
|
<Button
|
|
onClick={handleLoadMore}
|
|
disabled={loadingMore}
|
|
variant="outline"
|
|
size="lg"
|
|
>
|
|
{loadingMore ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
加载中...
|
|
</>
|
|
) : (
|
|
'加载更多'
|
|
)}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="text-center py-12">
|
|
<h3 className="text-lg font-medium text-foreground mb-2">
|
|
暂无{contentType === 'prompts' ? '提示词' : '运行结果'}
|
|
</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
尝试调整筛选条件或清除筛选器
|
|
</p>
|
|
<Button variant="outline" onClick={handleClearFilters}>
|
|
清除筛选器
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
)
|
|
} |