generated from fahricansecer/boilerplate-fe
main
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
LuBrain,
|
||||
LuChartBar,
|
||||
LuDownload,
|
||||
LuHistory,
|
||||
} from 'react-icons/lu';
|
||||
import {
|
||||
useGetProject,
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
|
||||
// Tab Components
|
||||
import ResearchTab from './tabs/ResearchTab';
|
||||
import { BriefTab, CharactersTab, ScriptTab, AnalysisTab } from './tabs';
|
||||
import { BriefTab, CharactersTab, ScriptTab, AnalysisTab, VersionsTab } from './tabs';
|
||||
|
||||
|
||||
interface ProjectDetailProps {
|
||||
@@ -192,6 +193,15 @@ export default function ProjectDetail({ projectId }: ProjectDetailProps) {
|
||||
<LuChartBar />
|
||||
{t('analysis')}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value='versions'>
|
||||
<LuHistory />
|
||||
Versiyonlar
|
||||
{project.currentVersionNumber > 0 && (
|
||||
<Badge ml={2} size='sm'>
|
||||
{project.currentVersionNumber}
|
||||
</Badge>
|
||||
)}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Box minH='500px'>
|
||||
@@ -234,6 +244,10 @@ export default function ProjectDetail({ projectId }: ProjectDetailProps) {
|
||||
isAnalyzing={neuroAnalysis.isPending || youtubeAudit.isPending}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value='versions'>
|
||||
<VersionsTab project={project} />
|
||||
</Tabs.Content>
|
||||
</Box>
|
||||
</Tabs.Root>
|
||||
</Container>
|
||||
|
||||
386
src/components/skriptai/tabs/VersionsTab.tsx
Normal file
386
src/components/skriptai/tabs/VersionsTab.tsx
Normal file
@@ -0,0 +1,386 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Heading,
|
||||
HStack,
|
||||
IconButton,
|
||||
Text,
|
||||
VStack,
|
||||
Badge,
|
||||
Input,
|
||||
Textarea,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
LuHistory,
|
||||
LuSave,
|
||||
LuUndo2,
|
||||
LuTrash2,
|
||||
LuGitCompare,
|
||||
LuCheck,
|
||||
LuClock,
|
||||
LuBot,
|
||||
LuUser,
|
||||
LuChevronDown,
|
||||
LuChevronUp,
|
||||
} from 'react-icons/lu';
|
||||
import {
|
||||
useVersions,
|
||||
useCreateSnapshot,
|
||||
useRestoreVersion,
|
||||
useDeleteVersion,
|
||||
} from '@/lib/api/skriptai';
|
||||
import { toaster } from '@/components/ui/feedback/toaster';
|
||||
import type { ScriptProject, ScriptVersionSummary } from '@/types/skriptai';
|
||||
|
||||
interface VersionsTabProps {
|
||||
project: ScriptProject;
|
||||
}
|
||||
|
||||
export default function VersionsTab({ project }: VersionsTabProps) {
|
||||
const t = useTranslations('skriptai');
|
||||
const { data: versions, isLoading } = useVersions(project.id);
|
||||
const createSnapshot = useCreateSnapshot(project.id);
|
||||
const restoreVersion = useRestoreVersion(project.id);
|
||||
const deleteVersion = useDeleteVersion(project.id);
|
||||
|
||||
const [showSaveForm, setShowSaveForm] = useState(false);
|
||||
const [saveLabel, setSaveLabel] = useState('');
|
||||
const [saveNote, setSaveNote] = useState('');
|
||||
const [expandedVersionId, setExpandedVersionId] = useState<string | null>(null);
|
||||
|
||||
const handleSave = async () => {
|
||||
await createSnapshot.mutateAsync({
|
||||
label: saveLabel || undefined,
|
||||
changeNote: saveNote || undefined,
|
||||
});
|
||||
setSaveLabel('');
|
||||
setSaveNote('');
|
||||
setShowSaveForm(false);
|
||||
toaster.create({
|
||||
title: 'Versiyon kaydedildi',
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const handleRestore = async (versionId: string, versionNumber: number) => {
|
||||
if (!confirm(`v${versionNumber} versiyonuna geri dönmek istediğinize emin misiniz? Mevcut durum otomatik olarak kaydedilecektir.`)) {
|
||||
return;
|
||||
}
|
||||
await restoreVersion.mutateAsync(versionId);
|
||||
toaster.create({
|
||||
title: `v${versionNumber} versiyonuna geri dönüldü`,
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (versionId: string, versionNumber: number) => {
|
||||
if (!confirm(`v${versionNumber} versiyonunu silmek istediğinize emin misiniz?`)) {
|
||||
return;
|
||||
}
|
||||
await deleteVersion.mutateAsync(versionId);
|
||||
toaster.create({
|
||||
title: `v${versionNumber} silindi`,
|
||||
type: 'info',
|
||||
});
|
||||
};
|
||||
|
||||
const getGeneratedByIcon = (generatedBy: string) => {
|
||||
switch (generatedBy) {
|
||||
case 'AI':
|
||||
return <LuBot />;
|
||||
case 'USER':
|
||||
return <LuUser />;
|
||||
default:
|
||||
return <LuClock />;
|
||||
}
|
||||
};
|
||||
|
||||
const getGeneratedByColor = (generatedBy: string) => {
|
||||
switch (generatedBy) {
|
||||
case 'AI':
|
||||
return 'purple';
|
||||
case 'USER':
|
||||
return 'blue';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
const getGeneratedByLabel = (generatedBy: string) => {
|
||||
switch (generatedBy) {
|
||||
case 'AI':
|
||||
return 'AI Üretimi';
|
||||
case 'USER':
|
||||
return 'Manuel Kayıt';
|
||||
default:
|
||||
return 'Otomatik Kayıt';
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleString('tr-TR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<VStack align='stretch' gap={6}>
|
||||
{/* Header */}
|
||||
<Flex justify='space-between' align='center' flexWrap='wrap' gap={4}>
|
||||
<HStack gap={3}>
|
||||
<LuHistory size={20} />
|
||||
<Heading size='md'>Versiyon Geçmişi</Heading>
|
||||
<Badge colorPalette='blue'>
|
||||
{versions?.length || 0} versiyon
|
||||
</Badge>
|
||||
<Badge colorPalette='green'>
|
||||
Şu an: v{project.currentVersionNumber}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
<Button
|
||||
colorPalette='blue'
|
||||
onClick={() => setShowSaveForm(!showSaveForm)}
|
||||
disabled={!project.segments || project.segments.length === 0}
|
||||
>
|
||||
<LuSave />
|
||||
Mevcut Durumu Kaydet
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{/* Save Form */}
|
||||
{showSaveForm && (
|
||||
<Card.Root p={4} borderColor='blue.200' borderWidth='2px'>
|
||||
<VStack align='stretch' gap={3}>
|
||||
<Text fontWeight='bold' fontSize='sm'>
|
||||
💾 Yeni Versiyon Kaydet
|
||||
</Text>
|
||||
<Input
|
||||
placeholder='Versiyon etiketi (ör. "Final Taslak", "Sponsor Revizyonu")'
|
||||
value={saveLabel}
|
||||
onChange={(e) => setSaveLabel(e.target.value)}
|
||||
size='sm'
|
||||
/>
|
||||
<Textarea
|
||||
placeholder='Değişiklik notu (isteğe bağlı)'
|
||||
value={saveNote}
|
||||
onChange={(e) => setSaveNote(e.target.value)}
|
||||
rows={2}
|
||||
size='sm'
|
||||
/>
|
||||
<HStack justify='end'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ghost'
|
||||
onClick={() => setShowSaveForm(false)}
|
||||
>
|
||||
İptal
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
colorPalette='green'
|
||||
onClick={handleSave}
|
||||
loading={createSnapshot.isPending}
|
||||
>
|
||||
<LuCheck />
|
||||
Kaydet
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Card.Root>
|
||||
)}
|
||||
|
||||
{/* No segments warning */}
|
||||
{(!project.segments || project.segments.length === 0) && (
|
||||
<Card.Root p={6}>
|
||||
<VStack>
|
||||
<Text color='gray.500'>
|
||||
Henüz script üretilmemiş. Önce bir script oluşturun, ardından versiyonları burada takip edebilirsiniz.
|
||||
</Text>
|
||||
</VStack>
|
||||
</Card.Root>
|
||||
)}
|
||||
|
||||
{/* Version List */}
|
||||
{isLoading ? (
|
||||
<Card.Root p={6}>
|
||||
<Text color='gray.500'>Versiyonlar yükleniyor...</Text>
|
||||
</Card.Root>
|
||||
) : versions && versions.length > 0 ? (
|
||||
<VStack align='stretch' gap={3}>
|
||||
{versions.map((version) => (
|
||||
<VersionCard
|
||||
key={version.id}
|
||||
version={version}
|
||||
isExpanded={expandedVersionId === version.id}
|
||||
onToggle={() =>
|
||||
setExpandedVersionId(
|
||||
expandedVersionId === version.id
|
||||
? null
|
||||
: version.id,
|
||||
)
|
||||
}
|
||||
onRestore={() =>
|
||||
handleRestore(version.id, version.versionNumber)
|
||||
}
|
||||
onDelete={() =>
|
||||
handleDelete(version.id, version.versionNumber)
|
||||
}
|
||||
isRestoring={restoreVersion.isPending}
|
||||
isDeleting={deleteVersion.isPending}
|
||||
getGeneratedByIcon={getGeneratedByIcon}
|
||||
getGeneratedByColor={getGeneratedByColor}
|
||||
getGeneratedByLabel={getGeneratedByLabel}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
project.segments &&
|
||||
project.segments.length > 0 && (
|
||||
<Card.Root p={6}>
|
||||
<VStack>
|
||||
<Text color='gray.500'>
|
||||
Henüz kayıtlı versiyon yok. "Mevcut Durumu Kaydet" butonuna tıklayarak ilk versiyonu oluşturun.
|
||||
</Text>
|
||||
<Text color='gray.400' fontSize='sm'>
|
||||
Script yeniden üretildiğinde versiyonlar otomatik olarak kaydedilir.
|
||||
</Text>
|
||||
</VStack>
|
||||
</Card.Root>
|
||||
)
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
// ========== Version Card ==========
|
||||
|
||||
interface VersionCardProps {
|
||||
version: ScriptVersionSummary;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
onRestore: () => void;
|
||||
onDelete: () => void;
|
||||
isRestoring: boolean;
|
||||
isDeleting: boolean;
|
||||
getGeneratedByIcon: (g: string) => React.ReactNode;
|
||||
getGeneratedByColor: (g: string) => string;
|
||||
getGeneratedByLabel: (g: string) => string;
|
||||
formatDate: (d: string) => string;
|
||||
}
|
||||
|
||||
function VersionCard({
|
||||
version,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
onRestore,
|
||||
onDelete,
|
||||
isRestoring,
|
||||
isDeleting,
|
||||
getGeneratedByIcon,
|
||||
getGeneratedByColor,
|
||||
getGeneratedByLabel,
|
||||
formatDate,
|
||||
}: VersionCardProps) {
|
||||
return (
|
||||
<Card.Root
|
||||
_hover={{ shadow: 'md', borderColor: 'blue.200' }}
|
||||
transition='all 0.2s'
|
||||
cursor='pointer'
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Card.Header py={3} px={4}>
|
||||
<Flex justify='space-between' align='center'>
|
||||
<HStack gap={3}>
|
||||
<Badge
|
||||
colorPalette={getGeneratedByColor(version.generatedBy)}
|
||||
variant='subtle'
|
||||
px={2}
|
||||
py={1}
|
||||
>
|
||||
{getGeneratedByIcon(version.generatedBy)}
|
||||
<Text ml={1} fontSize='xs'>
|
||||
v{version.versionNumber}
|
||||
</Text>
|
||||
</Badge>
|
||||
|
||||
<VStack align='start' gap={0}>
|
||||
<Text fontWeight='semibold' fontSize='sm'>
|
||||
{version.label || `Versiyon ${version.versionNumber}`}
|
||||
</Text>
|
||||
<Text fontSize='xs' color='gray.500'>
|
||||
{formatDate(version.createdAt)} •{' '}
|
||||
{getGeneratedByLabel(version.generatedBy)}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
<HStack gap={2}>
|
||||
<Badge colorPalette='gray' size='sm'>
|
||||
{version.segmentCount} bölüm
|
||||
</Badge>
|
||||
<Badge colorPalette='gray' size='sm'>
|
||||
~{version.totalWords} kelime
|
||||
</Badge>
|
||||
{isExpanded ? <LuChevronUp /> : <LuChevronDown />}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Card.Header>
|
||||
|
||||
{isExpanded && (
|
||||
<Card.Body pt={0} px={4} pb={4}>
|
||||
<VStack align='stretch' gap={3}>
|
||||
{version.changeNote && (
|
||||
<Box
|
||||
bg='gray.50'
|
||||
_dark={{ bg: 'whiteAlpha.100' }}
|
||||
p={3}
|
||||
borderRadius='md'
|
||||
>
|
||||
<Text fontSize='xs' fontWeight='bold' color='gray.500' mb={1}>
|
||||
📝 Değişiklik Notu
|
||||
</Text>
|
||||
<Text fontSize='sm'>{version.changeNote}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Flex gap={2} justify='end' onClick={(e) => e.stopPropagation()}>
|
||||
<Button
|
||||
size='xs'
|
||||
colorPalette='blue'
|
||||
variant='outline'
|
||||
onClick={onRestore}
|
||||
loading={isRestoring}
|
||||
>
|
||||
<LuUndo2 />
|
||||
Bu Versiyona Geri Dön
|
||||
</Button>
|
||||
<IconButton
|
||||
aria-label='Versiyonu sil'
|
||||
size='xs'
|
||||
colorPalette='red'
|
||||
variant='ghost'
|
||||
onClick={onDelete}
|
||||
loading={isDeleting}
|
||||
>
|
||||
<LuTrash2 />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</VStack>
|
||||
</Card.Body>
|
||||
)}
|
||||
</Card.Root>
|
||||
);
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export { default as BriefTab } from './BriefTab';
|
||||
export { default as CharactersTab } from './CharactersTab';
|
||||
export { default as ScriptTab } from './ScriptTab';
|
||||
export { default as AnalysisTab } from './AnalysisTab';
|
||||
export { default as VersionsTab } from './VersionsTab';
|
||||
|
||||
@@ -33,6 +33,8 @@ export {
|
||||
useDeleteSegment,
|
||||
useReorderSegments,
|
||||
useGenerateSegmentImage,
|
||||
useRegenerateSegment,
|
||||
useRegeneratePartial,
|
||||
ScriptsQueryKeys,
|
||||
} from './scripts/use-hooks';
|
||||
|
||||
@@ -64,6 +66,20 @@ export {
|
||||
AnalysisQueryKeys,
|
||||
} from './analysis/use-hooks';
|
||||
|
||||
// Services - Versions
|
||||
export { versionsService } from './versions/service';
|
||||
|
||||
// Hooks - Versions
|
||||
export {
|
||||
useVersions,
|
||||
useVersion,
|
||||
useCreateSnapshot,
|
||||
useRestoreVersion,
|
||||
useDeleteVersion,
|
||||
useCompareVersions,
|
||||
VersionsQueryKeys,
|
||||
} from './versions/use-hooks';
|
||||
|
||||
// Types - Projects
|
||||
export type {
|
||||
CreateProjectDto,
|
||||
|
||||
@@ -80,4 +80,19 @@ export const scriptsService = {
|
||||
method: 'post',
|
||||
});
|
||||
},
|
||||
regenerateSegment: (segmentId: string) => {
|
||||
return apiRequest<ApiResponse<ScriptSegment>>({
|
||||
url: `/skriptai/scripts/segments/${segmentId}/regenerate`,
|
||||
client: 'skriptai',
|
||||
method: 'post',
|
||||
});
|
||||
},
|
||||
regeneratePartial: (projectId: string, segmentIds: string[]) => {
|
||||
return apiRequest<ApiResponse<ScriptSegment[]>>({
|
||||
url: '/skriptai/scripts/regenerate-partial',
|
||||
client: 'skriptai',
|
||||
method: 'post',
|
||||
data: { projectId, segmentIds },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -134,3 +134,38 @@ export function useGenerateSegmentImage(projectId: string) {
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useRegenerateSegment(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<ApiResponse<ScriptSegment>, Error, string>({
|
||||
mutationFn: (segmentId) => scriptsService.regenerateSegment(segmentId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ProjectsQueryKeys.detail(projectId),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useRegeneratePartial(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<ScriptSegment[]>,
|
||||
Error,
|
||||
string[]
|
||||
>({
|
||||
mutationFn: (segmentIds) =>
|
||||
scriptsService.regeneratePartial(projectId, segmentIds),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ProjectsQueryKeys.detail(projectId),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
78
src/lib/api/skriptai/versions/service.ts
Normal file
78
src/lib/api/skriptai/versions/service.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { apiRequest } from '@/lib/api/api-service';
|
||||
import { ApiResponse } from '@/types/api-response';
|
||||
import type {
|
||||
ScriptVersionSummary,
|
||||
ScriptVersion,
|
||||
ScriptProject,
|
||||
VersionComparison,
|
||||
} from '@/types/skriptai';
|
||||
|
||||
/**
|
||||
* Versions Service - SkriptAI
|
||||
* Matches Backend: /api/skriptai/projects/:projectId/versions/*
|
||||
*/
|
||||
|
||||
const list = (projectId: string) => {
|
||||
return apiRequest<ApiResponse<ScriptVersionSummary[]>>({
|
||||
url: `/skriptai/projects/${projectId}/versions`,
|
||||
client: 'skriptai',
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
const getOne = (projectId: string, versionId: string) => {
|
||||
return apiRequest<ApiResponse<ScriptVersion>>({
|
||||
url: `/skriptai/projects/${projectId}/versions/${versionId}`,
|
||||
client: 'skriptai',
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
const createSnapshot = (
|
||||
projectId: string,
|
||||
data: { label?: string; changeNote?: string },
|
||||
) => {
|
||||
return apiRequest<ApiResponse<ScriptVersion>>({
|
||||
url: `/skriptai/projects/${projectId}/versions`,
|
||||
client: 'skriptai',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const restore = (projectId: string, versionId: string) => {
|
||||
return apiRequest<ApiResponse<ScriptProject>>({
|
||||
url: `/skriptai/projects/${projectId}/versions/${versionId}/restore`,
|
||||
client: 'skriptai',
|
||||
method: 'post',
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (projectId: string, versionId: string) => {
|
||||
return apiRequest<ApiResponse<{ deleted: boolean; versionNumber: number }>>({
|
||||
url: `/skriptai/projects/${projectId}/versions/${versionId}`,
|
||||
client: 'skriptai',
|
||||
method: 'delete',
|
||||
});
|
||||
};
|
||||
|
||||
const compare = (
|
||||
projectId: string,
|
||||
versionAId: string,
|
||||
versionBId: string,
|
||||
) => {
|
||||
return apiRequest<ApiResponse<VersionComparison>>({
|
||||
url: `/skriptai/projects/${projectId}/versions/compare?versionA=${versionAId}&versionB=${versionBId}`,
|
||||
client: 'skriptai',
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export const versionsService = {
|
||||
list,
|
||||
getOne,
|
||||
createSnapshot,
|
||||
restore,
|
||||
remove,
|
||||
compare,
|
||||
};
|
||||
136
src/lib/api/skriptai/versions/use-hooks.ts
Normal file
136
src/lib/api/skriptai/versions/use-hooks.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { ApiResponse } from '@/types/api-response';
|
||||
import { versionsService } from './service';
|
||||
import type {
|
||||
ScriptVersionSummary,
|
||||
ScriptVersion,
|
||||
ScriptProject,
|
||||
VersionComparison,
|
||||
} from '@/types/skriptai';
|
||||
import { ProjectsQueryKeys } from '../projects/use-hooks';
|
||||
|
||||
export const VersionsQueryKeys = {
|
||||
all: ['skriptai', 'versions'] as const,
|
||||
list: (projectId: string) =>
|
||||
['skriptai', 'versions', projectId] as const,
|
||||
detail: (projectId: string, versionId: string) =>
|
||||
['skriptai', 'versions', projectId, versionId] as const,
|
||||
compare: (projectId: string, vA: string, vB: string) =>
|
||||
['skriptai', 'versions', projectId, 'compare', vA, vB] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all versions for a project
|
||||
*/
|
||||
export function useVersions(projectId: string) {
|
||||
const { data, ...rest } = useQuery<ApiResponse<ScriptVersionSummary[]>>({
|
||||
queryKey: VersionsQueryKeys.list(projectId),
|
||||
queryFn: () => versionsService.list(projectId),
|
||||
enabled: !!projectId,
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single version with full snapshot data
|
||||
*/
|
||||
export function useVersion(projectId: string, versionId: string) {
|
||||
const { data, ...rest } = useQuery<ApiResponse<ScriptVersion>>({
|
||||
queryKey: VersionsQueryKeys.detail(projectId, versionId),
|
||||
queryFn: () => versionsService.getOne(projectId, versionId),
|
||||
enabled: !!projectId && !!versionId,
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a manual snapshot
|
||||
*/
|
||||
export function useCreateSnapshot(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<ScriptVersion>,
|
||||
Error,
|
||||
{ label?: string; changeNote?: string }
|
||||
>({
|
||||
mutationFn: (snapshotData) =>
|
||||
versionsService.createSnapshot(projectId, snapshotData),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: VersionsQueryKeys.list(projectId),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore to a specific version
|
||||
*/
|
||||
export function useRestoreVersion(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<ScriptProject>,
|
||||
Error,
|
||||
string
|
||||
>({
|
||||
mutationFn: (versionId) =>
|
||||
versionsService.restore(projectId, versionId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ProjectsQueryKeys.detail(projectId),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: VersionsQueryKeys.list(projectId),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a version
|
||||
*/
|
||||
export function useDeleteVersion(projectId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<{ deleted: boolean; versionNumber: number }>,
|
||||
Error,
|
||||
string
|
||||
>({
|
||||
mutationFn: (versionId) =>
|
||||
versionsService.remove(projectId, versionId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: VersionsQueryKeys.list(projectId),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two versions
|
||||
*/
|
||||
export function useCompareVersions(
|
||||
projectId: string,
|
||||
versionAId: string,
|
||||
versionBId: string,
|
||||
) {
|
||||
const { data, ...rest } = useQuery<ApiResponse<VersionComparison>>({
|
||||
queryKey: VersionsQueryKeys.compare(projectId, versionAId, versionBId),
|
||||
queryFn: () =>
|
||||
versionsService.compare(projectId, versionAId, versionBId),
|
||||
enabled: !!projectId && !!versionAId && !!versionBId,
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
@@ -65,6 +65,8 @@ export type SourceType =
|
||||
| 'book'
|
||||
| 'document';
|
||||
|
||||
export type ProjectStatus = 'DRAFT' | 'RESEARCHING' | 'SCRIPTING' | 'ANALYZING' | 'COMPLETED';
|
||||
|
||||
export type CharacterRole =
|
||||
| 'Protagonist'
|
||||
| 'Antagonist'
|
||||
@@ -87,6 +89,8 @@ export interface ScriptProject {
|
||||
logline?: string;
|
||||
highConcept?: string;
|
||||
includeInterviews: boolean;
|
||||
status: ProjectStatus;
|
||||
currentVersionNumber: number;
|
||||
seoTitle?: string;
|
||||
seoDescription?: string;
|
||||
seoTags: string[];
|
||||
@@ -269,6 +273,7 @@ export interface ProjectListItem {
|
||||
topic: string;
|
||||
contentType: string;
|
||||
language: string;
|
||||
status?: ProjectStatus;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
_count: {
|
||||
@@ -276,3 +281,41 @@ export interface ProjectListItem {
|
||||
sources: number;
|
||||
};
|
||||
}
|
||||
|
||||
// Version History Types
|
||||
export interface ScriptVersionSummary {
|
||||
id: string;
|
||||
versionNumber: number;
|
||||
label?: string;
|
||||
generatedBy: 'AI' | 'USER' | 'AUTO_SAVE';
|
||||
segmentCount: number;
|
||||
totalWords: number;
|
||||
changeNote?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ScriptVersion extends ScriptVersionSummary {
|
||||
projectId: string;
|
||||
snapshotData: any[];
|
||||
seoSnapshot?: {
|
||||
seoTitle?: string;
|
||||
seoDescription?: string;
|
||||
seoTags?: string[];
|
||||
thumbnailIdeas?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface VersionDiff {
|
||||
index: number;
|
||||
type: 'added' | 'removed' | 'modified';
|
||||
segmentType?: string;
|
||||
narratorScript?: { before: string | null; after: string | null };
|
||||
visualDescription?: { before: string | null; after: string | null };
|
||||
}
|
||||
|
||||
export interface VersionComparison {
|
||||
versionA: { id: string; versionNumber: number; label?: string; createdAt: string };
|
||||
versionB: { id: string; versionNumber: number; label?: string; createdAt: string };
|
||||
totalDiffs: number;
|
||||
diffs: VersionDiff[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user