Skip to content

Commit fd85b80

Browse files
committed
Add Nebula Chat UI
1 parent adda60c commit fd85b80

File tree

15 files changed

+1148
-146
lines changed

15 files changed

+1148
-146
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/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"color": "^4.2.3",
6161
"compare-versions": "^6.1.0",
6262
"date-fns": "4.1.0",
63+
"fetch-event-stream": "^0.1.5",
6364
"flat": "^6.0.1",
6465
"framer-motion": "11.11.17",
6566
"fuse.js": "7.0.0",

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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// TODO - copy the source of this library to dashboard
2+
import { stream } from "fetch-event-stream";
3+
import { NEXT_PUBLIC_NEBULA_URL } from "../../../@/constants/env";
4+
import type { ExecuteConfig } from "./types";
5+
6+
// TODO - ready response as stream
7+
8+
type ChatStreamedResponse =
9+
| {
10+
event: "presence";
11+
data: {
12+
session_id: string;
13+
request_id: string;
14+
agent_type: "user" | "reviewer" | (string & {});
15+
description: string;
16+
};
17+
}
18+
| {
19+
event: "delta";
20+
data: {
21+
v: string;
22+
};
23+
};
24+
25+
type HandleStreamCallback = (res: ChatStreamedResponse) => void;
26+
27+
export async function promptNebula(params: {
28+
message: string;
29+
sessionId: string;
30+
config: ExecuteConfig | null;
31+
can_execute: boolean;
32+
authToken: string;
33+
handleStream: HandleStreamCallback;
34+
onStreamEnd: () => void;
35+
}) {
36+
const events = await stream(`${NEXT_PUBLIC_NEBULA_URL}/chat`, {
37+
method: "POST",
38+
headers: {
39+
Authorization: `Bearer ${params.authToken}`,
40+
"Content-Type": "application/json",
41+
},
42+
body: JSON.stringify({
43+
message: params.message,
44+
user_id: "default-user",
45+
session_id: params.sessionId,
46+
stream: true,
47+
Authorization: `Bearer ${params.authToken}`,
48+
}),
49+
});
50+
51+
for await (const event of events) {
52+
if (!event.data) {
53+
continue;
54+
}
55+
56+
// delta
57+
if (event.event === "delta") {
58+
params.handleStream({
59+
event: "delta",
60+
data: {
61+
v: JSON.parse(event.data).v,
62+
},
63+
});
64+
}
65+
66+
// presence
67+
if (event.event === "presence") {
68+
params.handleStream({
69+
event: "presence",
70+
data: JSON.parse(event.data),
71+
});
72+
}
73+
}
74+
75+
params.onStreamEnd();
76+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
type FetchWithKeyOptions = {
2+
endpoint: string;
3+
authToken: string;
4+
timeout?: number;
5+
} & (
6+
| {
7+
method: "POST" | "PUT" | "DELETE";
8+
body: Record<string, unknown>;
9+
}
10+
| {
11+
method: "GET";
12+
}
13+
);
14+
15+
export async function fetchWithAuthToken(options: FetchWithKeyOptions) {
16+
const timeout = options.timeout || 30000;
17+
18+
const controller = new AbortController();
19+
const timeoutId = setTimeout(() => controller.abort(), timeout);
20+
21+
try {
22+
const response = await fetch(options.endpoint, {
23+
method: options.method,
24+
headers: {
25+
"Content-Type": "application/json",
26+
Accept: "application/json",
27+
Authorization: `Bearer ${options.authToken}`,
28+
},
29+
body: options.method !== "GET" ? JSON.stringify(options.body) : undefined,
30+
signal: controller.signal,
31+
});
32+
33+
if (!response.ok) {
34+
if (response.status === 504) {
35+
throw new Error("Request timed out. Please try again.");
36+
}
37+
38+
throw new Error(`HTTP error! status: ${response.status}`);
39+
}
40+
41+
return response;
42+
} catch (error: unknown) {
43+
if (error instanceof Error && error.name === "AbortError") {
44+
throw new Error("Request timed out. Please try again.");
45+
}
46+
throw error;
47+
} finally {
48+
clearTimeout(timeoutId);
49+
}
50+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { NEXT_PUBLIC_NEBULA_URL } from "../../../@/constants/env";
2+
import { fetchWithAuthToken } from "./fetchWithAuthToken";
3+
import type { ExecuteConfig } from "./types";
4+
5+
// TODO - get the spec for return types on /session POST and PUT
6+
7+
export async function createSession(params: {
8+
authToken: string;
9+
config: ExecuteConfig | null;
10+
}) {
11+
const res = await fetchWithAuthToken({
12+
method: "POST",
13+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session`,
14+
body: {
15+
can_execute: !!params.config,
16+
config: params.config,
17+
},
18+
authToken: params.authToken,
19+
});
20+
21+
if (!res.ok) {
22+
throw new Error("Failed to create session");
23+
}
24+
const data = await res.json();
25+
26+
// TODO - need better type
27+
return data.result as {
28+
id: string;
29+
};
30+
}
31+
32+
export async function updateSession(params: {
33+
authToken: string;
34+
config: ExecuteConfig | null;
35+
sessionId: string;
36+
}) {
37+
const res = await fetchWithAuthToken({
38+
method: "PUT",
39+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/${params.sessionId}`,
40+
body: {
41+
can_execute: !!params.config,
42+
config: params.config,
43+
},
44+
authToken: params.authToken,
45+
});
46+
47+
if (!res.ok) {
48+
throw new Error("Failed to update session");
49+
}
50+
const data = await res.json();
51+
52+
// TODO - need proper types
53+
return data.result as
54+
| {
55+
session_id: string | undefined;
56+
}
57+
| undefined;
58+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
interface BaseExecuteConfig {
2+
mode: "engine" | "session_key" | "webhook" | "client";
3+
}
4+
5+
interface EngineConfig extends BaseExecuteConfig {
6+
mode: "engine";
7+
engine_url: string;
8+
engine_authorization_token: string;
9+
engine_backend_wallet_address: string;
10+
}
11+
12+
interface SessionKeyConfig extends BaseExecuteConfig {
13+
mode: "session_key";
14+
smart_account_address: string;
15+
smart_account_factory_address: string;
16+
smart_account_session_key: string;
17+
}
18+
19+
interface WebhookConfig extends BaseExecuteConfig {
20+
mode: "webhook";
21+
webhook_signing_url: string;
22+
webhook_metadata?: Record<string, unknown>;
23+
webhook_shared_secret?: string;
24+
}
25+
26+
interface ClientConfig extends BaseExecuteConfig {
27+
mode: "client";
28+
}
29+
30+
export type ExecuteConfig =
31+
| EngineConfig
32+
| SessionKeyConfig
33+
| WebhookConfig
34+
| ClientConfig;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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(props: {
8+
authToken: string;
9+
}) {
10+
const [executeMessages, setExecuteMessages] = useState<
11+
Array<{ text: string; sender: "user" | "chat" | "error" | "presence" }>
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+
return (
21+
<div className="flex h-screen overflow-hidden bg-background">
22+
{/* Left */}
23+
<aside className="hidden w-[260px] border-border border-r bg-muted/50 md:block">
24+
<div className="p-4">
25+
<div className="flex flex-col gap-3">
26+
<CustomConnectWallet />
27+
</div>
28+
</div>
29+
</aside>
30+
31+
{/* Right */}
32+
<ExecuteTab
33+
messages={executeMessages}
34+
setMessages={setExecuteMessages}
35+
inputValue={executeInputValue}
36+
setInputValue={setExecuteInputValue}
37+
isLoading={executeIsLoading}
38+
setIsLoading={setExecuteIsLoading}
39+
sessionId={executeSessionId}
40+
setSessionId={setExecuteSessionId}
41+
config={executeConfig}
42+
setConfig={setExecuteConfig}
43+
isConfigModalOpen={isConfigModalOpen}
44+
setIsConfigModalOpen={setIsConfigModalOpen}
45+
authToken={props.authToken}
46+
/>
47+
</div>
48+
);
49+
}

0 commit comments

Comments
 (0)