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

This commit is contained in:
Harun CAN
2026-05-09 05:58:09 +02:00
parent 1f8f24fcf5
commit 4b1abf1996
15 changed files with 3526 additions and 15 deletions
+1
View File
@@ -20,6 +20,7 @@
"axios": "^1.13.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.20",
"framer-motion": "^12.38.0",
"i18next": "^25.6.0",
"lucide-react": "^1.7.0",
+8
View File
@@ -41,6 +41,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
dayjs:
specifier: ^1.11.20
version: 1.11.20
framer-motion:
specifier: ^12.38.0
version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -1960,6 +1963,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -5657,6 +5663,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
dayjs@1.11.20: {}
debug@3.2.7:
dependencies:
ms: 2.1.3
@@ -61,19 +61,37 @@ function ToolCard({
whileHover={{ scale: 1.02 }}
className="group relative h-full glass p-6 rounded-2xl border border-[var(--color-border-faint)] overflow-hidden"
>
{/* Spotlight */}
{/* Spotlight & Glowing Border effect */}
<motion.div
className="pointer-events-none absolute -inset-px opacity-0 transition duration-500 group-hover:opacity-100 mix-blend-screen"
className="pointer-events-none absolute -inset-px opacity-0 transition duration-500 group-hover:opacity-100 z-10"
style={{
background: useMotionTemplate`
radial-gradient(
600px circle at ${mouseX}px ${mouseY}px,
400px circle at ${mouseX}px ${mouseY}px,
${spotlightColor},
transparent 40%
)
`,
}}
/>
{/* Border glow specifically tracking mouse */}
<motion.div
className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 transition duration-500 group-hover:opacity-100"
style={{
boxShadow: useMotionTemplate`
inset 0 0 0 1px rgba(255, 255, 255, 0.1),
0 0 20px 2px ${spotlightColor}
`,
background: useMotionTemplate`
radial-gradient(
200px circle at ${mouseX}px ${mouseY}px,
rgba(255,255,255,0.1),
transparent 40%
)
`,
}}
/>
{/* İçerik, Z-ekseninde hafifçe öne çıkarılır ki 3D efekti belirginleşsin */}
<div style={{ transform: "translateZ(40px)" }} className="flex flex-col h-full pointer-events-none">
@@ -137,6 +155,15 @@ export default function ToolsPage() {
colorClass="bg-orange-500/20 text-orange-400"
spotlightColor="rgba(249, 115, 22, 0.15)"
/>
<ToolCard
href="/dashboard/tools/tube-strategist"
title="Tube Strategist"
description="Eksiksiz veri analizi ve viral nöro-pazarlama motoru. İçeriğinizin stratejisini yapay zeka ile inşa edin."
icon={Video}
colorClass="bg-blue-500/20 text-blue-400"
spotlightColor="rgba(59, 130, 246, 0.15)"
/>
</div>
</div>
);
@@ -0,0 +1,811 @@
'use client';
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 { 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';
class ErrorBoundary extends Component<{children: ReactNode}, {hasError: boolean, error: Error | null}> {
constructor(props: {children: ReactNode}) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) { return { hasError: true, error }; }
componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error("ErrorBoundary caught an error", error, errorInfo); }
render() {
if (this.state.hasError) {
return (
<div className="p-6 m-6 bg-red-500/10 border border-red-500 rounded-xl">
<h2 className="text-xl font-bold text-red-500 mb-4">Uygulama Çöktü (ErrorBoundary)</h2>
<pre className="text-xs font-mono text-[var(--color-text-secondary)] whitespace-pre-wrap overflow-auto max-h-[500px]">
{this.state.error?.stack || this.state.error?.message || "Bilinmeyen bir hata oluştu"}
</pre>
<button onClick={() => window.location.reload()} className="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg font-bold">Sayfayı Yenile</button>
</div>
);
}
return this.props.children;
}
}
export default function EpisodeWorkbenchPage() {
const params = useParams();
const router = useRouter();
const projectId = params.projectId as string;
const episodeId = params.episodeId as string;
const [episode, setEpisode] = useState<EpisodeResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'titles' | 'questions' | 'seo' | 'gap' | 'segments' | 'friction' | 'visual' | 'guest' | 'crisis'>('titles');
const [isGeneratingQuestions, setIsGeneratingQuestions] = useState(false);
const [isGeneratingSeo, setIsGeneratingSeo] = useState(false);
const [isGeneratingCrisis, setIsGeneratingCrisis] = useState(false);
const [isGeneratingThumbnail, setIsGeneratingThumbnail] = useState(false);
const [isThumbnailExpanded, setIsThumbnailExpanded] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const fetchEpisode = async () => {
try {
const data = await getEpisodeById(episodeId);
setEpisode(data);
} catch (err) {
console.error(err);
setError("Bölüm yüklenemedi.");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchEpisode();
}, [episodeId]);
const handleAnalyze = async () => {
setIsAnalyzing(true);
setError(null);
try {
await analyzeEpisode(episodeId);
await fetchEpisode(); // Refresh after analysis
} catch (err: any) {
setError(err?.response?.data?.message || "Analiz sırasında bir hata oluştu.");
} finally {
setIsAnalyzing(false);
}
};
const handleGenerateMoreQuestions = async () => {
setIsGeneratingQuestions(true);
setError(null);
try {
await generateMoreQuestions(episodeId);
await fetchEpisode();
} catch (err: any) {
setError(err?.response?.data?.message || "Yeni soru üretilirken hata oluştu.");
} finally {
setIsGeneratingQuestions(false);
}
};
const handleGenerateQuestions = async () => {
setIsGeneratingQuestions(true);
setError(null);
try {
await generateEpisodeQuestions(episodeId);
await fetchEpisode();
} catch (err: any) {
setError(err?.response?.data?.message || "Sorular üretilirken hata oluştu.");
} finally {
setIsGeneratingQuestions(false);
}
};
const handleGenerateSeoMarketing = async () => {
setIsGeneratingSeo(true);
setError(null);
try {
await generateEpisodeSeoMarketing(episodeId);
await fetchEpisode();
} catch (err: any) {
setError(err?.response?.data?.message || "SEO verisi üretilirken hata oluştu.");
} finally {
setIsGeneratingSeo(false);
}
};
const handleGenerateCrisisSponsors = async () => {
setIsGeneratingCrisis(true);
setError(null);
try {
await generateEpisodeCrisisSponsors(episodeId);
await fetchEpisode();
} catch (err: any) {
setError(err?.response?.data?.message || "Kriz ve Sponsor verisi üretilirken hata oluştu.");
} finally {
setIsGeneratingCrisis(false);
}
};
const handleGenerateThumbnail = async () => {
setIsGeneratingThumbnail(true);
setError(null);
try {
await generateThumbnail(episodeId);
await fetchEpisode();
} catch (err: any) {
setError(err?.response?.data?.message || "Thumbnail üretilirken hata oluştu.");
} finally {
setIsGeneratingThumbnail(false);
}
};
const handleDownloadThumbnail = () => {
if (episode?.masterAnalysis?.thumbnailUrl) {
const link = document.createElement('a');
link.href = episode.masterAnalysis.thumbnailUrl;
link.download = `thumbnail-${episodeId}.jpg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
const handleCopyKeywords = () => {
if (episode?.masterAnalysis?.seoAnalysis?.targetKeywords) {
const keywords = episode.masterAnalysis.seoAnalysis.targetKeywords.join(', ');
navigator.clipboard.writeText(keywords);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<Loader2 className="w-10 h-10 animate-spin text-orange-500" />
</div>
);
}
if (!episode) {
return (
<div className="max-w-4xl mx-auto py-20 text-center">
<h2 className="text-2xl font-bold mb-4 text-[var(--color-text-primary)]">Bölüm Bulunamadı</h2>
<button onClick={() => router.push(`/dashboard/tools/tube-strategist/${projectId}`)} className="text-orange-500 font-bold underline">
Hub'a Dön
</button>
</div>
);
}
const analysis = episode.masterAnalysis;
return (
<ErrorBoundary>
<div className="max-w-7xl mx-auto space-y-8 pb-24 font-sans px-4 sm:px-0 pt-8">
{/* Header */}
<div className="flex items-center justify-between mb-8 flex-wrap gap-4">
<div className="flex items-center gap-4">
<button
onClick={() => router.push(`/dashboard/tools/tube-strategist/${projectId}`)}
className="w-10 h-10 rounded-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] flex items-center justify-center text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:border-[var(--color-border-default)] transition-colors"
>
<ArrowLeft size={20} />
</button>
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">
{episode.topic === 'AI_AUTO' ? (
<span className="flex items-center gap-2 text-orange-500 italic">
<Sparkles size={24} /> Yapay Zeka Tarafından Belirlenecek
</span>
) : (
episode.topic
)}
</h1>
<div className="px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider bg-orange-500/10 text-orange-500 border border-orange-500/20 flex items-center gap-1.5">
<Film size={12} /> Pre-Production
</div>
</div>
<div className="flex items-center gap-4 mt-2 text-xs font-medium text-[var(--color-text-secondary)]">
<span className="flex items-center gap-1"><Target size={14} /> {episode.targetAudience}</span>
<span className="flex items-center gap-1"><Clock size={14} /> {episode.duration}</span>
<span className="flex items-center gap-1"><Presentation size={14} /> {episode.format}</span>
</div>
</div>
</div>
{episode.status !== 'COMPLETED' ? (
<button
onClick={handleAnalyze}
disabled={isAnalyzing}
className="px-6 py-3 bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white font-bold text-sm rounded-xl flex items-center gap-2 shadow-lg shadow-orange-500/20 transition-all disabled:opacity-50"
>
{isAnalyzing ? <Loader2 size={18} className="animate-spin" /> : <Sparkles size={18} />}
{isAnalyzing ? 'Analiz Ediliyor...' : 'Analizi Başlat'}
</button>
) : (
<button
onClick={handleAnalyze}
disabled={isAnalyzing}
className="px-6 py-3 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-hover)] border border-[var(--color-border-default)] text-[var(--color-text-primary)] font-bold text-sm rounded-xl flex items-center gap-2 transition-all disabled:opacity-50"
>
{isAnalyzing ? <Loader2 size={18} className="animate-spin" /> : <Sparkles size={18} />}
{isAnalyzing ? 'Yeniden Analiz Ediliyor...' : 'Analizi Yeniden Yap'}
</button>
)}
</div>
<AnimatePresence>
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
className="p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-500 text-sm font-bold flex items-center gap-3"
>
<AlertTriangle size={18} />
{error}
</motion.div>
)}
</AnimatePresence>
{episode.status === 'COMPLETED' && analysis ? (
<div className="flex flex-col lg:flex-row gap-8">
{/* Sidebar Navigation */}
<div className="w-full lg:w-64 flex-shrink-0 space-y-2">
<button
onClick={() => setActiveTab('titles')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'titles' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<TypeIcon size={18} /> Konsept & Başlıklar
</button>
<button
onClick={() => setActiveTab('questions')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'questions' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<HelpCircle size={18} /> Kışkırtıcı Sorular
</button>
<button
onClick={() => setActiveTab('seo')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'seo' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<BarChart size={18} /> SEO & Pazarlama
</button>
<button
onClick={() => setActiveTab('gap')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'gap' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<Target size={18} /> Gap Analysis
</button>
<button
onClick={() => setActiveTab('segments')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'segments' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<Layers size={18} /> Segment Archetypes
</button>
<button
onClick={() => setActiveTab('friction')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'friction' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<ShieldAlert size={18} /> Friction & Hook
</button>
<button
onClick={() => setActiveTab('visual')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'visual' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<Camera size={18} /> Visual DNA
</button>
<button
onClick={() => setActiveTab('guest')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'guest' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<Users size={18} /> Guest Briefing
</button>
<button
onClick={() => setActiveTab('crisis')}
className={cn(
"w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all",
activeTab === 'crisis' ? "bg-orange-500/10 text-orange-500 border border-orange-500/20" : "text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] border border-transparent"
)}
>
<Zap size={18} /> Kriz & Sponsor
</button>
</div>
{/* Content Area */}
<div className="flex-1 bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] rounded-3xl p-8 min-h-[600px] overflow-hidden">
{activeTab === 'titles' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
<div>
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<TypeIcon className="text-orange-500" /> Başlık Önerileri
</h2>
{analysis.titleSuggestions && analysis.titleSuggestions.length > 0 ? (
<div className="grid gap-4">
{analysis.titleSuggestions.map((item: any, idx: number) => {
const isObj = typeof item === 'object' && item !== null;
const title = isObj ? item.title : item;
const seoScore = isObj ? item.seoScore : null;
return (
<div key={idx} className="p-4 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl flex justify-between items-center group hover:border-orange-500/50 transition-colors">
<div className="flex gap-3 items-center">
<div className="w-8 h-8 rounded-full bg-orange-500/10 text-orange-500 flex items-center justify-center font-bold flex-shrink-0">
{idx + 1}
</div>
<span className="text-lg text-[var(--color-text-primary)] font-medium">{title}</span>
</div>
{seoScore && (
<div className="px-3 py-1 rounded-full bg-green-500/10 border border-green-500/20 text-green-500 text-xs font-bold whitespace-nowrap flex items-center gap-1.5">
<BarChart size={12} /> SEO: {seoScore}
</div>
)}
</div>
);
})}
</div>
) : (
<p className="text-sm text-[var(--color-text-secondary)]">Eski JSON yapısı sebebiyle başlık önerileri bulunamadı. Lütfen analizi yeniden yapın.</p>
)}
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="p-6 bg-orange-500/5 border border-orange-500/20 rounded-2xl h-fit">
<h3 className="font-bold text-orange-500 mb-2 flex items-center gap-2"><Sparkles size={16}/> Psikolojik Tema</h3>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed">{analysis.psychologicalTheme || "Veri yok."}</p>
</div>
<div className="p-6 bg-red-500/5 border border-red-500/20 rounded-2xl space-y-4">
<div>
<h3 className="font-bold text-red-500 mb-2 flex items-center gap-2"><ShieldAlert size={16}/> Thumbnail Konsepti</h3>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed">{analysis.thumbnailConcept || "Veri yok."}</p>
</div>
<div className="pt-4 border-t border-red-500/10">
{analysis.thumbnailUrl ? (
<div className="space-y-4">
<div className="relative group rounded-xl overflow-hidden cursor-pointer" onClick={() => setIsThumbnailExpanded(true)}>
<img src={analysis.thumbnailUrl} alt="Thumbnail Preview" className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105" />
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center gap-2">
<Maximize2 className="text-white" size={32} />
<span className="text-white font-bold text-sm">Büyütmek İçin Tıkla</span>
</div>
</div>
<div className="flex gap-2">
<button onClick={handleDownloadThumbnail} className="flex-1 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white font-bold rounded-lg flex items-center justify-center gap-2 transition-colors text-sm">
<Download size={16} /> İndir
</button>
<button onClick={handleGenerateThumbnail} disabled={isGeneratingThumbnail} className="flex-1 px-4 py-2 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-hover)] border border-[var(--color-border-default)] text-[var(--color-text-primary)] font-bold rounded-lg flex items-center justify-center gap-2 transition-colors text-sm disabled:opacity-50">
{isGeneratingThumbnail ? <Loader2 size={16} className="animate-spin" /> : <RefreshCw size={16} />}
{isGeneratingThumbnail ? 'Üretiliyor...' : 'Yeniden Üret'}
</button>
</div>
</div>
) : (
<button onClick={handleGenerateThumbnail} disabled={isGeneratingThumbnail} className="w-full px-4 py-3 bg-red-500 hover:bg-red-600 text-white font-bold rounded-lg flex items-center justify-center gap-2 transition-colors text-sm disabled:opacity-50">
{isGeneratingThumbnail ? <Loader2 size={16} className="animate-spin" /> : <Sparkles size={16} />}
{isGeneratingThumbnail ? 'Görsel Üretiliyor...' : 'Yapay Zeka Görseli Üret'}
</button>
)}
</div>
</div>
</div>
</motion.div>
)}
{activeTab === 'questions' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] flex items-center gap-3">
<HelpCircle className="text-orange-500" /> Kışkırtıcı & Derin Sorular
</h2>
<div className="flex gap-2">
<button
onClick={handleGenerateQuestions}
disabled={isGeneratingQuestions}
className="px-4 py-2 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-hover)] border border-[var(--color-border-default)] text-[var(--color-text-primary)] text-sm font-bold rounded-lg transition-all flex items-center gap-2 disabled:opacity-50"
>
{isGeneratingQuestions ? <Loader2 size={16} className="animate-spin" /> : <RefreshCw size={16} />}
{isGeneratingQuestions ? 'Üretiliyor...' : (analysis.interviewQuestions && analysis.interviewQuestions.length > 0 ? 'Soruları Yeniden Üret' : 'Analizi Yap')}
</button>
{analysis.interviewQuestions && analysis.interviewQuestions.length > 0 && (
<button
onClick={handleGenerateMoreQuestions}
disabled={isGeneratingQuestions}
className="px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white text-sm font-bold rounded-lg transition-all flex items-center gap-2 disabled:opacity-50"
>
{isGeneratingQuestions ? <Loader2 size={16} className="animate-spin" /> : <Sparkles size={16} />}
{isGeneratingQuestions ? 'Üretiliyor...' : '5 Yeni Soru Üret'}
</button>
)}
</div>
</div>
{analysis.interviewQuestions && analysis.interviewQuestions.length > 0 ? (
<div className="grid gap-6">
{analysis.interviewQuestions.map((q: any, idx: number) => {
const isObj = typeof q === 'object' && q !== null;
const questionText = isObj ? q.question : q;
const neuroDirection = isObj ? q.neuroMarketingAnswerDirection : null;
const neuroScore = isObj ? q.neuroMarketingScore : null;
const targetArea = isObj ? q.targetArea : null;
return (
<div key={idx} className="p-6 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-2xl relative overflow-hidden group hover:border-orange-500/50 transition-colors">
<div className="absolute top-0 left-0 w-1.5 h-full bg-gradient-to-b from-orange-500 to-red-500 opacity-50 group-hover:opacity-100 transition-opacity"></div>
{/* Tags */}
<div className="flex gap-2 justify-end mb-4 absolute top-4 right-4">
{targetArea && (
<span className="px-2 py-1 bg-blue-500/10 border border-blue-500/20 text-blue-500 text-[10px] font-bold rounded-md uppercase tracking-wider flex items-center gap-1">
<Target size={10} /> {targetArea}
</span>
)}
{neuroScore && (
<span className="px-2 py-1 bg-red-500/10 border border-red-500/20 text-red-500 text-[10px] font-bold rounded-md uppercase tracking-wider flex items-center gap-1">
<Star size={10} /> Skoru: {neuroScore}
</span>
)}
</div>
<div className="flex gap-4 pr-32">
<div className="w-10 h-10 rounded-full bg-[var(--color-bg-base)] border border-[var(--color-border-strong)] flex items-center justify-center font-bold text-orange-500 flex-shrink-0 shadow-sm">
{idx + 1}
</div>
<div className="space-y-3 flex-1">
<h3 className="text-lg font-bold text-[var(--color-text-primary)] leading-snug">{questionText}</h3>
{neuroDirection && (
<div className="p-4 bg-orange-500/5 rounded-xl border border-orange-500/10 mt-3">
<h4 className="text-xs font-bold uppercase tracking-wider text-orange-500 mb-1 flex items-center gap-1.5"><Zap size={12} /> Neuro-Marketing Yanıt Yönlendirmesi</h4>
<p className="text-sm text-[var(--color-text-secondary)] leading-relaxed">{neuroDirection}</p>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-12 bg-[var(--color-bg-surface)] border border-dashed border-[var(--color-border-default)] rounded-xl">
<p className="text-sm text-[var(--color-text-secondary)] mb-4">Soru analizi bulunamadı.</p>
</div>
)}
</motion.div>
)}
{activeTab === 'seo' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] flex items-center gap-3">
<BarChart className="text-orange-500" /> SEO & Pazarlama Analizi
</h2>
<button
onClick={handleGenerateSeoMarketing}
disabled={isGeneratingSeo}
className="px-4 py-2 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-hover)] border border-[var(--color-border-default)] text-[var(--color-text-primary)] text-sm font-bold rounded-lg transition-all flex items-center gap-2 disabled:opacity-50"
>
{isGeneratingSeo ? <Loader2 size={16} className="animate-spin" /> : <RefreshCw size={16} />}
{isGeneratingSeo ? 'Analiz Ediliyor...' : ((!analysis.seoAnalysis || !analysis.marketingAnalysis) ? 'Analizi Yap' : 'Yeniden Üret')}
</button>
</div>
{analysis.seoAnalysis || analysis.marketingAnalysis ? (
<div className="space-y-8">
{/* SEO Section */}
{analysis.seoAnalysis && (
<div className="space-y-4">
<h3 className="text-lg font-bold text-[var(--color-text-primary)] border-b border-[var(--color-border-faint)] pb-2">Arama Motoru Optimizasyonu (SEO)</h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="p-5 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)]">
<div className="flex items-center justify-between mb-4">
<span className="text-xs font-bold uppercase tracking-wider text-[var(--color-text-secondary)]">Hedef Anahtar Kelimeler</span>
<button
onClick={handleCopyKeywords}
className="text-[var(--color-text-secondary)] hover:text-orange-500 transition-colors flex items-center gap-1 text-xs font-medium"
>
{isCopied ? <Check size={14} className="text-green-500" /> : <Copy size={14} />}
{isCopied ? 'Kopyalandı' : 'Kopyala'}
</button>
</div>
<div className="flex flex-wrap gap-2 max-h-48 overflow-y-auto pr-2 custom-scrollbar">
{(analysis.seoAnalysis.targetKeywords || []).map((kw: string, i: number) => (
<span key={i} className="px-2.5 py-1 bg-[var(--color-bg-elevated)] border border-[var(--color-border-strong)] rounded-md text-sm font-medium">{kw}</span>
))}
</div>
</div>
<div className="p-5 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)]">
<span className="text-xs font-bold uppercase tracking-wider text-[var(--color-text-secondary)] block mb-2">Arama Niyeti (Search Intent)</span>
<p className="text-sm text-[var(--color-text-primary)]">{analysis.seoAnalysis.searchIntent || "Belirtilmemiş"}</p>
</div>
</div>
<div className="p-5 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)]">
<span className="text-xs font-bold uppercase tracking-wider text-[var(--color-text-secondary)] block mb-2">Açıklama Şablonu (Description Template)</span>
<p className="text-sm text-[var(--color-text-secondary)] whitespace-pre-wrap">{analysis.seoAnalysis.descriptionTemplate || "Belirtilmemiş"}</p>
</div>
</div>
)}
{/* Marketing Section */}
{analysis.marketingAnalysis && (
<div className="space-y-4">
<h3 className="text-lg font-bold text-[var(--color-text-primary)] border-b border-[var(--color-border-faint)] pb-2">Nöro-Pazarlama Stratejisi</h3>
<div className="grid md:grid-cols-2 gap-4">
<div className="p-5 bg-orange-500/5 rounded-xl border border-orange-500/20">
<span className="text-xs font-bold uppercase tracking-wider text-orange-500 block mb-2">Nöro-Pazarlama Tetikleyicileri</span>
<ul className="list-disc list-inside text-sm text-[var(--color-text-primary)] space-y-1">
{(analysis.marketingAnalysis.neuroMarketingTriggers || []).map((tr: string, i: number) => (
<li key={i}>{tr}</li>
))}
</ul>
</div>
<div className="p-5 bg-blue-500/5 rounded-xl border border-blue-500/20">
<span className="text-xs font-bold uppercase tracking-wider text-blue-500 block mb-2">İzleyici Psikolojisi</span>
<p className="text-sm text-[var(--color-text-primary)]">{analysis.marketingAnalysis.audiencePsychology || "Belirtilmemiş"}</p>
</div>
</div>
<div className="p-5 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)]">
<span className="text-xs font-bold uppercase tracking-wider text-[var(--color-text-secondary)] block mb-2">Thumbnail & Kanca Uyumu</span>
<p className="text-sm text-[var(--color-text-secondary)]">{analysis.marketingAnalysis.thumbnailHookAlignment || "Belirtilmemiş"}</p>
</div>
</div>
)}
</div>
) : (
<div className="text-center py-12 bg-[var(--color-bg-surface)] border border-dashed border-[var(--color-border-default)] rounded-xl">
<p className="text-sm text-[var(--color-text-secondary)]">SEO & Pazarlama analizi bulunamadı.</p>
</div>
)}
</motion.div>
)}
{activeTab === 'gap' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-6">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<Target className="text-orange-500" /> Gap Analysis
</h2>
<div className="prose prose-invert max-w-none text-sm text-[var(--color-text-secondary)] leading-relaxed whitespace-pre-wrap">
{analysis.gapAnalysis || "Veri bulunamadı."}
</div>
</motion.div>
)}
{activeTab === 'segments' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-6">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<Layers className="text-orange-500" /> Segment Archetypes
</h2>
<div className="prose prose-invert max-w-none text-sm text-[var(--color-text-secondary)] leading-relaxed whitespace-pre-wrap">
{analysis.segmentArchetypes || "Veri bulunamadı."}
</div>
</motion.div>
)}
{activeTab === 'friction' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-6">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<ShieldAlert className="text-orange-500" /> Devil's Advocate / Friction Points
</h2>
{analysis.frictionPoints && analysis.frictionPoints.length > 0 ? (
<div className="grid gap-4">
{analysis.frictionPoints.map((point: string, idx: number) => (
<div key={idx} className="p-5 bg-[var(--color-bg-surface)] border border-orange-500/20 rounded-xl relative overflow-hidden">
<div className="absolute top-0 left-0 w-1 h-full bg-orange-500"></div>
<p className="text-sm text-[var(--color-text-primary)] font-medium leading-relaxed">{point}</p>
</div>
))}
</div>
) : (
<p className="text-sm text-[var(--color-text-secondary)]">Veri bulunamadı.</p>
)}
</motion.div>
)}
{activeTab === 'visual' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-6">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<Camera className="text-orange-500" /> Visual DNA & Shot List
</h2>
{analysis.visualDna && analysis.visualDna.length > 0 ? (
<div className="grid gap-4">
{analysis.visualDna.map((item: any, idx: number) => (
<div key={idx} className="flex gap-4 p-4 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl items-start">
<span className="px-3 py-1 rounded-lg bg-[var(--color-bg-base)] border border-[var(--color-border-strong)] text-xs font-bold text-orange-500 flex-shrink-0">
{item.timestamp || '00:00'}
</span>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed mt-0.5">{item.suggestion}</p>
</div>
))}
</div>
) : (
<p className="text-sm text-[var(--color-text-secondary)]">Veri bulunamadı.</p>
)}
</motion.div>
)}
{activeTab === 'guest' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-6">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<Users className="text-orange-500" /> Guest Briefing Notes
</h2>
<div className="prose prose-invert max-w-none text-sm text-[var(--color-text-secondary)] leading-relaxed bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-default)] whitespace-pre-wrap">
{analysis.guestBriefing || "Veri bulunamadı."}
</div>
</motion.div>
)}
{activeTab === 'crisis' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-[var(--color-text-primary)] flex items-center gap-3">
<Zap className="text-orange-500" /> Kriz Yönetimi & Sponsorlar
</h2>
<button
onClick={handleGenerateCrisisSponsors}
disabled={isGeneratingCrisis}
className="px-4 py-2 bg-[var(--color-bg-surface)] hover:bg-[var(--color-bg-hover)] border border-[var(--color-border-default)] text-[var(--color-text-primary)] text-sm font-bold rounded-lg transition-all flex items-center gap-2 disabled:opacity-50"
>
{isGeneratingCrisis ? <Loader2 size={16} className="animate-spin" /> : <RefreshCw size={16} />}
{isGeneratingCrisis ? 'Analiz Ediliyor...' : ((!analysis.crisisManagement || !analysis.sponsors || analysis.sponsors.length === 0) ? 'Analizi Yap' : 'Yeniden Üret')}
</button>
</div>
{(analysis.crisisManagement || (analysis.sponsors && analysis.sponsors.length > 0)) ? (
<div className="space-y-8">
{/* Crisis Section */}
{analysis.crisisManagement && (
<div className="grid md:grid-cols-2 gap-6">
<div className="p-6 bg-red-500/5 rounded-2xl border border-red-500/20">
<h3 className="font-bold text-red-500 mb-2 flex items-center gap-2"><AlertTriangle size={16}/> Potansiyel Linç İhtimali</h3>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed">{analysis.crisisManagement.potentialBacklash || "Veri yok."}</p>
</div>
<div className="p-6 bg-green-500/5 rounded-2xl border border-green-500/20">
<h3 className="font-bold text-green-500 mb-2 flex items-center gap-2"><ShieldCheck size={16}/> PR Stratejisi</h3>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed">{analysis.crisisManagement.prStrategy || "Veri yok."}</p>
</div>
</div>
)}
{/* Sponsors Section */}
{analysis.sponsors && analysis.sponsors.length > 0 && (
<div className="space-y-6">
<h3 className="text-xl font-bold text-[var(--color-text-primary)] border-b border-[var(--color-border-faint)] pb-2 flex items-center gap-2">
<Star size={20} className="text-orange-500" /> Potansiyel Sponsor Markalar
</h3>
<div className="grid gap-6">
{analysis.sponsors.map((sponsor: any, idx: number) => (
<div key={idx} className="p-6 bg-[var(--color-bg-surface)] rounded-2xl border border-[var(--color-border-default)] hover:border-orange-500/30 transition-colors">
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-1 space-y-4">
<div>
<h4 className="text-lg font-bold text-[var(--color-text-primary)] mb-1">{sponsor.brandName}</h4>
<p className="text-sm text-[var(--color-text-secondary)]">{sponsor.reasoning}</p>
</div>
<div className="p-4 bg-[var(--color-bg-base)] rounded-xl border border-[var(--color-border-strong)]">
<h5 className="text-xs font-bold uppercase tracking-wider text-orange-500 mb-2">Entegrasyon Fikri</h5>
<p className="text-sm text-[var(--color-text-primary)]">{sponsor.integrationIdea}</p>
</div>
</div>
<div className="flex-1 flex flex-col">
<h5 className="text-xs font-bold uppercase tracking-wider text-[var(--color-text-secondary)] mb-2 flex items-center gap-2">
<Mail size={14} /> Türkçe E-Posta Taslağı
</h5>
<div className="flex-1 p-4 bg-[var(--color-bg-base)] rounded-xl border border-[var(--color-border-strong)] text-sm text-[var(--color-text-secondary)] whitespace-pre-wrap font-mono text-xs overflow-auto max-h-48">
{sponsor.emailDraft}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="text-center py-12 bg-[var(--color-bg-surface)] border border-dashed border-[var(--color-border-default)] rounded-xl">
<p className="text-sm text-[var(--color-text-secondary)]">Kriz ve Sponsor analizi bulunamadı.</p>
</div>
)}
</motion.div>
)}
</div>
</div>
) : (
<div className="py-32 flex flex-col items-center justify-center text-center border-2 border-dashed border-[var(--color-border-default)] rounded-3xl bg-[var(--color-bg-base)]">
<div className="w-20 h-20 bg-orange-500/10 rounded-full flex items-center justify-center mb-6">
<Film size={32} className="text-orange-500" />
</div>
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-2">Pre-Production Hazır Değil</h3>
<p className="text-[var(--color-text-secondary)] font-medium max-w-sm mb-8">
Bölüm tasarımınızı başlatmak için analiz butonuna tıklayın. Yapay zeka, referans verilerinizi kullanarak 5 katmanlı bir üretim dosyası hazırlayacaktır.
</p>
{episode.status !== 'ANALYZING' && (
<button
onClick={handleAnalyze}
className="px-8 py-4 bg-orange-500 hover:bg-orange-600 text-white font-bold rounded-xl shadow-xl shadow-orange-500/20 transition-all flex items-center gap-2"
>
<Sparkles size={20} /> Şimdi Analiz Et
</button>
)}
</div>
)}
</div>
{/* Full Screen Loading Overlay */}
<AnimatePresence>
{isAnalyzing && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 bg-[var(--color-bg-base)]/90 backdrop-blur-xl flex flex-col items-center justify-center p-8 text-center"
>
<Loader2 className="animate-spin text-orange-500 mb-8" size={64} strokeWidth={1.5} />
<h3 className="text-3xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-4 tracking-tight">Pre-Production Yapılıyor</h3>
<p className="text-[var(--color-text-secondary)] font-bold text-base max-w-md">
Yeni bölümünüz için boşluk analizi, şeytanın avukatı argümanları, segment yapıları ve görsel DNA oluşturuluyor...
</p>
</motion.div>
)}
</AnimatePresence>
{/* Thumbnail Lightbox Overlay */}
<AnimatePresence>
{isThumbnailExpanded && episode?.masterAnalysis?.thumbnailUrl && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] bg-black/90 backdrop-blur-sm flex items-center justify-center p-8 cursor-zoom-out"
onClick={() => setIsThumbnailExpanded(false)}
>
<motion.img
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
exit={{ scale: 0.9 }}
src={episode.masterAnalysis.thumbnailUrl}
alt="Expanded Thumbnail"
className="max-w-full max-h-full object-contain rounded-2xl shadow-2xl border border-white/10"
onClick={(e) => e.stopPropagation()}
/>
</motion.div>
)}
</AnimatePresence>
</ErrorBoundary>
);
}
@@ -0,0 +1,725 @@
"use client";
import React, { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import {
ArrowLeft, Zap, Video, Film, Loader2, PlayCircle, Eye, MessageCircle,
Settings, FileText, CheckCircle2, Sparkles, Target, AlignLeft, Users, Clock, User, FilePlus, X
} from 'lucide-react';
import {
getProjectById, ProjectResponse, addVideoToProject, addDocumentToProject,
updateProject, createEpisode, getTopicSuggestions, TopicSuggestion, EpisodeResponse
} from '../services/strategistApi';
class ErrorBoundary extends React.Component<{children: React.ReactNode}, {hasError: boolean, error: Error | null}> {
constructor(props: {children: React.ReactNode}) { super(props); this.state = { hasError: false, error: null }; }
static getDerivedStateFromError(error: Error) { return { hasError: true, error }; }
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error("ErrorBoundary caught an error", error, errorInfo); }
render() {
if (this.state.hasError) {
return (
<div className="p-10 flex flex-col items-center justify-center text-center">
<h2 className="text-xl font-bold text-red-500 mb-4">Sayfa Yüklenirken Hata Oluştu (ErrorBoundary)</h2>
<pre className="text-left bg-gray-900 p-4 rounded text-xs text-red-300 w-full max-w-2xl overflow-auto">{this.state.error?.toString()}</pre>
<button onClick={() => window.location.reload()} className="mt-6 px-4 py-2 bg-red-500 text-white rounded font-bold">Sayfayı Yenile</button>
</div>
);
}
return this.props.children;
}
}
export default function StrategistHubPage() {
const router = useRouter();
const params = useParams();
const projectId = params.projectId as string;
const [project, setProject] = useState<ProjectResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState("");
// Dataset State
const [activeTab, setActiveTab] = useState<'videos' | 'documents'>('videos');
// Add Video State
const [videoUrl, setVideoUrl] = useState("");
const [isAddingVideo, setIsAddingVideo] = useState(false);
const [videoError, setVideoError] = useState("");
// Add Document State
const [docTitle, setDocTitle] = useState("");
const [docContent, setDocContent] = useState("");
const [docType, setDocType] = useState<'transcript' | 'comments'>('transcript');
const [isAddingDoc, setIsAddingDoc] = useState(false);
const [docError, setDocError] = useState("");
// Settings Modal State
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [settingsForm, setSettingsForm] = useState({
name: "", tone: "", targetDuration: "", speakerName: "", targetAudience: "", formatDescription: ""
});
const [isUpdatingSettings, setIsUpdatingSettings] = useState(false);
// New Episode Modal State
const [isEpisodeModalOpen, setIsEpisodeModalOpen] = useState(false);
const [episodeForm, setEpisodeForm] = useState({
topic: "", format: "", targetAudience: "", duration: ""
});
const [isAiTopic, setIsAiTopic] = useState(false);
const [suggestions, setSuggestions] = useState<TopicSuggestion[]>([]);
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
const [isCreatingEpisode, setIsCreatingEpisode] = useState(false);
const [episodeError, setEpisodeError] = useState("");
const fetchProject = async () => {
try {
setIsLoading(true);
const data = await getProjectById(projectId);
setProject(data);
setSettingsForm({
name: data.name || "",
tone: data.tone || "",
targetDuration: data.targetDuration || "",
speakerName: data.speakerName || "",
targetAudience: data.targetAudience || "",
formatDescription: data.formatDescription || ""
});
// Set initial values for episode modal if not set
setEpisodeForm(prev => ({
...prev,
targetAudience: data.targetAudience || "",
duration: data.targetDuration || "",
format: data.formatDescription ? data.formatDescription.substring(0, 50) + '...' : ""
}));
} catch (err: any) {
setError("Proje yüklenemedi.");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (projectId) {
fetchProject();
}
}, [projectId]);
const handleUpdateSettings = async (e: React.FormEvent) => {
e.preventDefault();
setIsUpdatingSettings(true);
try {
await updateProject(projectId, settingsForm);
await fetchProject();
setIsSettingsOpen(false);
} catch (err) {
console.error("Failed to update settings", err);
} finally {
setIsUpdatingSettings(false);
}
};
const handleAddVideo = async (e: React.FormEvent) => {
e.preventDefault();
if (!videoUrl) return;
setIsAddingVideo(true);
setVideoError("");
try {
await addVideoToProject(projectId, videoUrl);
setVideoUrl("");
await fetchProject();
} catch (err: any) {
setVideoError(err?.response?.data?.message || "Video eklenemedi.");
} finally {
setIsAddingVideo(false);
}
};
const handleAddDocument = async (e: React.FormEvent) => {
e.preventDefault();
if (!docTitle || !docContent) return;
setIsAddingDoc(true);
setDocError("");
try {
await addDocumentToProject(projectId, docTitle, docContent, docType);
setDocTitle("");
setDocContent("");
await fetchProject();
} catch (err: any) {
setDocError(err?.response?.data?.message || "Doküman eklenemedi.");
} finally {
setIsAddingDoc(false);
}
};
const handleGetSuggestions = async () => {
setIsLoadingSuggestions(true);
try {
const res = await getTopicSuggestions(projectId);
setSuggestions(res.suggestions || []);
} catch (err) {
console.error("Failed to get suggestions", err);
} finally {
setIsLoadingSuggestions(false);
}
};
const handleCreateEpisode = async (e: React.FormEvent) => {
e.preventDefault();
const finalTopic = isAiTopic ? "AI_AUTO" : episodeForm.topic;
if (!finalTopic && !isAiTopic) {
setEpisodeError("Lütfen bir konu başlığı girin veya yapay zeka önerisi seçin.");
return;
}
setIsCreatingEpisode(true);
setEpisodeError("");
try {
await createEpisode(
projectId,
finalTopic,
episodeForm.format,
episodeForm.targetAudience,
episodeForm.duration
);
setIsEpisodeModalOpen(false);
setEpisodeForm(prev => ({ ...prev, topic: "" }));
setIsAiTopic(false);
setSuggestions([]);
await fetchProject(); // Refresh the list
} catch (err: any) {
setEpisodeError(err?.response?.data?.message || "Bölüm oluşturulamadı.");
} finally {
setIsCreatingEpisode(false);
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<Loader2 className="w-10 h-10 animate-spin text-red-500" />
</div>
);
}
if (!project) {
return (
<div className="max-w-4xl mx-auto py-20 text-center">
<h2 className="text-2xl font-bold mb-4 text-[var(--color-text-primary)]">Proje Bulunamadı</h2>
<button onClick={() => router.push(`/${params.locale}/dashboard/tools/tube-strategist`)} className="text-red-500 font-bold underline">
Geri Dön
</button>
</div>
);
}
const youtubeVideos = project.videos?.filter(v => !v.videoId.startsWith('doc://')) || [];
const manualDocs = project.videos?.filter(v => v.videoId.startsWith('doc://')) || [];
return (
<ErrorBoundary>
<div className="max-w-6xl mx-auto space-y-8 pb-24 font-sans px-4 sm:px-0 pt-8">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-4">
<button
onClick={() => router.push(`/${params.locale}/dashboard/tools/tube-strategist`)}
className="w-10 h-10 rounded-full bg-[var(--color-bg-elevated)] border border-[var(--color-border-faint)] flex items-center justify-center text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:border-[var(--color-border-default)] transition-colors"
>
<ArrowLeft size={20} />
</button>
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">{project.name}</h1>
<div className="px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider bg-purple-500/10 text-purple-500 border border-purple-500/20 flex items-center gap-1.5">
<Film size={12} /> Hub Merkezi
</div>
</div>
<p className="text-sm text-[var(--color-text-secondary)] mt-1 font-medium">
Formatınızı yönetin, verisetinizi genişletin ve yeni bölümler tasarlayın.
</p>
</div>
</div>
<button
onClick={() => setIsSettingsOpen(true)}
className="px-4 py-2 bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] hover:bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] font-bold text-sm rounded-xl flex items-center gap-2 transition-all"
>
<Settings size={16} /> Ayarları Düzenle
</button>
</div>
{/* Format Info Card */}
<div className="card p-6 border border-[var(--color-border-faint)] bg-gradient-to-br from-[var(--color-bg-elevated)] to-[var(--color-bg-base)]">
<div className="flex items-start justify-between">
<div className="space-y-4 flex-1">
<h3 className="text-lg font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Target className="text-blue-500" size={20} /> Format & Konsept
</h3>
<p className="text-sm text-[var(--color-text-secondary)] leading-relaxed whitespace-pre-wrap">
{project.formatDescription || "Format açıklaması girilmemiş. Ayarlardan ekleyebilirsiniz."}
</p>
<div className="flex flex-wrap items-center gap-4 pt-2">
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-default)]">
<Users size={14} className="text-purple-500" /> {project.targetAudience || '-'}
</div>
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-default)]">
<AlignLeft size={14} className="text-green-500" /> {project.tone || '-'}
</div>
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-default)]">
<Clock size={14} className="text-orange-500" /> {project.targetDuration || '-'}
</div>
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-default)]">
<User size={14} className="text-blue-500" /> {project.speakerName || '-'}
</div>
</div>
</div>
</div>
</div>
<div className="grid lg:grid-cols-2 gap-8">
{/* Left Column: DATASET */}
<div className="space-y-6">
<div className="card p-6 border border-[var(--color-border-faint)] bg-[var(--color-bg-elevated)]">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Video className="text-red-500" size={20} />
Veriseti ({project.videos?.length || 0})
</h2>
</div>
{/* Tabs */}
<div className="flex items-center gap-2 mb-6 p-1 bg-[var(--color-bg-base)] rounded-xl border border-[var(--color-border-default)]">
<button
onClick={() => setActiveTab('videos')}
className={`flex-1 py-2 text-xs font-bold rounded-lg transition-colors ${activeTab === 'videos' ? 'bg-[var(--color-bg-elevated)] shadow-sm text-[var(--color-text-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}`}
>
YouTube Videoları ({youtubeVideos.length})
</button>
<button
onClick={() => setActiveTab('documents')}
className={`flex-1 py-2 text-xs font-bold rounded-lg transition-colors ${activeTab === 'documents' ? 'bg-[var(--color-bg-elevated)] shadow-sm text-[var(--color-text-primary)]' : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'}`}
>
Metin / Doküman ({manualDocs.length})
</button>
</div>
{activeTab === 'videos' && (
<>
<form onSubmit={handleAddVideo} className="flex items-center gap-3 mb-6 bg-[var(--color-bg-base)] p-2 rounded-xl border border-[var(--color-border-default)]">
<input
type="url"
placeholder="https://youtube.com/watch?v=..."
value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)}
className="flex-1 bg-transparent border-none px-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-0"
required
/>
<button
type="submit"
disabled={isAddingVideo || !videoUrl.trim()}
className="px-4 py-2 rounded-lg bg-[var(--color-bg-elevated)] border border-[var(--color-border-strong)] text-[var(--color-text-primary)] font-bold text-xs flex items-center gap-2 hover:bg-[var(--color-bg-surface)] disabled:opacity-50 transition-colors"
>
{isAddingVideo ? <Loader2 size={14} className="animate-spin" /> : <Video size={14} />}
Ekle
</button>
</form>
{videoError && <p className="text-xs text-red-500 mb-4">{videoError}</p>}
{youtubeVideos.length === 0 ? (
<div className="py-8 flex flex-col items-center justify-center text-center border-2 border-dashed border-[var(--color-border-default)] rounded-2xl bg-[var(--color-bg-base)]">
<PlayCircle className="w-8 h-8 text-[var(--color-text-ghost)] mb-2" />
<p className="text-[var(--color-text-secondary)] text-sm font-medium">Video eklenmedi.</p>
</div>
) : (
<div className="space-y-3 max-h-[400px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-[var(--color-border-strong)] scrollbar-track-transparent">
{youtubeVideos.map((vid: any) => (
<div key={vid.id} className="p-3 rounded-xl bg-[var(--color-bg-base)] border border-[var(--color-border-default)] flex gap-3">
<img src={vid.thumbnail} alt={vid.title} className="w-24 aspect-video object-cover rounded-lg shadow-sm flex-shrink-0" />
<div className="flex-1 min-w-0 flex flex-col justify-center">
<h3 className="text-[13px] font-bold text-[var(--color-text-primary)] line-clamp-2 leading-tight mb-1.5">
{vid.title}
</h3>
<div className="flex items-center gap-3 text-[10px] font-medium text-[var(--color-text-secondary)]">
<span className="flex items-center gap-1"><Eye size={12} className="text-blue-400" /> {Number(vid.viewCount || 0).toLocaleString('tr-TR')}</span>
<span className="flex items-center gap-1"><MessageCircle size={12} className="text-purple-400" /> {Number(vid.totalComments || 0).toLocaleString('tr-TR')}</span>
</div>
</div>
</div>
))}
</div>
)}
</>
)}
{activeTab === 'documents' && (
<>
<form onSubmit={handleAddDocument} className="mb-6 space-y-3 bg-[var(--color-bg-base)] p-4 rounded-xl border border-[var(--color-border-default)]">
<div className="flex gap-3">
<input
type="text"
placeholder="Doküman Başlığı"
value={docTitle}
onChange={(e) => setDocTitle(e.target.value)}
className="flex-1 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-lg px-3 py-2 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-1 focus:ring-red-500"
required
/>
<select
value={docType}
onChange={(e) => setDocType(e.target.value as any)}
className="bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-lg px-3 py-2 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-1 focus:ring-red-500"
>
<option value="transcript">Transkript</option>
<option value="comments">Yorumlar</option>
</select>
</div>
<textarea
placeholder="Metin içeriğini buraya yapıştırın..."
value={docContent}
onChange={(e) => setDocContent(e.target.value)}
rows={4}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-lg px-3 py-2 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-1 focus:ring-red-500 resize-none"
required
/>
<button
type="submit"
disabled={isAddingDoc || !docTitle.trim() || !docContent.trim()}
className="w-full px-4 py-2.5 rounded-lg bg-[var(--color-bg-elevated)] border border-[var(--color-border-strong)] text-[var(--color-text-primary)] font-bold text-xs flex items-center justify-center gap-2 hover:bg-[var(--color-bg-surface)] disabled:opacity-50 transition-colors"
>
{isAddingDoc ? <Loader2 size={14} className="animate-spin" /> : <FilePlus size={14} />}
Dokümanı Verisetine Ekle
</button>
</form>
{docError && <p className="text-xs text-red-500 mb-4">{docError}</p>}
{manualDocs.length === 0 ? (
<div className="py-8 flex flex-col items-center justify-center text-center border-2 border-dashed border-[var(--color-border-default)] rounded-2xl bg-[var(--color-bg-base)]">
<FileText className="w-8 h-8 text-[var(--color-text-ghost)] mb-2" />
<p className="text-[var(--color-text-secondary)] text-sm font-medium">Manuel doküman eklenmedi.</p>
</div>
) : (
<div className="space-y-3 max-h-[300px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-[var(--color-border-strong)] scrollbar-track-transparent">
{manualDocs.map((vid: any) => (
<div key={vid.id} className="p-3 rounded-xl bg-[var(--color-bg-base)] border border-[var(--color-border-default)] flex gap-3 items-center">
<div className="w-12 h-12 rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-strong)] flex items-center justify-center flex-shrink-0">
<FileText className={vid.totalComments > 0 ? "text-purple-500" : "text-blue-500"} size={20} />
</div>
<div className="flex-1 min-w-0">
<h3 className="text-[13px] font-bold text-[var(--color-text-primary)] line-clamp-1 mb-1">
{vid.title}
</h3>
<div className="text-[10px] font-medium text-[var(--color-text-secondary)]">
{vid.totalComments > 0 ? "Yorum Verisi" : "Transkript Verisi"}
</div>
</div>
</div>
))}
</div>
)}
</>
)}
</div>
</div>
{/* Right Column: EPISODES */}
<div className="space-y-6">
<div className="card p-6 border border-[var(--color-border-faint)] bg-[var(--color-bg-base)] shadow-inner">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Film className="text-orange-500" size={20} />
Bölüm Tasarımları ({project.episodes?.length || 0})
</h2>
<button
onClick={() => setIsEpisodeModalOpen(true)}
disabled={project.videos.length === 0}
className="px-4 py-2 bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white font-bold text-xs rounded-lg flex items-center gap-1.5 shadow-md shadow-red-500/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
title={project.videos.length === 0 ? "Önce en az 1 referans video eklemelisiniz." : ""}
>
<Zap size={14} /> Yeni Bölüm
</button>
</div>
{!project.episodes || project.episodes.length === 0 ? (
<div className="py-16 flex flex-col items-center justify-center text-center">
<div className="w-16 h-16 rounded-2xl bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] flex items-center justify-center mb-4 shadow-sm">
<Sparkles className="w-8 h-8 text-orange-500" />
</div>
<h3 className="text-base font-bold text-[var(--color-text-primary)] mb-1">Henüz Bölüm Yok</h3>
<p className="text-[var(--color-text-secondary)] text-sm max-w-[250px] mx-auto">
Verisetinizi ekledikten sonra ilk bölüm tasarımınızı oluşturun.
</p>
</div>
) : (
<div className="space-y-4">
{project.episodes.map((ep: any) => (
<div
key={ep.id}
onClick={() => router.push(`/${params.locale}/dashboard/tools/tube-strategist/${projectId}/episode/${ep.id}`)}
className="group cursor-pointer p-4 rounded-xl bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] hover:border-orange-500/50 hover:shadow-lg hover:shadow-orange-500/5 transition-all relative overflow-hidden"
>
<div className="absolute top-0 left-0 w-1 h-full bg-orange-500/20 group-hover:bg-orange-500 transition-colors" />
<div className="flex items-start justify-between mb-3">
<div className="flex-1 pr-4">
<h3 className="text-[15px] font-bold text-[var(--color-text-primary)] line-clamp-1 mb-1">
{ep.topic === 'AI_AUTO' ? '✨ Yapay Zeka Belirliyor...' : ep.topic}
</h3>
<div className="flex items-center gap-3 text-[11px] text-[var(--color-text-secondary)] font-medium">
<span className="flex items-center gap-1"><Users size={12} /> {ep.targetAudience}</span>
<span className="flex items-center gap-1"><Clock size={12} /> {ep.duration}</span>
</div>
</div>
{/* Status Badge */}
<div className={`px-2.5 py-1 text-[10px] font-bold uppercase tracking-wider rounded-md whitespace-nowrap ${
ep.status === 'COMPLETED' ? 'bg-green-500/10 text-green-500 border border-green-500/20' :
ep.status === 'ANALYZING' ? 'bg-blue-500/10 text-blue-500 border border-blue-500/20 animate-pulse' :
'bg-[var(--color-bg-surface)] text-[var(--color-text-secondary)] border border-[var(--color-border-strong)]'
}`}>
{ep.status === 'COMPLETED' ? 'TAMAMLANDI' : ep.status === 'ANALYZING' ? 'ANALİZ EDİLİYOR' : 'BEKLİYOR'}
</div>
</div>
{/* Mini Reports Indicators (Only if completed) */}
{ep.status === 'COMPLETED' && ep.masterAnalysis && (
<div className="flex items-center gap-2 pt-3 mt-3 border-t border-[var(--color-border-faint)]">
<span className={`text-[10px] font-bold px-2 py-0.5 rounded flex items-center gap-1 ${ep.masterAnalysis.seo ? 'bg-blue-500/10 text-blue-500' : 'bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)]'}`}>
{ep.masterAnalysis.seo && <CheckCircle2 size={10} />} SEO
</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded flex items-center gap-1 ${ep.masterAnalysis.commercial ? 'bg-purple-500/10 text-purple-500' : 'bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)]'}`}>
{ep.masterAnalysis.commercial && <CheckCircle2 size={10} />} Sponsorluk
</span>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
{/* Settings Modal */}
{isSettingsOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-[var(--color-bg-elevated)] w-full max-w-2xl rounded-2xl border border-[var(--color-border-faint)] shadow-2xl flex flex-col max-h-[90vh]">
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border-faint)]">
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">Proje Ayarları</h2>
<button onClick={() => setIsSettingsOpen(false)} className="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<X size={20} />
</button>
</div>
<form onSubmit={handleUpdateSettings} className="p-6 overflow-y-auto space-y-5">
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Proje Adı</label>
<input
type="text"
value={settingsForm.name}
onChange={(e) => setSettingsForm({...settingsForm, name: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Format / Ana Konsept</label>
<textarea
value={settingsForm.formatDescription}
onChange={(e) => setSettingsForm({...settingsForm, formatDescription: e.target.value})}
rows={4}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50 resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Hedef Kitle</label>
<input
type="text"
value={settingsForm.targetAudience}
onChange={(e) => setSettingsForm({...settingsForm, targetAudience: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Hedef Süre</label>
<input
type="text"
value={settingsForm.targetDuration}
onChange={(e) => setSettingsForm({...settingsForm, targetDuration: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">Sunucu / Yüz</label>
<input
type="text"
value={settingsForm.speakerName}
onChange={(e) => setSettingsForm({...settingsForm, speakerName: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div>
<label className="block text-xs font-bold text-[var(--color-text-secondary)] uppercase tracking-wider mb-2">İçerik Tonu</label>
<input
type="text"
value={settingsForm.tone}
onChange={(e) => setSettingsForm({...settingsForm, tone: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
</div>
<div className="pt-4 flex justify-end gap-3">
<button type="button" onClick={() => setIsSettingsOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">İptal</button>
<button type="submit" disabled={isUpdatingSettings} className="px-5 py-2.5 bg-white text-black font-bold text-sm rounded-xl flex items-center gap-2 hover:bg-gray-100 disabled:opacity-50">
{isUpdatingSettings ? <Loader2 size={16} className="animate-spin" /> : "Değişiklikleri Kaydet"}
</button>
</div>
</form>
</div>
</div>
)}
{/* New Episode Modal */}
{isEpisodeModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
<div className="bg-[var(--color-bg-elevated)] w-full max-w-3xl rounded-2xl border border-[var(--color-border-faint)] shadow-2xl flex flex-col max-h-[90vh]">
<div className="flex items-center justify-between p-6 border-b border-[var(--color-border-faint)]">
<div>
<h2 className="text-xl font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<Zap className="text-orange-500" /> Yeni Bölüm Tasarla
</h2>
<p className="text-sm text-[var(--color-text-secondary)] mt-1">
Bu format için yeni bir bölümün Ön-Yapım (Pre-Production) sürecini başlatın.
</p>
</div>
<button onClick={() => setIsEpisodeModalOpen(false)} className="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
<X size={20} />
</button>
</div>
<form onSubmit={handleCreateEpisode} className="p-6 overflow-y-auto space-y-6">
{/* Topic Section */}
<div className="p-5 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-base)] space-y-4">
<div className="flex items-center justify-between">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5">
<Target size={14} className="text-red-500" /> Konu Başlığı
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={isAiTopic}
onChange={(e) => {
setIsAiTopic(e.target.checked);
if (!e.target.checked) setSuggestions([]);
}}
className="rounded border-[var(--color-border-strong)] text-orange-500 focus:ring-orange-500 bg-[var(--color-bg-surface)]"
/>
<span className="text-xs font-bold text-[var(--color-text-primary)] flex items-center gap-1">
<Sparkles size={12} className="text-orange-500" /> Konuyu Yapay Zeka Belirlesin
</span>
</label>
</div>
{!isAiTopic ? (
<input
type="text"
value={episodeForm.topic}
onChange={(e) => setEpisodeForm({...episodeForm, topic: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-strong)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
placeholder="Örn: Modern İlişkilerde Sınır Çizmek"
/>
) : (
<div className="space-y-4">
{suggestions.length === 0 ? (
<div className="flex flex-col items-center justify-center p-6 border border-dashed border-orange-500/30 rounded-xl bg-orange-500/5">
<p className="text-sm text-[var(--color-text-secondary)] mb-4 text-center max-w-md">
Yapay zeka, verisetinizdeki boşlukları ve izleyici yorumlarındaki talepleri analiz ederek size 5 benzersiz konu önerebilir.
</p>
<button
type="button"
onClick={handleGetSuggestions}
disabled={isLoadingSuggestions}
className="px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white font-bold text-sm rounded-lg flex items-center gap-2 transition-colors disabled:opacity-50"
>
{isLoadingSuggestions ? <Loader2 size={16} className="animate-spin" /> : <Sparkles size={16} />}
5 Konu Önerisi Getir
</button>
</div>
) : (
<div className="grid gap-3">
{suggestions.map((sug, idx) => (
<div
key={idx}
onClick={() => {
setEpisodeForm({...episodeForm, topic: sug.title});
setIsAiTopic(false);
}}
className="p-4 rounded-xl border border-[var(--color-border-strong)] bg-[var(--color-bg-surface)] hover:border-orange-500 cursor-pointer transition-all group"
>
<h4 className="font-bold text-[14px] text-[var(--color-text-primary)] mb-1 group-hover:text-orange-500 transition-colors">{sug.title}</h4>
<p className="text-xs text-[var(--color-text-secondary)] mb-2 line-clamp-2">{sug.description}</p>
<div className="text-[10px] text-blue-400 bg-blue-500/10 px-2 py-1 rounded inline-block">
<span className="font-bold">Neden?</span> {sug.reasoning}
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Other Params */}
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5">
<Users size={14}/> Hedef Kitle
</label>
<input
type="text"
value={episodeForm.targetAudience}
onChange={(e) => setEpisodeForm({...episodeForm, targetAudience: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5">
<Clock size={14}/> Uzunluk
</label>
<input
type="text"
value={episodeForm.duration}
onChange={(e) => setEpisodeForm({...episodeForm, duration: e.target.value})}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
</div>
{episodeError && <p className="text-sm text-red-500 font-medium">{episodeError}</p>}
<div className="pt-4 flex justify-end gap-3 border-t border-[var(--color-border-faint)]">
<button type="button" onClick={() => setIsEpisodeModalOpen(false)} className="px-5 py-2.5 rounded-xl font-bold text-sm text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">İptal</button>
<button
type="submit"
disabled={isCreatingEpisode || (!isAiTopic && !episodeForm.topic.trim())}
className="px-6 py-2.5 bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white font-bold text-sm rounded-xl flex items-center gap-2 shadow-lg shadow-red-500/20 disabled:opacity-50 transition-all"
>
{isCreatingEpisode ? <Loader2 size={16} className="animate-spin" /> : <Zap size={16} />}
Bölüm Taslağını Oluştur
</button>
</div>
</form>
</div>
</div>
)}
</ErrorBoundary>
);
}
@@ -0,0 +1,19 @@
import React from 'react';
import { UploadCloud } from 'lucide-react';
export const LegacyUploader = () => {
return (
<div className="card p-12 flex flex-col items-center justify-center text-center border border-dashed border-[var(--color-border-default)]">
<div className="w-20 h-20 bg-[var(--color-bg-elevated)] rounded-full flex items-center justify-center mb-6">
<UploadCloud className="w-10 h-10 text-[var(--color-text-ghost)]" />
</div>
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-2">Eski TXT Yükleyici</h3>
<p className="text-[var(--color-text-secondary)] text-sm max-w-md mb-8">
(Legacy) TXT dosyalarını manuel olarak yükleyerek analiz yapmak için bu alanı kullanabilirsiniz.
</p>
<button className="px-6 py-3 rounded-xl bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] text-white font-bold flex items-center gap-2">
Dosya Seç
</button>
</div>
);
};
@@ -0,0 +1,179 @@
import React, { useEffect, useRef, useState } from 'react';
import { GoogleGenAI, LiveServerMessage, Modality } from "@google/genai";
import { audioContexts, decode, decodeAudioData, float32ToPcm16, encode } from '../services/strategistApi';
import { Mic, MicOff, Volume2, X, Loader2 } from 'lucide-react';
import { motion } from 'framer-motion';
interface LiveBrainstormProps {
context: string;
onClose: () => void;
}
const LiveBrainstorm: React.FC<LiveBrainstormProps> = ({ context, onClose }) => {
const [isActive, setIsActive] = useState(false);
const [status, setStatus] = useState("Bağlanmaya Hazır");
const [isLoading, setIsLoading] = useState(false);
const sessionRef = useRef<Promise<any> | null>(null);
const nextStartTimeRef = useRef<number>(0);
const sourcesRef = useRef<Set<AudioBufferSourceNode>>(new Set());
const startSession = async () => {
setIsLoading(true);
setStatus("Bağlanıyor...");
try {
// TR: Google GenAI istemcisini başlat. Guideline'a göre API key doğrudan process.env'den alınmalı.
// EN: Initialize Google GenAI client. API key must be taken directly from process.env per guidelines.
const ai = new GoogleGenAI({ apiKey: process.env.NEXT_PUBLIC_GEMINI_API_KEY });
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const sessionPromise = ai.live.connect({
model: 'gemini-2.5-flash-native-audio-preview-12-2025',
config: {
responseModalities: [Modality.AUDIO],
systemInstruction: `Sen uzman bir YouTube Stratejistisin. Şu an şu video planı üzerine konuşuyoruz: ${context}.
Kısa, enerjik ve videoyu nasıl daha viral yapabileceğimize odaklanan cevaplar ver.
Stratejik, samimi ve yaratıcı ol.`,
speechConfig: {
voiceConfig: { prebuiltVoiceConfig: { voiceName: 'Kore' } }
}
},
callbacks: {
onopen: () => {
setStatus("Bağlandı! Konuşabilirsiniz.");
setIsActive(true);
setIsLoading(false);
const source = audioContexts.input.createMediaStreamSource(stream);
const processor = audioContexts.input.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
const pcmData = float32ToPcm16(inputData);
const base64 = encode(pcmData);
// TR: Yarış durumunu önlemek için sessionPromise kullan.
// EN: Use sessionPromise to prevent race conditions.
sessionPromise.then(session => {
session.sendRealtimeInput({
media: {
mimeType: 'audio/pcm;rate=16000',
data: base64
}
});
});
};
source.connect(processor);
processor.connect(audioContexts.input.destination);
},
onmessage: async (msg: LiveServerMessage) => {
const base64Audio = msg.serverContent?.modelTurn?.parts?.[0]?.inlineData?.data;
if (base64Audio) {
// TR: Gelen ses verisini decode et ve oynatma kuyruğuna ekle.
// EN: Decode incoming audio data and add to playback queue.
const audioData = await decode(base64Audio);
const audioBuffer = await decodeAudioData(audioData, audioContexts.output);
const source = audioContexts.output.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContexts.output.destination);
const currentTime = audioContexts.output.currentTime;
const startTime = Math.max(currentTime, nextStartTimeRef.current);
source.start(startTime);
nextStartTimeRef.current = startTime + audioBuffer.duration;
sourcesRef.current.add(source);
source.onended = () => sourcesRef.current.delete(source);
}
},
onclose: () => {
setStatus("Bağlantı Kesildi");
setIsActive(false);
setIsLoading(false);
},
onerror: (err) => {
console.error(err);
setStatus("Bir hata oluştu");
setIsActive(false);
setIsLoading(false);
}
}
});
sessionRef.current = sessionPromise;
} catch (e) {
console.error(e);
setStatus("Mikrofon erişimi engellendi");
setIsLoading(false);
}
};
const stopSession = () => {
window.location.reload();
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-[var(--color-bg-base)]/80 backdrop-blur-xl flex items-center justify-center z-[100] p-4"
>
<motion.div
initial={{ scale: 0.95, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0, y: 20 }}
className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] rounded-[2rem] p-8 w-full max-w-md shadow-2xl relative overflow-hidden"
>
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-red-500 to-orange-500"></div>
{isActive && (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-red-500/10 rounded-full blur-3xl animate-pulse pointer-events-none"></div>
)}
<div className="flex justify-between items-center mb-8 relative z-10">
<h2 className="text-xl font-[family-name:var(--font-display)] font-bold flex items-center gap-3 text-[var(--color-text-primary)]">
<Volume2 className="text-red-500" /> Canlı Beyin Fırtınası
</h2>
<button onClick={onClose} className="text-[var(--color-text-ghost)] hover:text-[var(--color-text-primary)] transition-colors p-2 bg-[var(--color-bg-surface)] rounded-full hover:bg-[var(--color-bg-hover)] border border-[var(--color-border-faint)]">
<X size={18} />
</button>
</div>
<div className="text-center mb-10 relative z-10">
<div className={`text-4xl font-[family-name:var(--font-display)] font-black mb-3 tracking-tighter transition-all duration-700 ${isActive ? 'text-red-500 scale-110 drop-shadow-[0_0_15px_rgba(239,68,68,0.5)]' : 'text-[var(--color-border-hover)]'}`}>
{isActive ? "DİNLİYORUM" : "ÇEVRİMDIŞI"}
</div>
<p className="text-[var(--color-text-secondary)] font-medium text-sm">{status}</p>
</div>
<div className="flex justify-center relative z-10">
{!isActive ? (
<button
onClick={startSession}
disabled={isLoading}
className="bg-gradient-to-b from-red-500 to-red-600 hover:from-red-400 hover:to-red-500 text-white rounded-full p-8 transition-all shadow-xl shadow-red-500/20 hover:scale-105 active:scale-95 disabled:opacity-50 border border-red-400/20"
>
{isLoading ? <Loader2 className="animate-spin" size={36}/> : <Mic size={36} />}
</button>
) : (
<button
onClick={stopSession}
className="bg-[var(--color-bg-surface)] hover:bg-[var(--color-border-hover)] text-[var(--color-text-primary)] rounded-full p-8 transition-all shadow-xl border border-[var(--color-border-default)] hover:scale-105 active:scale-95"
>
<MicOff size={36} />
</button>
)}
</div>
<p className="text-[9px] text-center text-[var(--color-text-ghost)] mt-10 relative z-10 uppercase font-black tracking-widest">
Gemini Live Native Audio 24.0kHz PCM
</p>
</motion.div>
</motion.div>
);
};
export default LiveBrainstorm;
@@ -0,0 +1,885 @@
import React, { useState, useEffect } from 'react';
import { StrategyResult, VideoDuration, TargetAudience, NeuroReport, MarketingInsights, SeoAnalysis } from '../types';
import { AreaChart, Area, Tooltip, ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts';
import { MessageSquare, Mic, Image as ImageIcon, Search, Download, RefreshCw, Printer, Zap, Briefcase, Mail, Plus, Activity, Heart, Lightbulb, MonitorPlay, ChevronRight, Loader2, Target, Brain, TrendingUp, Users, Award, Sparkles, Layout, Database, Copy, Check, AlertTriangle, FileText, Layers, Video, Camera, Handshake, MessageCircle } from 'lucide-react';
import LiveBrainstorm from './LiveBrainstorm';
import { generateThumbnailImage, generateDeepCommercialAnalysis, generateNeuroReport, generateMarketingReport, generateSeoReport } from '../services/strategistApi';
import { motion, AnimatePresence } from 'framer-motion';
interface StrategyViewProps {
strategy: StrategyResult;
onReset: () => void;
onRegenerate?: () => void;
isRegenerating?: boolean;
transcriptsContent?: any[];
commentsContent?: any[];
currentTone: string;
currentDuration: VideoDuration;
speakerName: string;
targetAudience?: TargetAudience;
projectId: string;
}
const StrategyView: React.FC<StrategyViewProps> = ({ strategy: initialStrategy, onReset, onRegenerate, isRegenerating, currentTone, currentDuration, speakerName, targetAudience, projectId }) => {
// Safe defaults for incomplete masterAnalysis data
const safeInitial: StrategyResult = {
title: initialStrategy?.title || 'Strateji Raporu',
thumbnailConcept: initialStrategy?.thumbnailConcept || '',
generatedThumbnail: initialStrategy?.generatedThumbnail,
hook: initialStrategy?.hook || '',
segments: initialStrategy?.segments || [],
chartData: initialStrategy?.chartData || [],
selectedComments: initialStrategy?.selectedComments || [],
interviewQuestions: initialStrategy?.interviewQuestions || [],
wowFactor: initialStrategy?.wowFactor || '',
psychologicalTheme: initialStrategy?.psychologicalTheme || '',
commercialAnalysis: initialStrategy?.commercialAnalysis || { suitableIndustries: [], brandSafetyScore: 0, integrationIdeas: [], monetizationPotential: 'Low', suggestedBrands: [] },
inspiredByGap: initialStrategy?.inspiredByGap,
provenanceNotes: initialStrategy?.provenanceNotes,
neuroReport: initialStrategy?.neuroReport,
marketingInsights: initialStrategy?.marketingInsights,
seoAnalysis: initialStrategy?.seoAnalysis,
projectDNA: initialStrategy?.projectDNA,
trendAnalysis: initialStrategy?.trendAnalysis || [],
comboShorts: initialStrategy?.comboShorts || [],
crisisManagement: initialStrategy?.crisisManagement,
bRollSuggestions: initialStrategy?.bRollSuggestions || [],
communityHooks: initialStrategy?.communityHooks || [],
sponsorIntegration: initialStrategy?.sponsorIntegration || '',
};
const [strategy, setStrategy] = useState<StrategyResult>(safeInitial);
useEffect(() => {
if (initialStrategy) {
setStrategy({
title: initialStrategy.title || 'Strateji Raporu',
thumbnailConcept: initialStrategy.thumbnailConcept || '',
generatedThumbnail: initialStrategy.generatedThumbnail,
hook: initialStrategy.hook || '',
segments: initialStrategy.segments || [],
chartData: initialStrategy.chartData || [],
selectedComments: initialStrategy.selectedComments || [],
interviewQuestions: initialStrategy.interviewQuestions || [],
wowFactor: initialStrategy.wowFactor || '',
psychologicalTheme: initialStrategy.psychologicalTheme || '',
commercialAnalysis: initialStrategy.commercialAnalysis || { suitableIndustries: [], brandSafetyScore: 0, integrationIdeas: [], monetizationPotential: 'Low', suggestedBrands: [] },
inspiredByGap: initialStrategy.inspiredByGap,
provenanceNotes: initialStrategy.provenanceNotes,
neuroReport: initialStrategy.neuroReport,
marketingInsights: initialStrategy.marketingInsights,
seoAnalysis: initialStrategy.seoAnalysis,
projectDNA: initialStrategy.projectDNA,
trendAnalysis: initialStrategy.trendAnalysis || [],
comboShorts: initialStrategy.comboShorts || [],
crisisManagement: initialStrategy.crisisManagement,
bRollSuggestions: initialStrategy.bRollSuggestions || [],
communityHooks: initialStrategy.communityHooks || [],
sponsorIntegration: initialStrategy.sponsorIntegration || '',
});
}
}, [initialStrategy]);
const [activeTab, setActiveTab] = useState<'strategy' | 'neuro' | 'marketing' | 'seo' | 'commercial'>('strategy');
const [showLive, setShowLive] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [copied, setCopied] = useState(false);
const [isThumbnailExpanded, setIsThumbnailExpanded] = useState(false);
// Modular Analysis Handlers - All update the central 'strategy' object
const handleNeuroAnalysis = async () => {
setIsGenerating(true);
try {
const report = await generateNeuroReport(projectId);
setStrategy(prev => ({ ...prev, neuroReport: report }));
} catch (e) { alert("Nöro-analiz başarısız oldu."); }
setIsGenerating(false);
};
const handleMarketingAnalysis = async () => {
setIsGenerating(true);
try {
const report = await generateMarketingReport(projectId);
setStrategy(prev => ({ ...prev, marketingInsights: report }));
} catch (e) { alert("Marketing analizi başarısız oldu."); }
setIsGenerating(false);
};
const handleSeoAnalysis = async () => {
setIsGenerating(true);
try {
const report = await generateSeoReport(projectId);
if (report) {
setStrategy(prev => ({ ...prev, seoAnalysis: report }));
}
} catch (e) { alert("SEO analizi sırasında hata."); }
setIsGenerating(false);
};
const handleDeepCommercial = async () => {
setIsGenerating(true);
try {
const report = await generateDeepCommercialAnalysis(projectId);
setStrategy(prev => ({
...prev,
commercialAnalysis: { ...prev.commercialAnalysis, deepAnalysis: report }
}));
} catch (e) { alert("Ticari analiz başarısız."); }
setIsGenerating(false);
};
const handleGenThumbnail = async () => {
setIsGenerating(true);
try {
const url = await generateThumbnailImage(projectId, strategy.thumbnailConcept);
if (url) {
setStrategy(prev => ({ ...prev, generatedThumbnail: url }));
}
} catch (e) { alert("Görsel üretilemedi."); }
setIsGenerating(false);
};
// FULL EXPORT: JSON Format (Object dump)
const exportJSON = () => {
const blob = new Blob([JSON.stringify(strategy, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `TubeStrategist_Master_${strategy.title.replace(/\s+/g, '_')}.json`;
a.click();
alert("Master JSON Save (Tüm veriler dahil) indirildi.");
};
// MASTER EXPORT: HTML Format (Visual dump of EVERYTHING)
const exportHTML = () => {
const htmlContent = `
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>${strategy.title} - Master Strateji Raporu</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap');
body { font-family: 'Inter', sans-serif; padding: 40px; color: #1e293b; line-height: 1.6; background: #f1f5f9; }
.master-container { max-width: 1000px; margin: auto; background: white; padding: 60px; border-radius: 30px; box-shadow: 0 20px 50px rgba(0,0,0,0.05); }
h1 { font-size: 48px; font-weight: 900; color: #e11d48; margin-bottom: 10px; line-height: 1.1; }
.hook-box { background: #fff1f2; border-left: 6px solid #e11d48; padding: 25px; border-radius: 0 15px 15px 0; margin: 30px 0; font-style: italic; font-size: 20px; color: #881337; }
.section-title { font-size: 24px; font-weight: 900; color: #0f172a; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-top: 60px; margin-bottom: 30px; display: flex; align-items: center; gap: 10px; text-transform: uppercase; letter-spacing: 1px; }
.thumbnail { width: 100%; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); margin-bottom: 40px; }
.card { background: #f8fafc; border: 1px solid #e2e8f0; padding: 25px; border-radius: 15px; margin-bottom: 20px; }
.badge { background: #e11d48; color: white; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 900; text-transform: uppercase; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.tag { display: inline-block; background: #e2e8f0; color: #475569; padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: bold; margin: 2px; }
.question-list { counter-reset: q-counter; list-style: none; padding: 0; }
.question-item { display: flex; gap: 15px; margin-bottom: 15px; background: #fff; padding: 15px; border-radius: 12px; border: 1px solid #f1f5f9; }
.question-item::before { counter-increment: q-counter; content: counter(q-counter); background: #e11d48; color: white; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 900; flex-shrink: 0; }
.neuro-score { background: #0f172a; color: white; padding: 10px 20px; border-radius: 10px; text-align: center; }
.email-draft { background: #0f172a; color: #cbd5e1; padding: 30px; border-radius: 15px; font-family: monospace; font-size: 13px; white-space: pre-wrap; line-height: 1.8; }
footer { text-align: center; margin-top: 60px; font-size: 10px; color: #94a3b8; font-weight: 900; letter-spacing: 2px; text-transform: uppercase; }
</style>
</head>
<body>
<div class="master-container">
<h1>${strategy.title}</h1>
<p style="color: #64748b; font-weight: bold;">TubeStrategist Master Analysis Report</p>
${strategy.generatedThumbnail ? `<img src="${strategy.generatedThumbnail}" class="thumbnail" />` : ''}
<div class="hook-box">"${strategy.hook}"</div>
<div class="grid-2">
<div class="card"><strong>Psikolojik Tema:</strong><br/>${strategy.psychologicalTheme}</div>
<div class="card"><strong>WoW Faktörü:</strong><br/>${strategy.wowFactor}</div>
</div>
<h2 class="section-title">🎬 Senaryo Akışı & Zaman Çizelgesi</h2>
${(strategy.segments || []).map(s => `
<div class="card" style="border-left: 5px solid #e11d48;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<span class="badge">${s.duration}</span>
<span style="font-size: 11px; font-weight: 900; color: #e11d48; text-transform: uppercase;">${s.neuroObjective || 'Viral Katılım'}</span>
</div>
<h3 style="margin: 0 0 10px 0; font-size: 18px;">${s.type}</h3>
<p style="font-size: 14px; color: #475569;">${s.description}</p>
<div style="margin-top: 10px;">
${(s.keyPoints || []).map(k => `<span class="tag">${k}</span>`).join('')}
</div>
</div>
`).join('')}
<h2 class="section-title">🎤 Kalbe Dokunan 20 Soru</h2>
<div class="question-list">
${(strategy.interviewQuestions || []).map(q => `<div class="question-item">${q}</div>`).join('')}
</div>
${strategy.neuroReport ? `
<h2 class="section-title">🧠 Nöro-Pazarlama Laboratuvarı</h2>
<div class="grid-2">
<div class="card">
<strong>Göz Odağı (Eye Tracking):</strong><br/>
<p style="font-size: 13px;">${strategy.neuroReport.eyeTrackingFocus}</p>
</div>
<div class="card">
<strong>Renk Psikolojisi:</strong><br/>
<p style="font-size: 13px;">${strategy.neuroReport.colorPsychology}</p>
</div>
</div>
<div class="card">
<strong>Dopamin Tetikleyiciler:</strong><br/>
${strategy.neuroReport.dopamineTriggers.map(t => `<span class="tag" style="background: #fae8ff; color: #a21caf;">${t}</span>`).join('')}
</div>
` : ''}
${strategy.marketingInsights ? `
<h2 class="section-title">📈 Pazarlama & Viral Stratejisi</h2>
<div class="grid-2">
<div class="card">
<strong>Hedef Personalar:</strong><br/>
<ul style="font-size: 13px; padding-left: 20px;">
${strategy.marketingInsights.targetPersonas.map(p => `<li>${p}</li>`).join('')}
</ul>
</div>
<div class="card">
<strong>Viral Yayılma Kancaları:</strong><br/>
${strategy.marketingInsights.socialMediaHooks.map(h => `<p style="font-size: 11px; background: #eff6ff; padding: 10px; border-radius: 8px;"><strong>${h.platform}:</strong> ${h.text}</p>`).join('')}
</div>
</div>
` : ''}
${strategy.seoAnalysis ? `
<h2 class="section-title">🔍 SEO Master Verileri</h2>
<div class="card">
<strong>Optimize Ana Başlık:</strong><br/>
<p style="font-size: 20px; font-weight: 900; color: #e11d48;">${strategy.seoAnalysis.optimizedTitle}</p>
</div>
<div class="card">
<strong>Etiketler (Copy-Paste):</strong><br/>
<p style="font-size: 12px; font-family: monospace; background: #fff; padding: 15px; border-radius: 10px; border: 1px solid #e2e8f0;">${strategy.seoAnalysis.tags.join(', ')}</p>
</div>
<h3>A/B Başlık Testleri (Neuro-Scores)</h3>
${strategy.seoAnalysis.alternativeTitles.map(alt => `
<div class="question-item" style="justify-content: space-between; align-items: center;">
<span><strong>${alt.title}</strong><br/><small style="color: #64748b;">${alt.psychologicalAngle}</small></span>
<div class="neuro-score"><span style="font-size: 20px; font-weight: 900;">${alt.neuroScore}</span><br/><small>SCORE</small></div>
</div>
`).join('')}
` : ''}
${strategy.commercialAnalysis.deepAnalysis ? `
<h2 class="section-title">🤝 Ticari İş Birliği & Sponsorluk</h2>
<div class="card" style="background: #f0fdf4; border-color: #bbf7d0;">
<strong>Tahmini Gelir Projeksiyonu:</strong><br/>
<span style="font-size: 24px; font-weight: 900; color: #166534;">${strategy.commercialAnalysis.deepAnalysis.estimatedRevenue}</span>
</div>
<h3>Sponsorluk Mail Taslağı</h3>
<div class="email-draft">${strategy.commercialAnalysis.deepAnalysis.emailDraft}</div>
` : ''}
<h2 class="section-title">💬 Seçilmiş İzleyici Yorumları</h2>
${(strategy.selectedComments || []).map(c => `
<div class="card">
<p style="font-style: italic;">"${c.text}"</p>
<p style="font-size: 11px; color: #64748b; margin-top: 10px;">Yazar: <strong>${c.username}</strong> | Kaynak Dosya: <strong>${c.sourceFile}</strong></p>
<div style="background: #f1f5f9; padding: 15px; border-radius: 10px; margin-top: 10px; font-size: 13px;">
<strong>Stratejik İçgörü:</strong> ${c.insightValue}
</div>
</div>
`).join('')}
<footer>TUBE STRATEGIST AI MASTER REPORT • v2.5 FINAL • UNIFIED DATA</footer>
</div>
</body>
</html>
`;
const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = `TubeStrategist_MASTER_REPORT_${strategy.title.replace(/\s+/g, '_')}.html`; a.click();
alert("Master Görsel Rapor (Eksiksiz) başarıyla oluşturuldu.");
};
const tabs = [
{ id: 'strategy', label: 'STRATEJİ', icon: <Target size={14} /> },
{ id: 'neuro', label: 'NEURO-LAB', icon: <Brain size={14} /> },
{ id: 'marketing', label: 'MARKETING', icon: <TrendingUp size={14} /> },
{ id: 'seo', label: 'SEO-MASTER', icon: <Search size={14} /> },
{ id: 'commercial', label: 'TİCARİ', icon: <Briefcase size={14} /> }
];
return (
<div className="max-w-7xl mx-auto space-y-8 pb-24 font-sans px-4 sm:px-0">
{/* Lightbox for Thumbnail */}
<AnimatePresence>
{isThumbnailExpanded && strategy.generatedThumbnail && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-sm p-4 cursor-zoom-out"
onClick={() => setIsThumbnailExpanded(false)}
>
<motion.img
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
src={strategy.generatedThumbnail}
className="max-w-full max-h-[90vh] object-contain rounded-2xl shadow-2xl"
/>
</motion.div>
)}
</AnimatePresence>
{/* Navigation */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col xl:flex-row justify-between items-center bg-[var(--color-bg-elevated)]/80 p-4 rounded-2xl border border-[var(--color-border-default)] gap-4 sticky top-4 z-50 shadow-2xl backdrop-blur-xl"
>
<div className="flex gap-4">
<button onClick={onReset} className="text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] text-sm flex items-center gap-2 font-black uppercase tracking-widest transition-colors">
<ChevronRight className="rotate-180" size={14} /> GERİ DÖN
</button>
{onRegenerate && (
<button
onClick={onRegenerate}
disabled={isRegenerating}
className="text-red-500 hover:text-red-400 text-sm flex items-center gap-2 font-black uppercase tracking-widest transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRegenerating ? <Loader2 size={14} className="animate-spin" /> : <RefreshCw size={14} />} ANALİZİ YENİDEN YAP
</button>
)}
</div>
<div className="flex gap-2 bg-[var(--color-bg-surface)] p-1.5 rounded-xl border border-[var(--color-border-default)] overflow-x-auto max-w-full custom-scrollbar">
{tabs.map((t) => (
<button
key={t.id} onClick={() => setActiveTab(t.id as any)}
className={`flex items-center gap-2 px-5 py-2 rounded-lg text-[11px] font-black transition-all uppercase tracking-[0.1em] whitespace-nowrap
${activeTab === t.id
? 'bg-gradient-to-r from-red-600 to-orange-500 text-white shadow-lg'
: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-bg-hover)]'
}`}
>
{t.icon} {t.label}
</button>
))}
</div>
<div className="flex gap-2">
<button onClick={exportJSON} className="px-4 py-2 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] hover:border-emerald-500/50 hover:bg-emerald-500/10 rounded-xl text-xs font-bold text-[var(--color-text-primary)] flex items-center gap-2 transition-all shadow-sm active:scale-95 group">
<Database size={14} className="text-emerald-500 group-hover:scale-110 transition-transform" /> JSON
</button>
<button onClick={exportHTML} className="px-4 py-2 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] hover:border-blue-500/50 hover:bg-blue-500/10 rounded-xl text-xs font-bold text-[var(--color-text-primary)] flex items-center gap-2 transition-all shadow-sm active:scale-95 group">
<Printer size={14} className="text-blue-500 group-hover:scale-110 transition-transform" /> HTML
</button>
<button onClick={() => setShowLive(true)} className="px-4 py-2 bg-gradient-to-r from-red-600 to-orange-500 hover:from-red-500 hover:to-orange-400 rounded-xl text-xs font-bold text-white flex items-center gap-2 transition-all shadow-lg shadow-red-500/20 active:scale-95 group">
<Mic size={14} className="group-hover:scale-110 transition-transform" /> CANLI LAB
</button>
</div>
</motion.div>
<div className="animate-fade-in relative z-10">
{/* TAB: STRATEGY */}
{activeTab === 'strategy' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="grid xl:grid-cols-3 gap-6">
<div className="xl:col-span-2 space-y-6">
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl relative overflow-hidden group">
<div className="absolute top-0 right-0 w-64 h-64 bg-red-500/10 rounded-full blur-3xl -mr-32 -mt-32 transition-all group-hover:bg-red-500/20"></div>
<div className="flex justify-between items-start mb-6 relative z-10">
<h1 className="text-3xl md:text-5xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] leading-tight pr-12">{strategy.title}</h1>
<div className="absolute top-0 right-0 flex gap-2">
<span className="bg-red-500/10 text-red-500 border border-red-500/20 text-[9px] font-black px-3 py-1.5 rounded-full uppercase tracking-widest backdrop-blur-sm shadow-inner">Verified Strategy</span>
</div>
</div>
<p className="text-[var(--color-text-secondary)] italic border-l-4 border-red-500 pl-5 py-3 text-lg md:text-xl relative z-10 leading-relaxed bg-[var(--color-bg-surface)]/50 rounded-r-2xl backdrop-blur-sm">"{strategy.hook || strategy.projectDNA?.coreMessage}"</p>
<div className="flex flex-wrap gap-4 mt-8 relative z-10">
<div className="px-4 py-2 bg-red-500/5 rounded-xl border border-red-500/20 flex gap-3 items-center backdrop-blur-sm">
<Heart className="text-red-500" size={16}/>
<p className="text-[10px] text-red-400 font-bold uppercase tracking-widest">Atmosfer: {strategy.projectDNA?.tone || currentTone}</p>
</div>
{strategy.inspiredByGap && (
<div className="px-4 py-2 bg-orange-500/5 rounded-xl border border-orange-500/20 flex gap-3 items-center backdrop-blur-sm">
<Sparkles className="text-orange-500" size={16}/>
<p className="text-[10px] text-orange-400 font-bold uppercase tracking-widest">İçgörü: {strategy.inspiredByGap}</p>
</div>
)}
{strategy.projectDNA?.audiencePersona && (
<div className="px-4 py-2 bg-blue-500/5 rounded-xl border border-blue-500/20 flex gap-3 items-center backdrop-blur-sm">
<Users className="text-blue-500" size={16}/>
<p className="text-[10px] text-blue-400 font-bold uppercase tracking-widest">Hedef Kitle: {strategy.projectDNA.audiencePersona}</p>
</div>
)}
</div>
</div>
{strategy.interviewQuestions && strategy.interviewQuestions.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><MessageSquare size={24} className="text-red-500" /> Kalbe Dokunan 20 Soru</h2>
<div className="grid gap-4">
{strategy.interviewQuestions.map((q, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-5 rounded-2xl border border-[var(--color-border-faint)] flex gap-4 items-start hover:border-red-500/30 hover:bg-[var(--color-bg-hover)] transition-all group">
<span className="w-8 h-8 rounded-xl bg-red-500/10 text-red-500 flex items-center justify-center text-[11px] font-bold shrink-0 group-hover:scale-110 transition-transform">{i+1}</span>
<p className="text-[var(--color-text-primary)] text-sm leading-relaxed pt-1.5">{q}</p>
</div>
))}
</div>
</div>
)}
{strategy.trendAnalysis && strategy.trendAnalysis.length > 0 && strategy.trendAnalysis[0].sentimentScore !== undefined && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><TrendingUp size={24} className="text-blue-500" /> Proje Trend Analizi</h2>
<div className="h-[300px] w-full mt-4">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={strategy.trendAnalysis} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorSentiment" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorArousal" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.8}/>
<stop offset="95%" stopColor="#ef4444" stopOpacity={0}/>
</linearGradient>
</defs>
<Tooltip
contentStyle={{backgroundColor: 'var(--color-bg-elevated)', borderColor: 'var(--color-border-default)', borderRadius: '12px', color: 'var(--color-text-primary)'}}
labelStyle={{color: 'var(--color-text-secondary)', fontWeight: 'bold', marginBottom: '8px'}}
/>
<Area type="monotone" name="Duygu (Pozitiflik)" dataKey="sentimentScore" stroke="#3b82f6" fillOpacity={1} fill="url(#colorSentiment)" />
<Area type="monotone" name="Heyecan (Arousal)" dataKey="arousalScore" stroke="#ef4444" fillOpacity={1} fill="url(#colorArousal)" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
)}
{strategy.comboShorts && strategy.comboShorts.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><MonitorPlay size={24} className="text-orange-500" /> Master Combo Shorts (Timecode'lu)</h2>
<div className="grid gap-6">
{strategy.comboShorts.map((short, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] border border-[var(--color-border-faint)] p-6 rounded-2xl shadow-sm hover:shadow-md hover:border-orange-500/30 transition-all">
<h3 className="font-bold text-lg text-[var(--color-text-primary)] mb-3">{short.title}</h3>
<p className="text-[var(--color-text-secondary)] text-sm mb-4 leading-relaxed">{short.description}</p>
<div className="flex flex-wrap gap-2">
{(short.timecodes || []).map((tc, j) => (
<span key={j} className="text-[11px] font-mono bg-orange-500/10 text-orange-500 border border-orange-500/20 px-3 py-1.5 rounded-lg font-bold shadow-sm">
⏱ {tc}
</span>
))}
</div>
</div>
))}
</div>
</div>
)}
{/* YENİ EKLENEN VERİ NOKTALARI */}
{strategy.crisisManagement && strategy.crisisManagement.potentialBacklash && (
<div className="bg-red-500/5 border border-red-500/20 p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-red-500 mb-6 flex items-center gap-3">
<AlertTriangle size={24} /> Kriz & Linç Yönetimi
</h2>
<div className="grid gap-4 md:grid-cols-2">
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-red-500/10 shadow-sm">
<h3 className="font-bold text-sm text-[var(--color-text-secondary)] mb-2 uppercase tracking-wider">Potansiyel Tepki</h3>
<p className="text-[var(--color-text-primary)] leading-relaxed">{strategy.crisisManagement.potentialBacklash}</p>
</div>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-green-500/10 shadow-sm">
<h3 className="font-bold text-sm text-[var(--color-text-secondary)] mb-2 uppercase tracking-wider">PR & Savunma Stratejisi</h3>
<p className="text-[var(--color-text-primary)] leading-relaxed">{strategy.crisisManagement.prStrategy}</p>
</div>
</div>
</div>
)}
{strategy.bRollSuggestions && strategy.bRollSuggestions.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<Video size={24} className="text-purple-500" /> B-Roll (Ara Görüntü) Önerileri
</h2>
<div className="flex flex-wrap gap-3">
{strategy.bRollSuggestions.map((broll, i) => (
<div key={i} className="px-4 py-3 bg-[var(--color-bg-surface)] border border-purple-500/20 rounded-xl flex items-center gap-3 shadow-sm hover:border-purple-500/50 transition-colors">
<Camera size={16} className="text-purple-400" />
<span className="text-sm text-[var(--color-text-primary)]">{broll}</span>
</div>
))}
</div>
</div>
)}
{strategy.communityHooks && strategy.communityHooks.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-6 flex items-center gap-3">
<MessageCircle size={24} className="text-cyan-500" /> Topluluk Etkileşim Kancaları
</h2>
<p className="text-sm text-[var(--color-text-secondary)] mb-4">İzleyiciyi videonun sonunda yoruma teşvik edecek kışkırtıcı veya düşündürücü sorular:</p>
<div className="grid gap-3">
{strategy.communityHooks.map((hook, i) => (
<div key={i} className="bg-cyan-500/5 p-4 rounded-xl border border-cyan-500/20 flex items-start gap-3">
<span className="w-6 h-6 rounded-lg bg-cyan-500/20 text-cyan-600 flex items-center justify-center text-xs font-bold shrink-0">{i+1}</span>
<p className="text-[var(--color-text-primary)] text-sm leading-relaxed">{hook}</p>
</div>
))}
</div>
</div>
)}
{strategy.sponsorIntegration && (
<div className="bg-yellow-500/5 border border-yellow-500/20 p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-yellow-600 mb-4 flex items-center gap-3">
<Handshake size={24} /> Doğal Sponsor Geçişi (Native Integration)
</h2>
<p className="text-[var(--color-text-primary)] leading-relaxed p-4 bg-[var(--color-bg-surface)] rounded-xl border border-yellow-500/10 shadow-sm">
{strategy.sponsorIntegration}
</p>
</div>
)}
{/* YENİ EKLENEN VERİ NOKTALARI SONU */}
{strategy.segments && strategy.segments.length > 0 && (
<div className="space-y-6 pt-4">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] flex items-center gap-3"><Activity size={24} className="text-red-500" /> Senaryo Akışı (Süreli Segmentler)</h2>
{strategy.segments.map((s, i) => (
<div key={i} className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-lg group hover:border-[var(--color-border-hover)] transition-all relative overflow-hidden">
<div className="absolute left-0 top-0 bottom-0 w-1.5 bg-gradient-to-b from-red-500 to-orange-500"></div>
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-5">
<div className="flex items-center gap-3">
<span className="bg-red-500/10 text-red-500 border border-red-500/20 font-mono text-[11px] px-3 py-1 rounded-lg font-black">{s.duration}</span>
<h4 className="font-bold text-xl text-[var(--color-text-primary)] tracking-tight">{s.type}</h4>
</div>
<span className="text-[10px] bg-purple-500/10 text-purple-400 px-4 py-1.5 rounded-full border border-purple-500/20 font-bold uppercase tracking-widest">{s.neuroObjective}</span>
</div>
<p className="text-[var(--color-text-secondary)] text-sm mb-6 leading-relaxed">{s.description}</p>
<div className="flex flex-wrap gap-2">
{(s.keyPoints || []).map((kp, ki) => <span key={ki} className="text-[11px] bg-[var(--color-bg-surface)] px-3 py-1.5 rounded-lg border border-[var(--color-border-faint)] text-[var(--color-text-secondary)] font-medium shadow-sm">{kp}</span>)}
</div>
</div>
))}
</div>
)}
{strategy.selectedComments && strategy.selectedComments.length > 0 && (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 rounded-[2rem] shadow-xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><Users size={24} className="text-blue-500" /> İzleyici Yorumları & Kaynak Analizi</h2>
<div className="grid gap-6">
{(strategy.selectedComments || []).map((c, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] hover:border-blue-500/30 transition-all">
<div className="flex items-center gap-2 mb-4">
<FileText size={14} className="text-[var(--color-text-ghost)]" />
<span className="text-[10px] font-black text-[var(--color-text-ghost)] uppercase tracking-widest">{c.sourceFile}</span>
<span className="text-[10px] font-black text-blue-500 uppercase tracking-widest ml-auto px-2 py-1 bg-blue-500/10 rounded-md">{c.username}</span>
</div>
<p className="text-[var(--color-text-primary)] italic mb-5 text-sm leading-relaxed border-l-2 border-blue-500/30 pl-4">&quot;{c.text}&quot;</p>
<div className="p-4 bg-blue-500/5 rounded-xl border border-blue-500/10">
<span className="text-[10px] font-black text-blue-400 uppercase tracking-widest block mb-2 flex items-center gap-1.5"><Lightbulb size={12}/> Stratejik Değer:</span>
<p className="text-xs text-[var(--color-text-secondary)] leading-relaxed font-medium">{c.insightValue}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
<div className="space-y-6">
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-6 rounded-[2rem] shadow-xl sticky top-28">
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase tracking-widest mb-4 flex items-center gap-2"><ImageIcon size={14}/> Thumbnail Konsepti</h3>
<div className="aspect-video bg-[var(--color-bg-surface)] rounded-2xl flex items-center justify-center border border-[var(--color-border-faint)] overflow-hidden relative group shadow-inner">
{strategy.generatedThumbnail ? (
<img
src={strategy.generatedThumbnail}
className="w-full h-full object-cover animate-fade-in cursor-zoom-in"
onClick={() => setIsThumbnailExpanded(true)}
/>
) : (
<button onClick={handleGenThumbnail} disabled={isGenerating} className="text-xs bg-red-500/10 hover:bg-red-500/20 text-red-500 px-6 py-3 rounded-xl font-bold transition-all border border-red-500/20 uppercase tracking-widest flex items-center gap-2 shadow-sm active:scale-95 disabled:opacity-50">
{isGenerating ? <Loader2 className="animate-spin" size={16}/> : <Zap size={16}/>} ÖNİZLEME ÜRET
</button>
)}
{strategy.generatedThumbnail && (
<button onClick={handleGenThumbnail} className="absolute bottom-3 right-3 p-2.5 bg-black/60 hover:bg-black/80 backdrop-blur-md rounded-xl text-white opacity-0 group-hover:opacity-100 transition-all border border-white/10 active:scale-95 shadow-xl">
<RefreshCw size={14} />
</button>
)}
</div>
{strategy.thumbnailConcept && (
<div className="mt-5 p-4 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<p className="text-xs text-[var(--color-text-secondary)] font-medium leading-relaxed">{strategy.thumbnailConcept}</p>
</div>
)}
</div>
{/* Data Integrity Widget */}
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-6 rounded-[2rem] shadow-xl flex flex-col gap-4">
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase tracking-widest flex items-center gap-2"><Database size={14}/> Analiz Durumu</h3>
<div className="space-y-3">
<div className="flex justify-between items-center p-3 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<span className="text-[10px] text-[var(--color-text-secondary)] font-bold uppercase tracking-wider">Nöro-Lab</span>
{strategy.neuroReport ? <Check size={14} className="text-emerald-500"/> : <span className="w-2 h-2 rounded-full bg-[var(--color-border-hover)]"></span>}
</div>
<div className="flex justify-between items-center p-3 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<span className="text-[10px] text-[var(--color-text-secondary)] font-bold uppercase tracking-wider">SEO-Master</span>
{strategy.seoAnalysis ? <Check size={14} className="text-emerald-500"/> : <span className="w-2 h-2 rounded-full bg-[var(--color-border-hover)]"></span>}
</div>
<div className="flex justify-between items-center p-3 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<span className="text-[10px] text-[var(--color-text-secondary)] font-bold uppercase tracking-wider">Marketing-Hub</span>
{strategy.marketingInsights ? <Check size={14} className="text-emerald-500"/> : <span className="w-2 h-2 rounded-full bg-[var(--color-border-hover)]"></span>}
</div>
<div className="flex justify-between items-center p-3 bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-faint)]">
<span className="text-[10px] text-[var(--color-text-secondary)] font-bold uppercase tracking-wider">Ticari-Derin</span>
{strategy.commercialAnalysis?.deepAnalysis ? <Check size={14} className="text-emerald-500"/> : <span className="w-2 h-2 rounded-full bg-[var(--color-border-hover)]"></span>}
</div>
</div>
<div className="mt-2 pt-4 border-t border-[var(--color-border-default)]">
<p className="text-[9px] text-center text-[var(--color-text-ghost)] font-black uppercase tracking-widest flex items-center justify-center gap-1.5"><Layers size={10}/> Master Save her şeyi tek dosyada tutar.</p>
</div>
</div>
</div>
</motion.div>
)}
{/* TAB: NEURO LAB */}
{activeTab === 'neuro' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
{!strategy.neuroReport ? (
<div className="h-[400px] flex flex-col items-center justify-center bg-[var(--color-bg-elevated)]/50 border border-[var(--color-border-default)] rounded-[2rem] border-dashed shadow-inner">
<Brain size={64} className="text-[var(--color-text-ghost)] mb-6 animate-pulse" />
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 uppercase tracking-widest">Nöro-Pazarlama Motoru</h2>
<button onClick={handleNeuroAnalysis} disabled={isGenerating} className="bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white px-10 py-4 rounded-2xl font-black flex items-center gap-3 transition-all shadow-xl shadow-purple-500/20 active:scale-95 disabled:opacity-50">
{isGenerating ? <Loader2 className="animate-spin" size={20} /> : <Zap size={20} />} ANALİZİ BAŞLAT
</button>
</div>
) : (
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><Brain className="text-purple-500"/> Nöro-Psikolojik Sentez</h2>
<div className="space-y-6">
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] shadow-inner">
<h4 className="text-[11px] font-black text-purple-400 uppercase mb-3 tracking-widest flex items-center gap-2"><Target size={14}/> Göz Odağı</h4>
<p className="text-sm text-[var(--color-text-secondary)] italic leading-relaxed">"{strategy.neuroReport.eyeTrackingFocus}"</p>
</div>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] shadow-inner">
<h4 className="text-[11px] font-black text-purple-400 uppercase mb-4 tracking-widest flex items-center gap-2"><Activity size={14}/> Dopamin Tetikleyiciler</h4>
<div className="flex flex-wrap gap-2">
{(strategy.neuroReport.dopamineTriggers || []).map((t, i) => <span key={i} className="px-4 py-1.5 bg-purple-500/10 text-purple-400 rounded-xl text-[11px] font-bold border border-purple-500/20 shadow-sm">{t}</span>)}
</div>
</div>
</div>
</div>
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] h-[500px] shadow-2xl flex flex-col">
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase tracking-widest mb-6 text-center">Dikkat Süresi Projeksiyonu</h3>
<div className="flex-1 bg-[var(--color-bg-surface)]/50 rounded-2xl border border-[var(--color-border-faint)] p-4 shadow-inner">
<ResponsiveContainer width="100%" height="100%">
<RadarChart data={strategy.neuroReport.attentionSpans || []}>
<PolarGrid stroke="var(--color-border-default)" />
<PolarAngleAxis dataKey="phase" stroke="var(--color-text-secondary)" fontSize={11} tick={{fill: 'var(--color-text-secondary)', fontWeight: 600}} />
<Radar name="Skor" dataKey="score" stroke="#a855f7" strokeWidth={2} fill="#a855f7" fillOpacity={0.4} />
<Tooltip contentStyle={{backgroundColor: 'var(--color-bg-elevated)', borderColor: 'var(--color-border-default)', borderRadius: '12px', color: 'var(--color-text-primary)'}} itemStyle={{color: '#a855f7', fontWeight: 'bold'}} />
</RadarChart>
</ResponsiveContainer>
</div>
</div>
</div>
)}
</motion.div>
)}
{/* TAB: MARKETING */}
{activeTab === 'marketing' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
{!strategy.marketingInsights ? (
<div className="h-[400px] flex flex-col items-center justify-center bg-[var(--color-bg-elevated)]/50 border border-[var(--color-border-default)] rounded-[2rem] border-dashed shadow-inner">
<TrendingUp size={64} className="text-[var(--color-text-ghost)] mb-6" />
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 uppercase tracking-widest">Marketing Hub</h2>
<button onClick={handleMarketingAnalysis} disabled={isGenerating} className="bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 text-white px-10 py-4 rounded-2xl font-black flex items-center gap-3 transition-all shadow-xl shadow-blue-500/20 active:scale-95 disabled:opacity-50">
{isGenerating ? <Loader2 className="animate-spin" size={20} /> : <Zap size={20} />} PAZARLAMA PLANINI ÜRET
</button>
</div>
) : (
<div className="grid md:grid-cols-2 gap-6">
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><Users className="text-blue-500"/> Hedef Personalar</h2>
<div className="grid gap-4">
{(strategy.marketingInsights.targetPersonas || []).map((p, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-5 rounded-2xl border border-[var(--color-border-faint)] flex items-center gap-4 hover:border-blue-500/30 transition-all group">
<div className="w-10 h-10 shrink-0 rounded-xl bg-blue-500/10 flex items-center justify-center text-blue-500 font-bold text-sm shadow-inner group-hover:scale-110 transition-transform">{i+1}</div>
<p className="text-sm text-[var(--color-text-primary)] font-medium leading-relaxed">{p}</p>
</div>
))}
</div>
</div>
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl">
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 flex items-center gap-3"><Award className="text-orange-500"/> Viral Yayılma Kancaları</h2>
<div className="space-y-4">
{(strategy.marketingInsights.socialMediaHooks || []).map((h, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] hover:border-orange-500/30 transition-all relative overflow-hidden group">
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500/50 group-hover:bg-orange-500 transition-colors"></div>
<span className="text-[10px] font-black uppercase text-orange-500 block mb-3 tracking-widest">{h.platform} HOOK</span>
<p className="text-sm italic text-[var(--color-text-secondary)] leading-relaxed">"{h.text}"</p>
</div>
))}
</div>
</div>
</div>
)}
</motion.div>
)}
{/* TAB: SEO MASTER */}
{activeTab === 'seo' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
{!strategy.seoAnalysis ? (
<div className="h-[400px] flex flex-col items-center justify-center bg-[var(--color-bg-elevated)]/50 border border-[var(--color-border-default)] rounded-[2rem] border-dashed shadow-inner">
<Search size={64} className="text-[var(--color-text-ghost)] mb-6" />
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-8 uppercase tracking-widest">SEO Master Veri Tabanı</h2>
<button onClick={handleSeoAnalysis} disabled={isGenerating} className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white px-10 py-4 rounded-2xl font-black flex items-center gap-3 transition-all active:scale-95 shadow-xl shadow-emerald-500/20 disabled:opacity-50">
{isGenerating ? <Loader2 className="animate-spin" size={20} /> : <Zap size={20} />} SEO ANALİZİNİ BAŞLAT
</button>
</div>
) : (
<div className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] p-8 md:p-10 rounded-[2rem] shadow-2xl">
<div className="grid lg:grid-cols-12 gap-10">
<div className="lg:col-span-5 space-y-8">
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 flex items-center gap-2 tracking-[0.1em]"><Layout size={14}/> Optimize Ana Başlık</h3>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] text-lg md:text-xl font-bold text-[var(--color-text-primary)] shadow-inner leading-relaxed">{strategy.seoAnalysis.optimizedTitle}</div>
</div>
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 flex items-center gap-2 tracking-[0.1em]"><Target size={14}/> YouTube Etiketleri (Kopyalamaya Hazır)</h3>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] relative group shadow-inner">
<button
onClick={() => {
navigator.clipboard.writeText(strategy.seoAnalysis?.tags?.join(', ') || '');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}}
className="absolute top-4 right-4 p-2.5 bg-[var(--color-bg-elevated)] rounded-xl hover:bg-[var(--color-bg-hover)] text-[var(--color-text-secondary)] border border-[var(--color-border-default)] transition-all active:scale-90 shadow-sm"
title="Kopyala"
>
{copied ? <Check size={16} className="text-emerald-500"/> : <Copy size={16}/>}
</button>
<p className="text-xs text-[var(--color-text-secondary)] leading-relaxed font-mono pr-10 whitespace-pre-wrap">{strategy.seoAnalysis.tags?.join(', ')}</p>
</div>
<p className="text-[9px] text-[var(--color-text-ghost)] mt-3 font-bold uppercase tracking-widest pl-2">Sponsorlu anahtar kelimeler ve rakip açıkları dahil edilmiştir.</p>
</div>
<div className="p-6 bg-red-500/5 border border-red-500/20 rounded-2xl">
<h3 className="text-[11px] font-black text-red-500 uppercase mb-3 flex items-center gap-2"><AlertTriangle size={14}/> Rakip Boşluğu (Gap)</h3>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed font-medium">{strategy.seoAnalysis.competitorGap}</p>
</div>
</div>
<div className="lg:col-span-7 space-y-8">
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 tracking-[0.1em] flex items-center gap-2"><Activity size={14}/> A/B Başlık Alternatifleri & Neuro-Performans</h3>
<div className="grid gap-4">
{(strategy.seoAnalysis.alternativeTitles || []).map((alt, i) => (
<div key={i} className="bg-[var(--color-bg-surface)] p-5 rounded-2xl border border-[var(--color-border-faint)] flex items-center justify-between hover:border-red-500/30 transition-all group shadow-sm">
<div className="pr-4 flex-1">
<p className="font-bold text-[var(--color-text-primary)] mb-2 text-sm md:text-base group-hover:text-red-400 transition-colors leading-tight">{alt.title}</p>
<span className="text-[10px] text-[var(--color-text-secondary)] uppercase font-black tracking-widest bg-[var(--color-bg-elevated)] px-2 py-1 rounded-md border border-[var(--color-border-default)]">{alt.psychologicalAngle}</span>
</div>
<div className="text-center bg-[var(--color-bg-elevated)] p-3 rounded-xl border border-[var(--color-border-default)] min-w-[80px] shadow-inner shrink-0">
<span className={`text-2xl font-black ${alt.neuroScore > 85 ? 'text-emerald-500' : alt.neuroScore > 70 ? 'text-orange-500' : 'text-[var(--color-text-secondary)]'}`}>{alt.neuroScore}</span>
<p className="text-[9px] text-[var(--color-text-ghost)] font-black uppercase tracking-widest mt-1">Neuro</p>
</div>
</div>
))}
</div>
</div>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] shadow-inner">
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-3 tracking-[0.1em] flex items-center gap-2"><FileText size={14}/> Meta Açıklama Projeksiyonu</h3>
<p className="text-sm text-[var(--color-text-primary)] leading-relaxed italic border-l-2 border-red-500/30 pl-4">"{strategy.seoAnalysis.metaDescription}"</p>
</div>
</div>
</div>
</div>
)}
</motion.div>
)}
{/* TAB: COMMERCIAL */}
{activeTab === 'commercial' && (
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] rounded-[2rem] overflow-hidden shadow-2xl">
<div className="p-8 md:p-10 border-b border-[var(--color-border-default)] bg-[var(--color-bg-surface)]/50 flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div>
<h2 className="text-2xl font-[family-name:var(--font-display)] font-bold text-[var(--color-text-primary)] mb-2 flex items-center gap-3"><Briefcase className="text-emerald-500" /> Sponsorluk & Marka İş Birlikleri</h2>
<p className="text-[var(--color-text-secondary)] text-sm font-medium tracking-tight">İçeriğe doğal entegrasyon ve yerel marka önerileri.</p>
</div>
{!strategy.commercialAnalysis.deepAnalysis && (
<button onClick={handleDeepCommercial} disabled={isGenerating} className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-500 hover:to-teal-500 text-white px-8 py-3.5 rounded-xl font-black text-xs uppercase tracking-widest flex items-center gap-2 shadow-xl shadow-emerald-500/20 active:scale-95 transition-all disabled:opacity-50 shrink-0">
{isGenerating ? <Loader2 className="animate-spin" size={16}/> : <Plus size={16}/>} DERİN ANALİZ ÜRET
</button>
)}
</div>
<div className="grid lg:grid-cols-12 gap-0">
<div className="lg:col-span-4 border-b lg:border-b-0 lg:border-r border-[var(--color-border-default)] p-8 md:p-10 space-y-8 bg-[var(--color-bg-surface)]/30">
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 tracking-widest flex items-center gap-2"><Target size={14}/> Önerilen Türk Markaları</h3>
<div className="flex flex-wrap gap-2">
{(strategy.commercialAnalysis.suggestedBrands || []).map((brand, i) => (
<span key={i} className="px-4 py-2 bg-emerald-500/10 text-emerald-500 rounded-xl text-[11px] font-black border border-emerald-500/20 shadow-sm">{brand}</span>
))}
</div>
</div>
<div className="bg-[var(--color-bg-surface)] p-6 rounded-2xl border border-[var(--color-border-faint)] shadow-inner">
<h3 className="text-[11px] font-black text-blue-500 uppercase mb-2 tracking-widest flex items-center gap-2"><Check size={14}/> Marka Güven Skoru</h3>
<p className="text-5xl font-[family-name:var(--font-display)] font-black text-[var(--color-text-primary)]">%{strategy.commercialAnalysis.brandSafetyScore}</p>
<div className="w-full bg-[var(--color-bg-elevated)] h-2 rounded-full mt-4 overflow-hidden border border-[var(--color-border-default)]">
<div className="bg-gradient-to-r from-blue-600 to-cyan-500 h-full transition-all duration-1000" style={{width: `${strategy.commercialAnalysis.brandSafetyScore}%`}}></div>
</div>
</div>
<div>
<h3 className="text-[11px] font-black text-[var(--color-text-secondary)] uppercase mb-4 tracking-widest flex items-center gap-2"><Layers size={14}/> Uygun Sektörler</h3>
<div className="grid gap-3">
{(strategy.commercialAnalysis.suitableIndustries || []).map((ind, i) => (
<div key={i} className="flex items-center gap-3 text-sm text-[var(--color-text-primary)] font-medium bg-[var(--color-bg-surface)] px-4 py-3 rounded-xl border border-[var(--color-border-faint)]">
<ChevronRight size={14} className="text-emerald-500"/> {ind}
</div>
))}
</div>
</div>
</div>
<div className="lg:col-span-8 p-8 md:p-10 min-h-[400px]">
{strategy.commercialAnalysis.deepAnalysis ? (
<div className="space-y-6 animate-fade-in">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h3 className="text-xl font-bold text-[var(--color-text-primary)] flex items-center gap-3"><Mail className="text-emerald-500" /> Profesyonel Sponsorluk Maili</h3>
<button onClick={() => {
navigator.clipboard.writeText(strategy.commercialAnalysis.deepAnalysis?.emailDraft || '');
alert("Taslak kopyalandı!");
}} className="px-4 py-2 bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl text-xs font-black text-blue-500 hover:text-blue-400 hover:border-blue-500/50 flex items-center gap-2 uppercase tracking-widest transition-all active:scale-95 shadow-sm">
<Copy size={14}/> Taslağı Kopyala
</button>
</div>
<div className="bg-[var(--color-bg-surface)] p-8 rounded-2xl border border-[var(--color-border-default)] font-serif text-[var(--color-text-secondary)] whitespace-pre-wrap leading-relaxed shadow-inner max-h-[500px] overflow-y-auto custom-scrollbar border-t-4 border-t-emerald-500 text-sm md:text-base relative">
<div className="absolute top-4 right-4 opacity-5"><Mail size={64}/></div>
<div className="relative z-10">{strategy.commercialAnalysis.deepAnalysis.emailDraft}</div>
</div>
<div className="p-6 bg-[var(--color-bg-surface)] rounded-2xl border border-[var(--color-border-faint)] flex flex-col sm:flex-row justify-between items-center gap-4 text-[11px] font-black uppercase tracking-widest">
<span className="text-[var(--color-text-secondary)] flex items-center gap-2"><TrendingUp size={14}/> Tahmini Bütçe/Gelir Projeksiyonu:</span>
<span className="text-emerald-500 text-xl md:text-2xl drop-shadow-sm px-4 py-2 bg-emerald-500/10 rounded-xl border border-emerald-500/20">{strategy.commercialAnalysis.deepAnalysis.estimatedRevenue}</span>
</div>
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-[var(--color-text-ghost)] italic animate-pulse gap-4 text-center">
<Briefcase size={48} className="opacity-20"/>
<p>Derin analiz butonuyla marka stratejisini ve mail taslaklarını detaylandırabilirsiniz.</p>
</div>
)}
</div>
</div>
</motion.div>
)}
</div>
<AnimatePresence>
{showLive && <LiveBrainstorm context={JSON.stringify(strategy)} onClose={() => setShowLive(false)} />}
</AnimatePresence>
</div>
);
};
export default StrategyView;
@@ -0,0 +1,59 @@
'use client';
import { useEffect } from 'react';
import { AlertTriangle } from 'lucide-react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error('TUBE STRATEGIST ERROR.TSX CAUGHT:', error);
}, [error]);
return (
<div className="flex h-screen flex-col items-center justify-center p-8 bg-[var(--color-bg-base)]">
<div className="max-w-2xl w-full p-8 bg-red-500/10 border border-red-500 rounded-2xl">
<div className="flex items-center gap-4 mb-6">
<AlertTriangle className="text-red-500 w-12 h-12" />
<h2 className="text-3xl font-bold text-red-500">Kritik Uygulama Hatası</h2>
</div>
<p className="text-[var(--color-text-secondary)] mb-6 text-lg">
Beklenmeyen bir hata oluştu. Lütfen aşağıdaki hata detayını kopyalayarak AI asistana iletin:
</p>
<div className="bg-black/50 p-6 rounded-xl overflow-x-auto mb-8 border border-red-500/30">
<p className="text-red-400 font-bold mb-2">Hata Mesajı:</p>
<pre className="text-red-300 font-mono text-sm whitespace-pre-wrap">
{error.message}
</pre>
{error.stack && (
<>
<p className="text-red-400 font-bold mt-4 mb-2">Stack Trace:</p>
<pre className="text-red-300/80 font-mono text-xs whitespace-pre-wrap max-h-60 overflow-y-auto">
{error.stack}
</pre>
</>
)}
{error.digest && (
<p className="text-gray-400 font-mono text-xs mt-4">Digest: {error.digest}</p>
)}
</div>
<button
onClick={() => reset()}
className="px-6 py-3 bg-red-500 text-white font-bold rounded-xl hover:bg-red-600 transition-colors"
>
Yeniden Dene
</button>
</div>
</div>
);
}
@@ -0,0 +1,345 @@
'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { getProjects, createProject, 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 { cn } from '@/lib/utils';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/tr';
import { TargetAudience, VideoDuration } from './types';
dayjs.extend(relativeTime);
dayjs.locale('tr');
export default function TubeStrategistDashboard() {
const router = useRouter();
const params = useParams();
const locale = params.locale as string;
const [projects, setProjects] = useState<ProjectResponse[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [viewMode, setViewMode] = useState<'projects' | 'legacy'>('projects');
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
// New Project Form State
const [name, setName] = useState('');
const [tone, setTone] = useState("Samimi, sıcak ve kalbe dokunan");
const [duration, setDuration] = useState<VideoDuration>('45-60min');
const [speakerName, setSpeakerName] = useState("");
const [topicFocus, setTopicFocus] = useState("");
const [targetAudience, setTargetAudience] = useState<TargetAudience>("Genel İzleyici (Basit, Anlaşılır)");
const [isCreating, setIsCreating] = useState(false);
const fetchProjects = async () => {
try {
setIsLoading(true);
const data = await getProjects();
setProjects(data);
} catch (err) {
console.error("Failed to fetch projects:", err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchProjects();
}, []);
const handleCreateProject = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
try {
setIsCreating(true);
const newProj = await createProject(name, tone, duration, speakerName, topicFocus, targetAudience);
setIsCreateModalOpen(false);
router.push(`/dashboard/tools/tube-strategist/${newProj.id}`);
} catch (error) {
console.error("Proje oluşturulamadı:", error);
alert("Proje oluşturulurken bir hata oluştu.");
} finally {
setIsCreating(false);
}
};
return (
<div className="max-w-6xl mx-auto space-y-8 pb-24 font-sans px-4 sm:px-0">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col md:flex-row items-center justify-between gap-6 pt-8 pb-6 border-b border-[var(--color-border-faint)]"
>
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-tr from-red-500/20 to-orange-500/20 border border-red-500/30 flex items-center justify-center shadow-[0_0_30px_rgba(239,68,68,0.15)]">
<MonitorPlay className="text-red-500 w-8 h-8" />
</div>
<div>
<h1 className="font-[family-name:var(--font-display)] text-3xl font-bold tracking-tight text-[var(--color-text-primary)]">
TubeStrategist <span className="text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-orange-500">AI</span>
</h1>
<p className="text-[var(--color-text-secondary)] text-sm font-medium">YouTube Kanal Zekası ve Viral İçerik Kurgulama Motoru</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className="bg-[var(--color-bg-elevated)] p-1 rounded-xl border border-[var(--color-border-faint)] flex shadow-sm">
<button
onClick={() => setViewMode('projects')}
className={cn(
"px-4 py-2 rounded-lg text-sm font-bold flex items-center gap-2 transition-all",
viewMode === 'projects'
? "bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] shadow-sm"
: "text-[var(--color-text-ghost)] hover:text-[var(--color-text-secondary)]"
)}
>
<FolderKanban size={16} /> Projeler
</button>
<button
onClick={() => setViewMode('legacy')}
className={cn(
"px-4 py-2 rounded-lg text-sm font-bold flex items-center gap-2 transition-all",
viewMode === 'legacy'
? "bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] shadow-sm"
: "text-[var(--color-text-ghost)] hover:text-[var(--color-text-secondary)]"
)}
>
<History size={16} /> Legacy TXT Modu
</button>
</div>
</div>
</motion.div>
{viewMode === 'legacy' ? (
<LegacyUploader />
) : (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-[var(--color-text-primary)] flex items-center gap-2">
<FolderKanban className="text-red-500" size={20} /> Strateji Projeleri
</h2>
<button
onClick={() => setIsCreateModalOpen(true)}
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-red-600 to-orange-500 text-white text-sm font-bold flex items-center gap-2 hover:shadow-[0_0_20px_rgba(239,68,68,0.3)] hover:scale-[1.02] transition-all"
>
<Plus size={18} /> Yeni Proje
</button>
</div>
{isLoading ? (
<div className="flex flex-col items-center justify-center py-20 text-[var(--color-text-ghost)]">
<Loader2 className="animate-spin w-8 h-8 text-red-500 mb-4" />
<p className="font-medium text-sm">Projeleriniz Yükleniyor...</p>
</div>
) : projects.length === 0 ? (
<div className="card p-12 flex flex-col items-center justify-center text-center border border-dashed border-[var(--color-border-default)]">
<div className="w-20 h-20 bg-[var(--color-bg-elevated)] rounded-full flex items-center justify-center mb-6">
<LayoutTemplate className="w-10 h-10 text-[var(--color-text-ghost)]" />
</div>
<h3 className="text-xl font-bold text-[var(--color-text-primary)] mb-2">Henüz Projeniz Yok</h3>
<p className="text-[var(--color-text-secondary)] text-sm max-w-md mb-8">
Hemen yeni bir Tube Strategist projesi oluşturun, rakiplerinizin veya kendi videolarınızın linklerini ekleyerek analizlere başlayın.
</p>
<button
onClick={() => setIsCreateModalOpen(true)}
className="px-6 py-3 rounded-xl bg-gradient-to-r from-red-600 to-orange-500 text-white font-bold flex items-center gap-2 shadow-sm"
>
<Plus size={18} /> Proje Oluştur
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{projects.map((proj) => (
<motion.div
key={proj.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
whileHover={{ y: -5 }}
onClick={() => router.push(`/${locale}/dashboard/tools/tube-strategist/${proj.id}`)}
className="card p-6 cursor-pointer hover:border-red-500/30 transition-all flex flex-col h-full relative overflow-hidden group"
>
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-red-500/5 to-orange-500/5 rounded-bl-full -z-10 group-hover:scale-110 transition-transform"></div>
<div className="flex items-start justify-between mb-4">
<h3 className="text-lg font-bold text-[var(--color-text-primary)] line-clamp-1 group-hover:text-red-500 transition-colors">{proj.name}</h3>
<div className={cn(
"px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider",
proj.status === 'COMPLETED' ? "bg-emerald-500/10 text-emerald-500 border border-emerald-500/20" :
proj.status === 'ANALYZING' ? "bg-amber-500/10 text-amber-500 border border-amber-500/20 flex items-center gap-1" :
"bg-[var(--color-bg-elevated)] text-[var(--color-text-secondary)] border border-[var(--color-border-faint)]"
)}>
{proj.status === 'ANALYZING' && <Loader2 className="w-3 h-3 animate-spin" />}
{proj.status === 'COMPLETED' ? 'Tamamlandı' : proj.status === 'ANALYZING' ? 'Analiz Ediliyor' : 'Bekliyor'}
</div>
</div>
<div className="space-y-3 mb-6 flex-1">
<div className="flex items-center gap-2 text-[12px] text-[var(--color-text-secondary)] font-medium">
<PlayCircle size={14} className="text-blue-500" />
<span>{proj._count?.videos ?? proj.videos?.length ?? 0} Yüklenmiş Video</span>
</div>
<div className="flex items-center gap-2 text-[12px] text-[var(--color-text-secondary)] font-medium">
<Target size={14} className="text-orange-500" />
<span className="line-clamp-1">{proj.formatDescription || proj.topicFocus || 'Format belirtilmedi'}</span>
</div>
</div>
<div className="pt-4 border-t border-[var(--color-border-faint)] flex items-center justify-between text-[11px] text-[var(--color-text-ghost)] font-medium">
<div className="flex items-center gap-1.5">
<Calendar size={12} />
{dayjs(proj.createdAt).fromNow()}
</div>
<div className="flex items-center gap-1 text-red-500 opacity-0 group-hover:opacity-100 transition-opacity">
<span>İncele</span>
<TrendingUp size={14} />
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</div>
)}
</div>
)}
{/* Create Project Modal */}
<AnimatePresence>
{isCreateModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center px-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsCreateModalOpen(false)}
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="card relative w-full max-w-2xl overflow-hidden shadow-2xl p-0 border-[var(--color-border-default)]"
>
{/* Modal Header */}
<div className="px-6 py-4 border-b border-[var(--color-border-faint)] flex items-center justify-between bg-[var(--color-bg-elevated)]">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-red-500/10 text-red-500 flex items-center justify-center">
<Sparkles size={16} />
</div>
<h3 className="text-lg font-bold text-[var(--color-text-primary)]">Yeni Strateji Projesi</h3>
</div>
<button
onClick={() => setIsCreateModalOpen(false)}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[var(--color-bg-surface)] text-[var(--color-text-ghost)] hover:text-[var(--color-text-primary)] transition-colors"
>
<X size={18} />
</button>
</div>
{/* Modal Body */}
<form onSubmit={handleCreateProject} className="p-6 overflow-y-auto max-h-[80vh]">
<div className="space-y-6">
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider">Proje Adı</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
placeholder="Örn: 2025 AI Trendleri Analizi"
required
/>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5"><User size={14}/> Sunucu Adı</label>
<input
type="text"
value={speakerName}
onChange={(e) => setSpeakerName(e.target.value)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
placeholder="Örn: Barış Özcan"
/>
</div>
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5"><Calendar size={14}/> Hedef Süre</label>
<select
value={duration}
onChange={(e) => setDuration(e.target.value as any)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
>
<option value="30-45min">30 - 45 Dakika</option>
<option value="45-60min">45 - 60 Dakika</option>
<option value="1-2hours">1 - 2 Saat</option>
</select>
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5"><Target size={14}/> Format / Ana Konsept</label>
<textarea
value={topicFocus}
onChange={(e) => setTopicFocus(e.target.value)}
rows={3}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50 resize-none"
placeholder="Bu projenin genel formatını ve konseptini tanımlayın. Örn: Masa başı sohbet formatında, konukla birlikte güncel psikoloji araştırmalarını halkın anlayabileceği dilde tartışıyoruz..."
/>
</div>
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider flex items-center gap-1.5"><Users size={14}/> Hedef Kitle</label>
<select
value={targetAudience}
onChange={(e) => setTargetAudience(e.target.value as any)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
>
<option value="Genel İzleyici (Basit, Anlaşılır)">Genel İzleyici</option>
<option value="Gen Z (Hızlı, Argo, Samimi)">Gen Z</option>
<option value="Millennials (Nostaljik, Bilgi Odaklı)">Millennials</option>
<option value="Teknoloji Meraklıları (Jargonlu, Detaycı)">Teknoloji Meraklıları</option>
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-[var(--color-text-secondary)] font-semibold text-[11px] uppercase tracking-wider">İçerik Tonu</label>
<input
type="text"
value={tone}
onChange={(e) => setTone(e.target.value)}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-3 text-sm text-[var(--color-text-primary)] focus:outline-none focus:ring-2 focus:ring-red-500/50"
/>
</div>
</div>
<div className="mt-8 pt-6 border-t border-[var(--color-border-faint)] flex items-center justify-end gap-3">
<button
type="button"
onClick={() => setIsCreateModalOpen(false)}
className="px-5 py-2.5 rounded-xl font-bold text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors"
>
İptal
</button>
<button
type="submit"
disabled={isCreating || !name.trim()}
className="px-6 py-2.5 rounded-xl bg-gradient-to-r from-red-600 to-orange-500 text-white font-bold text-sm flex items-center gap-2 hover:shadow-[0_0_20px_rgba(239,68,68,0.3)] disabled:opacity-50 transition-all"
>
{isCreating ? <Loader2 size={16} className="animate-spin" /> : <Plus size={16} />}
{isCreating ? 'Oluşturuluyor...' : 'Projeyi Başlat'}
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}
@@ -0,0 +1,209 @@
import { apiClient } from '@/lib/api/api-service';
export interface VideoDetail {
id: string;
youtubeUrl: string;
videoId: string;
title: string;
thumbnail: string;
transcript?: string;
transcriptDuration?: number;
totalComments: number;
mainComments: number;
replyComments: number;
viewCount: string;
likeCount: string;
commentsJson?: any;
tier1Analysis?: any;
createdAt: string;
updatedAt: string;
}
export interface EpisodeResponse {
id: string;
projectId: string;
topic: string;
targetAudience: string;
duration: string;
format: string;
status: string;
masterAnalysis: any;
createdAt: string;
updatedAt: string;
}
export interface TopicSuggestion {
title: string;
description: string;
reasoning: string;
}
export interface ProjectResponse {
id: string;
name: string;
status: string;
tone: string;
targetDuration: string;
speakerName: string;
topicFocus: string;
targetAudience: string;
formatDescription?: string;
videos: VideoDetail[];
episodes?: EpisodeResponse[];
_count?: { videos: number };
masterAnalysis: any;
createdAt: string;
updatedAt: string;
}
export const getProjects = async (): Promise<ProjectResponse[]> => {
return apiClient.get<ProjectResponse[]>('/youtube-tools/strategist/projects').then(r => r.data as unknown as ProjectResponse[]);
};
export const createProject = async (
name: string,
tone: string,
duration: string,
speakerName: string,
formatDescription: string,
targetAudience: string
): Promise<ProjectResponse> => {
return apiClient.post<ProjectResponse>('/youtube-tools/strategist/projects', {
name, tone, targetDuration: duration, speakerName, formatDescription, targetAudience
}).then(r => r.data as unknown as ProjectResponse);
};
export const updateProject = async (
projectId: string,
data: {
name?: string;
tone?: string;
targetDuration?: string;
speakerName?: string;
targetAudience?: string;
formatDescription?: string;
}
): Promise<ProjectResponse> => {
return apiClient.put<ProjectResponse>(`/youtube-tools/strategist/projects/${projectId}`, data).then(r => r.data as unknown as ProjectResponse);
};
export const getProjectById = async (projectId: string): Promise<ProjectResponse> => {
return apiClient.get<ProjectResponse>(`/youtube-tools/strategist/projects/${projectId}`).then(r => r.data as unknown as ProjectResponse);
};
export const addVideoToProject = async (projectId: string, url: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/video`, { youtubeUrl: url }).then(r => r.data);
};
export const addDocumentToProject = async (
projectId: string,
title: string,
content: string,
type: 'transcript' | 'comments'
): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/document`, { title, content, type }).then(r => r.data);
};
export const getTopicSuggestions = async (projectId: string): Promise<{ suggestions: TopicSuggestion[] }> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/topic-suggestions`).then(r => r.data);
};
export const createEpisode = async (
projectId: string,
topic: string,
format: string,
targetAudience: string,
duration: string
): Promise<EpisodeResponse> => {
return apiClient.post<EpisodeResponse>(`/youtube-tools/strategist/projects/${projectId}/episode`, {
topic, format, targetAudience, duration
}).then(r => r.data as unknown as EpisodeResponse);
};
export const getEpisodesByProject = async (projectId: string): Promise<EpisodeResponse[]> => {
return apiClient.get<EpisodeResponse[]>(`/youtube-tools/strategist/projects/${projectId}/episodes`).then(r => r.data as unknown as EpisodeResponse[]);
};
export const getEpisodeById = async (episodeId: string): Promise<EpisodeResponse> => {
return apiClient.get<EpisodeResponse>(`/youtube-tools/strategist/episodes/${episodeId}`).then(r => r.data as unknown as EpisodeResponse);
};
export const analyzeEpisode = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/analyze`).then(r => r.data);
};
export const generateMoreQuestions = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-more-questions`).then(r => r.data);
};
export const generateEpisodeQuestions = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-questions`).then(r => r.data);
};
export const generateEpisodeSeoMarketing = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-seo`).then(r => r.data);
};
export const generateEpisodeCrisisSponsors = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-crisis`).then(r => r.data);
};
export const generateThumbnail = async (episodeId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/episodes/${episodeId}/generate-thumbnail`).then(r => r.data);
};
export const generateNeuroReport = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/neuro`).then(r => r.data);
};
export const generateMarketingReport = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/marketing`).then(r => r.data);
};
export const generateSeoReport = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/seo`).then(r => r.data);
};
export const generateDeepCommercialAnalysis = async (projectId: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/commercial`).then(r => r.data);
};
export const generateThumbnailImage = async (projectId: string, prompt: string): Promise<any> => {
return apiClient.post<any>(`/youtube-tools/strategist/projects/${projectId}/thumbnail`, { prompt }).then(r => r.data);
};
// --- Audio Utilities for LiveBrainstorm ---
export const audioContexts: { [key: string]: AudioContext } = {};
export const decode = async (base64Audio: string): Promise<ArrayBuffer> => {
const binaryString = window.atob(base64Audio);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
};
export const decodeAudioData = async (buffer: ArrayBuffer, context: AudioContext): Promise<AudioBuffer> => {
return new Promise((resolve, reject) => {
context.decodeAudioData(buffer, resolve, reject);
});
};
export const float32ToPcm16 = (float32Array: Float32Array): Int16Array => {
const pcm16 = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
const s = Math.max(-1, Math.min(1, float32Array[i]));
pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
return pcm16;
};
export const encode = (pcm16Array: Int16Array): string => {
const bytes = new Uint8Array(pcm16Array.buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
};
@@ -0,0 +1,201 @@
export enum SegmentType {
INTRO = 'INTRO',
SKETCH = 'SKETCH',
NARRATION = 'NARRATION',
B_ROLL = 'B_ROLL',
Q_AND_A = 'Q_AND_A',
OUTRO = 'OUTRO',
DEEP_DIVE = 'DEEP_DIVE',
EMOTIONAL_PEAK = 'EMOTIONAL_PEAK',
PSYCH_TRICK = 'PSYCH_TRICK',
BRAND_INTEGRATION = 'BRAND_INTEGRATION',
NEURO_HOOK = 'NEURO_HOOK',
RE_ENGAGEMENT = 'RE_ENGAGEMENT'
}
export interface VideoSegment {
type: SegmentType;
duration: string;
description: string;
keyPoints: string[];
visualCues?: string;
neuroObjective?: string;
inspirationSource?: string;
}
export interface ChartDataPoint {
topic: string;
emotionalArousal: number;
}
export interface SelectedComment {
username: string;
text: string;
reason: string;
insightValue: string;
sourceFile: string;
}
export interface AlternativeTitle {
title: string;
thumbnailOverlay: string;
neuroScore: number;
psychologicalAngle: string;
}
export interface NeuroReport {
eyeTrackingFocus: string;
colorPsychology: string;
dopamineTriggers: string[];
limbicSystemGoal: string;
attentionSpans: { phase: string; score: number }[];
}
export interface MarketingInsights {
targetPersonas: string[];
socialMediaHooks: { platform: string; text: string }[];
emailSubjectLines: string[];
viralHooks: string[];
}
export interface SeoAnalysis {
mainKeywords: string[];
secondaryKeywords: string[];
competitorGap: string;
optimizedTitle: string;
metaDescription: string;
tags: string[];
alternativeTitles: AlternativeTitle[];
}
export interface DeepCommercialAnalysis {
targetBrands: string[];
emailDraft: string;
estimatedRevenue: string;
affiliateIdeas: string[];
negotiationTip: string;
}
export interface CommercialAnalysis {
suitableIndustries: string[];
brandSafetyScore: number;
integrationIdeas: string[];
monetizationPotential: 'High' | 'Medium' | 'Low';
suggestedBrands: string[];
deepAnalysis?: DeepCommercialAnalysis;
}
export interface TrendAnalysisPoint {
videoIndex: number;
videoTitle: string;
sentimentScore: number;
arousalScore: number;
}
export interface ComboShort {
title: string;
description: string;
timecodes: string[];
}
export interface ProjectDNA {
tone: string;
audiencePersona: string;
coreMessage: string;
}
export interface CrisisManagement {
potentialBacklash: string;
prStrategy: string;
}
export interface StrategyResult {
title: string;
thumbnailConcept: string;
generatedThumbnail?: string; // AI tarafından üretilen base64 görsel verisi
hook: string;
segments: VideoSegment[];
chartData: ChartDataPoint[];
selectedComments: SelectedComment[];
interviewQuestions: string[];
wowFactor: string;
psychologicalTheme: string;
commercialAnalysis: CommercialAnalysis;
inspiredByGap?: string;
provenanceNotes?: string;
neuroReport?: NeuroReport;
marketingInsights?: MarketingInsights;
seoAnalysis?: SeoAnalysis;
projectDNA?: ProjectDNA;
trendAnalysis?: TrendAnalysisPoint[];
comboShorts?: ComboShort[];
crisisManagement?: CrisisManagement;
bRollSuggestions?: string[];
communityHooks?: string[];
sponsorIntegration?: string;
// 5 New Pre-Production Fields
gapAnalysis?: string;
segmentArchetypes?: string;
frictionPoints?: string[];
visualDna?: { timestamp: string; suggestion: string }[];
guestBriefing?: string;
}
export interface TubeStrategistVideo {
id: string;
youtubeUrl: string;
videoId: string;
title: string;
thumbnail?: string;
viewCount?: string;
likeCount?: string;
totalComments: number;
createdAt: string;
}
export interface TubeStrategistEpisode {
id: string;
projectId: string;
topic: string;
targetAudience?: string;
duration?: string;
format?: string;
status: 'PENDING' | 'ANALYZING' | 'COMPLETED' | 'FAILED';
masterAnalysis?: StrategyResult;
createdAt: string;
updatedAt: string;
}
export interface TubeStrategistProject {
id: string;
userId: string;
name: string;
tone?: string;
duration?: string;
speakerName?: string;
targetAudience?: string;
topicFocus?: string;
status: 'PENDING' | 'ANALYZING' | 'COMPLETED' | 'FAILED';
masterAnalysis?: StrategyResult; // Legacy support or project-level DNA
createdAt: string;
updatedAt: string;
videos: TubeStrategistVideo[];
episodes: TubeStrategistEpisode[];
}
export interface UploadedFile {
name: string;
content: string;
type: 'transcript' | 'comments';
}
export type VideoDuration = '30-45min' | '45-60min' | '1-2hours' | '2hours+';
export type TargetAudience =
| 'Gen Z (Hızlı, Argo, Samimi)'
| 'Millennials (Nostaljik, Bilgi Odaklı)'
| 'Gen X / Boomers (Ciddi, TV Tadında)'
| 'Teknoloji Meraklıları (Jargonlu, Detaycı)'
| 'Genel İzleyici (Basit, Anlaşılır)';
@@ -41,7 +41,20 @@ export default function YoutubeAnalyzerPage() {
setActiveTab("summary");
try {
const data = await toolsApi.getYoutubeAnalysisById(id);
setResult(data.analysisData);
let parsedData = data.analysisData;
while (typeof parsedData === 'string') {
try {
const nextParse = JSON.parse(parsedData);
if (typeof nextParse === 'string' && nextParse === parsedData) break;
parsedData = nextParse;
} catch(e) {
console.error("JSON parse error:", e);
break;
}
}
setResult(parsedData);
setUrl(data.videoUrl);
setIsHistoryOpen(false);
window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -178,6 +178,20 @@ export default function YoutubeSeoPage() {
setResult(null);
try {
const data = await toolsApi.getYoutubeSeoAnalysisById(id);
if (data && data.seoAnalysis) {
while (typeof data.seoAnalysis === 'string') {
try {
const nextParse = JSON.parse(data.seoAnalysis);
if (typeof nextParse === 'string' && nextParse === data.seoAnalysis) break;
data.seoAnalysis = nextParse;
} catch(e) {
console.error("JSON parse error:", e);
break;
}
}
}
setResult(data);
setUrl(data.videoUrl);
setIsHistoryOpen(false);
@@ -363,8 +377,8 @@ export default function YoutubeSeoPage() {
{result.videoDetails?.title}
</h2>
<div className="flex gap-4 text-sm text-white/60 mb-6">
<span>{Number(result.videoDetails?.viewCount || 0).toLocaleString()} İzl.</span>
<span>{Number(result.videoDetails?.likeCount || 0).toLocaleString()} Beğeni</span>
<span suppressHydrationWarning>{Number(result.videoDetails?.viewCount || 0).toLocaleString()} İzl.</span>
<span suppressHydrationWarning>{Number(result.videoDetails?.likeCount || 0).toLocaleString()} Beğeni</span>
</div>
<div className="border-t border-white/10 pt-6">
@@ -467,12 +481,21 @@ export default function YoutubeSeoPage() {
<h3 className="text-xl font-semibold flex items-center gap-2 text-white">
<HelpCircle className="w-5 h-5 text-purple-400" /> Sık Sorulan Sorular (SSS)
</h3>
<CopyButton text={result.seoAnalysis?.faqQuestions?.join("\n") || ""} />
<CopyButton text={result.seoAnalysis?.faqQuestions?.map((q: any) => `Q: ${q.question || q}\nA: ${q.answer || ''}`).join("\n\n") || ""} />
</div>
<ul className="space-y-3">
{result.seoAnalysis?.faqQuestions?.map((q: string, i: number) => (
<li key={i} className="flex gap-3 text-sm text-white/80 bg-black/30 p-3 rounded-xl border border-white/5">
<span className="text-purple-400 font-bold">Q:</span> {q}
{result.seoAnalysis?.faqQuestions?.map((q: any, i: number) => (
<li key={i} className="flex flex-col gap-1 text-sm text-white/80 bg-black/30 p-3 rounded-xl border border-white/5">
<div className="flex gap-2">
<span className="text-purple-400 font-bold">Q:</span>
<span className="font-semibold">{q.question || q}</span>
</div>
{q.answer && (
<div className="flex gap-2 pl-6 mt-1 text-white/60">
<span className="text-blue-400 font-bold">A:</span>
<span>{q.answer}</span>
</div>
)}
</li>
))}
</ul>
@@ -520,14 +543,20 @@ export default function YoutubeSeoPage() {
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-start gap-2 mb-1">
<h4 className="font-semibold text-white">{idea.title}</h4>
{idea.timestamp && (
<h4 className="font-semibold text-white">{idea.title || idea.topic}</h4>
{(idea.timestamp || idea.timecode) && (
<span className="inline-flex items-center px-2 py-1 bg-red-500/10 border border-red-500/20 text-red-400 text-xs rounded font-medium whitespace-nowrap">
{idea.timestamp}
{idea.timestamp || idea.timecode}
</span>
)}
</div>
<p className="text-sm text-white/60">{idea.context}</p>
<p className="text-sm text-white/60 mb-2">{idea.context || idea.description}</p>
{idea.hook && (
<div className="bg-black/30 rounded-xl p-3 mt-2">
<p className="text-xs text-white/40 mb-1">Önerilen Kanca (Hook):</p>
<p className="text-sm text-white/80 font-medium">{idea.hook}</p>
</div>
)}
</div>
</div>
))}
+1 -1
View File
File diff suppressed because one or more lines are too long