@@ -79,7 +79,10 @@ export function LoginAndOnboardingPage(props: {
}>
-
+
@@ -108,7 +111,7 @@ function LoadingCard() {
}
function PageContent(props: {
- nextPath: string | undefined;
+ redirectPath: string;
account: Account | undefined;
}) {
const [screen, setScreen] = useState<
@@ -127,11 +130,7 @@ function PageContent(props: {
function onComplete() {
setScreen({ id: "complete" });
- if (props.nextPath && isValidRedirectPath(props.nextPath)) {
- router.replace(props.nextPath);
- } else {
- router.replace("/team");
- }
+ router.replace(props.redirectPath);
}
if (connectionStatus === "connecting") {
@@ -148,7 +147,7 @@ function PageContent(props: {
@@ -215,19 +214,6 @@ function CustomConnectEmbed(props: {
);
}
-function isValidRedirectPath(encodedPath: string): boolean {
- try {
- // Decode the URI component
- const decodedPath = decodeURIComponent(encodedPath);
- // ensure the path always starts with a _single_ slash
- // double slash could be interpreted as `//example.com` which is not allowed
- return decodedPath.startsWith("/") && !decodedPath.startsWith("//");
- } catch {
- // If decoding fails, return false
- return false;
- }
-}
-
type AuroraProps = {
size: { width: string; height: string };
pos: { top: string; left: string };
diff --git a/apps/dashboard/src/app/login/isValidEncodedRedirectPath.ts b/apps/dashboard/src/app/login/isValidEncodedRedirectPath.ts
new file mode 100644
index 00000000000..79f8255975f
--- /dev/null
+++ b/apps/dashboard/src/app/login/isValidEncodedRedirectPath.ts
@@ -0,0 +1,12 @@
+export function isValidEncodedRedirectPath(encodedPath: string): boolean {
+ try {
+ // Decode the URI component
+ const decodedPath = decodeURIComponent(encodedPath);
+ // ensure the path always starts with a _single_ slash
+ // double slash could be interpreted as `//example.com` which is not allowed
+ return decodedPath.startsWith("/") && !decodedPath.startsWith("//");
+ } catch {
+ // If decoding fails, return false
+ return false;
+ }
+}
diff --git a/apps/dashboard/src/app/login/loginRedirect.ts b/apps/dashboard/src/app/login/loginRedirect.ts
index f6985a4bdd9..829403ebb97 100644
--- a/apps/dashboard/src/app/login/loginRedirect.ts
+++ b/apps/dashboard/src/app/login/loginRedirect.ts
@@ -1,5 +1,9 @@
import { redirect } from "next/navigation";
-export function loginRedirect(path: string): never {
+export function loginRedirect(path?: string): never {
+ if (!path) {
+ redirect("/login");
+ }
+
redirect(`/login?next=${encodeURIComponent(path)}`);
}
diff --git a/apps/dashboard/src/app/login/page.tsx b/apps/dashboard/src/app/login/page.tsx
index 9d86a8e0224..1364df759bc 100644
--- a/apps/dashboard/src/app/login/page.tsx
+++ b/apps/dashboard/src/app/login/page.tsx
@@ -1,5 +1,6 @@
import { getRawAccount } from "../account/settings/getAccount";
import { LoginAndOnboardingPage } from "./LoginPage";
+import { isValidEncodedRedirectPath } from "./isValidEncodedRedirectPath";
export default async function Page(props: {
searchParams: Promise<{
@@ -15,5 +16,10 @@ export default async function Page(props: {
// if the user is already logged in, wallet is connected and onboarding is complete
// user will be redirected to the next path on the client side without having to do anything
- return
;
+ const redirectPath =
+ nextPath && isValidEncodedRedirectPath(nextPath) ? nextPath : "/team";
+
+ return (
+
+ );
}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/api/chat.ts b/apps/dashboard/src/app/nebula-app/(app)/api/chat.ts
new file mode 100644
index 00000000000..1135e87c3f9
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/api/chat.ts
@@ -0,0 +1,162 @@
+import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
+// TODO - copy the source of this library to dashboard
+import { stream } from "fetch-event-stream";
+import type { SendTransactionOption } from "thirdweb/dist/types/wallets/interfaces/wallet";
+import type { ExecuteConfig } from "./types";
+
+export type ContextFilters = {
+ chainIds?: string[];
+ contractAddresses?: string[];
+};
+
+export async function promptNebula(params: {
+ message: string;
+ sessionId: string;
+ config: ExecuteConfig;
+ authToken: string;
+ handleStream: (res: ChatStreamedResponse) => void;
+ abortController: AbortController;
+ contextFilters: undefined | ContextFilters;
+}) {
+ const body: Record
= {
+ message: params.message,
+ user_id: "default-user",
+ session_id: params.sessionId,
+ stream: true,
+ execute_config: params.config,
+ };
+
+ if (params.contextFilters) {
+ body.context_filter = {
+ chain_ids: params.contextFilters.chainIds || [],
+ contract_addresses: params.contextFilters.contractAddresses || [],
+ };
+ }
+
+ const events = await stream(`${NEXT_PUBLIC_NEBULA_URL}/chat`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${params.authToken}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ signal: params.abortController.signal,
+ });
+
+ for await (const _event of events) {
+ if (!_event.data) {
+ continue;
+ }
+
+ const event = _event as ChatStreamedEvent;
+
+ switch (event.event) {
+ case "delta": {
+ params.handleStream({
+ event: "delta",
+ data: {
+ v: JSON.parse(event.data).v,
+ },
+ });
+ break;
+ }
+
+ case "presence": {
+ params.handleStream({
+ event: "presence",
+ data: JSON.parse(event.data),
+ });
+ break;
+ }
+
+ case "action": {
+ const data = JSON.parse(event.data);
+
+ if (data.type === "sign_transaction") {
+ let txData = null;
+
+ try {
+ const parsedTxData = JSON.parse(data.data);
+ if (
+ parsedTxData !== null &&
+ typeof parsedTxData === "object" &&
+ parsedTxData.chainId
+ ) {
+ txData = parsedTxData;
+ }
+ } catch (e) {
+ console.error("failed to parse action data", e);
+ }
+
+ params.handleStream({
+ event: "action",
+ type: "sign_transaction",
+ data: txData,
+ });
+ }
+
+ break;
+ }
+
+ case "init": {
+ const data = JSON.parse(event.data);
+ params.handleStream({
+ event: "init",
+ data: {
+ session_id: data.session_id,
+ request_id: data.request_id,
+ },
+ });
+ break;
+ }
+ }
+ }
+}
+
+type ChatStreamedResponse =
+ | {
+ event: "init";
+ data: {
+ session_id: string;
+ request_id: string;
+ };
+ }
+ | {
+ event: "presence";
+ data: {
+ session_id: string;
+ request_id: string;
+ source: "user" | "reviewer" | (string & {});
+ data: string;
+ };
+ }
+ | {
+ event: "delta";
+ data: {
+ v: string;
+ };
+ }
+ | {
+ event: "action";
+ type: "sign_transaction" & (string & {});
+ data: SendTransactionOption;
+ };
+
+type ChatStreamedEvent =
+ | {
+ event: "init";
+ data: string;
+ }
+ | {
+ event: "presence";
+ data: string;
+ }
+ | {
+ event: "delta";
+ data: string;
+ }
+ | {
+ event: "action";
+ type: "sign_transaction" & (string & {});
+ data: string;
+ };
diff --git a/apps/dashboard/src/app/nebula-app/(app)/api/feedback.ts b/apps/dashboard/src/app/nebula-app/(app)/api/feedback.ts
new file mode 100644
index 00000000000..a1104ff98e1
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/api/feedback.ts
@@ -0,0 +1,25 @@
+import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
+import { fetchWithAuthToken } from "../../../../utils/fetchWithAuthToken";
+
+export async function submitFeedback(params: {
+ authToken: string;
+ sessionId: string;
+ requestId: string;
+ rating: "good" | "bad" | "neutral";
+}) {
+ const res = await fetchWithAuthToken({
+ method: "POST",
+ endpoint: `${NEXT_PUBLIC_NEBULA_URL}/feedback`,
+ body: {
+ session_id: params.sessionId,
+ request_id: params.requestId,
+ feedback_rating:
+ params.rating === "good" ? 1 : params.rating === "bad" ? -1 : 0,
+ },
+ authToken: params.authToken,
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to submit feedback");
+ }
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/api/session.ts b/apps/dashboard/src/app/nebula-app/(app)/api/session.ts
new file mode 100644
index 00000000000..23581e990cc
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/api/session.ts
@@ -0,0 +1,131 @@
+import { NEXT_PUBLIC_NEBULA_URL } from "@/constants/env";
+import { fetchWithAuthToken } from "../../../../utils/fetchWithAuthToken";
+import type { ContextFilters } from "./chat";
+import type { ExecuteConfig, SessionInfo, TruncatedSessionInfo } from "./types";
+
+// TODO - get the spec for return types on /session POST and PUT
+
+export async function createSession(params: {
+ authToken: string;
+ config: ExecuteConfig | null;
+ contextFilters: ContextFilters | undefined;
+}) {
+ const body: Record = {
+ can_execute: !!params.config,
+ };
+ if (params.config) {
+ body.execute_config = params.config;
+ }
+
+ if (params.contextFilters) {
+ body.context_filter = {
+ chain_ids: params.contextFilters.chainIds || [],
+ contract_addresses: params.contextFilters.contractAddresses || [],
+ };
+ }
+
+ const res = await fetchWithAuthToken({
+ method: "POST",
+ endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session`,
+ body: body,
+ authToken: params.authToken,
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to create session");
+ }
+ const data = await res.json();
+
+ return data.result as SessionInfo;
+}
+
+export async function updateSession(params: {
+ authToken: string;
+ config: ExecuteConfig | null;
+ sessionId: string;
+ contextFilters: ContextFilters | undefined;
+}) {
+ const body: Record = {
+ can_execute: !!params.config,
+ };
+ if (params.config) {
+ body.execute_config = params.config;
+ }
+
+ if (params.contextFilters) {
+ body.context_filter = {
+ chain_ids: params.contextFilters.chainIds || [],
+ contract_addresses: params.contextFilters.contractAddresses || [],
+ };
+ }
+
+ const res = await fetchWithAuthToken({
+ method: "PUT",
+ endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/${params.sessionId}`,
+ body: body,
+ authToken: params.authToken,
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to update session");
+ }
+ const data = await res.json();
+
+ return data.result as SessionInfo;
+}
+
+export async function deleteSession(params: {
+ authToken: string;
+ sessionId: string;
+}) {
+ const res = await fetchWithAuthToken({
+ method: "DELETE",
+ endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/${params.sessionId}`,
+ authToken: params.authToken,
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to update session");
+ }
+ const data = await res.json();
+
+ return data.result as {
+ id: string;
+ deleted_at: string;
+ };
+}
+
+export async function getSessions(params: {
+ authToken: string;
+}) {
+ const res = await fetchWithAuthToken({
+ method: "GET",
+ endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/list`,
+ authToken: params.authToken,
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to update session");
+ }
+ const data = await res.json();
+
+ return data.result as TruncatedSessionInfo[];
+}
+
+export async function getSessionById(params: {
+ authToken: string;
+ sessionId: string;
+}) {
+ const res = await fetchWithAuthToken({
+ method: "GET",
+ endpoint: `${NEXT_PUBLIC_NEBULA_URL}/session/${params.sessionId}`,
+ authToken: params.authToken,
+ });
+
+ if (!res.ok) {
+ throw new Error("Failed to update session");
+ }
+ const data = await res.json();
+
+ return data.result as SessionInfo;
+}
diff --git a/apps/dashboard/src/app/nebula-app/(app)/api/types.ts b/apps/dashboard/src/app/nebula-app/(app)/api/types.ts
new file mode 100644
index 00000000000..e2663ac95d7
--- /dev/null
+++ b/apps/dashboard/src/app/nebula-app/(app)/api/types.ts
@@ -0,0 +1,49 @@
+type EngineConfig = {
+ mode: "engine";
+ engine_url: string;
+ engine_authorization_token: string;
+ engine_backend_wallet_address: string;
+};
+
+type SessionKeyConfig = {
+ mode: "session_key";
+ smart_account_address: string;
+ smart_account_factory_address: string;
+ smart_account_session_key: string;
+};
+
+type ClientConfig = {
+ mode: "client";
+ signer_wallet_address: string;
+};
+
+export type ExecuteConfig = EngineConfig | SessionKeyConfig | ClientConfig;
+
+export type SessionInfo = {
+ id: string;
+ account_id: string;
+ modal_name: string;
+ archive_at: string | null;
+ can_execute: boolean;
+ execute_config: ExecuteConfig | null;
+ created_at: string;
+ deleted_at: string | null;
+ history: Array<{
+ role: "user" | "assistant"; // role: action is coming up
+ content: string;
+ timestamp: number;
+ }> | null;
+ updated_at: string;
+ archived_at: string | null;
+ title: string | null;
+ is_public: boolean | null;
+ // memory
+ // action: array