Files

445 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}