Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Skeleton } from "ui/skeleton"; | |
| import { ScrollArea } from "ui/scroll-area"; | |
| import { toast } from "sonner"; | |
| import { Loader2, Plus, Play, Pause, AlertCircle } from "lucide-react"; | |
| import { cn } from "lib/utils"; | |
| import dynamic from "next/dynamic"; | |
| import { Label } from "ui/label"; | |
| import { Input } from "ui/input"; | |
| import { Textarea } from "ui/textarea"; | |
| const LightRays = dynamic(() => import("@/components/ui/light-rays"), { | |
| ssr: false, | |
| }); | |
| interface EternityProject { | |
| project_name: string; | |
| goal: string; | |
| deadline: string; | |
| current_mode: string; | |
| is_active: boolean; | |
| priority: string; | |
| latest_brief: string | null; | |
| created_at: string; | |
| time_remaining_str: string; | |
| } | |
| export default function EternityDashboard() { | |
| const [projects, setProjects] = useState<EternityProject[]>([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [isRefreshing, setIsRefreshing] = useState(false); | |
| const [isModalOpen, setIsModalOpen] = useState(false); | |
| // Form states | |
| const [projName, setProjName] = useState(""); | |
| const [projGoal, setProjGoal] = useState(""); | |
| const [projDeadline, setProjDeadline] = useState(1.0); | |
| const [projPriority, setProjPriority] = useState("low"); | |
| const [isSubmitting, setIsSubmitting] = useState(false); | |
| const fetchProjects = async (silent = false) => { | |
| if (!silent) setIsRefreshing(true); | |
| try { | |
| const res = await fetch("/api/eternity"); | |
| if (!res.ok) { | |
| throw new Error(await res.text()); | |
| } | |
| const data = await res.json(); | |
| setProjects(data.projects || []); | |
| } catch (err: any) { | |
| toast.error(`Failed to load projects: ${err.message}`); | |
| } finally { | |
| setIsLoading(false); | |
| setIsRefreshing(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchProjects(); | |
| const interval = setInterval(() => fetchProjects(true), 5000); | |
| return () => clearInterval(interval); | |
| }, []); | |
| const handleToggleActive = async (name: string, currentStatus: boolean) => { | |
| try { | |
| const res = await fetch("/api/eternity", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| action: "toggle", | |
| project_name: name, | |
| is_active: !currentStatus, | |
| }), | |
| }); | |
| if (!res.ok) throw new Error("Failed to toggle status"); | |
| toast.success(`Project ${!currentStatus ? "resumed" : "paused"}`); | |
| fetchProjects(true); | |
| } catch (err: any) { | |
| toast.error(err.message); | |
| } | |
| }; | |
| const handleSetPriority = async (name: string, priority: string) => { | |
| try { | |
| const res = await fetch("/api/eternity", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| action: "set-priority", | |
| project_name: name, | |
| priority: priority, | |
| }), | |
| }); | |
| if (!res.ok) throw new Error("Failed to set priority"); | |
| toast.success(`Priority updated to ${priority}`); | |
| fetchProjects(true); | |
| } catch (err: any) { | |
| toast.error(err.message); | |
| } | |
| }; | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!projGoal.trim()) { | |
| toast.error("Please enter a goal statement."); | |
| return; | |
| } | |
| setIsSubmitting(true); | |
| try { | |
| const res = await fetch("/api/eternity", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ | |
| action: "init", | |
| project_name: projName.trim(), | |
| goal: projGoal.trim(), | |
| deadline_hours: projDeadline, | |
| priority: projPriority, | |
| }), | |
| }); | |
| if (!res.ok) throw new Error(await res.text()); | |
| toast.success("Eternity R&D Goal Started successfully!"); | |
| setIsModalOpen(false); | |
| setProjName(""); | |
| setProjGoal(""); | |
| fetchProjects(true); | |
| } catch (err: any) { | |
| toast.error(`Init failed: ${err.message}`); | |
| } finally { | |
| setIsSubmitting(false); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <div className="absolute opacity-30 pointer-events-none top-0 left-0 w-full h-full z-10"> | |
| <LightRays className="bg-transparent" /> | |
| </div> | |
| <ScrollArea className="h-full w-full z-45"> | |
| <div className="pt-8 flex-1 relative flex flex-col gap-6 px-8 max-w-3xl h-full mx-auto pb-8"> | |
| <div className="flex items-center pb-4 border-b border-border/40"> | |
| <div> | |
| <h1 className="text-2xl font-bold flex items-center gap-2"> | |
| Eternity R&D Lab | |
| {isRefreshing && <Loader2 className="size-4 animate-spin text-muted-foreground" />} | |
| </h1> | |
| <p className="text-xs text-muted-foreground mt-1">Autonomous multi-agent loop orchestrator panel</p> | |
| </div> | |
| <div className="flex-1" /> | |
| <Button className="font-semibold gap-1 bg-primary text-primary-foreground" onClick={() => setIsModalOpen(true)}> | |
| <Plus className="size-4" /> | |
| New R&D Goal | |
| </Button> | |
| </div> | |
| {isLoading ? ( | |
| <div className="flex flex-col gap-4"> | |
| <Skeleton className="h-32 w-full" /> | |
| <Skeleton className="h-32 w-full" /> | |
| </div> | |
| ) : projects.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center space-y-4 my-20 text-center"> | |
| <AlertCircle className="size-12 text-muted-foreground/50" /> | |
| <h3 className="text-xl font-semibold">No R&D goals configured</h3> | |
| <p className="text-muted-foreground max-w-md text-sm"> | |
| Click "New R&D Goal" to initialize your first eternity loop. | |
| </p> | |
| </div> | |
| ) : ( | |
| <div className="flex flex-col gap-6"> | |
| {projects.map((p) => { | |
| const isBuild = p.current_mode === "build"; | |
| return ( | |
| <div key={p.project_name} className="border rounded-xl p-6 bg-card text-card-foreground shadow-sm flex flex-col gap-4 hover:border-accent transition-all relative overflow-hidden"> | |
| <div className="flex items-start justify-between"> | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-2"> | |
| <h3 className="text-lg font-bold font-mono text-foreground">{p.project_name}</h3> | |
| <span className={cn( | |
| "px-2 py-0.5 rounded text-[10px] font-bold border", | |
| p.is_active | |
| ? "bg-green-500/10 text-green-400 border-green-500/20" | |
| : "bg-muted text-muted-foreground border-border" | |
| )}> | |
| {p.is_active ? "ACTIVE" : "PAUSED"} | |
| </span> | |
| <span className={cn( | |
| "px-2 py-0.5 rounded text-[10px] font-bold border", | |
| p.priority === "supreme" | |
| ? "bg-red-500/10 text-red-400 border-red-500/20" | |
| : "bg-blue-500/10 text-blue-400 border-blue-500/20" | |
| )}> | |
| {p.priority.toUpperCase()} | |
| </span> | |
| </div> | |
| <p className="text-[10px] text-muted-foreground"> | |
| Started on {new Date(p.created_at).toLocaleString()} | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <select | |
| value={p.priority} | |
| onChange={(e) => handleSetPriority(p.project_name, e.target.value)} | |
| className="bg-background border border-input rounded px-2.5 py-1 text-xs text-foreground focus:outline-none cursor-pointer" | |
| > | |
| <option value="supreme">Supreme</option> | |
| <option value="low">Low</option> | |
| </select> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => handleToggleActive(p.project_name, p.is_active)} | |
| className={cn( | |
| "text-xs px-3 py-1 font-semibold gap-1", | |
| p.is_active | |
| ? "border-red-500/30 text-red-400 bg-red-500/5 hover:bg-red-500/10" | |
| : "border-green-500/30 text-green-400 bg-green-500/5 hover:bg-green-500/10" | |
| )} | |
| > | |
| {p.is_active ? <Pause className="size-3" /> : <Play className="size-3" />} | |
| {p.is_active ? "Pause" : "Resume"} | |
| </Button> | |
| </div> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <div className="text-xs font-semibold text-muted-foreground">Goal Description:</div> | |
| <div className="text-sm text-foreground bg-muted/30 p-3 rounded border leading-relaxed"> | |
| {p.goal} | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-2 gap-4 text-xs"> | |
| <div className="bg-muted/40 p-3 rounded border space-y-1"> | |
| <div className="text-[10px] text-muted-foreground">Mode Status</div> | |
| <div className={cn( | |
| "font-bold uppercase", | |
| isBuild ? "text-amber-500" : "text-green-500" | |
| )}> | |
| {p.current_mode} Mode | |
| </div> | |
| </div> | |
| <div className="bg-muted/40 p-3 rounded border space-y-1"> | |
| <div className="text-[10px] text-muted-foreground">Deadline Countdown</div> | |
| <div className="font-bold text-foreground font-mono"> | |
| {p.time_remaining_str} | |
| </div> | |
| </div> | |
| </div> | |
| {p.latest_brief && ( | |
| <div className="bg-primary/5 border border-primary/20 rounded p-4 text-xs text-foreground leading-relaxed space-y-1"> | |
| <div className="font-bold text-primary flex items-center gap-1"> | |
| 📘 Latest Research Brief Summary | |
| </div> | |
| <div className="mt-1 text-muted-foreground">{p.latest_brief}</div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| </ScrollArea> | |
| {/* MODAL */} | |
| {isModalOpen && ( | |
| <div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200"> | |
| <form onSubmit={handleSubmit} className="bg-card border border-border p-6 rounded-xl max-w-md w-full space-y-4 shadow-xl"> | |
| <h3 className="text-lg font-bold text-card-foreground">Initialize R&D Goal</h3> | |
| <div className="space-y-3 text-sm"> | |
| <div className="space-y-1"> | |
| <Label htmlFor="modal-name">Project Name (Slugified automatically if blank)</Label> | |
| <Input | |
| id="modal-name" | |
| type="text" | |
| placeholder="e.g. stoichiometry-solver" | |
| value={projName} | |
| onChange={(e) => setProjName(e.target.value)} | |
| /> | |
| </div> | |
| <div className="space-y-1"> | |
| <Label htmlFor="modal-goal">Problem Statement / Goal</Label> | |
| <Textarea | |
| id="modal-goal" | |
| rows={4} | |
| placeholder="Describe the goal in detail..." | |
| value={projGoal} | |
| onChange={(e) => setProjGoal(e.target.value)} | |
| required | |
| /> | |
| </div> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="space-y-1"> | |
| <Label htmlFor="modal-deadline">Deadline (Hours)</Label> | |
| <Input | |
| id="modal-deadline" | |
| type="number" | |
| step="0.01" | |
| value={projDeadline} | |
| onChange={(e) => setProjDeadline(parseFloat(e.target.value) || 1.0)} | |
| /> | |
| </div> | |
| <div className="space-y-1"> | |
| <Label htmlFor="modal-priority">Priority</Label> | |
| <select | |
| id="modal-priority" | |
| value={projPriority} | |
| onChange={(e) => setProjPriority(e.target.value)} | |
| className="w-full bg-background border border-input rounded p-2 text-foreground" | |
| > | |
| <option value="supreme">Supreme Priority</option> | |
| <option value="low">Low Priority</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex justify-end gap-3 text-sm pt-2"> | |
| <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}> | |
| Cancel | |
| </Button> | |
| <Button type="submit" disabled={isSubmitting}> | |
| {isSubmitting ? "Starting..." : "Start R&D"} | |
| </Button> | |
| </div> | |
| </form> | |
| </div> | |
| )} | |
| </> | |
| ); | |
| } | |