Add YouTube transcripts tab to Mission Control
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user