diff --git a/apps/dashboard/src/app/nebula-app/(app)/api/chat.ts b/apps/dashboard/src/app/nebula-app/(app)/api/chat.ts index 1135e87c3f9..262103605e9 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/api/chat.ts +++ b/apps/dashboard/src/app/nebula-app/(app)/api/chat.ts @@ -7,6 +7,7 @@ import type { ExecuteConfig } from "./types"; export type ContextFilters = { chainIds?: string[]; contractAddresses?: string[]; + walletAddresses?: string[]; }; export async function promptNebula(params: { diff --git a/apps/dashboard/src/app/nebula-app/(app)/api/session.ts b/apps/dashboard/src/app/nebula-app/(app)/api/session.ts index c4cadb128ee..12052c24810 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/api/session.ts +++ b/apps/dashboard/src/app/nebula-app/(app)/api/session.ts @@ -27,6 +27,7 @@ export async function createSession(params: { body.context_filter = { chain_ids: params.contextFilters.chainIds || [], contract_addresses: params.contextFilters.contractAddresses || [], + wallet_addresses: params.contextFilters.walletAddresses || [], }; } @@ -62,6 +63,7 @@ export async function updateSession(params: { body.context_filter = { chain_ids: params.contextFilters.chainIds || [], contract_addresses: params.contextFilters.contractAddresses || [], + wallet_addresses: params.contextFilters.walletAddresses || [], }; } diff --git a/apps/dashboard/src/app/nebula-app/(app)/api/types.ts b/apps/dashboard/src/app/nebula-app/(app)/api/types.ts index 638d40ecb01..315d1ffbd37 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/api/types.ts +++ b/apps/dashboard/src/app/nebula-app/(app)/api/types.ts @@ -19,6 +19,12 @@ type ClientConfig = { export type ExecuteConfig = EngineConfig | SessionKeyConfig | ClientConfig; +type SessionContextFilter = { + chain_ids: string[] | null; + contract_addresses: string[] | null; + wallet_addresses: string[] | null; +}; + export type SessionInfo = { id: string; account_id: string; @@ -37,10 +43,7 @@ export type SessionInfo = { archived_at: string | null; title: string | null; is_public: boolean | null; - context_filter: { - chain_ids: string[]; - contract_addresses: string[]; - } | null; + context_filter: SessionContextFilter | null; // memory // action: array | null; <-- type of this is not available on https://nebula-api.thirdweb-dev.com/docs#/default/get_session_session__session_id__get }; @@ -50,10 +53,7 @@ export type UpdatedSessionInfo = { modal_name: string; account_id: string; execute_config: ExecuteConfig | null; - context_filter: { - chain_ids: string[]; - contract_addresses: string[]; - } | null; + context_filter: SessionContextFilter | null; }; export type DeletedSessionInfo = { diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx index 92dc3e64aef..498c7eddb09 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ChatPageContent.tsx @@ -5,7 +5,8 @@ import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow"; import { useThirdwebClient } from "@/constants/thirdweb.client"; import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; import { useMutation } from "@tanstack/react-query"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useActiveAccount } from "thirdweb/react"; import { type ContextFilters, promptNebula } from "../api/chat"; import { createSession, updateSession } from "../api/session"; import type { ExecuteConfig, SessionInfo } from "../api/types"; @@ -22,6 +23,7 @@ export function ChatPageContent(props: { type: "landing" | "new-chat"; account: Account; }) { + const address = useActiveAccount()?.address; const client = useThirdwebClient(); const [userHasSubmittedMessage, setUserHasSubmittedMessage] = useState(false); const [messages, setMessages] = useState>(() => { @@ -36,17 +38,47 @@ export function ChatPageContent(props: { }); const [_config, setConfig] = useState(); - const [contextFilters, setContextFilters] = useState< + const [hasUserUpdatedContextFilters, setHasUserUpdatedContextFilters] = + useState(false); + + const [_contextFilters, _setContextFilters] = useState< ContextFilters | undefined >(() => { const contextFilterRes = props.session?.context_filter; - if (contextFilterRes) { + const value: ContextFilters = { + chainIds: contextFilterRes?.chain_ids || undefined, + contractAddresses: contextFilterRes?.contract_addresses || undefined, + walletAddresses: contextFilterRes?.wallet_addresses || undefined, + }; + + return value; + }); + + function setContextFilters(filters: ContextFilters | undefined) { + _setContextFilters(filters); + setHasUserUpdatedContextFilters(true); + } + + const isNewSession = !props.session; + + // if this is a new session, user has not manually updated context filters + // and no wallet address is set in context filters, add the current wallet address + const contextFilters = useMemo(() => { + if ( + isNewSession && + !hasUserUpdatedContextFilters && + address && + (!_contextFilters?.walletAddresses || + _contextFilters.walletAddresses.length === 0) + ) { return { - chainIds: contextFilterRes.chain_ids, - contractAddresses: contextFilterRes.contract_addresses, + ..._contextFilters, + walletAddresses: [address], }; } - }); + + return _contextFilters; + }, [_contextFilters, address, isNewSession, hasUserUpdatedContextFilters]); const config = _config || { mode: "client", @@ -293,7 +325,7 @@ export function ChatPageContent(props: { return (
-
+
+
+ + + + + + + + + + +
diff --git a/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx b/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx index 29af4ac1184..d2f9852fbfe 100644 --- a/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx +++ b/apps/dashboard/src/app/nebula-app/(app)/components/ContextFilters.tsx @@ -1,6 +1,7 @@ "use client"; import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -42,14 +43,46 @@ export default function ContextFiltersButton(props: { mutationFn: props.updateContextFilters, }); + const chainIds = props.contextFilters?.chainIds; + const contractAddresses = props.contextFilters?.contractAddresses; + const walletAddresses = props.contextFilters?.walletAddresses; + return ( - @@ -74,6 +107,18 @@ export default function ContextFiltersButton(props: { ); } +const commaSeparateListOfAddresses = z.string().refine( + (s) => { + if (s.trim() === "") { + return true; + } + return s.split(",").every((s) => isAddress(s.trim())); + }, + { + message: "Must be a comma-separated list of valid addresses", + }, +); + const formSchema = z.object({ chainIds: z.string().refine( (s) => { @@ -89,18 +134,8 @@ const formSchema = z.object({ message: "Chain IDs must be a comma-separated list of integers", }, ), - contractAddresses: z.string().refine( - (s) => { - if (s.trim() === "") { - return true; - } - return s.trim().split(",").every(isAddress); - }, - { - message: - "Contract addresses must be a comma-separated list of valid addresses", - }, - ), + contractAddresses: commaSeparateListOfAddresses, + walletAddresses: commaSeparateListOfAddresses, }); function ContextFilterDialogContent(props: { @@ -117,25 +152,31 @@ function ContextFilterDialogContent(props: { contractAddresses: props.contextFilters?.contractAddresses ? props.contextFilters.contractAddresses.join(",") : "", + walletAddresses: props.contextFilters?.walletAddresses + ? props.contextFilters.walletAddresses.join(",") + : "", }, reValidateMode: "onChange", }); function onSubmit(values: z.infer) { - const { chainIds, contractAddresses } = values; - const chainIdsArray = chainIds.trim().split(",").filter(Boolean); + const { chainIds, contractAddresses, walletAddresses } = values; + + const chainIdsArray = chainIds.split(",").filter((id) => id.trim()); + const contractAddressesArray = contractAddresses - .trim() .split(",") - .filter(Boolean); - if (chainIdsArray.length === 0 && contractAddressesArray.length === 0) { - props.updateFilters(undefined); - } else { - props.updateFilters({ - chainIds: chainIdsArray, - contractAddresses: contractAddressesArray, - }); - } + .filter((v) => v.trim()); + + const walletAddressesArray = walletAddresses + .split(",") + .filter((v) => v.trim()); + + props.updateFilters({ + chainIds: chainIdsArray, + contractAddresses: contractAddressesArray, + walletAddresses: walletAddressesArray, + }); } return ( @@ -195,6 +236,27 @@ function ContextFilterDialogContent(props: { )} /> + + ( + + Wallet Addresses + + + + + Comma separated list of wallet addresses + + + + )} + />
diff --git a/apps/dashboard/src/app/nebula-app/opengraph-image.png b/apps/dashboard/src/app/nebula-app/opengraph-image.png new file mode 100644 index 00000000000..6c632879f47 Binary files /dev/null and b/apps/dashboard/src/app/nebula-app/opengraph-image.png differ