-
- Yeni Bölüm Tasarla
-
-
- Bu format için yeni bir bölümün Ön-Yapım (Pre-Production) sürecini başlatın.
-
+
+
+
+
+
+
+ Yeni Bölüm Tasarla
+
+
+ Bu format için yeni bir bölümün Ön-Yapım (Pre-Production) sürecini başlatın.
+
+
+
-
-
-
-
+
)}
diff --git a/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/page.tsx b/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/page.tsx
index a4ba85d..060a1bd 100644
--- a/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/page.tsx
+++ b/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/page.tsx
@@ -2,10 +2,10 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
-import { getProjects, createProject, ProjectResponse } from './services/strategistApi';
+import { getProjects, createProject, deleteProject, ProjectResponse } from './services/strategistApi';
import { useRouter, useParams } from 'next/navigation';
import { LegacyUploader } from './components/LegacyUploader';
-import { MonitorPlay, Plus, FolderKanban, Loader2, Calendar, LayoutTemplate, X, TrendingUp, Sparkles, User, Target, Users, PlayCircle, History } from 'lucide-react';
+import { MonitorPlay, Plus, FolderKanban, Loader2, Calendar, LayoutTemplate, X, TrendingUp, Sparkles, User, Target, Users, PlayCircle, History, Trash2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
@@ -66,6 +66,21 @@ export default function TubeStrategistDashboard() {
}
};
+ const handleDeleteProject = async (e: React.MouseEvent, projectId: string) => {
+ e.stopPropagation();
+ if (!window.confirm("Bu projeyi silmek istediğinize emin misiniz? Bu işlem geri alınamaz ve proje içindeki tüm analiz, video ve bölümler silinecektir.")) {
+ return;
+ }
+
+ try {
+ await deleteProject(projectId);
+ setProjects(projects.filter(p => p.id !== projectId));
+ } catch (error) {
+ console.error("Proje silinirken hata:", error);
+ alert("Proje silinirken bir hata oluştu.");
+ }
+ };
+
return (
{/* Header */}
@@ -166,15 +181,24 @@ export default function TubeStrategistDashboard() {
-
{proj.name}
-
- {proj.status === 'ANALYZING' &&
}
- {proj.status === 'COMPLETED' ? 'Tamamlandı' : proj.status === 'ANALYZING' ? 'Analiz Ediliyor' : 'Bekliyor'}
+
{proj.name}
+
+
+ {proj.status === 'ANALYZING' && }
+ {proj.status === 'COMPLETED' ? 'Tamamlandı' : proj.status === 'ANALYZING' ? 'Analiz Ediliyor' : 'Bekliyor'}
+
+
diff --git a/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/services/strategistApi.ts b/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/services/strategistApi.ts
index cae995e..1b124de 100644
--- a/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/services/strategistApi.ts
+++ b/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/services/strategistApi.ts
@@ -28,6 +28,9 @@ export interface EpisodeResponse {
format: string;
status: string;
masterAnalysis: any;
+ thumbnailMatrix?: any;
+ shortsConcepts?: any;
+ sponsorshipPitch?: any;
createdAt: string;
updatedAt: string;
}
@@ -52,6 +55,7 @@ export interface ProjectResponse {
episodes?: EpisodeResponse[];
_count?: { videos: number };
masterAnalysis: any;
+ communityInsights?: any;
createdAt: string;
updatedAt: string;
}
@@ -91,6 +95,10 @@ export const getProjectById = async (projectId: string): Promise
(`/youtube-tools/strategist/projects/${projectId}`).then(r => r.data as unknown as ProjectResponse);
};
+export const deleteProject = async (projectId: string): Promise => {
+ return apiClient.delete(`/youtube-tools/strategist/projects/${projectId}`).then(r => r.data);
+};
+
export const addVideoToProject = async (projectId: string, url: string): Promise => {
return apiClient.post(`/youtube-tools/strategist/projects/${projectId}/video`, { youtubeUrl: url }).then(r => r.data);
};
@@ -136,6 +144,22 @@ export const generateMoreQuestions = async (episodeId: string): Promise =>
return apiClient.post(`/youtube-tools/strategist/episodes/${episodeId}/generate-more-questions`).then(r => r.data);
};
+export const generateCommunityIdeas = async (projectId: string): Promise => {
+ return apiClient.post(`/youtube-tools/strategist/projects/${projectId}/community-ideas`).then(r => r.data);
+};
+
+export const generateThumbnailMatrix = async (episodeId: string): Promise => {
+ return apiClient.post(`/youtube-tools/strategist/episodes/${episodeId}/thumbnail-matrix`).then(r => r.data);
+};
+
+export const generateEpisodeShorts = async (episodeId: string): Promise => {
+ return apiClient.post(`/youtube-tools/strategist/episodes/${episodeId}/shorts-concepts`).then(r => r.data);
+};
+
+export const generateEpisodeSponsorship = async (episodeId: string): Promise => {
+ return apiClient.post(`/youtube-tools/strategist/episodes/${episodeId}/sponsorship`).then(r => r.data);
+};
+
export const generateEpisodeQuestions = async (episodeId: string): Promise => {
return apiClient.post(`/youtube-tools/strategist/episodes/${episodeId}/generate-questions`).then(r => r.data);
};
diff --git a/src/app/[locale]/(dashboard)/dashboard/tools/voicebox/page.tsx b/src/app/[locale]/(dashboard)/dashboard/tools/voicebox/page.tsx
new file mode 100644
index 0000000..2e27c89
--- /dev/null
+++ b/src/app/[locale]/(dashboard)/dashboard/tools/voicebox/page.tsx
@@ -0,0 +1,403 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { voiceboxApi } from '@/services/voiceboxApi';
+import { Play, Download, Mic, Settings2, Loader2, Sparkles, Volume2, AlertTriangle, History, Clock, ChevronDown, ChevronUp } from 'lucide-react';
+
+export default function VoiceBoxStudio() {
+ const [text, setText] = useState('');
+ const [profiles, setProfiles] = useState([]);
+ const [selectedProfile, setSelectedProfile] = useState('');
+ const [historyItems, setHistoryItems] = useState([]);
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [audioUrl, setAudioUrl] = useState(null);
+
+ // Advanced settings
+ const [engine, setEngine] = useState('kokoro');
+ const [language, setLanguage] = useState('tr');
+ const [modelSize, setModelSize] = useState('1.7B');
+ const [instruct, setInstruct] = useState('');
+ const [seed, setSeed] = useState('');
+ const [showAdvanced, setShowAdvanced] = useState(false);
+
+ // Derive if current profile is a preset
+ const currentProfile = profiles.find(p => p.id === selectedProfile);
+ const isPresetProfile = currentProfile?.voice_type === 'preset';
+
+ useEffect(() => {
+ const fetchInitialData = async () => {
+ try {
+ const profileData = await voiceboxApi.getProfiles();
+ const fetchedProfiles = Array.isArray(profileData) ? profileData : (profileData?.profiles || []);
+
+ if (fetchedProfiles && fetchedProfiles.length > 0) {
+ setProfiles(fetchedProfiles);
+ setSelectedProfile(fetchedProfiles[0].id);
+ }
+ } catch (error) {
+ console.error('Failed to load profiles', error);
+ }
+
+ try {
+ const historyData = await voiceboxApi.getHistory();
+ setHistoryItems(historyData?.items || []);
+ } catch (error) {
+ console.error('Failed to load history', error);
+ }
+ };
+ fetchInitialData();
+ }, []);
+
+ // Sync engine when profile changes
+ useEffect(() => {
+ if (currentProfile) {
+ if (currentProfile.voice_type === 'preset' && currentProfile.preset_engine) {
+ setEngine(currentProfile.preset_engine);
+ } else if (currentProfile.default_engine) {
+ setEngine(currentProfile.default_engine);
+ } else {
+ setEngine('qwen'); // default for cloned voices if not specified
+ }
+ }
+ }, [selectedProfile, currentProfile]);
+
+ const handleGenerate = async () => {
+ if (!text.trim()) {
+ alert('Lütfen dönüştürülecek metni girin.');
+ return;
+ }
+
+ setIsGenerating(true);
+ setAudioUrl(null);
+
+ try {
+ const options = {
+ language,
+ engine,
+ modelSize: engine === 'qwen' ? modelSize : undefined,
+ instruct: instruct.trim() || undefined,
+ seed: seed !== '' ? Number(seed) : undefined,
+ };
+
+ const audioBlob = await voiceboxApi.generateSpeech(text, selectedProfile, options);
+ const blob = new Blob([audioBlob], { type: 'audio/wav' });
+ const url = URL.createObjectURL(blob);
+ setAudioUrl(url);
+
+ // Refresh history after generation
+ const historyData = await voiceboxApi.getHistory();
+ setHistoryItems(historyData?.items || []);
+ } catch (error: any) {
+ alert(`Ses üretilirken bir hata oluştu: ${error.message || 'Bilinmeyen hata'}\n\nAyarları (özellikle ağır modelleri) kontrol edin.`);
+ } finally {
+ setIsGenerating(false);
+ }
+ };
+
+ const handleDownload = (url: string, filename: string) => {
+ if (!url) return;
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ };
+
+ const insertTag = (tag: string) => {
+ setText((prev) => (prev ? `${prev} ${tag}` : tag));
+ };
+
+ const formatDate = (dateString: string) => {
+ const d = new Date(dateString);
+ return new Intl.DateTimeFormat('tr-TR', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }).format(d);
+ };
+
+ return (
+
+
+
+
+
+ VoiceBox Studio Pro
+
+
+ Gelişmiş AI Ses Sentezi ve Klonlama Arayüzü
+
+
+
+
+
+
+ {/* LEFT COLUMN: Prompt & Results */}
+
+
+
+
+ {audioUrl && (
+
+
+
+
+
+
+ Üretim Başarılı
+
+
+
+
+
+
+ )}
+
+
+
+ {/* RIGHT COLUMN: Settings & History */}
+
+
+ {/* Settings Card */}
+
+
+
+
Temel Ayarlar
+
+
+
+
+
+
+
+
+
+
+
+
+ {isPresetProfile && (
+
Hazır profillerin motoru değiştirilemez.
+ )}
+
+
+
+
+
+
+
+
+ {/* Advanced Settings Accordion */}
+
+
+
+
+ {showAdvanced && (
+
+
+
+ {engine === 'qwen' && (
+
+
+
+ Uyarı: Qwen veya büyük boyutlu (1.7B, 4B) modeller, sınırlı RAM'e sahip ortamlarda (Raspberry Pi vb.) stabilite sorunları ve bellek taşması (Out of Memory) yaratabilir. Varsayılan Kokoro motoru daha verimlidir.
+
+
+ )}
+
+
+
+
+
+
+
+
+ setInstruct(e.target.value)}
+ placeholder="Örn: Fısıldayarak konuş..."
+ className="w-full rounded-lg border border-input bg-background/50 px-3 py-2 text-sm focus:ring-1 focus:ring-purple-500"
+ />
+
+
+
+
+ setSeed(e.target.value ? Number(e.target.value) : '')}
+ placeholder="Boş bırakırsanız rastgele"
+ className="w-full rounded-lg border border-input bg-background/50 px-3 py-2 text-sm focus:ring-1 focus:ring-purple-500"
+ />
+
+
+
+ )}
+
+
+
+
+ {/* History Card */}
+
+
+
+
+
Üretim Geçmişi
+
+
+ {historyItems.length} Kayıt
+
+
+
+
+ {historyItems.length === 0 ? (
+
+
+
Henüz bir ses üretmediniz.
+
+ ) : (
+ historyItems.map((item) => (
+
+
+
+
+ {formatDate(item.created_at)}
+
+ {item.engine}
+ {item.language}
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/global.css b/src/app/[locale]/global.css
index 3a3c312..3a73ac7 100644
--- a/src/app/[locale]/global.css
+++ b/src/app/[locale]/global.css
@@ -251,7 +251,7 @@ body {
/* ── Page Transition ── */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
- to { opacity: 1; transform: translateY(0); }
+ to { opacity: 1; transform: none; }
}
.page-enter {
animation: fadeIn var(--duration-slow) var(--ease-out-expo) forwards;
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
index 156bc72..9663e55 100644
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -71,12 +71,18 @@ const handler = NextAuth({
token.refreshToken = user.refreshToken;
token.id = user.id;
token.roles = user.roles;
+ token.name = user.name;
+ token.email = user.email;
}
return token;
},
async session({ session, token }: any) {
- session.user.id = token.id;
- session.user.roles = token.roles;
+ if (session.user) {
+ session.user.id = token.id;
+ session.user.roles = token.roles;
+ session.user.name = token.name;
+ session.user.email = token.email;
+ }
session.accessToken = token.accessToken;
session.refreshToken = token.refreshToken;
return session;
diff --git a/src/components/layout/header/header.tsx b/src/components/layout/header/header.tsx
index 34c3327..825258d 100644
--- a/src/components/layout/header/header.tsx
+++ b/src/components/layout/header/header.tsx
@@ -80,7 +80,10 @@ export default function Header() {
return (
-
+