main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-04-25 14:37:34 +02:00
parent ec3b67010b
commit cf12fc3942
3 changed files with 145 additions and 11 deletions
@@ -3,7 +3,7 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { useCreateFromDocument } from "@/hooks/use-api";
import { useExtractDocumentTopics, useCreateFromExtractedText } from "@/hooks/use-api";
import { useToast } from "@/components/ui/toast";
import {
FileText,
@@ -42,9 +42,13 @@ export default function DocumentToVideoPage() {
const router = useRouter();
const { toast } = useToast();
const createFromDocument = useCreateFromDocument();
const extractDocumentTopics = useExtractDocumentTopics();
const createFromExtractedText = useCreateFromExtractedText();
const [file, setFile] = useState<File | null>(null);
const [extractedData, setExtractedData] = useState<{text: string; topics: string[]; originalFilename: string} | null>(null);
const [selectedTopic, setSelectedTopic] = useState<string | null>(null);
const [style, setStyle] = useState("CINEMATIC");
const [cinematicReference, setCinematicReference] = useState("");
const [duration, setDuration] = useState(60);
@@ -54,18 +58,40 @@ export default function DocumentToVideoPage() {
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
setFile(e.target.files[0]);
setExtractedData(null);
setSelectedTopic(null);
}
};
const handleGenerate = async () => {
const handleExtractTopics = async () => {
if (!file) {
toast("error", "Lütfen bir belge seçin.");
return;
}
try {
const result: any = await createFromDocument.mutateAsync({
file,
const result = await extractDocumentTopics.mutateAsync({ file });
setExtractedData(result);
if (result.topics.length > 0) {
setSelectedTopic(result.topics[0]);
}
toast("success", "Belge incelendi ve konular çıkarıldı!");
} catch (error) {
toast("error", "Konu çıkarılırken hata oluştu. Belki belge okunamıyor veya çok büyük.");
}
};
const handleGenerate = async () => {
if (!extractedData || !selectedTopic) {
toast("error", "Lütfen bir belge yükleyip konu seçin.");
return;
}
try {
const result: any = await createFromExtractedText.mutateAsync({
text: extractedData.text,
topic: selectedTopic,
originalFilename: extractedData.originalFilename,
language,
aspectRatio,
videoStyle: style,
@@ -73,7 +99,7 @@ export default function DocumentToVideoPage() {
targetDuration: duration,
});
toast("success", "Belge → Video projesi oluşturuldu!");
toast("success", "Video projesi oluşturuldu!");
router.push(`/dashboard/projects/${result.id}`);
} catch (error) {
toast("error", "Proje oluşturulurken hata oluştu.");
@@ -115,6 +141,51 @@ export default function DocumentToVideoPage() {
/>
</div>
</div>
{!extractedData && file && (
<button
onClick={handleExtractTopics}
disabled={extractDocumentTopics.isPending}
className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl font-medium text-white shadow-lg bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
{extractDocumentTopics.isPending ? (
<>
<Loader2 size={18} className="animate-spin" />
Belge İnceleniyor...
</>
) : (
<>
<FileText size={18} />
Konu Çıkar
</>
)}
</button>
)}
{extractedData && (
<div className="mt-6 p-4 rounded-xl bg-blue-500/5 border border-blue-500/20">
<h3 className="font-medium text-blue-400 mb-3 text-sm flex items-center gap-2">
<Wand2 size={16} />
Şu Konulardan Birini Seçin:
</h3>
<div className="space-y-2">
{extractedData.topics.map((topic, i) => (
<div
key={i}
onClick={() => setSelectedTopic(topic)}
className={cn(
"p-3 rounded-lg border text-sm cursor-pointer transition-all",
selectedTopic === topic
? "bg-blue-500/20 border-blue-500 text-blue-400"
: "bg-[var(--color-bg-surface)] border-[var(--color-border-faint)] text-[var(--color-text-secondary)] hover:border-blue-500/50"
)}
>
{topic}
</div>
))}
</div>
</div>
)}
</div>
{/* Video Settings */}
@@ -235,16 +306,16 @@ export default function DocumentToVideoPage() {
<button
onClick={handleGenerate}
disabled={createFromDocument.isPending || !file}
disabled={createFromExtractedText.isPending || !selectedTopic}
className={cn(
"w-full py-4 rounded-xl font-semibold text-base flex items-center justify-center gap-2 transition-all",
createFromDocument.isPending
createFromExtractedText.isPending
? "bg-blue-500/20 text-blue-400 cursor-wait"
: "bg-blue-500 hover:bg-blue-600 text-white shadow-lg shadow-blue-500/20",
!file && "opacity-50 cursor-not-allowed"
!selectedTopic && "opacity-50 cursor-not-allowed"
)}
>
{createFromDocument.isPending ? (
{createFromExtractedText.isPending ? (
<>
<Loader2 size={20} className="animate-spin" />
<span>Video Projesi Oluşturuluyor... (Bu işlem uzun sürebilir)</span>
@@ -252,7 +323,7 @@ export default function DocumentToVideoPage() {
) : (
<>
<Wand2 size={20} />
<span>Belge Video Oluştur</span>
<span>Konudan Video Oluştur</span>
<ArrowRight size={16} />
</>
)}
+27
View File
@@ -16,6 +16,9 @@ import {
type CreateFromTweetPayload,
type CreateFromYoutubePayload,
type CreateFromDocumentPayload,
type ExtractDocumentTopicsPayload,
type CreateFromExtractedTextPayload,
type ExtractDocumentTopicsResponse,
type Template,
type PaginatedResponse,
} from '@/lib/api/api-service';
@@ -181,6 +184,11 @@ export function useGenerateSceneImage() {
projectsApi.generateSceneImage(projectId, sceneId, customPrompt),
onSuccess: (updatedScene, variables) => {
qc.invalidateQueries({ queryKey: queryKeys.projects.detail(variables.projectId) });
toast.success('Görsel başarıyla üretildi');
},
onError: (error: any) => {
console.error('Görsel üretme hatası:', error);
toast.error(error.response?.data?.message || 'Görsel üretilemedi');
},
});
}
@@ -378,6 +386,25 @@ export function useCreateFromDocument() {
});
}
/** Dokümandan konu önerileri çıkar */
export function useExtractDocumentTopics() {
return useMutation({
mutationFn: (data: ExtractDocumentTopicsPayload) => projectsApi.extractDocumentTopics(data),
});
}
/** Çıkarılan metin ve konudan proje üret */
export function useCreateFromExtractedText() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: CreateFromExtractedTextPayload) => projectsApi.createFromExtractedText(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: queryKeys.projects.all });
qc.invalidateQueries({ queryKey: queryKeys.dashboard.stats });
},
});
}
// ═══════════════════════════════════════════════════════
// NOTIFICATIONS — Bildirim hook'ları
// ═══════════════════════════════════════════════════════
+36
View File
@@ -256,6 +256,7 @@ export interface CreateFromYoutubePayload {
language?: string;
aspectRatio?: string;
videoStyle?: string;
cinematicReference?: string;
targetDuration?: number;
}
@@ -265,6 +266,28 @@ export interface CreateFromDocumentPayload {
language?: string;
aspectRatio?: string;
videoStyle?: string;
cinematicReference?: string;
targetDuration?: number;
}
export interface ExtractDocumentTopicsPayload {
file: File;
}
export interface ExtractDocumentTopicsResponse {
text: string;
topics: string[];
originalFilename: string;
}
export interface CreateFromExtractedTextPayload {
text: string;
topic: string;
originalFilename?: string;
language?: string;
aspectRatio?: string;
videoStyle?: string;
cinematicReference?: string;
targetDuration?: number;
}
@@ -346,6 +369,19 @@ export const projectsApi = {
}).then((r) => r.data);
},
extractDocumentTopics: (data: ExtractDocumentTopicsPayload) => {
const formData = new FormData();
formData.append('file', data.file);
return apiClient.post<ExtractDocumentTopicsResponse>('/projects/extract-document-topics', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
}).then((r) => r.data);
},
createFromExtractedText: (data: CreateFromExtractedTextPayload) =>
apiClient.post<Project>('/projects/document-from-topic', data).then((r) => r.data),
updateScene: (projectId: string, sceneId: string, data: Partial<Scene>) =>
apiClient.patch<Scene>(`/projects/${projectId}/scenes/${sceneId}`, data).then((r) => r.data),