From 7d3020577fac280dbaed842f0df05409524df623 Mon Sep 17 00:00:00 2001 From: CTO Agent Date: Mon, 6 Apr 2026 20:14:23 +0000 Subject: [PATCH 01/20] feat: pass email query param to API for accountId override Read email query param from URL on chat/landing pages and thread it through the component tree to include in the /api/chat request body. Co-Authored-By: Paperclip --- app/chat/page.tsx | 6 ++++-- app/page.tsx | 6 ++++-- components/Home/HomePage.tsx | 4 +++- components/VercelChat/chat.tsx | 7 ++++--- hooks/useVercelChat.ts | 5 ++++- providers/VercelChatProvider.tsx | 3 +++ 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/chat/page.tsx b/app/chat/page.tsx index ef49f5474..13b9edbbd 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -10,12 +10,14 @@ interface ChatPageProps { export default async function ChatPage({ searchParams }: ChatPageProps) { const id = generateUUID(); - const initialMessage = (await searchParams)?.q as string; + const params = await searchParams; + const initialMessage = params?.q as string; const initialMessages = getMessages(initialMessage); + const email = params?.email as string | undefined; return (
- +
); } diff --git a/app/page.tsx b/app/page.tsx index 979fff06c..7a0b63608 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,8 +10,10 @@ interface ChatPageProps { export default async function Home({ searchParams }: ChatPageProps) { const id = generateUUID(); - const initialMessage = (await searchParams)?.q as string; + const params = await searchParams; + const initialMessage = params?.q as string; const initialMessages = getMessages(initialMessage); + const email = params?.email as string | undefined; - return ; + return ; } diff --git a/components/Home/HomePage.tsx b/components/Home/HomePage.tsx index 325321da0..4bc200f9d 100644 --- a/components/Home/HomePage.tsx +++ b/components/Home/HomePage.tsx @@ -8,9 +8,11 @@ import { UIMessage } from "ai"; const HomePage = ({ id, initialMessages, + email, }: { id: string; initialMessages?: UIMessage[]; + email?: string; }) => { const { setFrameReady, isFrameReady } = useMiniKit(); @@ -22,7 +24,7 @@ const HomePage = ({ return (
- +
); }; diff --git a/components/VercelChat/chat.tsx b/components/VercelChat/chat.tsx index 64059d856..ba577d232 100644 --- a/components/VercelChat/chat.tsx +++ b/components/VercelChat/chat.tsx @@ -25,14 +25,15 @@ interface ChatProps { id: string; reportId?: string; initialMessages?: UIMessage[]; + email?: string; } -export function Chat({ id, reportId, initialMessages }: ChatProps) { +export function Chat({ id, reportId, initialMessages, email }: ChatProps) { const { selectedOrgId } = useOrganization(); const providerKey = `${id}-${selectedOrgId ?? "personal"}`; - + return ( - + ); diff --git a/hooks/useVercelChat.ts b/hooks/useVercelChat.ts index 5990ba669..e16f44146 100644 --- a/hooks/useVercelChat.ts +++ b/hooks/useVercelChat.ts @@ -30,6 +30,7 @@ interface UseVercelChatProps { initialMessages?: UIMessage[]; attachments?: FileUIPart[]; textAttachments?: TextAttachment[]; + email?: string; } /** @@ -42,6 +43,7 @@ export function useVercelChat({ initialMessages, attachments = [], textAttachments = [], + email, }: UseVercelChatProps) { const { userData } = useUserProvider(); const { selectedArtist } = useArtistProvider(); @@ -169,9 +171,10 @@ export function useVercelChat({ artistId, // Only include organizationId if it's not null (schema expects string | undefined) ...(organizationId && { organizationId }), + ...(email && { email }), model, }), - [id, artistId, organizationId, model], + [id, artistId, organizationId, email, model], ); const { messages, status, stop, sendMessage, setMessages, regenerate } = diff --git a/providers/VercelChatProvider.tsx b/providers/VercelChatProvider.tsx index 74fe0da14..96027ba0a 100644 --- a/providers/VercelChatProvider.tsx +++ b/providers/VercelChatProvider.tsx @@ -59,6 +59,7 @@ interface VercelChatProviderProps { children: ReactNode; chatId: string; initialMessages?: UIMessage[]; + email?: string; } /** @@ -68,6 +69,7 @@ export function VercelChatProvider({ children, chatId, initialMessages, + email, }: VercelChatProviderProps) { const { attachments, @@ -117,6 +119,7 @@ export function VercelChatProvider({ initialMessages, attachments, textAttachments, + email, }); const reload = useCallback(() => { From 519f15af14ad1f4ad1b4008a3af3f1947c4a8894 Mon Sep 17 00:00:00 2001 From: CTO Agent Date: Mon, 6 Apr 2026 21:03:56 +0000 Subject: [PATCH 02/20] fix: handle array-valued query params safely Use Array.isArray checks instead of type assertions for q and email search params to handle duplicate query param edge case. Co-Authored-By: Paperclip --- app/chat/page.tsx | 4 ++-- app/page.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 13b9edbbd..24c2ecced 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -11,9 +11,9 @@ interface ChatPageProps { export default async function ChatPage({ searchParams }: ChatPageProps) { const id = generateUUID(); const params = await searchParams; - const initialMessage = params?.q as string; + const initialMessage = Array.isArray(params?.q) ? params.q[0] : params?.q; const initialMessages = getMessages(initialMessage); - const email = params?.email as string | undefined; + const email = Array.isArray(params?.email) ? params.email[0] : params?.email; return (
diff --git a/app/page.tsx b/app/page.tsx index 7a0b63608..79634cd98 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -11,9 +11,9 @@ interface ChatPageProps { export default async function Home({ searchParams }: ChatPageProps) { const id = generateUUID(); const params = await searchParams; - const initialMessage = params?.q as string; + const initialMessage = Array.isArray(params?.q) ? params.q[0] : params?.q; const initialMessages = getMessages(initialMessage); - const email = params?.email as string | undefined; + const email = Array.isArray(params?.email) ? params.email[0] : params?.email; return ; } From 7cc0ddb11f64434881c7586f6bf3426db53eda81 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 16:43:28 -0500 Subject: [PATCH 03/20] =?UTF-8?q?refactor:=20resolve=20email=20=E2=86=92?= =?UTF-8?q?=20accountId=20client-side=20via=20GET=20/api/accounts/{email}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of passing email to /api/chat, the Chat component resolves email → accountId using the new accounts endpoint, then passes accountId in the chat body using the existing override mechanism. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/VercelChat/chat.tsx | 11 ++++++-- hooks/useEmailAccountId.ts | 44 ++++++++++++++++++++++++++++++++ hooks/useVercelChat.ts | 8 +++--- providers/VercelChatProvider.tsx | 23 ++++++++++------- 4 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 hooks/useEmailAccountId.ts diff --git a/components/VercelChat/chat.tsx b/components/VercelChat/chat.tsx index ba577d232..ec7f3bd51 100644 --- a/components/VercelChat/chat.tsx +++ b/components/VercelChat/chat.tsx @@ -20,6 +20,7 @@ import FileDragOverlay from "./FileDragOverlay"; import { Loader } from "lucide-react"; import { memo } from "react"; import { useOrganization } from "@/providers/OrganizationProvider"; +import { useEmailAccountId } from "@/hooks/useEmailAccountId"; interface ChatProps { id: string; @@ -30,10 +31,16 @@ interface ChatProps { export function Chat({ id, reportId, initialMessages, email }: ChatProps) { const { selectedOrgId } = useOrganization(); + const accountIdOverride = useEmailAccountId(email); const providerKey = `${id}-${selectedOrgId ?? "personal"}`; return ( - + ); @@ -84,7 +91,7 @@ function ChatContentMemoized({ "px-4 md:px-0 pb-4 flex flex-col h-full items-center w-full relative", { "justify-between": messages.length > 0, - } + }, )} {...getRootProps()} > diff --git a/hooks/useEmailAccountId.ts b/hooks/useEmailAccountId.ts new file mode 100644 index 000000000..f4ec78ae3 --- /dev/null +++ b/hooks/useEmailAccountId.ts @@ -0,0 +1,44 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { usePrivy } from "@privy-io/react-auth"; +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; + +/** + * Resolves an email address to an account ID via GET /api/accounts/{email}. + * + * @param email - Optional email to resolve + * @returns The resolved account ID, or undefined if not yet resolved or no email + */ +export function useEmailAccountId(email?: string) { + const { getAccessToken } = usePrivy(); + const [accountId, setAccountId] = useState(); + + useEffect(() => { + if (!email) return; + + const resolve = async () => { + const accessToken = await getAccessToken(); + if (!accessToken) return; + + const baseUrl = getClientApiBaseUrl(); + const response = await fetch( + `${baseUrl}/api/accounts/${encodeURIComponent(email)}`, + { + headers: { Authorization: `Bearer ${accessToken}` }, + }, + ); + + if (!response.ok) return; + + const data = await response.json(); + if (data.account?.account_id) { + setAccountId(data.account.account_id); + } + }; + + resolve(); + }, [email, getAccessToken]); + + return accountId; +} diff --git a/hooks/useVercelChat.ts b/hooks/useVercelChat.ts index e16f44146..13633742b 100644 --- a/hooks/useVercelChat.ts +++ b/hooks/useVercelChat.ts @@ -30,7 +30,7 @@ interface UseVercelChatProps { initialMessages?: UIMessage[]; attachments?: FileUIPart[]; textAttachments?: TextAttachment[]; - email?: string; + accountIdOverride?: string; } /** @@ -43,7 +43,7 @@ export function useVercelChat({ initialMessages, attachments = [], textAttachments = [], - email, + accountIdOverride, }: UseVercelChatProps) { const { userData } = useUserProvider(); const { selectedArtist } = useArtistProvider(); @@ -171,10 +171,10 @@ export function useVercelChat({ artistId, // Only include organizationId if it's not null (schema expects string | undefined) ...(organizationId && { organizationId }), - ...(email && { email }), + ...(accountIdOverride && { accountId: accountIdOverride }), model, }), - [id, artistId, organizationId, email, model], + [id, artistId, organizationId, accountIdOverride, model], ); const { messages, status, stop, sendMessage, setMessages, regenerate } = diff --git a/providers/VercelChatProvider.tsx b/providers/VercelChatProvider.tsx index 96027ba0a..2347c3adb 100644 --- a/providers/VercelChatProvider.tsx +++ b/providers/VercelChatProvider.tsx @@ -34,16 +34,21 @@ interface VercelChatContextType { attachments: FileUIPart[]; pendingAttachments: FileUIPart[]; setAttachments: ( - attachments: FileUIPart[] | ((prev: FileUIPart[]) => FileUIPart[]) + attachments: FileUIPart[] | ((prev: FileUIPart[]) => FileUIPart[]), ) => void; removeAttachment: (index: number) => void; clearAttachments: () => void; hasPendingUploads: boolean; textAttachments: TextAttachment[]; setTextAttachments: ( - attachments: TextAttachment[] | ((prev: TextAttachment[]) => TextAttachment[]) + attachments: + | TextAttachment[] + | ((prev: TextAttachment[]) => TextAttachment[]), ) => void; - addTextAttachment: (file: File, type: TextAttachment["type"]) => Promise; + addTextAttachment: ( + file: File, + type: TextAttachment["type"], + ) => Promise; removeTextAttachment: (index: number) => void; model: string; setModel: (model: string) => void; @@ -51,7 +56,7 @@ interface VercelChatContextType { // Create the context const VercelChatContext = createContext( - undefined + undefined, ); // Props for the provider component @@ -59,7 +64,7 @@ interface VercelChatProviderProps { children: ReactNode; chatId: string; initialMessages?: UIMessage[]; - email?: string; + accountIdOverride?: string; } /** @@ -69,7 +74,7 @@ export function VercelChatProvider({ children, chatId, initialMessages, - email, + accountIdOverride, }: VercelChatProviderProps) { const { attachments, @@ -119,7 +124,7 @@ export function VercelChatProvider({ initialMessages, attachments, textAttachments, - email, + accountIdOverride, }); const reload = useCallback(() => { @@ -128,7 +133,7 @@ export function VercelChatProvider({ // When a message is sent successfully, clear the attachments const handleSendMessageWithClear = async ( - event: React.FormEvent + event: React.FormEvent, ) => { await handleSendMessage(event); @@ -188,7 +193,7 @@ export function useVercelChatContext() { if (context === undefined) { throw new Error( - "useVercelChatContext must be used within a VercelChatProvider" + "useVercelChatContext must be used within a VercelChatProvider", ); } From 01b529f548e8b2ae0dd12b63cc1fab65d86b84d3 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:31:40 -0500 Subject: [PATCH 04/20] refactor: use AccountOverrideSync instead of prop drilling email Follow the ApiOverrideSync pattern: AccountOverrideSync reads ?email= query param, resolves it to an accountId via GET /api/accounts/{email}, and stores it in session storage. useVercelChat reads the override from session storage. No prop drilling through 6 files. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/chat/page.tsx | 4 +-- app/page.tsx | 4 +-- components/Home/HomePage.tsx | 4 +-- components/VercelChat/chat.tsx | 6 +--- hooks/useEmailAccountId.ts | 44 ------------------------- hooks/useVercelChat.ts | 9 ++++-- lib/consts.ts | 1 + providers/AccountOverrideSync.tsx | 54 +++++++++++++++++++++++++++++++ providers/Providers.tsx | 26 ++++++++------- providers/VercelChatProvider.tsx | 3 -- 10 files changed, 79 insertions(+), 76 deletions(-) delete mode 100644 hooks/useEmailAccountId.ts create mode 100644 providers/AccountOverrideSync.tsx diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 24c2ecced..b654c70f1 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -13,11 +13,9 @@ export default async function ChatPage({ searchParams }: ChatPageProps) { const params = await searchParams; const initialMessage = Array.isArray(params?.q) ? params.q[0] : params?.q; const initialMessages = getMessages(initialMessage); - const email = Array.isArray(params?.email) ? params.email[0] : params?.email; - return (
- +
); } diff --git a/app/page.tsx b/app/page.tsx index 79634cd98..2b312bd16 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -13,7 +13,5 @@ export default async function Home({ searchParams }: ChatPageProps) { const params = await searchParams; const initialMessage = Array.isArray(params?.q) ? params.q[0] : params?.q; const initialMessages = getMessages(initialMessage); - const email = Array.isArray(params?.email) ? params.email[0] : params?.email; - - return ; + return ; } diff --git a/components/Home/HomePage.tsx b/components/Home/HomePage.tsx index 4bc200f9d..325321da0 100644 --- a/components/Home/HomePage.tsx +++ b/components/Home/HomePage.tsx @@ -8,11 +8,9 @@ import { UIMessage } from "ai"; const HomePage = ({ id, initialMessages, - email, }: { id: string; initialMessages?: UIMessage[]; - email?: string; }) => { const { setFrameReady, isFrameReady } = useMiniKit(); @@ -24,7 +22,7 @@ const HomePage = ({ return (
- +
); }; diff --git a/components/VercelChat/chat.tsx b/components/VercelChat/chat.tsx index ec7f3bd51..542bbfc3f 100644 --- a/components/VercelChat/chat.tsx +++ b/components/VercelChat/chat.tsx @@ -20,18 +20,15 @@ import FileDragOverlay from "./FileDragOverlay"; import { Loader } from "lucide-react"; import { memo } from "react"; import { useOrganization } from "@/providers/OrganizationProvider"; -import { useEmailAccountId } from "@/hooks/useEmailAccountId"; interface ChatProps { id: string; reportId?: string; initialMessages?: UIMessage[]; - email?: string; } -export function Chat({ id, reportId, initialMessages, email }: ChatProps) { +export function Chat({ id, reportId, initialMessages }: ChatProps) { const { selectedOrgId } = useOrganization(); - const accountIdOverride = useEmailAccountId(email); const providerKey = `${id}-${selectedOrgId ?? "personal"}`; return ( @@ -39,7 +36,6 @@ export function Chat({ id, reportId, initialMessages, email }: ChatProps) { key={providerKey} chatId={id} initialMessages={initialMessages} - accountIdOverride={accountIdOverride} >
diff --git a/hooks/useEmailAccountId.ts b/hooks/useEmailAccountId.ts deleted file mode 100644 index f4ec78ae3..000000000 --- a/hooks/useEmailAccountId.ts +++ /dev/null @@ -1,44 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { usePrivy } from "@privy-io/react-auth"; -import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; - -/** - * Resolves an email address to an account ID via GET /api/accounts/{email}. - * - * @param email - Optional email to resolve - * @returns The resolved account ID, or undefined if not yet resolved or no email - */ -export function useEmailAccountId(email?: string) { - const { getAccessToken } = usePrivy(); - const [accountId, setAccountId] = useState(); - - useEffect(() => { - if (!email) return; - - const resolve = async () => { - const accessToken = await getAccessToken(); - if (!accessToken) return; - - const baseUrl = getClientApiBaseUrl(); - const response = await fetch( - `${baseUrl}/api/accounts/${encodeURIComponent(email)}`, - { - headers: { Authorization: `Bearer ${accessToken}` }, - }, - ); - - if (!response.ok) return; - - const data = await response.json(); - if (data.account?.account_id) { - setAccountId(data.account.account_id); - } - }; - - resolve(); - }, [email, getAccessToken]); - - return accountId; -} diff --git a/hooks/useVercelChat.ts b/hooks/useVercelChat.ts index 13633742b..9c5616aeb 100644 --- a/hooks/useVercelChat.ts +++ b/hooks/useVercelChat.ts @@ -12,7 +12,7 @@ import { useConversationsProvider } from "@/providers/ConversationsProvider"; import { UIMessage, FileUIPart } from "ai"; import useAvailableModels from "./useAvailableModels"; import { useLocalStorage } from "usehooks-ts"; -import { DEFAULT_MODEL } from "@/lib/consts"; +import { DEFAULT_MODEL, ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts"; import { usePaymentProvider } from "@/providers/PaymentProvider"; import useArtistFilesForMentions from "@/hooks/useArtistFilesForMentions"; import type { KnowledgeBaseEntry } from "@/lib/supabase/getArtistKnowledge"; @@ -30,7 +30,6 @@ interface UseVercelChatProps { initialMessages?: UIMessage[]; attachments?: FileUIPart[]; textAttachments?: TextAttachment[]; - accountIdOverride?: string; } /** @@ -43,7 +42,6 @@ export function useVercelChat({ initialMessages, attachments = [], textAttachments = [], - accountIdOverride, }: UseVercelChatProps) { const { userData } = useUserProvider(); const { selectedArtist } = useArtistProvider(); @@ -165,6 +163,11 @@ export function useVercelChat({ return outputs; }, [knowledgeFiles]); + const accountIdOverride = + typeof window !== "undefined" + ? window.sessionStorage.getItem(ACCOUNT_OVERRIDE_STORAGE_KEY) + : null; + const chatRequestBody = useMemo( () => ({ roomId: id, diff --git a/lib/consts.ts b/lib/consts.ts index df817746f..0323e5e86 100644 --- a/lib/consts.ts +++ b/lib/consts.ts @@ -5,6 +5,7 @@ export const NEW_API_BASE_URL = IS_PROD ? "https://recoup-api.vercel.app" : "https://test-recoup-api.vercel.app"; export const API_OVERRIDE_STORAGE_KEY = "recoup_api_override"; +export const ACCOUNT_OVERRIDE_STORAGE_KEY = "recoup_account_override"; export const IN_PROCESS_PROTOCOL_ADDRESS = IS_PROD ? ("0x540C18B7f99b3b599c6FeB99964498931c211858" as Address) : ("0x6832A997D8616707C7b68721D6E9332E77da7F6C" as Address); diff --git a/providers/AccountOverrideSync.tsx b/providers/AccountOverrideSync.tsx new file mode 100644 index 000000000..b54db3b9a --- /dev/null +++ b/providers/AccountOverrideSync.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useEffect } from "react"; +import { useSearchParams } from "next/navigation"; +import { usePrivy } from "@privy-io/react-auth"; +import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts"; +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; + +export default function AccountOverrideSync() { + const searchParams = useSearchParams(); + const { getAccessToken } = usePrivy(); + + useEffect(() => { + const emailParam = searchParams.get("email"); + + try { + if (emailParam === "clear") { + window.sessionStorage.removeItem(ACCOUNT_OVERRIDE_STORAGE_KEY); + return; + } + + if (!emailParam) { + return; + } + + const resolve = async () => { + const accessToken = await getAccessToken(); + if (!accessToken) return; + + const baseUrl = getClientApiBaseUrl(); + const response = await fetch( + `${baseUrl}/api/accounts/${encodeURIComponent(emailParam)}`, + { headers: { Authorization: `Bearer ${accessToken}` } }, + ); + + if (!response.ok) return; + + const data = await response.json(); + if (data.account?.account_id) { + window.sessionStorage.setItem( + ACCOUNT_OVERRIDE_STORAGE_KEY, + data.account.account_id, + ); + } + }; + + resolve(); + } catch { + // Ignore fetch failures and storage errors. + } + }, [searchParams, getAccessToken]); + + return null; +} diff --git a/providers/Providers.tsx b/providers/Providers.tsx index dc29b1c5b..ca4559197 100644 --- a/providers/Providers.tsx +++ b/providers/Providers.tsx @@ -14,33 +14,35 @@ import { MiniAppProvider } from "./MiniAppProvider"; import { ThemeProvider } from "./ThemeProvider"; import { OrganizationProvider } from "./OrganizationProvider"; import ApiOverrideSync from "./ApiOverrideSync"; +import AccountOverrideSync from "./AccountOverrideSync"; const queryClient = new QueryClient(); const Providers = ({ children }: { children: React.ReactNode }) => ( - + - - - - - {children} - - - - + + + + + {children} + + + + diff --git a/providers/VercelChatProvider.tsx b/providers/VercelChatProvider.tsx index 2347c3adb..dc02dd39c 100644 --- a/providers/VercelChatProvider.tsx +++ b/providers/VercelChatProvider.tsx @@ -64,7 +64,6 @@ interface VercelChatProviderProps { children: ReactNode; chatId: string; initialMessages?: UIMessage[]; - accountIdOverride?: string; } /** @@ -74,7 +73,6 @@ export function VercelChatProvider({ children, chatId, initialMessages, - accountIdOverride, }: VercelChatProviderProps) { const { attachments, @@ -124,7 +122,6 @@ export function VercelChatProvider({ initialMessages, attachments, textAttachments, - accountIdOverride, }); const reload = useCallback(() => { From dc1b80a76509f744f33dd485881b7ffce56ca575 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:33:08 -0500 Subject: [PATCH 05/20] revert: remove unrelated page changes from PR Co-Authored-By: Claude Opus 4.6 (1M context) --- app/chat/page.tsx | 4 ++-- app/page.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/chat/page.tsx b/app/chat/page.tsx index b654c70f1..ef49f5474 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -10,9 +10,9 @@ interface ChatPageProps { export default async function ChatPage({ searchParams }: ChatPageProps) { const id = generateUUID(); - const params = await searchParams; - const initialMessage = Array.isArray(params?.q) ? params.q[0] : params?.q; + const initialMessage = (await searchParams)?.q as string; const initialMessages = getMessages(initialMessage); + return (
diff --git a/app/page.tsx b/app/page.tsx index 2b312bd16..979fff06c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,8 +10,8 @@ interface ChatPageProps { export default async function Home({ searchParams }: ChatPageProps) { const id = generateUUID(); - const params = await searchParams; - const initialMessage = Array.isArray(params?.q) ? params.q[0] : params?.q; + const initialMessage = (await searchParams)?.q as string; const initialMessages = getMessages(initialMessage); + return ; } From 15473384d9a17fc86f960abc6c5abf70902d90d8 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:33:28 -0500 Subject: [PATCH 06/20] revert: remove formatting-only changes from chat.tsx Co-Authored-By: Claude Opus 4.6 (1M context) --- components/VercelChat/chat.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/components/VercelChat/chat.tsx b/components/VercelChat/chat.tsx index 542bbfc3f..64059d856 100644 --- a/components/VercelChat/chat.tsx +++ b/components/VercelChat/chat.tsx @@ -30,13 +30,9 @@ interface ChatProps { export function Chat({ id, reportId, initialMessages }: ChatProps) { const { selectedOrgId } = useOrganization(); const providerKey = `${id}-${selectedOrgId ?? "personal"}`; - + return ( - + ); @@ -87,7 +83,7 @@ function ChatContentMemoized({ "px-4 md:px-0 pb-4 flex flex-col h-full items-center w-full relative", { "justify-between": messages.length > 0, - }, + } )} {...getRootProps()} > From e2770614a92654c8f9e3f2df9ad3d953d4f85ef1 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:34:55 -0500 Subject: [PATCH 07/20] revert: remove formatting-only changes from VercelChatProvider.tsx Co-Authored-By: Claude Opus 4.6 (1M context) --- providers/VercelChatProvider.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/providers/VercelChatProvider.tsx b/providers/VercelChatProvider.tsx index dc02dd39c..74fe0da14 100644 --- a/providers/VercelChatProvider.tsx +++ b/providers/VercelChatProvider.tsx @@ -34,21 +34,16 @@ interface VercelChatContextType { attachments: FileUIPart[]; pendingAttachments: FileUIPart[]; setAttachments: ( - attachments: FileUIPart[] | ((prev: FileUIPart[]) => FileUIPart[]), + attachments: FileUIPart[] | ((prev: FileUIPart[]) => FileUIPart[]) ) => void; removeAttachment: (index: number) => void; clearAttachments: () => void; hasPendingUploads: boolean; textAttachments: TextAttachment[]; setTextAttachments: ( - attachments: - | TextAttachment[] - | ((prev: TextAttachment[]) => TextAttachment[]), + attachments: TextAttachment[] | ((prev: TextAttachment[]) => TextAttachment[]) ) => void; - addTextAttachment: ( - file: File, - type: TextAttachment["type"], - ) => Promise; + addTextAttachment: (file: File, type: TextAttachment["type"]) => Promise; removeTextAttachment: (index: number) => void; model: string; setModel: (model: string) => void; @@ -56,7 +51,7 @@ interface VercelChatContextType { // Create the context const VercelChatContext = createContext( - undefined, + undefined ); // Props for the provider component @@ -130,7 +125,7 @@ export function VercelChatProvider({ // When a message is sent successfully, clear the attachments const handleSendMessageWithClear = async ( - event: React.FormEvent, + event: React.FormEvent ) => { await handleSendMessage(event); @@ -190,7 +185,7 @@ export function useVercelChatContext() { if (context === undefined) { throw new Error( - "useVercelChatContext must be used within a VercelChatProvider", + "useVercelChatContext must be used within a VercelChatProvider" ); } From 3e7af9349071143aa9043972c2cbd91b3b191789 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:35:55 -0500 Subject: [PATCH 08/20] refactor: use tanstack useQuery for AccountOverrideSync Replaces manual useEffect fetch with useQuery for caching and deduplication. Added comment explaining why placement differs from ApiOverrideSync (needs usePrivy for auth). Co-Authored-By: Claude Opus 4.6 (1M context) --- providers/AccountOverrideSync.tsx | 61 ++++++++++++++++--------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/providers/AccountOverrideSync.tsx b/providers/AccountOverrideSync.tsx index b54db3b9a..fa5b36533 100644 --- a/providers/AccountOverrideSync.tsx +++ b/providers/AccountOverrideSync.tsx @@ -3,52 +3,55 @@ import { useEffect } from "react"; import { useSearchParams } from "next/navigation"; import { usePrivy } from "@privy-io/react-auth"; +import { useQuery } from "@tanstack/react-query"; import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts"; import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; +/** + * Syncs the ?email= query param to session storage as an account ID override. + * Follows the same pattern as ApiOverrideSync. + * Placed inside PrivyProvider because it needs getAccessToken for the API call. + */ export default function AccountOverrideSync() { const searchParams = useSearchParams(); const { getAccessToken } = usePrivy(); + const emailParam = searchParams.get("email"); - useEffect(() => { - const emailParam = searchParams.get("email"); + const { data: accountId } = useQuery({ + queryKey: ["accountOverride", emailParam], + queryFn: async () => { + const accessToken = await getAccessToken(); + if (!accessToken) return null; + + const baseUrl = getClientApiBaseUrl(); + const response = await fetch( + `${baseUrl}/api/accounts/${encodeURIComponent(emailParam!)}`, + { headers: { Authorization: `Bearer ${accessToken}` } }, + ); + + if (!response.ok) return null; + const data = await response.json(); + return data.account?.account_id ?? null; + }, + enabled: !!emailParam && emailParam !== "clear", + staleTime: Infinity, + }); + + useEffect(() => { try { if (emailParam === "clear") { window.sessionStorage.removeItem(ACCOUNT_OVERRIDE_STORAGE_KEY); return; } - if (!emailParam) { - return; + if (accountId) { + window.sessionStorage.setItem(ACCOUNT_OVERRIDE_STORAGE_KEY, accountId); } - - const resolve = async () => { - const accessToken = await getAccessToken(); - if (!accessToken) return; - - const baseUrl = getClientApiBaseUrl(); - const response = await fetch( - `${baseUrl}/api/accounts/${encodeURIComponent(emailParam)}`, - { headers: { Authorization: `Bearer ${accessToken}` } }, - ); - - if (!response.ok) return; - - const data = await response.json(); - if (data.account?.account_id) { - window.sessionStorage.setItem( - ACCOUNT_OVERRIDE_STORAGE_KEY, - data.account.account_id, - ); - } - }; - - resolve(); } catch { - // Ignore fetch failures and storage errors. + // Ignore storage errors. } - }, [searchParams, getAccessToken]); + }, [emailParam, accountId]); return null; } From b14f607ab207f0904cf8b19a183cac5364d43016 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:37:33 -0500 Subject: [PATCH 09/20] refactor: SRP - extract fetchAccountIdByEmail to lib Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/accounts/fetchAccountIdByEmail.ts | 24 ++++++++++++++++++++++++ providers/AccountOverrideSync.tsx | 14 ++------------ 2 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 lib/accounts/fetchAccountIdByEmail.ts diff --git a/lib/accounts/fetchAccountIdByEmail.ts b/lib/accounts/fetchAccountIdByEmail.ts new file mode 100644 index 000000000..faf61d975 --- /dev/null +++ b/lib/accounts/fetchAccountIdByEmail.ts @@ -0,0 +1,24 @@ +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; + +/** + * Fetches an account ID by email address via GET /api/accounts/{email}. + * + * @param email - The email address to look up + * @param accessToken - Bearer token for authentication + * @returns The account ID, or null if not found + */ +export async function fetchAccountIdByEmail( + email: string, + accessToken: string, +): Promise { + const baseUrl = getClientApiBaseUrl(); + const response = await fetch( + `${baseUrl}/api/accounts/${encodeURIComponent(email)}`, + { headers: { Authorization: `Bearer ${accessToken}` } }, + ); + + if (!response.ok) return null; + + const data = await response.json(); + return data.account?.account_id ?? null; +} diff --git a/providers/AccountOverrideSync.tsx b/providers/AccountOverrideSync.tsx index fa5b36533..279923361 100644 --- a/providers/AccountOverrideSync.tsx +++ b/providers/AccountOverrideSync.tsx @@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation"; import { usePrivy } from "@privy-io/react-auth"; import { useQuery } from "@tanstack/react-query"; import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts"; -import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; +import { fetchAccountIdByEmail } from "@/lib/accounts/fetchAccountIdByEmail"; /** * Syncs the ?email= query param to session storage as an account ID override. @@ -22,17 +22,7 @@ export default function AccountOverrideSync() { queryFn: async () => { const accessToken = await getAccessToken(); if (!accessToken) return null; - - const baseUrl = getClientApiBaseUrl(); - const response = await fetch( - `${baseUrl}/api/accounts/${encodeURIComponent(emailParam!)}`, - { headers: { Authorization: `Bearer ${accessToken}` } }, - ); - - if (!response.ok) return null; - - const data = await response.json(); - return data.account?.account_id ?? null; + return fetchAccountIdByEmail(emailParam!, accessToken); }, enabled: !!emailParam && emailParam !== "clear", staleTime: Infinity, From c8bfa392a94e3ca518a9d849e06b32fa4b290b5a Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:43:12 -0500 Subject: [PATCH 10/20] feat: add AccountOverrideBadge pill to layout Shows a fixed pill at the top center with "Viewing as {email}" and an X button to clear the override when ?email= is active. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/layout.tsx | 8 +++-- components/AccountOverrideBadge.tsx | 56 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 components/AccountOverrideBadge.tsx diff --git a/app/layout.tsx b/app/layout.tsx index da9869e8d..9bb63f5b9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -13,6 +13,7 @@ import { ToastContainer } from "react-toastify"; import { Toaster } from "sonner"; import { Geist, Geist_Mono } from "next/font/google"; import DeferredAnalytics from "@/components/DeferredAnalytics"; +import AccountOverrideBadge from "@/components/AccountOverrideBadge"; const geist = Geist({ subsets: ["latin"], @@ -71,14 +72,15 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - +
diff --git a/components/AccountOverrideBadge.tsx b/components/AccountOverrideBadge.tsx new file mode 100644 index 000000000..d5cb0c618 --- /dev/null +++ b/components/AccountOverrideBadge.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { X } from "lucide-react"; +import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts"; + +/** + * Displays a pill badge when an account override is active via ?email= query param. + * Shows the overridden email and an X button to clear the override. + */ +export default function AccountOverrideBadge() { + const searchParams = useSearchParams(); + const router = useRouter(); + const [isActive, setIsActive] = useState(false); + const email = searchParams.get("email"); + + useEffect(() => { + if (!email || email === "clear") { + setIsActive(false); + return; + } + + const accountId = window.sessionStorage.getItem( + ACCOUNT_OVERRIDE_STORAGE_KEY, + ); + setIsActive(!!accountId); + }, [email]); + + if (!isActive || !email) return null; + + const handleClear = () => { + window.sessionStorage.removeItem(ACCOUNT_OVERRIDE_STORAGE_KEY); + const params = new URLSearchParams(searchParams.toString()); + params.delete("email"); + const newPath = params.toString() + ? `${window.location.pathname}?${params.toString()}` + : window.location.pathname; + router.replace(newPath); + setIsActive(false); + }; + + return ( +
+ Viewing as + {email} + +
+ ); +} From 0aab24f55c4581f0e87a4ceb49ccd3f3eed0d405 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:46:16 -0500 Subject: [PATCH 11/20] refactor: DRY - use shared tanstack query cache for AccountOverrideBadge Reads from the same queryKey as AccountOverrideSync instead of duplicating session storage checks. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/AccountOverrideBadge.tsx | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/components/AccountOverrideBadge.tsx b/components/AccountOverrideBadge.tsx index d5cb0c618..ae8e8a724 100644 --- a/components/AccountOverrideBadge.tsx +++ b/components/AccountOverrideBadge.tsx @@ -1,33 +1,34 @@ "use client"; -import { useState, useEffect } from "react"; import { useSearchParams, useRouter } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; +import { usePrivy } from "@privy-io/react-auth"; import { X } from "lucide-react"; import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts"; +import { fetchAccountIdByEmail } from "@/lib/accounts/fetchAccountIdByEmail"; /** * Displays a pill badge when an account override is active via ?email= query param. - * Shows the overridden email and an X button to clear the override. + * Reads from the same tanstack query cache as AccountOverrideSync (DRY). */ export default function AccountOverrideBadge() { const searchParams = useSearchParams(); const router = useRouter(); - const [isActive, setIsActive] = useState(false); + const { getAccessToken } = usePrivy(); const email = searchParams.get("email"); - useEffect(() => { - if (!email || email === "clear") { - setIsActive(false); - return; - } + const { data: accountId } = useQuery({ + queryKey: ["accountOverride", email], + queryFn: async () => { + const accessToken = await getAccessToken(); + if (!accessToken) return null; + return fetchAccountIdByEmail(email!, accessToken); + }, + enabled: !!email && email !== "clear", + staleTime: Infinity, + }); - const accountId = window.sessionStorage.getItem( - ACCOUNT_OVERRIDE_STORAGE_KEY, - ); - setIsActive(!!accountId); - }, [email]); - - if (!isActive || !email) return null; + if (!accountId || !email) return null; const handleClear = () => { window.sessionStorage.removeItem(ACCOUNT_OVERRIDE_STORAGE_KEY); @@ -37,7 +38,6 @@ export default function AccountOverrideBadge() { ? `${window.location.pathname}?${params.toString()}` : window.location.pathname; router.replace(newPath); - setIsActive(false); }; return ( From b3aa9bd744cd7bea04307b323271b4dc09f7e2fc Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:50:27 -0500 Subject: [PATCH 12/20] refactor: DRY - read from session storage set by AccountOverrideSync Badge polls session storage instead of duplicating the useQuery. AccountOverrideSync writes it, badge reads it. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/AccountOverrideBadge.tsx | 35 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/components/AccountOverrideBadge.tsx b/components/AccountOverrideBadge.tsx index ae8e8a724..56299baa8 100644 --- a/components/AccountOverrideBadge.tsx +++ b/components/AccountOverrideBadge.tsx @@ -1,34 +1,36 @@ "use client"; +import { useState, useEffect } from "react"; import { useSearchParams, useRouter } from "next/navigation"; -import { useQuery } from "@tanstack/react-query"; -import { usePrivy } from "@privy-io/react-auth"; import { X } from "lucide-react"; import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts"; -import { fetchAccountIdByEmail } from "@/lib/accounts/fetchAccountIdByEmail"; /** * Displays a pill badge when an account override is active via ?email= query param. - * Reads from the same tanstack query cache as AccountOverrideSync (DRY). + * Reads from session storage populated by AccountOverrideSync. */ export default function AccountOverrideBadge() { const searchParams = useSearchParams(); const router = useRouter(); - const { getAccessToken } = usePrivy(); const email = searchParams.get("email"); + const [isActive, setIsActive] = useState(false); - const { data: accountId } = useQuery({ - queryKey: ["accountOverride", email], - queryFn: async () => { - const accessToken = await getAccessToken(); - if (!accessToken) return null; - return fetchAccountIdByEmail(email!, accessToken); - }, - enabled: !!email && email !== "clear", - staleTime: Infinity, - }); + useEffect(() => { + if (!email || email === "clear") { + setIsActive(false); + return; + } - if (!accountId || !email) return null; + const check = () => { + setIsActive(!!window.sessionStorage.getItem(ACCOUNT_OVERRIDE_STORAGE_KEY)); + }; + + check(); + const interval = setInterval(check, 1000); + return () => clearInterval(interval); + }, [email]); + + if (!isActive || !email) return null; const handleClear = () => { window.sessionStorage.removeItem(ACCOUNT_OVERRIDE_STORAGE_KEY); @@ -38,6 +40,7 @@ export default function AccountOverrideBadge() { ? `${window.location.pathname}?${params.toString()}` : window.location.pathname; router.replace(newPath); + setIsActive(false); }; return ( From 50a871335d4a6945517d1f28f6b4c058db4bd85e Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 17:51:44 -0500 Subject: [PATCH 13/20] refactor: remove useEffect, derive badge visibility from query param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No polling or state needed — if ?email= is in the URL, show the badge. Co-Authored-By: Claude Opus 4.6 (1M context) --- components/AccountOverrideBadge.tsx | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/components/AccountOverrideBadge.tsx b/components/AccountOverrideBadge.tsx index 56299baa8..104615bc2 100644 --- a/components/AccountOverrideBadge.tsx +++ b/components/AccountOverrideBadge.tsx @@ -1,6 +1,5 @@ "use client"; -import { useState, useEffect } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { X } from "lucide-react"; import { ACCOUNT_OVERRIDE_STORAGE_KEY } from "@/lib/consts"; @@ -13,24 +12,8 @@ export default function AccountOverrideBadge() { const searchParams = useSearchParams(); const router = useRouter(); const email = searchParams.get("email"); - const [isActive, setIsActive] = useState(false); - useEffect(() => { - if (!email || email === "clear") { - setIsActive(false); - return; - } - - const check = () => { - setIsActive(!!window.sessionStorage.getItem(ACCOUNT_OVERRIDE_STORAGE_KEY)); - }; - - check(); - const interval = setInterval(check, 1000); - return () => clearInterval(interval); - }, [email]); - - if (!isActive || !email) return null; + if (!email || email === "clear") return null; const handleClear = () => { window.sessionStorage.removeItem(ACCOUNT_OVERRIDE_STORAGE_KEY); @@ -40,7 +23,6 @@ export default function AccountOverrideBadge() { ? `${window.location.pathname}?${params.toString()}` : window.location.pathname; router.replace(newPath); - setIsActive(false); }; return ( From b7d5d4ec19f508c7438888bf22c535dd4338e7e1 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 18:00:29 -0500 Subject: [PATCH 14/20] fix: high-contrast amber badge for account override Co-Authored-By: Claude Opus 4.6 (1M context) --- components/AccountOverrideBadge.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/AccountOverrideBadge.tsx b/components/AccountOverrideBadge.tsx index 104615bc2..c1aafa489 100644 --- a/components/AccountOverrideBadge.tsx +++ b/components/AccountOverrideBadge.tsx @@ -26,15 +26,15 @@ export default function AccountOverrideBadge() { }; return ( -
- Viewing as - {email} +
+ Viewing as + {email}
); From b934e9e102125d9267692f28f2ee1d9ff277ce72 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 6 Apr 2026 18:01:03 -0500 Subject: [PATCH 15/20] fix: brighter amber-400 badge in dark mode for visibility Co-Authored-By: Claude Opus 4.6 (1M context) --- components/AccountOverrideBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/AccountOverrideBadge.tsx b/components/AccountOverrideBadge.tsx index c1aafa489..1397269fb 100644 --- a/components/AccountOverrideBadge.tsx +++ b/components/AccountOverrideBadge.tsx @@ -26,7 +26,7 @@ export default function AccountOverrideBadge() { }; return ( -
+
Viewing as {email}