diff --git a/src/components/skriptai/ProjectDetail.tsx b/src/components/skriptai/ProjectDetail.tsx
index 46adf65..6f975f2 100644
--- a/src/components/skriptai/ProjectDetail.tsx
+++ b/src/components/skriptai/ProjectDetail.tsx
@@ -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) {
{t('analysis')}
+
+
+ Versiyonlar
+ {project.currentVersionNumber > 0 && (
+
+ {project.currentVersionNumber}
+
+ )}
+
@@ -234,6 +244,10 @@ export default function ProjectDetail({ projectId }: ProjectDetailProps) {
isAnalyzing={neuroAnalysis.isPending || youtubeAudit.isPending}
/>
+
+
+
+
diff --git a/src/components/skriptai/tabs/VersionsTab.tsx b/src/components/skriptai/tabs/VersionsTab.tsx
new file mode 100644
index 0000000..d6b8ab0
--- /dev/null
+++ b/src/components/skriptai/tabs/VersionsTab.tsx
@@ -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(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 ;
+ case 'USER':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ 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 (
+
+ {/* Header */}
+
+
+
+ Versiyon Geçmişi
+
+ {versions?.length || 0} versiyon
+
+
+ Şu an: v{project.currentVersionNumber}
+
+
+
+
+
+
+ {/* Save Form */}
+ {showSaveForm && (
+
+
+
+ 💾 Yeni Versiyon Kaydet
+
+ setSaveLabel(e.target.value)}
+ size='sm'
+ />
+
+
+ )}
+
+ {/* No segments warning */}
+ {(!project.segments || project.segments.length === 0) && (
+
+
+
+ Henüz script üretilmemiş. Önce bir script oluşturun, ardından versiyonları burada takip edebilirsiniz.
+
+
+
+ )}
+
+ {/* Version List */}
+ {isLoading ? (
+
+ Versiyonlar yükleniyor...
+
+ ) : versions && versions.length > 0 ? (
+
+ {versions.map((version) => (
+
+ 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}
+ />
+ ))}
+
+ ) : (
+ project.segments &&
+ project.segments.length > 0 && (
+
+
+
+ Henüz kayıtlı versiyon yok. "Mevcut Durumu Kaydet" butonuna tıklayarak ilk versiyonu oluşturun.
+
+
+ Script yeniden üretildiğinde versiyonlar otomatik olarak kaydedilir.
+
+
+
+ )
+ )}
+
+ );
+}
+
+// ========== 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 (
+
+
+
+
+
+ {getGeneratedByIcon(version.generatedBy)}
+
+ v{version.versionNumber}
+
+
+
+
+
+ {version.label || `Versiyon ${version.versionNumber}`}
+
+
+ {formatDate(version.createdAt)} •{' '}
+ {getGeneratedByLabel(version.generatedBy)}
+
+
+
+
+
+
+ {version.segmentCount} bölüm
+
+
+ ~{version.totalWords} kelime
+
+ {isExpanded ? : }
+
+
+
+
+ {isExpanded && (
+
+
+ {version.changeNote && (
+
+
+ 📝 Değişiklik Notu
+
+ {version.changeNote}
+
+ )}
+
+ e.stopPropagation()}>
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/skriptai/tabs/index.ts b/src/components/skriptai/tabs/index.ts
index 17311a9..3e96fd1 100644
--- a/src/components/skriptai/tabs/index.ts
+++ b/src/components/skriptai/tabs/index.ts
@@ -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';
diff --git a/src/lib/api/skriptai/index.ts b/src/lib/api/skriptai/index.ts
index 8345670..1eaaa8f 100644
--- a/src/lib/api/skriptai/index.ts
+++ b/src/lib/api/skriptai/index.ts
@@ -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,
diff --git a/src/lib/api/skriptai/scripts/service.ts b/src/lib/api/skriptai/scripts/service.ts
index 26eb944..efb3ea1 100644
--- a/src/lib/api/skriptai/scripts/service.ts
+++ b/src/lib/api/skriptai/scripts/service.ts
@@ -80,4 +80,19 @@ export const scriptsService = {
method: 'post',
});
},
+ regenerateSegment: (segmentId: string) => {
+ return apiRequest>({
+ url: `/skriptai/scripts/segments/${segmentId}/regenerate`,
+ client: 'skriptai',
+ method: 'post',
+ });
+ },
+ regeneratePartial: (projectId: string, segmentIds: string[]) => {
+ return apiRequest>({
+ url: '/skriptai/scripts/regenerate-partial',
+ client: 'skriptai',
+ method: 'post',
+ data: { projectId, segmentIds },
+ });
+ },
};
diff --git a/src/lib/api/skriptai/scripts/use-hooks.ts b/src/lib/api/skriptai/scripts/use-hooks.ts
index 96f734d..cb6a544 100644
--- a/src/lib/api/skriptai/scripts/use-hooks.ts
+++ b/src/lib/api/skriptai/scripts/use-hooks.ts
@@ -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, 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,
+ Error,
+ string[]
+ >({
+ mutationFn: (segmentIds) =>
+ scriptsService.regeneratePartial(projectId, segmentIds),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: ProjectsQueryKeys.detail(projectId),
+ });
+ },
+ });
+
+ return { data: data?.data, ...rest };
+}
diff --git a/src/lib/api/skriptai/versions/service.ts b/src/lib/api/skriptai/versions/service.ts
new file mode 100644
index 0000000..578296e
--- /dev/null
+++ b/src/lib/api/skriptai/versions/service.ts
@@ -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>({
+ url: `/skriptai/projects/${projectId}/versions`,
+ client: 'skriptai',
+ method: 'get',
+ });
+};
+
+const getOne = (projectId: string, versionId: string) => {
+ return apiRequest>({
+ url: `/skriptai/projects/${projectId}/versions/${versionId}`,
+ client: 'skriptai',
+ method: 'get',
+ });
+};
+
+const createSnapshot = (
+ projectId: string,
+ data: { label?: string; changeNote?: string },
+) => {
+ return apiRequest>({
+ url: `/skriptai/projects/${projectId}/versions`,
+ client: 'skriptai',
+ method: 'post',
+ data,
+ });
+};
+
+const restore = (projectId: string, versionId: string) => {
+ return apiRequest>({
+ url: `/skriptai/projects/${projectId}/versions/${versionId}/restore`,
+ client: 'skriptai',
+ method: 'post',
+ });
+};
+
+const remove = (projectId: string, versionId: string) => {
+ return apiRequest>({
+ url: `/skriptai/projects/${projectId}/versions/${versionId}`,
+ client: 'skriptai',
+ method: 'delete',
+ });
+};
+
+const compare = (
+ projectId: string,
+ versionAId: string,
+ versionBId: string,
+) => {
+ return apiRequest>({
+ url: `/skriptai/projects/${projectId}/versions/compare?versionA=${versionAId}&versionB=${versionBId}`,
+ client: 'skriptai',
+ method: 'get',
+ });
+};
+
+export const versionsService = {
+ list,
+ getOne,
+ createSnapshot,
+ restore,
+ remove,
+ compare,
+};
diff --git a/src/lib/api/skriptai/versions/use-hooks.ts b/src/lib/api/skriptai/versions/use-hooks.ts
new file mode 100644
index 0000000..6dcbdc2
--- /dev/null
+++ b/src/lib/api/skriptai/versions/use-hooks.ts
@@ -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>({
+ 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>({
+ 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,
+ 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,
+ 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>({
+ queryKey: VersionsQueryKeys.compare(projectId, versionAId, versionBId),
+ queryFn: () =>
+ versionsService.compare(projectId, versionAId, versionBId),
+ enabled: !!projectId && !!versionAId && !!versionBId,
+ });
+
+ return { data: data?.data, ...rest };
+}
diff --git a/src/types/skriptai.ts b/src/types/skriptai.ts
index 4af2128..19b2b39 100644
--- a/src/types/skriptai.ts
+++ b/src/types/skriptai.ts
@@ -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[];
+}