Skip to content

Commit d7314d4

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

File tree

26 files changed

+1727
-162
lines changed

26 files changed

+1727
-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: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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: "init";
11+
data: {
12+
session_id: string;
13+
request_id: string;
14+
};
15+
}
16+
| {
17+
event: "presence";
18+
data: {
19+
session_id: string;
20+
request_id: string;
21+
source: "user" | "reviewer" | (string & {});
22+
data: string;
23+
};
24+
}
25+
| {
26+
event: "delta";
27+
data: {
28+
v: string;
29+
};
30+
}
31+
| {
32+
event: "action";
33+
type: "sign_transaction";
34+
data: string;
35+
}
36+
| {
37+
event: "action";
38+
// other types of actions
39+
type: string & {};
40+
};
41+
42+
type HandleStreamCallback = (res: ChatStreamedResponse) => void;
43+
44+
export async function promptNebula(params: {
45+
message: string;
46+
sessionId: string;
47+
config: ExecuteConfig;
48+
authToken: string;
49+
handleStream: HandleStreamCallback;
50+
}) {
51+
// TODO: properly type the body
52+
const body: Record<string, string | boolean | object> = {
53+
message: params.message,
54+
user_id: "default-user",
55+
session_id: params.sessionId,
56+
stream: true,
57+
Authorization: `Bearer ${params.authToken}`,
58+
};
59+
60+
if (params.config.mode === "client") {
61+
body.execute = {
62+
type: "client",
63+
signer_wallet_address: params.config.signer_wallet_address,
64+
};
65+
}
66+
67+
const events = await stream(`${NEXT_PUBLIC_NEBULA_URL}/chat`, {
68+
method: "POST",
69+
headers: {
70+
Authorization: `Bearer ${params.authToken}`,
71+
"Content-Type": "application/json",
72+
},
73+
body: JSON.stringify(body),
74+
});
75+
76+
for await (const event of events) {
77+
if (!event.data) {
78+
continue;
79+
}
80+
81+
// delta
82+
if (event.event === "delta") {
83+
params.handleStream({
84+
event: "delta",
85+
data: {
86+
v: JSON.parse(event.data).v,
87+
},
88+
});
89+
}
90+
91+
// presence
92+
if (event.event === "presence") {
93+
params.handleStream({
94+
event: "presence",
95+
data: JSON.parse(event.data),
96+
});
97+
}
98+
99+
// action
100+
if (event.event === "action") {
101+
const data = JSON.parse(event.data);
102+
params.handleStream({
103+
event: "action",
104+
type: data.type,
105+
data: data.data,
106+
});
107+
}
108+
109+
if (event.event === "init") {
110+
const data = JSON.parse(event.data);
111+
params.handleStream({
112+
event: "init",
113+
data: {
114+
session_id: data.session_id,
115+
request_id: data.request_id,
116+
},
117+
});
118+
}
119+
}
120+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
2+
import { fetchWithAuthToken } from "./fetchWithAuthToken";
3+
4+
export async function submitFeedback(params: {
5+
authToken: string;
6+
sessionId: string;
7+
requestId: string;
8+
rating: "good" | "bad" | "neutral";
9+
}) {
10+
const res = await fetchWithAuthToken({
11+
method: "POST",
12+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/feedback`,
13+
body: {
14+
session_id: params.sessionId,
15+
request_id: params.requestId,
16+
feedback_rating:
17+
params.rating === "good" ? 1 : params.rating === "bad" ? -1 : 0,
18+
},
19+
authToken: params.authToken,
20+
});
21+
22+
if (!res.ok) {
23+
throw new Error("Failed to submit feedback");
24+
}
25+
}
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";
8+
body: Record<string, unknown>;
9+
}
10+
| {
11+
method: "GET" | "DELETE";
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: "body" in options ? 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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
2+
import { fetchWithAuthToken } from "./fetchWithAuthToken";
3+
import type { ExecuteConfig, SessionInfo } 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+
}
59+
60+
export async function deleteSession(params: {
61+
authToken: string;
62+
sessionId: string;
63+
}) {
64+
const res = await fetchWithAuthToken({
65+
method: "DELETE",
66+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/${params.sessionId}`,
67+
authToken: params.authToken,
68+
});
69+
70+
if (!res.ok) {
71+
throw new Error("Failed to update session");
72+
}
73+
const data = await res.json();
74+
75+
return data.result as
76+
| {
77+
session_id: string | undefined;
78+
}
79+
| undefined;
80+
}
81+
82+
export async function getSessions(params: {
83+
authToken: string;
84+
}) {
85+
const res = await fetchWithAuthToken({
86+
method: "GET",
87+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/list`,
88+
authToken: params.authToken,
89+
});
90+
91+
if (!res.ok) {
92+
throw new Error("Failed to update session");
93+
}
94+
const data = await res.json();
95+
96+
return data.result as SessionInfo[];
97+
}
98+
99+
export async function getSessionById(params: {
100+
authToken: string;
101+
sessionId: string;
102+
}) {
103+
const res = await fetchWithAuthToken({
104+
method: "GET",
105+
endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/${params.sessionId}`,
106+
authToken: params.authToken,
107+
});
108+
109+
if (!res.ok) {
110+
throw new Error("Failed to update session");
111+
}
112+
const data = await res.json();
113+
114+
return data.result as SessionInfo;
115+
}

0 commit comments

Comments
 (0)