Skip to content

Commit 4a899b6

Browse files
committed
Add Nebula Chat UI
1 parent 6bd9683 commit 4a899b6

File tree

13 files changed

+1269
-2
lines changed

13 files changed

+1269
-2
lines changed

apps/dashboard/.env.example

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

97-
ANALYTICS_SERVICE_URL=""
97+
ANALYTICS_SERVICE_URL=""
98+
99+
# Nebula Chat
100+
NEBULA_ENDPOINT="" # required
101+
NEBULA_CHAT_ENDPOINT="" # required
102+
NEBULA_SESSION_ENDPOINT="" # required
103+
NEBULA_EXECUTE_ENDPOINT="" # required
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { NextResponse } from "next/server";
2+
import { fetchWithSecretKey } from "../serverUtils";
3+
4+
const NEBULA_CHAT_ENDPOINT = process.env.NEBULA_CHAT_ENDPOINT;
5+
6+
export async function POST(request: Request) {
7+
if (!NEBULA_CHAT_ENDPOINT) {
8+
throw new Error("Chat endpoint is not defined");
9+
}
10+
11+
try {
12+
const body = await request.json();
13+
const { message, user_id, session_id } = body;
14+
const secretKey = request.headers.get("x-thirdweb-key");
15+
16+
if (!message) {
17+
return NextResponse.json(
18+
{ error: "Missing message parameter" },
19+
{ status: 400 },
20+
);
21+
}
22+
if (!secretKey) {
23+
return NextResponse.json(
24+
{ error: "Missing ThirdWeb API key" },
25+
{ status: 401 },
26+
);
27+
}
28+
29+
try {
30+
const data = await fetchWithSecretKey({
31+
endpoint: NEBULA_CHAT_ENDPOINT,
32+
body: { message, user_id, session_id },
33+
secretKey,
34+
timeout: 60000, // 1 minute timeout for chat requests
35+
});
36+
37+
return NextResponse.json(data);
38+
} catch (error: unknown) {
39+
console.error(
40+
"Chat API Error:",
41+
error instanceof Error ? error.stack : error,
42+
);
43+
if (error instanceof Error && error.message.includes("timed out")) {
44+
return NextResponse.json(
45+
{ error: "Request timed out. Please try again." },
46+
{ status: 504 },
47+
);
48+
}
49+
50+
if (error instanceof Error && error.message.includes("401")) {
51+
return NextResponse.json(
52+
{ error: "Invalid Authorization header" },
53+
{ status: 401 },
54+
);
55+
}
56+
return NextResponse.json(
57+
{
58+
error:
59+
error instanceof Error ? error.message : "Failed to fetch from API",
60+
},
61+
{ status: 500 },
62+
);
63+
}
64+
} catch (error) {
65+
console.error(
66+
"Request Error:",
67+
error instanceof Error ? error.stack : error,
68+
);
69+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
70+
}
71+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { NextResponse } from "next/server";
2+
import { fetchWithSecretKey } from "../serverUtils";
3+
4+
export async function POST(request: Request) {
5+
try {
6+
const body = await request.json();
7+
const { message, user_id, session_id, config } = body;
8+
const secretKey = request.headers.get("x-thirdweb-key");
9+
10+
if (!message) {
11+
return NextResponse.json(
12+
{ error: "Missing message parameter" },
13+
{ status: 400 },
14+
);
15+
}
16+
if (!secretKey) {
17+
return NextResponse.json(
18+
{ error: "Missing ThirdWeb API key" },
19+
{ status: 401 },
20+
);
21+
}
22+
23+
const NEBULA_EXECUTE_ENDPOINT = process.env.NEBULA_EXECUTE_ENDPOINT;
24+
if (!NEBULA_EXECUTE_ENDPOINT) {
25+
throw new Error("Execute endpoint is not defined");
26+
}
27+
28+
try {
29+
const data = await fetchWithSecretKey({
30+
endpoint: NEBULA_EXECUTE_ENDPOINT,
31+
body: { message, user_id, session_id, config },
32+
secretKey,
33+
timeout: 120000, // 2 minute timeout for execute requests
34+
});
35+
36+
return NextResponse.json(data);
37+
} catch (error: unknown) {
38+
console.error("Execute API Error:", error);
39+
if (error instanceof Error && error.message.includes("timed out")) {
40+
return NextResponse.json(
41+
{ error: "Request timed out. Please try again." },
42+
{ status: 504 },
43+
);
44+
}
45+
const errorMessage =
46+
error instanceof Error ? error.message : "Failed to fetch from API";
47+
return NextResponse.json({ error: errorMessage }, { status: 500 });
48+
}
49+
} catch (error) {
50+
console.error("Request Error:", error);
51+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
52+
}
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { NextResponse } from "next/server";
2+
3+
export async function POST(request: Request) {
4+
try {
5+
const body = await request.json();
6+
const { message } = body;
7+
8+
if (!message) {
9+
return NextResponse.json(
10+
{ error: "Missing message parameter" },
11+
{ status: 400 },
12+
);
13+
}
14+
15+
const NEBULA_CHAT_ENDPOINT = process.env.NEBULA_CHAT_ENDPOINT;
16+
const DASHBOARD_SECRET_KEY = process.env.DASHBOARD_SECRET_KEY;
17+
18+
try {
19+
if (!NEBULA_CHAT_ENDPOINT) {
20+
throw new Error("Chat endpoint is not defined");
21+
}
22+
if (!DASHBOARD_SECRET_KEY) {
23+
throw new Error("ThirdWeb secret key is not defined");
24+
}
25+
26+
const response = await fetch(NEBULA_CHAT_ENDPOINT, {
27+
method: "POST",
28+
headers: {
29+
"Content-Type": "application/json",
30+
Accept: "application/json",
31+
"x-secret-key": DASHBOARD_SECRET_KEY,
32+
},
33+
body: JSON.stringify({ message }),
34+
});
35+
36+
if (!response.ok) {
37+
throw new Error(`HTTP error! status: ${response.status}`);
38+
}
39+
40+
const data = await response.json();
41+
return NextResponse.json(data);
42+
} catch (error) {
43+
console.error("Proxy error:", error);
44+
return NextResponse.json(
45+
{ error: "Failed to fetch from API" },
46+
{ status: 500 },
47+
);
48+
}
49+
} catch (error) {
50+
console.error("API Error:", error);
51+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
52+
}
53+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
interface FetchWithKeyOptions {
2+
endpoint: string;
3+
body: Record<string, unknown>;
4+
secretKey: string;
5+
method?: "GET" | "POST" | "PUT" | "DELETE";
6+
timeout?: number;
7+
}
8+
9+
export async function fetchWithSecretKey({
10+
endpoint,
11+
body,
12+
secretKey,
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+
"x-secret-key": secretKey,
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+
throw new Error(`HTTP error! status: ${response.status}`);
40+
}
41+
42+
const responseData = await response.json();
43+
console.log("Response data:", responseData);
44+
return responseData;
45+
} catch (error: unknown) {
46+
if (error instanceof Error && error.name === "AbortError") {
47+
throw new Error("Request timed out. Please try again.");
48+
}
49+
throw error;
50+
} finally {
51+
clearTimeout(timeoutId);
52+
}
53+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { NextResponse } from "next/server";
2+
import { fetchWithSecretKey } from "../serverUtils";
3+
4+
const NEBULA_SESSION_ENDPOINT = process.env.NEBULA_SESSION_ENDPOINT;
5+
6+
export async function POST(request: Request) {
7+
try {
8+
const body = await request.json();
9+
const { session_id, config = {}, action } = body;
10+
const secretKey = request.headers.get("x-thirdweb-key");
11+
12+
if (!secretKey) {
13+
return NextResponse.json(
14+
{ error: "Missing ThirdWeb API key" },
15+
{ status: 401 },
16+
);
17+
}
18+
19+
if (!NEBULA_SESSION_ENDPOINT) {
20+
throw new Error("Session endpoint is not defined");
21+
}
22+
23+
if (action === "create_session") {
24+
const requestBody = {
25+
can_execute: !!config,
26+
config,
27+
};
28+
29+
const data = await fetchWithSecretKey({
30+
endpoint: NEBULA_SESSION_ENDPOINT,
31+
body: requestBody,
32+
secretKey,
33+
});
34+
35+
return NextResponse.json(data);
36+
}
37+
38+
if (action === "update_session") {
39+
if (!session_id) {
40+
return NextResponse.json(
41+
{ error: "Missing session_id for update" },
42+
{ status: 400 },
43+
);
44+
}
45+
46+
const requestBody = {
47+
can_execute: !!config,
48+
config,
49+
};
50+
51+
const data = await fetchWithSecretKey({
52+
endpoint: `${NEBULA_SESSION_ENDPOINT}/${session_id}`,
53+
method: "PUT",
54+
body: requestBody,
55+
secretKey,
56+
});
57+
58+
return NextResponse.json(data);
59+
}
60+
61+
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
62+
} catch (error) {
63+
console.error("Session API Error:", error);
64+
return NextResponse.json({ error: "Invalid request" }, { status: 400 });
65+
}
66+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
3+
import { useState } from "react";
4+
import { ClientOnly } from "../../components/ClientOnly/ClientOnly";
5+
import ExecuteTab from "./components/ExecuteTab";
6+
import { ConfigureAPIKeyButton } from "./components/configure-api-key.client";
7+
import { useExecuteConfig } from "./hooks/useExecuteConfig";
8+
9+
export function ChatPage() {
10+
const [executeMessages, setExecuteMessages] = useState<
11+
Array<{ text: string; sender: "user" | "chat" | "error" }>
12+
>([]);
13+
14+
const [executeInputValue, setExecuteInputValue] = useState("");
15+
const [executeIsLoading, setExecuteIsLoading] = useState(false);
16+
const [executeSessionId, setExecuteSessionId] = useState<string | null>(null);
17+
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
18+
const [executeConfig, setExecuteConfig] = useExecuteConfig();
19+
20+
const [apiKey, setApiKey] = useState<string | null>(() => {
21+
try {
22+
return localStorage.getItem("thirdwebApiKey");
23+
} catch {
24+
return null;
25+
}
26+
});
27+
28+
return (
29+
<div className="flex h-screen overflow-hidden bg-background">
30+
{/* Left */}
31+
<aside className="hidden w-[260px] border-r bg-muted/50 md:block">
32+
<div className="p-4">
33+
<div className="flex flex-col gap-3">
34+
<CustomConnectWallet />
35+
<ClientOnly ssr={null}>
36+
<ConfigureAPIKeyButton
37+
apiKey={apiKey || ""}
38+
setApiKey={setApiKey}
39+
/>
40+
</ClientOnly>
41+
</div>
42+
</div>
43+
</aside>
44+
45+
{/* Right */}
46+
<ClientOnly ssr={null} className="flex grow flex-col p-6">
47+
{!apiKey ? (
48+
<div className="flex grow items-center justify-center">
49+
<p className="text-gray-400">
50+
Please enter your thirdweb API key to continue
51+
</p>
52+
</div>
53+
) : (
54+
<ExecuteTab
55+
messages={executeMessages}
56+
setMessages={setExecuteMessages}
57+
inputValue={executeInputValue}
58+
setInputValue={setExecuteInputValue}
59+
isLoading={executeIsLoading}
60+
setIsLoading={setExecuteIsLoading}
61+
sessionId={executeSessionId}
62+
setSessionId={setExecuteSessionId}
63+
config={executeConfig}
64+
setConfig={setExecuteConfig}
65+
isConfigModalOpen={isConfigModalOpen}
66+
setIsConfigModalOpen={setIsConfigModalOpen}
67+
/>
68+
)}
69+
</ClientOnly>
70+
</div>
71+
);
72+
}

0 commit comments

Comments
 (0)