diff --git a/Dockerfile b/Dockerfile index 4448c70..ef3fe53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ RUN apk add --no-cache libc6-compat WORKDIR /app # pnpm kurulumu (workspace kuralı gereği) -RUN corepack enable && corepack prepare pnpm@latest --activate +RUN corepack enable && corepack prepare pnpm@9.15.4 --activate COPY package.json pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile @@ -13,7 +13,7 @@ RUN pnpm install --frozen-lockfile FROM node:20-alpine AS builder WORKDIR /app -RUN corepack enable && corepack prepare pnpm@latest --activate +RUN corepack enable && corepack prepare pnpm@9.15.4 --activate COPY --from=deps /app/node_modules ./node_modules COPY . . diff --git a/src/app/[locale]/(dashboard)/dashboard/tools/page.tsx b/src/app/[locale]/(dashboard)/dashboard/tools/page.tsx index ff44536..7dbb989 100644 --- a/src/app/[locale]/(dashboard)/dashboard/tools/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/tools/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Wrench, Video, ArrowRight } from "lucide-react"; +import { Wrench, Video, ArrowRight, Mic } from "lucide-react"; import Link from "next/link"; import { motion, useMotionTemplate, useMotionValue, useSpring } from "framer-motion"; import { useState, useRef } from "react"; @@ -164,6 +164,15 @@ export default function ToolsPage() { colorClass="bg-blue-500/20 text-blue-400" spotlightColor="rgba(59, 130, 246, 0.15)" /> + + ); diff --git a/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/[projectId]/episode/[episodeId]/page.tsx b/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/[projectId]/episode/[episodeId]/page.tsx index a870f20..34fe40f 100644 --- a/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/[projectId]/episode/[episodeId]/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/[projectId]/episode/[episodeId]/page.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, Component, ErrorInfo, ReactNode } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { motion, AnimatePresence } from 'framer-motion'; -import { getEpisodeById, analyzeEpisode, generateMoreQuestions, generateEpisodeQuestions, generateEpisodeSeoMarketing, generateEpisodeCrisisSponsors, generateThumbnail, EpisodeResponse } from '../../../services/strategistApi'; +import { getEpisodeById, analyzeEpisode, generateMoreQuestions, generateEpisodeQuestions, generateEpisodeSeoMarketing, generateEpisodeCrisisSponsors, generateThumbnail, generateThumbnailMatrix, generateEpisodeShorts, generateEpisodeSponsorship, EpisodeResponse } from '../../../services/strategistApi'; import { Loader2, ArrowLeft, Sparkles, AlertTriangle, Layers, Target, FileText, Camera, ShieldAlert, Users, Film, Clock, Presentation, Type as TypeIcon, HelpCircle, BarChart, Zap, Download, Maximize2, RefreshCw, Star, ShieldCheck, Mail, Copy, Check } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -41,11 +41,14 @@ export default function EpisodeWorkbenchPage() { const [isAnalyzing, setIsAnalyzing] = useState(false); const [error, setError] = useState(null); - const [activeTab, setActiveTab] = useState<'titles' | 'questions' | 'seo' | 'gap' | 'segments' | 'friction' | 'visual' | 'guest' | 'crisis'>('titles'); + const [activeTab, setActiveTab] = useState<'titles' | 'questions' | 'seo' | 'gap' | 'segments' | 'friction' | 'visual' | 'guest' | 'crisis' | 'thumbnailMatrix' | 'shorts' | 'sponsorship'>('titles'); const [isGeneratingQuestions, setIsGeneratingQuestions] = useState(false); const [isGeneratingSeo, setIsGeneratingSeo] = useState(false); const [isGeneratingCrisis, setIsGeneratingCrisis] = useState(false); const [isGeneratingThumbnail, setIsGeneratingThumbnail] = useState(false); + const [isGeneratingThumbnailMatrix, setIsGeneratingThumbnailMatrix] = useState(false); + const [isGeneratingShorts, setIsGeneratingShorts] = useState(false); + const [isGeneratingSponsorship, setIsGeneratingSponsorship] = useState(false); const [isThumbnailExpanded, setIsThumbnailExpanded] = useState(false); const [isCopied, setIsCopied] = useState(false); @@ -143,6 +146,45 @@ export default function EpisodeWorkbenchPage() { } }; + const handleGenerateThumbnailMatrix = async () => { + setIsGeneratingThumbnailMatrix(true); + setError(null); + try { + await generateThumbnailMatrix(episodeId); + await fetchEpisode(); + } catch (err: any) { + setError(err?.response?.data?.message || "A/B test matrisi üretilirken hata oluştu."); + } finally { + setIsGeneratingThumbnailMatrix(false); + } + }; + + const handleGenerateShorts = async () => { + setIsGeneratingShorts(true); + setError(null); + try { + await generateEpisodeShorts(episodeId); + await fetchEpisode(); + } catch (err: any) { + setError(err?.response?.data?.message || "Shorts/Reels fikirleri üretilirken hata oluştu."); + } finally { + setIsGeneratingShorts(false); + } + }; + + const handleGenerateSponsorship = async () => { + setIsGeneratingSponsorship(true); + setError(null); + try { + await generateEpisodeSponsorship(episodeId); + await fetchEpisode(); + } catch (err: any) { + setError(err?.response?.data?.message || "Sponsorluk taslakları üretilirken hata oluştu."); + } finally { + setIsGeneratingSponsorship(false); + } + }; + const handleDownloadThumbnail = () => { if (episode?.masterAnalysis?.thumbnailUrl) { const link = document.createElement('a'); @@ -163,6 +205,226 @@ export default function EpisodeWorkbenchPage() { } }; + const handleDownloadHtml = () => { + if (!episode || !episode.masterAnalysis) return; + const analysis = episode.masterAnalysis; + + const htmlContent = ` + + + + + + Tube Strategist Analiz: ${episode.topic} + + + +
+
Pre-Production
+

${episode.topic === 'AI_AUTO' ? 'Yapay Zeka Tarafından Belirlenecek' : episode.topic}

+

Hedef Kitle: ${episode.targetAudience} • Süre: ${episode.duration} • Format: ${episode.format}

+
+ + ${analysis.titleSuggestions ? ` +
+

🎯 Başlık Önerileri

+
+ ${analysis.titleSuggestions.map((title: any) => ` +
+
+

${title.title}

+ ${title.seoScore ? `SEO Skoru: ${title.seoScore}` : ''} +
+

${title.description}

+

Neden: ${title.reasoning}

+
+ `).join('')} +
+
` : ''} + + ${analysis.seoAnalysis ? ` +
+

🔍 SEO & Boşluk Analizi

+ ${analysis.seoAnalysis.targetKeywords ? ` +

Hedef Anahtar Kelimeler

+
+ ${analysis.seoAnalysis.targetKeywords.map((kw: string) => `${kw}`).join('')} +
+ ` : ''} +
+
+

SEO Başlığı

+

${analysis.seoAnalysis.seoTitle || '-'}

+
+
+

SEO Açıklaması

+

${analysis.seoAnalysis.seoDescription || '-'}

+
+
+
` : ''} + + ${analysis.audienceGap ? ` +
+

🧩 Kitle & Sürtünme Analizi

+
+

Rakip İçeriklerdeki Boşluk (Audience Gap)

+

${analysis.audienceGap}

+
+ ${analysis.frictionPoints && analysis.frictionPoints.length > 0 ? ` +

İzleyiciyi Sıkabilecek Noktalar (Friction Points)

+
    + ${analysis.frictionPoints.map((fp: any) => ` +
  • Risk: ${fp.risk}
    Çözüm: ${fp.solution}
  • + `).join('')} +
+ ` : ''} +
` : ''} + + ${analysis.contentSegments && analysis.contentSegments.length > 0 ? ` +
+

⏱️ İçerik Akışı (Segmentler)

+ ${analysis.contentSegments.map((seg: any) => ` +
+
+

${seg.title}

+ ${seg.timestamp} +
+

${seg.description}

+

Hedef: ${seg.purpose}

+
+ `).join('')} +
` : ''} + + ${analysis.visualAudioIdeas && analysis.visualAudioIdeas.length > 0 ? ` +
+

🎬 Görsel & İşitsel Fikirler

+
+ ${analysis.visualAudioIdeas.map((idea: any) => ` +
+

Tür: ${idea.type}

+

${idea.description}

+

Amaç: ${idea.purpose}

+
+ `).join('')} +
+
` : ''} + + ${analysis.guestRecommendations && analysis.guestRecommendations.length > 0 ? ` +
+

👥 Konuk Önerileri

+
+ ${analysis.guestRecommendations.map((guest: any) => ` +
+

${guest.guestName}

+

${guest.reasoning}

+
+ `).join('')} +
+
` : ''} + + ${analysis.interviewQuestions && analysis.interviewQuestions.length > 0 ? ` +
+

❓ Mülakat Soruları ve Nöro-Pazarlama İpuçları

+ ${analysis.interviewQuestions.map((q: any, i: number) => ` +
+

Soru ${i + 1}: ${q.question}

+ ${q.targetArea || q.neuroMarketingScore ? ` +
+ ${q.targetArea ? `${q.targetArea}` : ''} + ${q.neuroMarketingScore ? `Nöro Skor: ${q.neuroMarketingScore}` : ''} +
+ ` : ''} + ${q.neuroMarketingAnswerDirection ? ` +

+ Cevaplama Stratejisi: ${q.neuroMarketingAnswerDirection} +

+ ` : ''} +
+ `).join('')} +
` : ''} + + ${analysis.crisisManagement || (analysis.sponsors && analysis.sponsors.length > 0) ? ` +
+

🛡️ Kriz Yönetimi & Sponsorlar

+ ${analysis.crisisManagement ? ` +
+

Linç / Tepki İhtimali

+

${analysis.crisisManagement.potentialBacklash}

+

PR Stratejisi

+

${analysis.crisisManagement.prStrategy}

+
+ ` : ''} + ${analysis.sponsors && analysis.sponsors.length > 0 ? ` +

Potansiyel Sponsor Markalar

+ ${analysis.sponsors.map((sponsor: any) => ` +
+

${sponsor.brandName}

+ ${sponsor.reasoning ? `

Seçim Nedeni: ${sponsor.reasoning}

` : ''} + ${sponsor.integrationIdea ? `

Entegrasyon Fikri: ${sponsor.integrationIdea}

` : ''} +
+

E-Posta Taslağı:

+
${sponsor.emailDraft}
+
+
+ `).join('')} + ` : ''} +
` : ''} + + ${analysis.thumbnailConcept || analysis.thumbnailUrl ? ` +
+

🖼️ Thumbnail Konsepti

+ ${analysis.thumbnailConcept ? `

${analysis.thumbnailConcept}

` : ''} + ${analysis.thumbnailUrl ? `Thumbnail` : ''} +
` : ''} + + +`; + + const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + const safeTopic = (episode.topic || 'bolum').replace(/[^a-z0-9ğüşıöç]/gi, '-').toLowerCase(); + link.download = `tube-strategist-${safeTopic}-analiz.html`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + if (isLoading) { return (
@@ -219,26 +481,37 @@ export default function EpisodeWorkbenchPage() {
- - {episode.status !== 'COMPLETED' ? ( - - ) : ( - - )} +
+ {analysis && ( + + )} + + {episode.status !== 'COMPLETED' ? ( + + ) : ( + + )} +
@@ -341,6 +614,39 @@ export default function EpisodeWorkbenchPage() { > Kriz & Sponsor + +
+
İleri Seviye (V16)
+ + + + + + {/* Content Area */} @@ -467,25 +773,24 @@ export default function EpisodeWorkbenchPage() {
- {/* Tags */} -
- {targetArea && ( - - {targetArea} - - )} - {neuroScore && ( - - Skoru: {neuroScore} - - )} -
- -
-
+
+
{idx + 1}
+ {/* Tags */} +
+ {targetArea && ( + + {targetArea} + + )} + {neuroScore && ( + + Skoru: {neuroScore} + + )} +

{questionText}

{neuroDirection && (
@@ -720,10 +1025,23 @@ export default function EpisodeWorkbenchPage() {
-
- Türkçe E-Posta Taslağı -
-
+
+
+ Türkçe E-Posta Taslağı +
+ +
+
{sponsor.emailDraft}
@@ -742,6 +1060,178 @@ export default function EpisodeWorkbenchPage() { )} + {activeTab === 'thumbnailMatrix' && ( + +
+

+ Thumbnail & Title Matrisi +

+ +
+ {episode.thumbnailMatrix?.concepts && episode.thumbnailMatrix.concepts.length > 0 ? ( +
+ {episode.thumbnailMatrix.concepts.map((concept: any, idx: number) => ( +
+
+

{concept.conceptName}

+ Option {idx + 1} +
+
+ Görsel Açıklaması +

{concept.visualDescription}

+
+
+ Başlık (Title) +

{concept.title}

+
+
+ Clickbait Seviyesi +
+
+
7 ? "bg-red-500" : concept.clickbaitLevel > 4 ? "bg-amber-500" : "bg-green-500")} + style={{ width: `${concept.clickbaitLevel * 10}%` }} + /> +
+ {concept.clickbaitLevel}/10 +
+
+
+ Görselde Metin +

{concept.textOnImage || "Yok"}

+
+
+ ))} +
+ ) : ( +
+

A/B test matrisi henüz üretilmedi.

+
+ )} + + )} + + {activeTab === 'shorts' && ( + +
+

+ Shorts / Reels Çarpanı +

+ +
+ {episode.shortsConcepts?.shorts && episode.shortsConcepts.shorts.length > 0 ? ( +
+ {episode.shortsConcepts.shorts.map((short: any, idx: number) => ( +
+
+

{short.topic}

+ {short.hookIdea} +
+
+
+
+ Hedef Platform +
+ {short.targetPlatform.map((p: string) => ( + {p} + ))} +
+
+
+ Tahmini Uzunluk +

{short.estimatedLength}

+
+
+
+ Script Taslağı (Senaryo) +
{short.scriptDraft}
+
+
+
+ ))} +
+ ) : ( +
+

Shorts fikirleri henüz üretilmedi.

+
+ )} +
+ )} + + {activeTab === 'sponsorship' && ( + +
+

+ Sponsorluk (Pitch Deck) +

+ +
+ {episode.sponsorshipPitch?.pitches && episode.sponsorshipPitch.pitches.length > 0 ? ( +
+ {episode.sponsorshipPitch.pitches.map((pitch: any, idx: number) => ( +
+
+
+

{pitch.industry}

+

Önerilen Marka(lar): {pitch.exampleBrands.join(", ")}

+
+
+ Bölümle Uyumu (Neden Mantıklı?) +

{pitch.reasoning}

+
+
+ Entegrasyon Formatı +

{pitch.integrationFormat}

+
+
+
+
+ Örnek E-Posta Taslağı + +
+
{pitch.emailDraft}
+
+
+ ))} +
+ ) : ( +
+

Sponsorluk pitch deck henüz üretilmedi.

+
+ )} +
+ )} +
) : ( diff --git a/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/[projectId]/page.tsx b/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/[projectId]/page.tsx index 3b008d2..2dd9ff1 100644 --- a/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/[projectId]/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/tools/tube-strategist/[projectId]/page.tsx @@ -8,7 +8,8 @@ import { } from 'lucide-react'; import { getProjectById, ProjectResponse, addVideoToProject, addDocumentToProject, - updateProject, createEpisode, getTopicSuggestions, TopicSuggestion, EpisodeResponse + updateProject, createEpisode, getTopicSuggestions, TopicSuggestion, EpisodeResponse, + generateCommunityIdeas } from '../services/strategistApi'; class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> { @@ -55,6 +56,7 @@ export default function StrategistHubPage() { // Settings Modal State const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isFormatExpanded, setIsFormatExpanded] = useState(false); const [settingsForm, setSettingsForm] = useState({ name: "", tone: "", targetDuration: "", speakerName: "", targetAudience: "", formatDescription: "" }); @@ -68,6 +70,7 @@ export default function StrategistHubPage() { const [isAiTopic, setIsAiTopic] = useState(false); const [suggestions, setSuggestions] = useState([]); const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false); + const [isGeneratingIdeas, setIsGeneratingIdeas] = useState(false); const [isCreatingEpisode, setIsCreatingEpisode] = useState(false); const [episodeError, setEpisodeError] = useState(""); @@ -163,6 +166,19 @@ export default function StrategistHubPage() { } }; + const handleGenerateCommunityIdeas = async () => { + setIsGeneratingIdeas(true); + try { + await generateCommunityIdeas(projectId); + await fetchProject(); + } catch (err) { + console.error("Failed to generate community ideas", err); + alert("Topluluk analizi yapılırken bir hata oluştu."); + } finally { + setIsGeneratingIdeas(false); + } + }; + const handleCreateEpisode = async (e: React.FormEvent) => { e.preventDefault(); const finalTopic = isAiTopic ? "AI_AUTO" : episodeForm.topic; @@ -256,9 +272,26 @@ export default function StrategistHubPage() {

Format & Konsept

-

- {project.formatDescription || "Format açıklaması girilmemiş. Ayarlardan ekleyebilirsiniz."} -

+
+ {project.formatDescription ? ( + <> + {isFormatExpanded || project.formatDescription.length <= 500 + ? project.formatDescription + : `${project.formatDescription.slice(0, 500)}...`} + + {project.formatDescription.length > 500 && ( + + )} + + ) : ( + "Format açıklaması girilmemiş. Ayarlardan ekleyebilirsiniz." + )} +
@@ -498,22 +531,79 @@ export default function StrategistHubPage() {
)}
+ + {/* Community Demand Radar Section */} +
+
+
+
+ +
+
+

Gelecek Bölüm Radarı

+

İzleyici talepleri ve analizleri

+
+
+ +
+ + {project.communityInsights?.insights && project.communityInsights.insights.length > 0 ? ( +
+ {project.communityInsights.insights.map((idea: any, idx: number) => ( +
+
+

{idea.topic}

+
+ Virallik Skoru +
+
+
+
+ {idea.viralityScore} +
+
+
+

{idea.demandReason}

+
+ Önerilen Başlık +

"{idea.proposedTitle}"

+
+
+ ))} +
+ ) : ( +
+

Henüz bir kitle analizi bulunmuyor. Referans ekledikten sonra analizi başlatabilirsiniz.

+
+ )} +
{/* Settings Modal */} {isSettingsOpen && ( -
-
-
-

Proje Ayarları

- -
- -
+
+
+
+
+

Proje Ayarları

+ +
+ +
+
)} {/* New Episode Modal */} {isEpisodeModalOpen && ( -
-
-
-
-

- 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. +

+
+
- -
- - + + {/* Topic Section */}
@@ -718,6 +810,7 @@ export default function StrategistHubPage() {
+
)} 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 */} +
+
+
+

Senaryo Girişi

+
+
+