Add YouTube transcripts tab to Mission Control
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function MissionBriefsPage() {
|
||||
const [morningBriefs, setMorningBriefs] = useState<any[]>([]);
|
||||
const [eodBriefs, setEodBriefs] = useState<any[]>([]);
|
||||
const [amunBriefs, setAmunBriefs] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeView, setActiveView] = useState<"mission" | "morning" | "eod" | "amun">("mission");
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllBriefs();
|
||||
}, []);
|
||||
|
||||
const fetchAllBriefs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [mRes, eRes, aRes] = await Promise.all([
|
||||
fetch("/api/morning"),
|
||||
fetch("/api/eod"),
|
||||
fetch("/api/amun")
|
||||
]);
|
||||
const [mData, eData, aData] = await Promise.all([
|
||||
mRes.json(),
|
||||
eRes.json(),
|
||||
aRes.json()
|
||||
]);
|
||||
setMorningBriefs(Array.isArray(mData) ? mData : []);
|
||||
setEodBriefs(Array.isArray(eData) ? eData : []);
|
||||
setAmunBriefs(Array.isArray(aData) ? aData : []);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const formatMorning = (data: any) => `☀️ MORNING - ${data.date}
|
||||
|
||||
🌤️ ${data.weather || ''}
|
||||
|
||||
📊 BTC: $${data.market?.BTC} (${data.market?.btcChange})
|
||||
ETH: $${data.market?.ETH} (${data.market?.ethChange})
|
||||
SOL: $${data.market?.SOL} (${data.market?.solChange})
|
||||
|
||||
🎯 ${data.priorities?.map((p: string, i: number) => `${i + 1}. ${p}`).join('\n ') || ''}
|
||||
|
||||
💰 ${data.goal || ''}`;
|
||||
|
||||
const formatEOD = (data: any) => `🌙 EOD - ${data.date}
|
||||
|
||||
✅ ${data.completed?.map((c: string) => `- ${c}`).join('\n') || ''}
|
||||
|
||||
📊 ${Object.entries(data.progress || {}).map(([k, v]) => `${k}: ${v}`).join('\n') || ''}
|
||||
|
||||
🧠 ${Object.entries(data.council || {}).map(([k, v]) => `- ${k}: ${v}`).join('\n') || ''}`;
|
||||
|
||||
const formatAmun = (data: any) => `👑 AMUN - ${data.date}
|
||||
|
||||
🔍 ${data.tests?.map((t: string) => `- ${t}`).join('\n') || ''}
|
||||
|
||||
🐛 ${data.bugs?.map((b: string) => `- ${b}`).join('\n') || ''}
|
||||
|
||||
📊 ${data.quality || ''}
|
||||
|
||||
💡 ${data.recommendations?.map((r: string) => `- ${r}`).join('\n') || ''}`;
|
||||
|
||||
const getLatest = () => {
|
||||
const latestMorning = morningBriefs[0];
|
||||
const latestEOD = eodBriefs[0];
|
||||
const latestAmun = amunBriefs[0];
|
||||
|
||||
return { latestMorning, latestEOD, latestAmun };
|
||||
};
|
||||
|
||||
const { latestMorning, latestEOD, latestAmun } = getLatest();
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-6">📋 Mission Control Briefs</h1>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex gap-2 mb-6 flex-wrap">
|
||||
<button
|
||||
onClick={() => setActiveView("mission")}
|
||||
className={`px-4 py-2 rounded-lg font-medium ${
|
||||
activeView === "mission" ? "bg-brand-pink text-white" : "bg-white/10 text-white/70 hover:bg-white/20"
|
||||
}`}
|
||||
>
|
||||
🎯 Mission
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("morning")}
|
||||
className={`px-4 py-2 rounded-lg font-medium ${
|
||||
activeView === "morning" ? "bg-brand-pink text-white" : "bg-white/10 text-white/70 hover:bg-white/20"
|
||||
}`}
|
||||
>
|
||||
☀️ Morning ({morningBriefs.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("eod")}
|
||||
className={`px-4 py-2 rounded-lg font-medium ${
|
||||
activeView === "eod" ? "bg-brand-pink text-white" : "bg-white/10 text-white/70 hover:bg-white/20"
|
||||
}`}
|
||||
>
|
||||
🌙 EOD ({eodBriefs.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveView("amun")}
|
||||
className={`px-4 py-2 rounded-lg font-medium ${
|
||||
activeView === "amun" ? "bg-brand-pink text-white" : "bg-white/10 text-white/70 hover:bg-white/20"
|
||||
}`}
|
||||
>
|
||||
👑 Amun ({amunBriefs.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-white/50">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Mission View - Latest from all */}
|
||||
{activeView === "mission" && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">🎯 Latest Overview</h2>
|
||||
|
||||
{latestMorning && (
|
||||
<div className="bg-white/10 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium mb-2">☀️ Latest Morning</h3>
|
||||
<pre className="whitespace-pre-wrap text-sm">{formatMorning(latestMorning)}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{latestEOD && (
|
||||
<div className="bg-white/10 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium mb-2">🌙 Latest EOD</h3>
|
||||
<pre className="whitespace-pre-wrap text-sm">{formatEOD(latestEOD)}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{latestAmun && (
|
||||
<div className="bg-white/10 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium mb-2">👑 Latest Amun</h3>
|
||||
<pre className="whitespace-pre-wrap text-sm">{formatAmun(latestAmun)}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!latestMorning && !latestEOD && !latestAmun && (
|
||||
<div className="text-white/50">No briefs yet. POST to /api/morning, /api/eod, or /api/amun</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Morning Briefs */}
|
||||
{activeView === "morning" && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">☀️ Morning Briefs</h2>
|
||||
{morningBriefs.length === 0 ? (
|
||||
<div className="text-white/50">No morning briefs yet</div>
|
||||
) : (
|
||||
morningBriefs.map((brief: any, i: number) => (
|
||||
<div key={i} className="bg-white/10 rounded-lg p-4">
|
||||
<pre className="whitespace-pre-wrap text-sm">{formatMorning(brief)}</pre>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* EOD Briefs */}
|
||||
{activeView === "eod" && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">🌙 End of Day Briefs</h2>
|
||||
{eodBriefs.length === 0 ? (
|
||||
<div className="text-white/50">No EOD briefs yet</div>
|
||||
) : (
|
||||
eodBriefs.map((brief: any, i: number) => (
|
||||
<div key={i} className="bg-white/10 rounded-lg p-4">
|
||||
<pre className="whitespace-pre-wrap text-sm">{formatEOD(brief)}</pre>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Amun Sessions */}
|
||||
{activeView === "amun" && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">👑 Amun QA Sessions</h2>
|
||||
{amunBriefs.length === 0 ? (
|
||||
<div className="text-white/50">No Amun sessions yet</div>
|
||||
) : (
|
||||
amunBriefs.map((brief: any, i: number) => (
|
||||
<div key={i} className="bg-white/10 rounded-lg p-4">
|
||||
<pre className="whitespace-pre-wrap text-sm">{formatAmun(brief)}</pre>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user