Skip to content

Commit d07b823

Browse files
committed
Add Nebula Chat UI
1 parent 46c3abb commit d07b823

File tree

20 files changed

+1153
-162
lines changed

20 files changed

+1153
-162
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/@/components/ui/code/code.client.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type CodeProps = {
1414
scrollableClassName?: string;
1515
loadingClassName?: string;
1616
keepPreviousDataOnCodeChange?: boolean;
17+
ignoreFormattingErrors?: boolean;
1718
};
1819

1920
export const CodeClient: React.FC<CodeProps> = ({
@@ -23,10 +24,14 @@ export const CodeClient: React.FC<CodeProps> = ({
2324
scrollableClassName,
2425
loadingClassName,
2526
keepPreviousDataOnCodeChange = false,
27+
ignoreFormattingErrors,
2628
}) => {
2729
const codeQuery = useQuery({
2830
queryKey: ["html", code],
29-
queryFn: () => getCodeHtml(code, lang),
31+
queryFn: () =>
32+
getCodeHtml(code, lang, {
33+
ignoreFormattingErrors: ignoreFormattingErrors,
34+
}),
3035
placeholderData: keepPreviousDataOnCodeChange
3136
? keepPreviousData
3237
: undefined,

apps/dashboard/src/@/components/ui/code/getCodeHtml.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,28 @@ function isPrettierSupportedLang(lang: BundledLanguage) {
1616
);
1717
}
1818

19-
export async function getCodeHtml(code: string, lang: BundledLanguage) {
19+
export async function getCodeHtml(
20+
code: string,
21+
lang: BundledLanguage,
22+
options?: {
23+
ignoreFormattingErrors?: boolean;
24+
},
25+
) {
2026
const formattedCode = isPrettierSupportedLang(lang)
2127
? await format(code, {
2228
parser: "babel-ts",
2329
plugins: [parserBabel, estree],
2430
printWidth: 60,
2531
}).catch((e) => {
26-
console.error(e);
27-
console.error("Failed to format code");
28-
console.log({
29-
code,
30-
lang,
31-
});
32+
if (!options?.ignoreFormattingErrors) {
33+
console.error(e);
34+
console.error("Failed to format code");
35+
console.log({
36+
code,
37+
lang,
38+
});
39+
}
40+
3241
return code;
3342
})
3443
: code;

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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
source: "user" | "reviewer" | (string & {});
15+
data: string;
16+
};
17+
}
18+
| {
19+
event: "delta";
20+
data: {
21+
v: string;
22+
};
23+
}
24+
| {
25+
event: "action";
26+
type: "sign_transaction";
27+
data: string;
28+
}
29+
| {
30+
event: "action";
31+
// other types of actions
32+
type: string & {};
33+
};
34+
35+
type HandleStreamCallback = (res: ChatStreamedResponse) => void;
36+
37+
export async function promptNebula(params: {
38+
message: string;
39+
sessionId: string;
40+
config: ExecuteConfig;
41+
authToken: string;
42+
handleStream: HandleStreamCallback;
43+
}) {
44+
// TODO: properly type the body
45+
const body: Record<string, string | boolean | object> = {
46+
message: params.message,
47+
user_id: "default-user",
48+
session_id: params.sessionId,
49+
stream: true,
50+
Authorization: `Bearer ${params.authToken}`,
51+
};
52+
53+
if (params.config.mode === "client") {
54+
body.execute = {
55+
type: "client",
56+
signer_wallet_address: params.config.signer_wallet_address,
57+
};
58+
}
59+
60+
const events = await stream(`${NEXT_PUBLIC_NEBULA_URL}/chat`, {
61+
method: "POST",
62+
headers: {
63+
Authorization: `Bearer ${params.authToken}`,
64+
"Content-Type": "application/json",
65+
},
66+
body: JSON.stringify(body),
67+
});
68+
69+
for await (const event of events) {
70+
if (!event.data) {
71+
continue;
72+
}
73+
74+
// delta
75+
if (event.event === "delta") {
76+
params.handleStream({
77+
event: "delta",
78+
data: {
79+
v: JSON.parse(event.data).v,
80+
},
81+
});
82+
}
83+
84+
// presence
85+
if (event.event === "presence") {
86+
params.handleStream({
87+
event: "presence",
88+
data: JSON.parse(event.data),
89+
});
90+
}
91+
92+
// action
93+
if (event.event === "action") {
94+
params.handleStream({
95+
event: "action",
96+
type: JSON.parse(event.data).type,
97+
data: JSON.parse(event.data).data,
98+
});
99+
}
100+
}
101+
}
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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
interface BaseExecuteConfig {
2+
mode: "engine" | "session_key" | "webhook" | "client";
3+
}
4+
5+
export 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+
export 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+
export 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+
signer_wallet_address: string;
29+
}
30+
31+
export type ExecuteConfig =
32+
| EngineConfig
33+
| SessionKeyConfig
34+
| WebhookConfig
35+
| ClientConfig;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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+
accountAddress: string;
10+
}) {
11+
const [executeMessages, setExecuteMessages] = useState<
12+
Array<{ text: string; sender: "user" | "chat" | "error" | "presence" }>
13+
>([]);
14+
15+
const [executeConfig, setExecuteConfig] = useExecuteConfig();
16+
17+
return (
18+
<div className="flex h-screen overflow-hidden bg-background">
19+
{/* Left */}
20+
<aside className="hidden w-[260px] border-border border-r bg-muted/50 md:block">
21+
<div className="p-4">
22+
<div className="flex flex-col gap-3">
23+
<CustomConnectWallet />
24+
</div>
25+
</div>
26+
</aside>
27+
28+
{/* Right */}
29+
<ExecuteTab
30+
messages={executeMessages}
31+
setMessages={setExecuteMessages}
32+
config={
33+
executeConfig || {
34+
mode: "client",
35+
signer_wallet_address: props.accountAddress,
36+
}
37+
}
38+
setConfig={setExecuteConfig}
39+
authToken={props.authToken}
40+
accountAddress={props.accountAddress}
41+
/>
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)