generated from fahricansecer/boilerplate-fe
main
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
|||||||
LuBrain,
|
LuBrain,
|
||||||
LuChartBar,
|
LuChartBar,
|
||||||
LuDownload,
|
LuDownload,
|
||||||
|
LuHistory,
|
||||||
} from 'react-icons/lu';
|
} from 'react-icons/lu';
|
||||||
import {
|
import {
|
||||||
useGetProject,
|
useGetProject,
|
||||||
@@ -38,7 +39,7 @@ import {
|
|||||||
|
|
||||||
// Tab Components
|
// Tab Components
|
||||||
import ResearchTab from './tabs/ResearchTab';
|
import ResearchTab from './tabs/ResearchTab';
|
||||||
import { BriefTab, CharactersTab, ScriptTab, AnalysisTab } from './tabs';
|
import { BriefTab, CharactersTab, ScriptTab, AnalysisTab, VersionsTab } from './tabs';
|
||||||
|
|
||||||
|
|
||||||
interface ProjectDetailProps {
|
interface ProjectDetailProps {
|
||||||
@@ -192,6 +193,15 @@ export default function ProjectDetail({ projectId }: ProjectDetailProps) {
|
|||||||
<LuChartBar />
|
<LuChartBar />
|
||||||
{t('analysis')}
|
{t('analysis')}
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value='versions'>
|
||||||
|
<LuHistory />
|
||||||
|
Versiyonlar
|
||||||
|
{project.currentVersionNumber > 0 && (
|
||||||
|
<Badge ml={2} size='sm'>
|
||||||
|
{project.currentVersionNumber}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<Box minH='500px'>
|
<Box minH='500px'>
|
||||||
@@ -234,6 +244,10 @@ export default function ProjectDetail({ projectId }: ProjectDetailProps) {
|
|||||||
isAnalyzing={neuroAnalysis.isPending || youtubeAudit.isPending}
|
isAnalyzing={neuroAnalysis.isPending || youtubeAudit.isPending}
|
||||||
/>
|
/>
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value='versions'>
|
||||||
|
<VersionsTab project={project} />
|
||||||
|
</Tabs.Content>
|
||||||
</Box>
|
</Box>
|
||||||
</Tabs.Root>
|
</Tabs.Root>
|
||||||
</Container>
|
</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 CharactersTab } from './CharactersTab';
|
||||||
export { default as ScriptTab } from './ScriptTab';
|
export { default as ScriptTab } from './ScriptTab';
|
||||||
export { default as AnalysisTab } from './AnalysisTab';
|
export { default as AnalysisTab } from './AnalysisTab';
|
||||||
|
export { default as VersionsTab } from './VersionsTab';
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ export {
|
|||||||
useDeleteSegment,
|
useDeleteSegment,
|
||||||
useReorderSegments,
|
useReorderSegments,
|
||||||
useGenerateSegmentImage,
|
useGenerateSegmentImage,
|
||||||
|
useRegenerateSegment,
|
||||||
|
useRegeneratePartial,
|
||||||
ScriptsQueryKeys,
|
ScriptsQueryKeys,
|
||||||
} from './scripts/use-hooks';
|
} from './scripts/use-hooks';
|
||||||
|
|
||||||
@@ -64,6 +66,20 @@ export {
|
|||||||
AnalysisQueryKeys,
|
AnalysisQueryKeys,
|
||||||
} from './analysis/use-hooks';
|
} 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
|
// Types - Projects
|
||||||
export type {
|
export type {
|
||||||
CreateProjectDto,
|
CreateProjectDto,
|
||||||
|
|||||||
@@ -80,4 +80,19 @@ export const scriptsService = {
|
|||||||
method: 'post',
|
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 };
|
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'
|
| 'book'
|
||||||
| 'document';
|
| 'document';
|
||||||
|
|
||||||
|
export type ProjectStatus = 'DRAFT' | 'RESEARCHING' | 'SCRIPTING' | 'ANALYZING' | 'COMPLETED';
|
||||||
|
|
||||||
export type CharacterRole =
|
export type CharacterRole =
|
||||||
| 'Protagonist'
|
| 'Protagonist'
|
||||||
| 'Antagonist'
|
| 'Antagonist'
|
||||||
@@ -87,6 +89,8 @@ export interface ScriptProject {
|
|||||||
logline?: string;
|
logline?: string;
|
||||||
highConcept?: string;
|
highConcept?: string;
|
||||||
includeInterviews: boolean;
|
includeInterviews: boolean;
|
||||||
|
status: ProjectStatus;
|
||||||
|
currentVersionNumber: number;
|
||||||
seoTitle?: string;
|
seoTitle?: string;
|
||||||
seoDescription?: string;
|
seoDescription?: string;
|
||||||
seoTags: string[];
|
seoTags: string[];
|
||||||
@@ -269,6 +273,7 @@ export interface ProjectListItem {
|
|||||||
topic: string;
|
topic: string;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
status?: ProjectStatus;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
_count: {
|
_count: {
|
||||||
@@ -276,3 +281,41 @@ export interface ProjectListItem {
|
|||||||
sources: number;
|
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