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
+444
View File
@@ -0,0 +1,444 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import Card from "@/components/ui/Card";
const today = () => new Date().toISOString().split("T")[0];
interface MarketUpdate {
btc: string;
eth: string;
sol: string;
btcChange: string;
ethChange: string;
solChange: string;
}
interface CompletedTask {
task: string;
completed: boolean;
}
interface ProjectProgress {
project: string;
progress: number;
}
interface CouncilFeedback {
horus: string;
amun: string;
[key: string]: string;
}
interface FormState {
date: string;
marketUpdate: MarketUpdate;
completedTasks: CompletedTask[];
tomorrowPriorities: string[];
councilFeedback: CouncilFeedback;
projectProgress: ProjectProgress[];
}
const defaultProjects = ["SiteMente", "HolaCompi", "WandVoice"];
const defaultForm: FormState = {
date: today(),
marketUpdate: { btc: "", eth: "", sol: "", btcChange: "", ethChange: "", solChange: "" },
completedTasks: [{ task: "", completed: true }],
tomorrowPriorities: ["", "", "", "", ""],
councilFeedback: { horus: "", amun: "" },
projectProgress: defaultProjects.map((p) => ({ project: p, progress: 0 })),
};
export default function EodBriefPage() {
const [form, setForm] = useState<FormState>(defaultForm);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [loadingDate, setLoadingDate] = useState(false);
const [extraAgents, setExtraAgents] = useState<string[]>([]);
const loadBriefForDate = useCallback(async (date: string) => {
setLoadingDate(true);
try {
const res = await fetch(`/api/eod?date=${date}`);
const data = await res.json();
if (data) {
const mu = typeof data.marketUpdate === "string" ? JSON.parse(data.marketUpdate) : (data.marketUpdate || defaultForm.marketUpdate);
const ct = typeof data.completedTasks === "string" ? JSON.parse(data.completedTasks) : (data.completedTasks || defaultForm.completedTasks);
const tp = typeof data.tomorrowPriorities === "string" ? JSON.parse(data.tomorrowPriorities) : (data.tomorrowPriorities || defaultForm.tomorrowPriorities);
const cf = typeof data.councilFeedback === "string" ? JSON.parse(data.councilFeedback) : (data.councilFeedback || defaultForm.councilFeedback);
const pp = typeof data.projectProgress === "string" ? JSON.parse(data.projectProgress) : (data.projectProgress || defaultForm.projectProgress);
setForm({ date: data.date, marketUpdate: mu, completedTasks: ct, tomorrowPriorities: tp, councilFeedback: cf, projectProgress: pp });
// Detect extra agents
const extras = Object.keys(cf).filter((k) => k !== "horus" && k !== "amun");
setExtraAgents(extras);
} else {
setForm({ ...defaultForm, date });
setExtraAgents([]);
}
} catch {
setForm({ ...defaultForm, date });
} finally {
setLoadingDate(false);
}
}, []);
useEffect(() => {
loadBriefForDate(today());
}, [loadBriefForDate]);
const validate = (): boolean => {
const errs: Record<string, string> = {};
if (!form.completedTasks.some((t) => t.task.trim())) errs.completedTasks = "At least one completed task is required";
if (!form.tomorrowPriorities.some((p) => p.trim())) errs.tomorrowPriorities = "At least one priority for tomorrow is required";
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSave = async () => {
if (!validate()) return;
setSaving(true);
try {
const res = await fetch("/api/eod", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
if (!res.ok) {
const err = await res.json();
setErrors({ submit: err.error || "Failed to save" });
} else {
setSaved(true);
setTimeout(() => setSaved(false), 3000);
}
} catch {
setErrors({ submit: "Network error. Please try again." });
} finally {
setSaving(false);
}
};
const updateTask = (index: number, field: keyof CompletedTask, value: string | boolean) => {
setForm((prev) => {
const tasks = [...prev.completedTasks];
tasks[index] = { ...tasks[index], [field]: value };
return { ...prev, completedTasks: tasks };
});
};
const addTask = () => {
setForm((prev) => ({ ...prev, completedTasks: [...prev.completedTasks, { task: "", completed: true }] }));
};
const removeTask = (index: number) => {
setForm((prev) => ({ ...prev, completedTasks: prev.completedTasks.filter((_, i) => i !== index) }));
};
const updatePriority = (index: number, value: string) => {
setForm((prev) => {
const arr = [...prev.tomorrowPriorities];
arr[index] = value;
return { ...prev, tomorrowPriorities: arr };
});
};
const addPriority = () => {
setForm((prev) => ({ ...prev, tomorrowPriorities: [...prev.tomorrowPriorities, ""] }));
};
const removePriority = (index: number) => {
setForm((prev) => ({ ...prev, tomorrowPriorities: prev.tomorrowPriorities.filter((_, i) => i !== index) }));
};
const updateProgress = (index: number, value: number) => {
setForm((prev) => {
const pp = [...prev.projectProgress];
pp[index] = { ...pp[index], progress: value };
return { ...prev, projectProgress: pp };
});
};
const addProject = () => {
setForm((prev) => ({ ...prev, projectProgress: [...prev.projectProgress, { project: "", progress: 0 }] }));
};
const updateProjectName = (index: number, name: string) => {
setForm((prev) => {
const pp = [...prev.projectProgress];
pp[index] = { ...pp[index], project: name };
return { ...prev, projectProgress: pp };
});
};
const updateCouncil = (agent: string, value: string) => {
setForm((prev) => ({ ...prev, councilFeedback: { ...prev.councilFeedback, [agent]: value } }));
};
const addAgent = () => {
const name = prompt("Agent name:");
if (name?.trim()) {
setExtraAgents((prev) => [...prev, name.trim()]);
setForm((prev) => ({ ...prev, councilFeedback: { ...prev.councilFeedback, [name.trim()]: "" } }));
}
};
const changeColor = (val: string) => {
const n = parseFloat(val);
if (isNaN(n)) return "text-slate-400";
return n >= 0 ? "text-emerald-400" : "text-red-400";
};
const progressColor = (p: number) => {
if (p >= 80) return "bg-emerald-500";
if (p >= 50) return "bg-amber-500";
if (p >= 25) return "bg-orange-500";
return "bg-red-500";
};
return (
<div className="p-6 max-w-4xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<span className="text-blue-400">🌙</span> End-of-Day Brief
</h1>
<p className="text-sm text-slate-500 mt-0.5">Horus + Amun daily debrief</p>
</div>
<div className="flex items-center gap-3">
<input
type="date"
value={form.date}
onChange={(e) => {
setForm((prev) => ({ ...prev, date: e.target.value }));
loadBriefForDate(e.target.value);
}}
className="bg-[#1a1d27] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-300 focus:outline-none focus:border-blue-500/50"
/>
<button
onClick={handleSave}
disabled={saving || loadingDate}
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Saved" : "Save Brief"}
</button>
</div>
</div>
{errors.submit && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
{errors.submit}
</div>
)}
{loadingDate && (
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-blue-400 text-sm">
Loading brief for selected date
</div>
)}
<div className="space-y-5">
{/* Market Update */}
<Card title="📈 Market Update" accent="blue">
<div className="grid grid-cols-3 gap-4">
{(["btc", "eth", "sol"] as const).map((coin) => (
<div key={coin}>
<label className="block text-xs text-slate-500 mb-1.5 uppercase font-mono">{coin}</label>
<input
type="text"
value={form.marketUpdate[coin]}
onChange={(e) =>
setForm((p) => ({ ...p, marketUpdate: { ...p.marketUpdate, [coin]: e.target.value } }))
}
placeholder="$0.00"
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-blue-500/50 mb-2"
/>
<input
type="text"
value={form.marketUpdate[`${coin}Change` as keyof MarketUpdate]}
onChange={(e) =>
setForm((p) => ({
...p,
marketUpdate: { ...p.marketUpdate, [`${coin}Change`]: e.target.value },
}))
}
placeholder="24h % (e.g. +2.5)"
className={`w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-blue-500/50 ${changeColor(form.marketUpdate[`${coin}Change` as keyof MarketUpdate])}`}
/>
</div>
))}
</div>
</Card>
{/* Completed Tasks */}
<Card title="✅ Completed Tasks" accent="green">
{errors.completedTasks && (
<p className="text-xs text-red-400 mb-2">{errors.completedTasks}</p>
)}
<div className="space-y-2">
{form.completedTasks.map((task, i) => (
<div key={i} className="flex items-center gap-2">
<button
onClick={() => updateTask(i, "completed", !task.completed)}
className={`w-5 h-5 rounded border flex-shrink-0 flex items-center justify-center transition-colors ${
task.completed
? "bg-emerald-500 border-emerald-500 text-white"
: "border-[#2a2d3a] bg-transparent"
}`}
>
{task.completed && (
<svg width="10" height="10" viewBox="0 0 12 12" fill="none">
<path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</button>
<input
type="text"
value={task.task}
onChange={(e) => updateTask(i, "task", e.target.value)}
placeholder={`Task ${i + 1}`}
className={`flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-emerald-500/50 ${
task.completed ? "text-slate-400 line-through" : "text-slate-200"
}`}
/>
{form.completedTasks.length > 1 && (
<button
onClick={() => removeTask(i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none"
>
×
</button>
)}
</div>
))}
<button
onClick={addTask}
className="text-xs text-emerald-400 hover:text-emerald-300 transition-colors mt-1"
>
+ Add task
</button>
</div>
</Card>
{/* Tomorrow's Priorities */}
<Card title="⚡ Priorities for Tomorrow" accent="amber">
{errors.tomorrowPriorities && (
<p className="text-xs text-red-400 mb-2">{errors.tomorrowPriorities}</p>
)}
<div className="space-y-2">
{form.tomorrowPriorities.map((p, i) => (
<div key={i} className="flex gap-2">
<span className="text-xs text-amber-500/70 font-mono mt-2.5 w-5">{i + 1}.</span>
<input
type="text"
value={p}
onChange={(e) => updatePriority(i, e.target.value)}
placeholder={`Priority ${i + 1}`}
className="flex-1 bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-amber-500/50"
/>
{form.tomorrowPriorities.length > 3 && (
<button
onClick={() => removePriority(i)}
className="text-slate-600 hover:text-red-400 transition-colors text-lg leading-none mt-1"
>
×
</button>
)}
</div>
))}
{form.tomorrowPriorities.length < 7 && (
<button
onClick={addPriority}
className="text-xs text-amber-400 hover:text-amber-300 transition-colors mt-1"
>
+ Add priority
</button>
)}
</div>
</Card>
{/* Council Feedback */}
<Card title="🏛 Council Feedback" subtitle="One sentence per agent" accent="purple">
<div className="space-y-3">
{(["horus", "amun", ...extraAgents] as string[]).map((agent) => (
<div key={agent}>
<label className="block text-xs text-slate-500 mb-1.5 capitalize font-medium">
{agent === "horus" ? "🦅 Horus" : agent === "amun" ? "⚙️ Amun" : `🤖 ${agent}`}
</label>
<input
type="text"
value={form.councilFeedback[agent] || ""}
onChange={(e) => updateCouncil(agent, e.target.value)}
placeholder={`${agent.charAt(0).toUpperCase() + agent.slice(1)}'s one-sentence feedback`}
className="w-full bg-[#0f1117] border border-[#2a2d3a] rounded-lg px-3 py-2 text-sm text-slate-200 focus:outline-none focus:border-purple-500/50"
/>
</div>
))}
<button
onClick={addAgent}
className="text-xs text-purple-400 hover:text-purple-300 transition-colors mt-1"
>
+ Add agent
</button>
</div>
</Card>
{/* Project Progress */}
<Card title="📊 Project Progress" accent="blue">
<div className="space-y-4">
{form.projectProgress.map((proj, i) => (
<div key={i}>
<div className="flex items-center justify-between mb-1.5">
<input
type="text"
value={proj.project}
onChange={(e) => updateProjectName(i, e.target.value)}
placeholder="Project name"
className="bg-transparent text-sm font-medium text-slate-300 focus:outline-none border-b border-transparent focus:border-blue-500/50 pb-0.5 w-40"
/>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500 font-mono">{proj.progress}%</span>
<input
type="number"
min="0"
max="100"
value={proj.progress}
onChange={(e) => updateProgress(i, Math.min(100, Math.max(0, parseInt(e.target.value) || 0)))}
className="w-16 bg-[#0f1117] border border-[#2a2d3a] rounded px-2 py-1 text-xs text-slate-300 focus:outline-none focus:border-blue-500/50 text-center"
/>
</div>
</div>
<div className="h-2 bg-[#0f1117] rounded-full overflow-hidden border border-[#2a2d3a]">
<div
className={`h-full rounded-full transition-all duration-300 ${progressColor(proj.progress)}`}
style={{ width: `${proj.progress}%` }}
/>
</div>
</div>
))}
<button
onClick={addProject}
className="text-xs text-blue-400 hover:text-blue-300 transition-colors mt-1"
>
+ Add project
</button>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end pb-6">
<button
onClick={handleSave}
disabled={saving}
className="px-6 py-2.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white font-semibold text-sm rounded-lg transition-colors"
>
{saving ? "Saving…" : saved ? "✓ Brief Saved!" : "Save EOD Brief"}
</button>
</div>
</div>
</div>
);
}