Add YouTube transcripts tab to Mission Control

This commit is contained in:
Horus
2026-02-27 15:00:38 +01:00
parent 0a4853bcda
commit d7cd81b293
18 changed files with 3517 additions and 12 deletions
+233
View File
@@ -0,0 +1,233 @@
"use client";
import { useState, useEffect } from "react";
import fs from "fs";
const AVAILABLE_CATEGORIES = [
"SiteMente",
"OpenClaw",
"Trading",
"Marketing",
"Technical",
"Voice AI",
"Business",
"Inspiration",
"Personal Growth"
];
const STORAGE_FILE = "/root/.openclaw/workspace/SiteMente/data/transcripts.json";
function getTranscripts() {
try {
if (fs.existsSync(STORAGE_FILE)) {
return JSON.parse(fs.readFileSync(STORAGE_FILE, "utf-8"));
}
} catch (e) {}
return [];
}
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(() => {
setTranscripts(getTranscripts());
}, []);
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);
// Extract video ID
const match = videoUrl.match(/(?:v=|\/)([0-9A-Za-z_-]{11})/);
const videoId = match ? match[1] : "";
const data = {
videoId,
videoUrl,
title: title || `Video ${videoId}`,
transcript: transcriptText,
categories: selectedCategories,
savedAt: new Date().toISOString()
};
// Save directly
const current = getTranscripts();
const existing = current.findIndex(t => t.videoId === videoId);
if (existing >= 0) {
current[existing] = data;
} else {
current.unshift(data);
}
fs.writeFileSync(STORAGE_FILE, JSON.stringify(current, null, 2));
setTranscripts(current);
setSaving(false);
setVideoUrl("");
setTranscriptText("");
setTitle("");
setSelectedCategories([]);
};
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={() => {
if (confirm("Delete this transcript?")) {
const updated = transcripts.filter(x => x.videoId !== t.videoId);
fs.writeFileSync(STORAGE_FILE, JSON.stringify(updated, null, 2));
setTranscripts(updated);
}
}}
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>
);
}