Prmbr/src/components/studio/VersionHistory.tsx
2025-07-30 00:25:54 +08:00

287 lines
9.2 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { Button } from '@/components/ui/button'
import { LoadingSpinner } from '@/components/ui/loading-spinner'
import {
History,
Clock,
GitBranch,
RotateCcw,
Eye,
GitCompare,
ChevronDown,
ChevronRight
} from 'lucide-react'
interface PromptVersion {
id: string
version: number
content: string
changelog: string
createdAt: string
}
interface VersionHistoryProps {
promptId: string
userId: string
onVersionRestore?: (version: PromptVersion) => void
onVersionSelect?: (version: PromptVersion) => void
}
export function VersionHistory({
promptId,
userId,
onVersionRestore,
onVersionSelect
}: VersionHistoryProps) {
const t = useTranslations('studio')
const [versions, setVersions] = useState<PromptVersion[]>([])
const [loading, setLoading] = useState(true)
const [expandedVersions, setExpandedVersions] = useState<Set<string>>(new Set())
const [selectedVersions, setSelectedVersions] = useState<string[]>([])
const [restoring, setRestoring] = useState<string | null>(null)
useEffect(() => {
fetchVersions()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [promptId, userId])
const fetchVersions = async () => {
try {
setLoading(true)
const response = await fetch(`/api/prompts/${promptId}/versions?userId=${userId}`)
if (response.ok) {
const data = await response.json()
setVersions(data)
}
} catch (error) {
console.error('Error fetching versions:', error)
} finally {
setLoading(false)
}
}
const handleRestoreVersion = async (version: PromptVersion) => {
if (restoring) return
try {
setRestoring(version.id)
const response = await fetch(
`/api/prompts/${promptId}/versions/${version.id}/restore`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId })
}
)
if (response.ok) {
await fetchVersions() // 刷新版本列表
onVersionRestore?.(version)
}
} catch (error) {
console.error('Error restoring version:', error)
} finally {
setRestoring(null)
}
}
const toggleVersionExpansion = (versionId: string) => {
const newExpanded = new Set(expandedVersions)
if (newExpanded.has(versionId)) {
newExpanded.delete(versionId)
} else {
newExpanded.add(versionId)
}
setExpandedVersions(newExpanded)
}
const handleVersionSelect = (versionId: string) => {
if (selectedVersions.includes(versionId)) {
setSelectedVersions(prev => prev.filter(id => id !== versionId))
} else if (selectedVersions.length < 2) {
setSelectedVersions(prev => [...prev, versionId])
} else {
// 替换第一个选中的版本
setSelectedVersions([selectedVersions[1], versionId])
}
}
const handleCompareVersions = () => {
if (selectedVersions.length === 2) {
const [fromId, toId] = selectedVersions
window.open(
`/studio/${promptId}/compare?from=${fromId}&to=${toId}`,
'_blank'
)
}
}
const formatDate = (dateString: string) => {
return new Intl.DateTimeFormat('default', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(dateString))
}
if (loading) {
return (
<div className="flex items-center justify-center p-8">
<LoadingSpinner size="lg" />
</div>
)
}
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<History className="h-5 w-5 text-muted-foreground" />
<h3 className="font-semibold text-foreground">{t('versionHistory')}</h3>
<span className="text-sm text-muted-foreground">
({versions.length} versions)
</span>
</div>
{selectedVersions.length === 2 && (
<Button
variant="outline"
size="sm"
onClick={handleCompareVersions}
className="flex items-center space-x-1"
>
<GitCompare className="h-4 w-4" />
<span>Compare</span>
</Button>
)}
</div>
{/* Version List */}
<div className="space-y-2">
{versions.map((version, index) => {
const isExpanded = expandedVersions.has(version.id)
const isSelected = selectedVersions.includes(version.id)
const isLatest = index === 0
return (
<div
key={version.id}
className={`border border-border rounded-lg transition-colors ${
isSelected ? 'ring-2 ring-primary' : ''
}`}
>
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<input
type="checkbox"
checked={isSelected}
onChange={() => handleVersionSelect(version.id)}
className="rounded"
disabled={selectedVersions.length >= 2 && !isSelected}
/>
<div className="flex items-center space-x-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="font-medium text-foreground">
Version {version.version}
</span>
{isLatest && (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-primary/10 text-primary rounded-full">
Current
</span>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-1 text-sm text-muted-foreground">
<Clock className="h-3 w-3" />
<span className="hidden sm:inline">{formatDate(version.createdAt)}</span>
<span className="sm:hidden">{new Date(version.createdAt).toLocaleDateString()}</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => toggleVersionExpansion(version.id)}
className="h-8 w-8 p-0"
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
</div>
</div>
{/* Changelog */}
<div className="mt-2">
<p className="text-sm text-muted-foreground">
{version.changelog}
</p>
</div>
{/* Expanded Content */}
{isExpanded && (
<div className="mt-4 pt-4 border-t border-border">
<div className="bg-muted rounded-lg p-3">
<pre className="text-sm text-foreground whitespace-pre-wrap font-mono">
{version.content.substring(0, 200)}
{version.content.length > 200 && '...'}
</pre>
</div>
<div className="flex items-center justify-between mt-3">
<Button
variant="outline"
size="sm"
onClick={() => onVersionSelect?.(version)}
className="flex items-center space-x-1"
>
<Eye className="h-3 w-3" />
<span>View Full</span>
</Button>
{!isLatest && (
<Button
variant="outline"
size="sm"
onClick={() => handleRestoreVersion(version)}
disabled={restoring === version.id}
className="flex items-center space-x-1"
>
{restoring === version.id ? (
<LoadingSpinner size="sm" />
) : (
<RotateCcw className="h-3 w-3" />
)}
<span>Restore</span>
</Button>
)}
</div>
</div>
)}
</div>
</div>
)
})}
</div>
{versions.length === 0 && (
<div className="text-center py-8">
<History className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground">No version history available</p>
</div>
)}
</div>
)
}