Skip to content

Commit 5fe26b0

Browse files
committed
Add Nebula Chat UI
1 parent b5baeae commit 5fe26b0

File tree

13 files changed

+1096
-3
lines changed

13 files changed

+1096
-3
lines changed

apps/dashboard/.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,7 @@ NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
9494
TURNSTILE_SECRET_KEY=""
9595
REDIS_URL=""
9696

97-
ANALYTICS_SERVICE_URL=""
97+
ANALYTICS_SERVICE_URL=""
98+
99+
# Required for Nebula Chat
100+
NEXT_PUBLIC_NEBULA_URL=""

apps/dashboard/src/@/constants/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,5 @@ export function getAbsoluteUrlFromPath(path: string) {
4646
url.pathname = path;
4747
return url;
4848
}
49+
50+
export const NEXT_PUBLIC_NEBULA_URL = process.env.NEXT_PUBLIC_NEBULA_URL;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { NextResponse } from "next/server";
2+
import { NEXT_PUBLIC_NEBULA_URL } from "../../../../@/constants/env";
3+
import { getAuthToken } from "../../lib/getAuthToken";
4+
import { fetchWithAuthToken } from "../serverUtils";
5+
6+
export async function POST(request: Request) {
7+
if (!NEXT_PUBLIC_NEBULA_URL) {
8+
throw new Error("Nebula endpoint is not defined");
9+
}
10+
11+
try {
12+
const authToken = await getAuthToken();
13+
14+
if (!authToken) {
15+
return NextResponse.json(
16+
{ error: "Missing Auth token" },
17+
{ status: 401 },
18+
);
19+
}
20+
21+
const body = await request.json();
22+
const { message, user_id, session_id } = body;
23+
24+
if (!message) {
25+
return NextResponse.json(
26+
{ error: "Missing message parameter" },
27+
{ status: 400 },
28+
);
29+
}
30+
31+
try {
32+
const data = await fetchWithAuthToken({
33+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/chat`,
34+
body: { message, user_id, session_id },
35+
authToken,
36+
timeout: 60000, // 1 minute timeout for chat requests
37+
});
38+
39+
return NextResponse.json(data);
40+
} catch (error: unknown) {
41+
console.error(
42+
"Chat API Error:",
43+
error instanceof Error ? error.stack : error,
44+
);
45+
if (error instanceof Error && error.message.includes("timed out")) {
46+
return NextResponse.json(
47+
{ error: "Request timed out. Please try again." },
48+
{ status: 504 },
49+
);
50+
}
51+
52+
if (error instanceof Error && error.message.includes("401")) {
53+
return NextResponse.json(
54+
{ error: "Invalid Authorization header" },
55+
{ status: 401 },
56+
);
57+
}
58+
return NextResponse.json(
59+
{
60+
error:
61+
error instanceof Error ? error.message : "Failed to fetch from API",
62+
},
63+
{ status: 500 },
64+
);
65+
}
66+
} catch (error) {
67+
console.error(
68+
"Request Error:",
69+
error instanceof Error ? error.stack : error,
70+
);
71+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
72+
}
73+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextResponse } from "next/server";
2+
import { NEXT_PUBLIC_NEBULA_URL } from "../../../../@/constants/env";
3+
import { getAuthToken } from "../../lib/getAuthToken";
4+
import { fetchWithAuthToken } from "../serverUtils";
5+
6+
export async function POST(request: Request) {
7+
if (!NEXT_PUBLIC_NEBULA_URL) {
8+
throw new Error("Nebula endpoint is not defined");
9+
}
10+
try {
11+
const body = await request.json();
12+
const { message, user_id, session_id, config } = body;
13+
14+
const authToken = await getAuthToken();
15+
16+
if (!authToken) {
17+
return NextResponse.json(
18+
{ error: "Missing Auth token" },
19+
{ status: 401 },
20+
);
21+
}
22+
23+
if (!message) {
24+
return NextResponse.json(
25+
{ error: "Missing message parameter" },
26+
{ status: 400 },
27+
);
28+
}
29+
30+
try {
31+
const data = await fetchWithAuthToken({
32+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/execute`,
33+
body: { message, user_id, session_id, config },
34+
authToken,
35+
timeout: 120000, // 2 minute timeout for execute requests
36+
});
37+
38+
return NextResponse.json(data);
39+
} catch (error: unknown) {
40+
console.error("Execute API Error:", error);
41+
if (error instanceof Error && error.message.includes("timed out")) {
42+
return NextResponse.json(
43+
{ error: "Request timed out. Please try again." },
44+
{ status: 504 },
45+
);
46+
}
47+
const errorMessage =
48+
error instanceof Error ? error.message : "Failed to fetch from API";
49+
return NextResponse.json({ error: errorMessage }, { status: 500 });
50+
}
51+
} catch (error) {
52+
console.error("Request Error:", error);
53+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
54+
}
55+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
interface FetchWithKeyOptions {
2+
endpoint: string;
3+
body: Record<string, unknown>;
4+
authToken: string;
5+
method?: "GET" | "POST" | "PUT" | "DELETE";
6+
timeout?: number;
7+
}
8+
9+
export async function fetchWithAuthToken({
10+
endpoint,
11+
body,
12+
authToken,
13+
method = "POST",
14+
timeout = 30000, // 30 second default timeout
15+
}: FetchWithKeyOptions) {
16+
console.log("Sending request to:", endpoint);
17+
console.log("Request method:", method);
18+
console.log("Request body:", body);
19+
20+
const controller = new AbortController();
21+
const timeoutId = setTimeout(() => controller.abort(), timeout);
22+
23+
try {
24+
const response = await fetch(endpoint, {
25+
method,
26+
headers: {
27+
"Content-Type": "application/json",
28+
Accept: "application/json",
29+
Authorization: `Bearer ${authToken}`,
30+
},
31+
body: JSON.stringify(body),
32+
signal: controller.signal,
33+
});
34+
35+
if (!response.ok) {
36+
if (response.status === 504) {
37+
throw new Error("Request timed out. Please try again.");
38+
}
39+
40+
throw new Error(`HTTP error! status: ${response.status}`);
41+
}
42+
43+
const responseData = await response.json();
44+
console.log("Response data:", responseData);
45+
return responseData;
46+
} catch (error: unknown) {
47+
if (error instanceof Error && error.name === "AbortError") {
48+
throw new Error("Request timed out. Please try again.");
49+
}
50+
throw error;
51+
} finally {
52+
clearTimeout(timeoutId);
53+
}
54+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { NextResponse } from "next/server";
2+
import { NEXT_PUBLIC_NEBULA_URL } from "../../../../@/constants/env";
3+
import { getAuthToken } from "../../lib/getAuthToken";
4+
import { fetchWithAuthToken } from "../serverUtils";
5+
6+
export async function POST(request: Request) {
7+
if (!NEXT_PUBLIC_NEBULA_URL) {
8+
throw new Error("Nebula endpoint is not defined");
9+
}
10+
11+
try {
12+
const authToken = await getAuthToken();
13+
14+
if (!authToken) {
15+
return NextResponse.json(
16+
{ error: "Missing Auth token" },
17+
{ status: 401 },
18+
);
19+
}
20+
21+
const body = await request.json();
22+
const { session_id, config = {}, action } = body;
23+
24+
if (action === "create_session") {
25+
const requestBody = {
26+
can_execute: !!config,
27+
config,
28+
};
29+
30+
const data = await fetchWithAuthToken({
31+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session`,
32+
body: requestBody,
33+
authToken,
34+
});
35+
36+
return NextResponse.json(data);
37+
}
38+
39+
if (action === "update_session") {
40+
if (!session_id) {
41+
return NextResponse.json(
42+
{ error: "Missing session_id for update" },
43+
{ status: 400 },
44+
);
45+
}
46+
47+
const requestBody = {
48+
can_execute: !!config,
49+
config,
50+
};
51+
52+
const data = await fetchWithAuthToken({
53+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/${session_id}`,
54+
method: "PUT",
55+
body: requestBody,
56+
authToken,
57+
});
58+
59+
return NextResponse.json(data);
60+
}
61+
62+
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
63+
} catch (error) {
64+
console.error("Session API Error:", error);
65+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
66+
}
67+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use client";
2+
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
3+
import { useState } from "react";
4+
import ExecuteTab from "./components/ExecuteTab";
5+
import { useExecuteConfig } from "./hooks/useExecuteConfig";
6+
7+
export function ChatPage() {
8+
const [executeMessages, setExecuteMessages] = useState<
9+
Array<{ text: string; sender: "user" | "chat" | "error" }>
10+
>([]);
11+
12+
const [executeInputValue, setExecuteInputValue] = useState("");
13+
const [executeIsLoading, setExecuteIsLoading] = useState(false);
14+
const [executeSessionId, setExecuteSessionId] = useState<string | null>(null);
15+
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
16+
const [executeConfig, setExecuteConfig] = useExecuteConfig();
17+
18+
return (
19+
<div className="flex h-screen overflow-hidden bg-background">
20+
{/* Left */}
21+
<aside className="hidden w-[260px] border-r bg-muted/50 md:block">
22+
<div className="p-4">
23+
<div className="flex flex-col gap-3">
24+
<CustomConnectWallet />
25+
</div>
26+
</div>
27+
</aside>
28+
29+
{/* Right */}
30+
<ExecuteTab
31+
messages={executeMessages}
32+
setMessages={setExecuteMessages}
33+
inputValue={executeInputValue}
34+
setInputValue={setExecuteInputValue}
35+
isLoading={executeIsLoading}
36+
setIsLoading={setExecuteIsLoading}
37+
sessionId={executeSessionId}
38+
setSessionId={setExecuteSessionId}
39+
config={executeConfig}
40+
setConfig={setExecuteConfig}
41+
isConfigModalOpen={isConfigModalOpen}
42+
setIsConfigModalOpen={setIsConfigModalOpen}
43+
/>
44+
</div>
45+
);
46+
}

0 commit comments

Comments
 (0)