229 lines
7.1 KiB
TypeScript
229 lines
7.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
|
|
const AVAILABLE_CATEGORIES = [
|
|
"SiteMente",
|
|
"OpenClaw",
|
|
"Trading",
|
|
"Marketing",
|
|
"Technical",
|
|
"Voice AI",
|
|
"Business",
|
|
"Inspiration",
|
|
"Personal Growth"
|
|
];
|
|
|
|
export default function TranscriptsPage() {
|
|
const [transcripts, setTranscripts] = useState<any[]>([]);
|
|
const [videoUrl, setVideoUrl] = useState("");
|
|
const [transcriptText, setTranscriptText] = useState("");
|
|
const [title, setTitle] = useState("");
|
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
|
const [filterCategory, setFilterCategory] = useState<string>("all");
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchTranscripts();
|
|
}, []);
|
|
|
|
const fetchTranscripts = async () => {
|
|
const res = await fetch("/api/transcripts");
|
|
const data = await res.json();
|
|
setTranscripts(data);
|
|
};
|
|
|
|
const toggleCategory = (cat: string) => {
|
|
setSelectedCategories(prev =>
|
|
prev.includes(cat)
|
|
? prev.filter(c => c !== cat)
|
|
: [...prev, cat]
|
|
);
|
|
};
|
|
|
|
const saveTranscript = async () => {
|
|
if (!videoUrl || !transcriptText) return;
|
|
setSaving(true);
|
|
|
|
const res = await fetch("/api/transcripts", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
videoUrl,
|
|
title: title || videoUrl,
|
|
transcript: transcriptText,
|
|
categories: selectedCategories
|
|
})
|
|
});
|
|
|
|
const data = await res.json();
|
|
setSaving(false);
|
|
|
|
if (data.transcripts) {
|
|
setTranscripts(data.transcripts);
|
|
setVideoUrl("");
|
|
setTranscriptText("");
|
|
setTitle("");
|
|
setSelectedCategories([]);
|
|
}
|
|
};
|
|
|
|
const deleteTranscript = async (videoId: string) => {
|
|
if (!confirm("Delete this transcript?")) return;
|
|
|
|
const res = await fetch("/api/transcripts", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ action: "delete", videoId })
|
|
});
|
|
|
|
const data = await res.json();
|
|
if (data.transcripts) {
|
|
setTranscripts(data.transcripts);
|
|
}
|
|
};
|
|
|
|
const filteredTranscripts = filterCategory === "all"
|
|
? transcripts
|
|
: transcripts.filter(t => t.categories?.includes(filterCategory));
|
|
|
|
const allCategories = [...new Set(transcripts.flatMap(t => t.categories || []))];
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<h1 className="text-2xl font-bold mb-6">🎬 YouTube Transcripts</h1>
|
|
|
|
{/* Add New Transcript */}
|
|
<div className="bg-white/10 rounded-lg p-4 mb-6">
|
|
<h2 className="text-lg font-semibold mb-3">Add Transcript</h2>
|
|
|
|
<input
|
|
type="text"
|
|
value={videoUrl}
|
|
onChange={(e) => setVideoUrl(e.target.value)}
|
|
placeholder="YouTube URL..."
|
|
className="w-full px-4 py-2 bg-black/30 border border-white/20 rounded-lg text-white placeholder-white/50 mb-3"
|
|
/>
|
|
|
|
<input
|
|
type="text"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="Title..."
|
|
className="w-full px-4 py-2 bg-black/30 border border-white/20 rounded-lg text-white placeholder-white/50 mb-3"
|
|
/>
|
|
|
|
<div className="mb-3">
|
|
<p className="text-white/70 mb-2">Categories:</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{AVAILABLE_CATEGORIES.map(cat => (
|
|
<button
|
|
key={cat}
|
|
onClick={() => toggleCategory(cat)}
|
|
className={`px-3 py-1 rounded-full text-sm ${
|
|
selectedCategories.includes(cat)
|
|
? "bg-brand-pink text-white"
|
|
: "bg-white/10 text-white/70 hover:bg-white/20"
|
|
}`}
|
|
>
|
|
{cat}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<textarea
|
|
value={transcriptText}
|
|
onChange={(e) => setTranscriptText(e.target.value)}
|
|
placeholder="Paste transcript here..."
|
|
rows={6}
|
|
className="w-full px-4 py-2 bg-black/30 border border-white/20 rounded-lg text-white placeholder-white/50 mb-3"
|
|
/>
|
|
|
|
<button
|
|
onClick={saveTranscript}
|
|
disabled={saving || !videoUrl || !transcriptText}
|
|
className="px-6 py-2 bg-brand-pink rounded-lg font-medium hover:bg-[#ff7bc0] disabled:opacity-50"
|
|
>
|
|
{saving ? "Saving..." : "Save Transcript"}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filter */}
|
|
<div className="flex gap-2 mb-4 flex-wrap">
|
|
<button
|
|
onClick={() => setFilterCategory("all")}
|
|
className={`px-3 py-1 rounded-full text-sm ${
|
|
filterCategory === "all" ? "bg-brand-pink" : "bg-white/10"
|
|
}`}
|
|
>
|
|
All ({transcripts.length})
|
|
</button>
|
|
{allCategories.map(cat => (
|
|
<button
|
|
key={cat}
|
|
onClick={() => setFilterCategory(cat)}
|
|
className={`px-3 py-1 rounded-full text-sm ${
|
|
filterCategory === cat ? "bg-brand-pink" : "bg-white/10"
|
|
}`}
|
|
>
|
|
{cat} ({transcripts.filter(t => t.categories?.includes(cat)).length})
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Saved Transcripts */}
|
|
<div className="space-y-4">
|
|
{filteredTranscripts.map((t: any, i: number) => (
|
|
<div key={i} className="bg-white/10 rounded-lg p-4">
|
|
<div className="flex justify-between items-start mb-2">
|
|
<div>
|
|
<h3 className="font-semibold">{t.title}</h3>
|
|
<a
|
|
href={t.videoUrl}
|
|
target="_blank"
|
|
className="text-brand-pink text-sm hover:underline"
|
|
>
|
|
YouTube ↗
|
|
</a>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<span className="text-white/40 text-xs">
|
|
{new Date(t.savedAt).toLocaleDateString()}
|
|
</span>
|
|
<button
|
|
onClick={() => deleteTranscript(t.videoId)}
|
|
className="text-red-400 hover:text-red-300"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-1 mb-2">
|
|
{t.categories?.map((c: string) => (
|
|
<span key={c} className="px-2 py-0.5 bg-brand-pink/30 rounded text-xs">
|
|
{c}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
<details>
|
|
<summary className="text-white/50 cursor-pointer text-sm">
|
|
View Transcript ({t.transcript?.length || 0} chars)
|
|
</summary>
|
|
<pre className="mt-2 text-xs text-white/70 whitespace-pre-wrap max-h-60 overflow-y-auto bg-black/20 p-2 rounded">
|
|
{t.transcript}
|
|
</pre>
|
|
</details>
|
|
</div>
|
|
))}
|
|
|
|
{filteredTranscripts.length === 0 && (
|
|
<div className="text-white/50">No transcripts saved yet</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|