Bot commited on
Commit
982b5dc
·
1 Parent(s): 8f7768e

Fix OpenAI API key crash, implement Eternity Dashboard page, add sidebar menu, and configure API proxy bridge

Browse files
src/app/(chat)/eternity/page.tsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import EternityDashboard from "@/components/eternity-dashboard";
2
+ import { getSession } from "auth/server";
3
+ import { redirect } from "next/navigation";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export default async function Page() {
8
+ const session = await getSession();
9
+ if (!session?.user) {
10
+ return redirect("/login");
11
+ }
12
+
13
+ return <EternityDashboard />;
14
+ }
src/app/api/eternity/route.ts ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSession } from "auth/server";
3
+
4
+ const getBackendUrl = () => {
5
+ if (process.env.BACKEND_API_URL) return process.env.BACKEND_API_URL;
6
+ const spaceId = process.env.SPACE_ID || "";
7
+ if (spaceId.startsWith("shyota/")) {
8
+ return "https://shyota-claude-code-backend.hf.space";
9
+ }
10
+ return "https://augment17-claude-code-backend.hf.space";
11
+ };
12
+
13
+ export async function GET(req: NextRequest) {
14
+ const session = await getSession();
15
+ if (!session?.user) {
16
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
17
+ }
18
+
19
+ const backendUrl = getBackendUrl();
20
+ const apiKey = process.env.BACKEND_API_KEY || "";
21
+
22
+ try {
23
+ const res = await fetch(`${backendUrl}/api/eternity/list`, {
24
+ headers: {
25
+ "Authorization": `Bearer ${apiKey}`,
26
+ },
27
+ cache: "no-store",
28
+ });
29
+
30
+ if (!res.ok) {
31
+ const txt = await res.text();
32
+ return NextResponse.json({ error: txt }, { status: res.status });
33
+ }
34
+
35
+ const data = await res.json();
36
+ return NextResponse.json(data);
37
+ } catch (err: any) {
38
+ return NextResponse.json({ error: err.message }, { status: 500 });
39
+ }
40
+ }
41
+
42
+ export async function POST(req: NextRequest) {
43
+ const session = await getSession();
44
+ if (!session?.user) {
45
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
46
+ }
47
+
48
+ const body = await req.json();
49
+ const { action, ...payload } = body;
50
+
51
+ const backendUrl = getBackendUrl();
52
+ const apiKey = process.env.BACKEND_API_KEY || "";
53
+
54
+ let targetPath = "";
55
+ if (action === "toggle") {
56
+ targetPath = "/api/eternity/toggle";
57
+ } else if (action === "set-priority") {
58
+ targetPath = "/api/eternity/set-priority";
59
+ } else if (action === "init") {
60
+ targetPath = "/api/eternity/init";
61
+ } else {
62
+ return NextResponse.json({ error: "Invalid action" }, { status: 400 });
63
+ }
64
+
65
+ try {
66
+ const res = await fetch(`${backendUrl}${targetPath}`, {
67
+ method: "POST",
68
+ headers: {
69
+ "Content-Type": "application/json",
70
+ "Authorization": `Bearer ${apiKey}`,
71
+ },
72
+ body: JSON.stringify(payload),
73
+ });
74
+
75
+ if (!res.ok) {
76
+ const txt = await res.text();
77
+ return NextResponse.json({ error: txt }, { status: res.status });
78
+ }
79
+
80
+ const data = await res.json();
81
+ return NextResponse.json(data);
82
+ } catch (err: any) {
83
+ return NextResponse.json({ error: err.message }, { status: 500 });
84
+ }
85
+ }
src/components/eternity-dashboard.tsx ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useEffect, useState, useMemo } from "react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { Skeleton } from "ui/skeleton";
5
+ import { ScrollArea } from "ui/scroll-area";
6
+ import { toast } from "sonner";
7
+ import { Loader2, Plus, Play, Pause, AlertCircle, RefreshCw } from "lucide-react";
8
+ import { cn } from "lib/utils";
9
+ import dynamic from "next/dynamic";
10
+ import { Label } from "ui/label";
11
+ import { Input } from "ui/input";
12
+ import { Textarea } from "ui/textarea";
13
+
14
+ const LightRays = dynamic(() => import("@/components/ui/light-rays"), {
15
+ ssr: false,
16
+ });
17
+
18
+ interface EternityProject {
19
+ project_name: string;
20
+ goal: string;
21
+ deadline: string;
22
+ current_mode: string;
23
+ is_active: boolean;
24
+ priority: string;
25
+ latest_brief: string | null;
26
+ created_at: string;
27
+ time_remaining_str: string;
28
+ }
29
+
30
+ export default function EternityDashboard() {
31
+ const [projects, setProjects] = useState<EternityProject[]>([]);
32
+ const [isLoading, setIsLoading] = useState(true);
33
+ const [isRefreshing, setIsRefreshing] = useState(false);
34
+ const [isModalOpen, setIsModalOpen] = useState(false);
35
+
36
+ // Form states
37
+ const [projName, setProjName] = useState("");
38
+ const [projGoal, setProjGoal] = useState("");
39
+ const [projDeadline, setProjDeadline] = useState(1.0);
40
+ const [projPriority, setProjPriority] = useState("low");
41
+ const [isSubmitting, setIsSubmitting] = useState(false);
42
+
43
+ const fetchProjects = async (silent = false) => {
44
+ if (!silent) setIsRefreshing(true);
45
+ try {
46
+ const res = await fetch("/api/eternity");
47
+ if (!res.ok) {
48
+ throw new Error(await res.text());
49
+ }
50
+ const data = await res.json();
51
+ setProjects(data.projects || []);
52
+ } catch (err: any) {
53
+ toast.error(`Failed to load projects: ${err.message}`);
54
+ } finally {
55
+ setIsLoading(false);
56
+ setIsRefreshing(false);
57
+ }
58
+ };
59
+
60
+ useEffect(() => {
61
+ fetchProjects();
62
+ const interval = setInterval(() => fetchProjects(true), 5000);
63
+ return () => clearInterval(interval);
64
+ }, []);
65
+
66
+ const handleToggleActive = async (name: string, currentStatus: boolean) => {
67
+ try {
68
+ const res = await fetch("/api/eternity", {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({
72
+ action: "toggle",
73
+ project_name: name,
74
+ is_active: !currentStatus,
75
+ }),
76
+ });
77
+ if (!res.ok) throw new Error("Failed to toggle status");
78
+ toast.success(`Project ${!currentStatus ? "resumed" : "paused"}`);
79
+ fetchProjects(true);
80
+ } catch (err: any) {
81
+ toast.error(err.message);
82
+ }
83
+ };
84
+
85
+ const handleSetPriority = async (name: string, priority: string) => {
86
+ try {
87
+ const res = await fetch("/api/eternity", {
88
+ method: "POST",
89
+ headers: { "Content-Type": "application/json" },
90
+ body: JSON.stringify({
91
+ action: "set-priority",
92
+ project_name: name,
93
+ priority: priority,
94
+ }),
95
+ });
96
+ if (!res.ok) throw new Error("Failed to set priority");
97
+ toast.success(`Priority updated to ${priority}`);
98
+ fetchProjects(true);
99
+ } catch (err: any) {
100
+ toast.error(err.message);
101
+ }
102
+ };
103
+
104
+ const handleSubmit = async (e: React.FormEvent) => {
105
+ e.preventDefault();
106
+ if (!projGoal.trim()) {
107
+ toast.error("Please enter a goal statement.");
108
+ return;
109
+ }
110
+ setIsSubmitting(true);
111
+ try {
112
+ const res = await fetch("/api/eternity", {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({
116
+ action: "init",
117
+ project_name: projName.trim(),
118
+ goal: projGoal.trim(),
119
+ deadline_hours: projDeadline,
120
+ priority: projPriority,
121
+ }),
122
+ });
123
+ if (!res.ok) throw new Error(await res.text());
124
+ toast.success("Eternity R&D Goal Started successfully!");
125
+ setIsModalOpen(false);
126
+ setProjName("");
127
+ setProjGoal("");
128
+ fetchProjects(true);
129
+ } catch (err: any) {
130
+ toast.error(`Init failed: ${err.message}`);
131
+ } finally {
132
+ setIsSubmitting(false);
133
+ }
134
+ };
135
+
136
+ return (
137
+ <>
138
+ <div className="absolute opacity-30 pointer-events-none top-0 left-0 w-full h-full z-10">
139
+ <LightRays className="bg-transparent" />
140
+ </div>
141
+ <ScrollArea className="h-full w-full z-45">
142
+ <div className="pt-8 flex-1 relative flex flex-col gap-6 px-8 max-w-3xl h-full mx-auto pb-8">
143
+ <div className="flex items-center pb-4 border-b border-border/40">
144
+ <div>
145
+ <h1 className="text-2xl font-bold flex items-center gap-2">
146
+ Eternity R&D Lab
147
+ {isRefreshing && <Loader2 className="size-4 animate-spin text-muted-foreground" />}
148
+ </h1>
149
+ <p className="text-xs text-muted-foreground mt-1">Autonomous multi-agent loop orchestrator panel</p>
150
+ </div>
151
+ <div className="flex-1" />
152
+ <Button className="font-semibold gap-1 bg-primary text-primary-foreground" onClick={() => setIsModalOpen(true)}>
153
+ <Plus className="size-4" />
154
+ New R&D Goal
155
+ </Button>
156
+ </div>
157
+
158
+ {isLoading ? (
159
+ <div className="flex flex-col gap-4">
160
+ <Skeleton className="h-32 w-full" />
161
+ <Skeleton className="h-32 w-full" />
162
+ </div>
163
+ ) : projects.length === 0 ? (
164
+ <div className="flex flex-col items-center justify-center space-y-4 my-20 text-center">
165
+ <AlertCircle className="size-12 text-muted-foreground/50" />
166
+ <h3 className="text-xl font-semibold">No R&D goals configured</h3>
167
+ <p className="text-muted-foreground max-w-md text-sm">
168
+ Click "New R&D Goal" to initialize your first eternity loop.
169
+ </p>
170
+ </div>
171
+ ) : (
172
+ <div className="flex flex-col gap-6">
173
+ {projects.map((p) => {
174
+ const isBuild = p.current_mode === "build";
175
+ return (
176
+ <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">
177
+ <div className="flex items-start justify-between">
178
+ <div className="space-y-1">
179
+ <div className="flex items-center gap-2">
180
+ <h3 className="text-lg font-bold font-mono text-foreground">{p.project_name}</h3>
181
+ <span className={cn(
182
+ "px-2 py-0.5 rounded text-[10px] font-bold border",
183
+ p.is_active
184
+ ? "bg-green-500/10 text-green-400 border-green-500/20"
185
+ : "bg-muted text-muted-foreground border-border"
186
+ )}>
187
+ {p.is_active ? "ACTIVE" : "PAUSED"}
188
+ </span>
189
+ <span className={cn(
190
+ "px-2 py-0.5 rounded text-[10px] font-bold border",
191
+ p.priority === "supreme"
192
+ ? "bg-red-500/10 text-red-400 border-red-500/20"
193
+ : "bg-blue-500/10 text-blue-400 border-blue-500/20"
194
+ )}>
195
+ {p.priority.toUpperCase()}
196
+ </span>
197
+ </div>
198
+ <p className="text-[10px] text-muted-foreground">
199
+ Started on {new Date(p.created_at).toLocaleString()}
200
+ </p>
201
+ </div>
202
+
203
+ <div className="flex items-center gap-3">
204
+ <select
205
+ value={p.priority}
206
+ onChange={(e) => handleSetPriority(p.project_name, e.target.value)}
207
+ className="bg-background border border-input rounded px-2.5 py-1 text-xs text-foreground focus:outline-none cursor-pointer"
208
+ >
209
+ <option value="supreme">Supreme</option>
210
+ <option value="low">Low</option>
211
+ </select>
212
+
213
+ <Button
214
+ variant="outline"
215
+ size="sm"
216
+ onClick={() => handleToggleActive(p.project_name, p.is_active)}
217
+ className={cn(
218
+ "text-xs px-3 py-1 font-semibold gap-1",
219
+ p.is_active
220
+ ? "border-red-500/30 text-red-400 bg-red-500/5 hover:bg-red-500/10"
221
+ : "border-green-500/30 text-green-400 bg-green-500/5 hover:bg-green-500/10"
222
+ )}
223
+ >
224
+ {p.is_active ? <Pause className="size-3" /> : <Play className="size-3" />}
225
+ {p.is_active ? "Pause" : "Resume"}
226
+ </Button>
227
+ </div>
228
+ </div>
229
+
230
+ <div className="space-y-1.5">
231
+ <div className="text-xs font-semibold text-muted-foreground">Goal Description:</div>
232
+ <div className="text-sm text-foreground bg-muted/30 p-3 rounded border leading-relaxed">
233
+ {p.goal}
234
+ </div>
235
+ </div>
236
+
237
+ <div className="grid grid-cols-2 gap-4 text-xs">
238
+ <div className="bg-muted/40 p-3 rounded border space-y-1">
239
+ <div className="text-[10px] text-muted-foreground">Mode Status</div>
240
+ <div className={cn(
241
+ "font-bold uppercase",
242
+ isBuild ? "text-amber-500" : "text-green-500"
243
+ )}>
244
+ {p.current_mode} Mode
245
+ </div>
246
+ </div>
247
+ <div className="bg-muted/40 p-3 rounded border space-y-1">
248
+ <div className="text-[10px] text-muted-foreground">Deadline Countdown</div>
249
+ <div className="font-bold text-foreground font-mono">
250
+ {p.time_remaining_str}
251
+ </div>
252
+ </div>
253
+ </div>
254
+
255
+ {p.latest_brief && (
256
+ <div className="bg-primary/5 border border-primary/20 rounded p-4 text-xs text-foreground leading-relaxed space-y-1">
257
+ <div className="font-bold text-primary flex items-center gap-1">
258
+ 📘 Latest Research Brief Summary
259
+ </div>
260
+ <div className="mt-1 text-muted-foreground">{p.latest_brief}</div>
261
+ </div>
262
+ )}
263
+ </div>
264
+ );
265
+ })}
266
+ </div>
267
+ )}
268
+ </div>
269
+ </ScrollArea>
270
+
271
+ {/* MODAL */}
272
+ {isModalOpen && (
273
+ <div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 animate-in fade-in duration-200">
274
+ <form onSubmit={handleSubmit} className="bg-card border border-border p-6 rounded-xl max-w-md w-full space-y-4 shadow-xl">
275
+ <h3 className="text-lg font-bold text-card-foreground">Initialize R&D Goal</h3>
276
+ <div className="space-y-3 text-sm">
277
+ <div className="space-y-1">
278
+ <Label htmlFor="modal-name">Project Name (Slugified automatically if blank)</Label>
279
+ <Input
280
+ id="modal-name"
281
+ type="text"
282
+ placeholder="e.g. stoichiometry-solver"
283
+ value={projName}
284
+ onChange={(e) => setProjName(e.target.value)}
285
+ />
286
+ </div>
287
+ <div className="space-y-1">
288
+ <Label htmlFor="modal-goal">Problem Statement / Goal</Label>
289
+ <Textarea
290
+ id="modal-goal"
291
+ rows={4}
292
+ placeholder="Describe the goal in detail..."
293
+ value={projGoal}
294
+ onChange={(e) => setProjGoal(e.target.value)}
295
+ required
296
+ />
297
+ </div>
298
+ <div className="grid grid-cols-2 gap-4">
299
+ <div className="space-y-1">
300
+ <Label htmlFor="modal-deadline">Deadline (Hours)</Label>
301
+ <Input
302
+ id="modal-deadline"
303
+ type="number"
304
+ step="0.01"
305
+ value={projDeadline}
306
+ onChange={(e) => setProjDeadline(parseFloat(e.target.value) || 1.0)}
307
+ />
308
+ </div>
309
+ <div className="space-y-1">
310
+ <Label htmlFor="modal-priority">Priority</Label>
311
+ <select
312
+ id="modal-priority"
313
+ value={projPriority}
314
+ onChange={(e) => setProjPriority(e.target.value)}
315
+ className="w-full bg-background border border-input rounded p-2 text-foreground"
316
+ >
317
+ <option value="supreme">Supreme Priority</option>
318
+ <option value="low">Low Priority</option>
319
+ </select>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ <div className="flex justify-end gap-3 text-sm pt-2">
324
+ <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
325
+ Cancel
326
+ </Button>
327
+ <Button type="submit" disabled={isSubmitting}>
328
+ {isSubmitting ? "Starting..." : "Start R&D"}
329
+ </Button>
330
+ </div>
331
+ </form>
332
+ </div>
333
+ )}
334
+ </>
335
+ );
336
+ }
src/components/layouts/app-sidebar-menus.tsx CHANGED
@@ -22,6 +22,7 @@ import {
22
  FolderOpenIcon,
23
  FolderSearchIcon,
24
  PlusIcon,
 
25
  Waypoints,
26
  } from "lucide-react";
27
  import { useCallback, useState } from "react";
@@ -101,6 +102,18 @@ export function AppSidebarMenus({ user }: { user?: BasicUser }) {
101
  </SidebarMenuItem>
102
  </Tooltip>
103
  </SidebarMenu>
 
 
 
 
 
 
 
 
 
 
 
 
104
  {getIsUserAdmin(user) && <AppSidebarAdmin />}
105
  <SidebarMenu className="group/archive">
106
  <Tooltip>
 
22
  FolderOpenIcon,
23
  FolderSearchIcon,
24
  PlusIcon,
25
+ RefreshCw,
26
  Waypoints,
27
  } from "lucide-react";
28
  import { useCallback, useState } from "react";
 
102
  </SidebarMenuItem>
103
  </Tooltip>
104
  </SidebarMenu>
105
+ <SidebarMenu>
106
+ <Tooltip>
107
+ <SidebarMenuItem>
108
+ <Link href="/eternity">
109
+ <SidebarMenuButton className="font-semibold">
110
+ <RefreshCw className="size-4" />
111
+ Eternity R&D
112
+ </SidebarMenuButton>
113
+ </Link>
114
+ </SidebarMenuItem>
115
+ </Tooltip>
116
+ </SidebarMenu>
117
  {getIsUserAdmin(user) && <AppSidebarAdmin />}
118
  <SidebarMenu className="group/archive">
119
  <Tooltip>
src/lib/ai/create-openai-compatiable.ts CHANGED
@@ -24,7 +24,7 @@ export function createOpenAICompatibleModels(
24
  const providerKey = provider;
25
  const customProvider = createOpenAICompatible({
26
  name: provider,
27
- apiKey: apiKey,
28
  baseURL: baseUrl!,
29
  });
30
 
 
24
  const providerKey = provider;
25
  const customProvider = createOpenAICompatible({
26
  name: provider,
27
+ apiKey: apiKey || "dummy-key",
28
  baseURL: baseUrl!,
29
  });
30