Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_SUPABASE_URL=
OPENAI_API_KEY=
NEXT_PUBLIC_PRIVY_APP_ID=
# Optional: local api or prod (see docs/PRE_TRIAL_LOCAL.md). Default local = test-recoup-api.
NEXT_PUBLIC_RECOUP_API_URL=http://localhost:3001
PERPLEXITY_MCP_SERVER=
XAI_API_KEY=
TWILIO_ACCOUNT_SID=your_account_sid
Expand Down
17 changes: 11 additions & 6 deletions components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { MenuIcon, PlusCircle } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import SideMenu from "../SideMenu";
import { useArtistProvider } from "@/providers/ArtistProvider";
import ImageWithFallback from "../ImageWithFallback";
Expand All @@ -10,8 +10,10 @@ import SideArtists from "../SideArtists";
import type { ArtistRecord } from "@/types/Artist";

const Header = () => {
const [mounted, setMounted] = useState(false);
const [isOpenMobileMenu, setIsOpenMobileMenu] = useState(false);
const [isOpenSideArtists, setIsOpenSideArtists] = useState(false);
useEffect(() => setMounted(true), []);
const {
selectedArtist,
toggleSettingModal,
Expand All @@ -20,6 +22,7 @@ const Header = () => {
sorted,
} = useArtistProvider();
const isMobile = useIsMobile();
const showMobileChrome = mounted && isMobile;
const isArtistSelected = selectedArtist !== null;

const handleClickPfp = () => {
Expand All @@ -38,7 +41,8 @@ const Header = () => {

return (
<>
<div className="z-[50] fixed bg-card left-0 right-0 top-0 md:hidden flex p-4 items-center justify-between w-auto">
<div className="z-[50] fixed bg-card left-0 right-0 top-0 md:hidden flex min-h-[56px] p-4 items-center justify-between w-auto">
{showMobileChrome ? (
<button
type="button"
className="md:hidden flex items-center gap-2 z-[50]"
Expand All @@ -47,9 +51,10 @@ const Header = () => {
>
<MenuIcon className="dark:text-white" />
</button>
) : null}

{/* Show Add/Select Artist button when on mobile, logged in, and no artist selected */}
{isMobile && !isArtistSelected && (
{/* After mount + mobile only — avoids hydration mismatch (useMediaQuery false on server) */}
{showMobileChrome && !isArtistSelected && (
<button
type="button"
onClick={
Expand All @@ -70,7 +75,7 @@ const Header = () => {
)}

{/* Show artist profile when artist is selected */}
{selectedArtist && isMobile && (
{selectedArtist && showMobileChrome && (
<div className="relative z-[50]">
<button
type="button"
Expand All @@ -86,7 +91,7 @@ const Header = () => {
</button>
</div>
)}
{isMobile && (
{showMobileChrome && (
<>
<SideMenu
isVisible={isOpenMobileMenu}
Expand Down
66 changes: 66 additions & 0 deletions docs/PRE_TRIAL_LOCAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Pre-trial: run Chat locally (Developer Requirements)

Matches [Developer Requirements](https://www.notion.so/Developer-Requirements-Document-15026a7d835c8062bbcde49b59aa00e8) — **fork, clone, run on localhost:3000**, screenshot for Sweetman.

## 1. Clone & install

```bash
git clone <your-fork-or-repo-url> chat
cd chat
pnpm install
# If pnpm complains about TTY: $env:CI='true'; pnpm install
```

## 2. Env (minimum to boot)

Copy `.env.example` → `.env.local`.

**Required for the app to start**

- `NEXT_PUBLIC_PRIVY_APP_ID` — create a free app at [Privy](https://dashboard.privy.io), add **http://localhost:3000** to allowed URLs.

**Required for account / DB features**

- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
- Often `SUPABASE_SERVICE_ROLE_KEY` (server routes)

Pre-trial devs: doc says you may **obtain your own** env or ask Sweetman for **specific** keys — not a generic “send everything” request.

## 3. Run

```bash
pnpm dev
```

Open **http://localhost:3000** → screenshot (that’s the pre-trial check).

## 4. Recoup API (401 locally)

Local/preview uses **`https://test-recoup-api.vercel.app`** by default. If calls return **401**, the test deployment may not accept your Privy JWT yet. Options:

| Option | What to do |
|--------|------------|
| A | Ask Sweetman to align **test** API with your Privy app (proper fix for “all local”). |
| B | Temporarily set `NEXT_PUBLIC_RECOUP_API_URL=https://recoup-api.vercel.app` only if you’re allowed to hit prod from localhost. |
| C | Run **api** locally — see **`api/docs/RUN_LOCAL.md`** — then `NEXT_PUBLIC_RECOUP_API_URL=http://localhost:3001` in chat. |

**Pre-trial goal** is usually: app runs + login works + screenshot — not necessarily every API green on day one.

## 5. “Run everything locally”

Not required for pre-trial. Full stack is roughly:

| Repo | Role |
|------|------|
| **chat** | UI (this repo) |
| **api** | Recoup HTTP API + sandbox setup route |
| **tasks** | Trigger.dev jobs (needs Trigger credentials) |
| **mono** | Cloned inside sandboxes by tasks — not run as a separate local server for chat |

For daily work, most devs use **deployed** test/prod API + local chat.

## 6. Windows gotchas

- **EBUSY on `.next`**: stop all `pnpm dev`, delete `.next`, one dev server only; exclude `.next` from OneDrive/antivirus if needed.
- **Node**: LTS **20** or **22** if Next acts up on Node 24.
37 changes: 37 additions & 0 deletions hooks/useSandboxSetupOnLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useEffect } from "react";
import { usePrivy } from "@privy-io/react-auth";
import { useAccessToken } from "@/hooks/useAccessToken";
import { setupSandbox } from "@/lib/sandboxes/setupSandbox";

const SESSION_KEY = "sandboxSetupDone";

export function useSandboxSetupOnLogin() {
const { authenticated } = usePrivy();
const accessToken = useAccessToken();

useEffect(() => {
if (!authenticated || !accessToken) {
return;
}

if (typeof window === "undefined") {
return;
}

if (window.sessionStorage.getItem(SESSION_KEY) === "1") {
return;
}

setupSandbox(accessToken)
.then(() => {
window.sessionStorage.setItem(SESSION_KEY, "1");
})
.catch((error) => {
// We deliberately log and continue so login UX is not blocked
console.error("Failed to setup sandbox on login:", error);
});
}, [authenticated, accessToken]);
}

2 changes: 1 addition & 1 deletion lib/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Address } from "viem";
export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
export const NEW_API_BASE_URL = IS_PROD
? "https://recoup-api.vercel.app"
: "https://test-recoup-api.vercel.app";
: "http://localhost:3001";
Comment on lines 4 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore non-prod API host fallback

NEW_API_BASE_URL now points all non-production environments to http://localhost:3001, which causes preview/test deployments (where NEXT_PUBLIC_VERCEL_ENV !== "production") to send API calls to loopback instead of Recoup’s deployed API; any module importing this constant (e.g., hooks and server routes) will fail unless an API server is running in the same host/container. This is a regression from the previous non-prod default (https://test-recoup-api.vercel.app) and also makes the newly documented NEXT_PUBLIC_RECOUP_API_URL ineffective because this constant never reads it.

Useful? React with 👍 / 👎.

Comment on lines 4 to +6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid hardcoding localhost as the non-prod API base URL.

Line 6 forces all non-prod clients to call http://localhost:3001, which breaks preview/staging usage and can fail under HTTPS due to mixed-content.

🔧 Proposed fix
 export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
-export const NEW_API_BASE_URL = IS_PROD
-  ? "https://recoup-api.vercel.app"
-  : "http://localhost:3001";
+const DEFAULT_NON_PROD_API_BASE_URL = "https://test-recoup-api.vercel.app";
+export const NEW_API_BASE_URL =
+  process.env.NEXT_PUBLIC_RECOUP_API_URL?.trim() ||
+  (IS_PROD
+    ? "https://recoup-api.vercel.app"
+    : DEFAULT_NON_PROD_API_BASE_URL);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const NEW_API_BASE_URL = IS_PROD
? "https://recoup-api.vercel.app"
: "https://test-recoup-api.vercel.app";
: "http://localhost:3001";
export const IS_PROD = process.env.NEXT_PUBLIC_VERCEL_ENV === "production";
const DEFAULT_NON_PROD_API_BASE_URL = "https://test-recoup-api.vercel.app";
export const NEW_API_BASE_URL =
process.env.NEXT_PUBLIC_RECOUP_API_URL?.trim() ||
(IS_PROD
? "https://recoup-api.vercel.app"
: DEFAULT_NON_PROD_API_BASE_URL);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/consts.ts` around lines 4 - 6, NEW_API_BASE_URL currently hardcodes
"http://localhost:3001" for non-prod which breaks staging/preview and HTTPS;
change NEW_API_BASE_URL to derive from an environment variable (e.g.,
NEXT_PUBLIC_API_BASE_URL or NEXT_PUBLIC_VERCEL_URL) with a sensible fallback
instead of localhost, using IS_PROD to choose production URL; update the logic
around NEW_API_BASE_URL and any callers expecting a string so non-prod
deployments can configure their real API host and avoid mixed-content issues.

export const IN_PROCESS_PROTOCOL_ADDRESS = IS_PROD
? ("0x540C18B7f99b3b599c6FeB99964498931c211858" as Address)
: ("0x6832A997D8616707C7b68721D6E9332E77da7F6C" as Address);
Expand Down
28 changes: 28 additions & 0 deletions lib/sandboxes/setupSandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NEW_API_BASE_URL } from "@/lib/consts";

type SetupSandboxResponse =
| {
status: "success";
runId: string;
}
| {
status: "error";
error: string;
};

export async function setupSandbox(accessToken: string): Promise<SetupSandboxResponse> {
const response = await fetch(`${NEW_API_BASE_URL}/api/sandboxes/setup`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});

if (!response.ok) {
throw new Error(`Failed to setup sandbox: ${response.status} ${response.statusText}`);
}

return (await response.json()) as SetupSandboxResponse;
}
Comment on lines +13 to +27
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle API payload failures, not only HTTP failures.

SetupSandboxResponse explicitly allows status: "error", but the function resolves that payload as success today. That can mark downstream flows complete when setup actually failed.

Suggested fix
 type SetupSandboxResponse =
   | {
       status: "success";
       runId: string;
     }
   | {
       status: "error";
       error: string;
     };

-export async function setupSandbox(accessToken: string): Promise<SetupSandboxResponse> {
+export async function setupSandbox(
+  accessToken: string
+): Promise<{ status: "success"; runId: string }> {
   const response = await fetch(`${NEW_API_BASE_URL}/api/sandboxes/setup`, {
     method: "POST",
     headers: {
       Authorization: `Bearer ${accessToken}`,
       "Content-Type": "application/json",
     },
   });

+  const data = (await response.json()) as SetupSandboxResponse;
+
   if (!response.ok) {
-    throw new Error(`Failed to setup sandbox: ${response.status} ${response.statusText}`);
+    throw new Error(
+      data.status === "error"
+        ? data.error
+        : `Failed to setup sandbox: ${response.status} ${response.statusText}`
+    );
   }

-  return (await response.json()) as SetupSandboxResponse;
+  if (data.status === "error") {
+    throw new Error(data.error);
+  }
+
+  return data;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/sandboxes/setupSandbox.ts` around lines 13 - 27, The current setupSandbox
function treats any HTTP 2xx response as success but ignores payload-level
failures signaled by SetupSandboxResponse.status === "error"; update
setupSandbox to parse the JSON response body (await response.json()), then if
the parsed payload has status === "error" throw an Error that includes the
payload error message/details so callers don't treat a failed setup as
successful, otherwise return the parsed payload as SetupSandboxResponse;
continue to preserve the existing HTTP-level response.ok check and include
payload inspection (referencing the setupSandbox function and the
SetupSandboxResponse type).


73 changes: 43 additions & 30 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,55 @@ const nextConfig = {
],
},
images: {
domains: [
"i.imgur.com",
"ipfs.decentralized-content.com",
"pbs.twimg.com", // Twitter profile images
"abs.twimg.com", // Twitter media
"cdn.discordapp.com", // Discord
"scontent.xx.fbcdn.net", // Facebook
"scontent.cdninstagram.com", // Instagram
"instagram.fyvr4-1.fna.fbcdn.net", // Instagram
"platform-lookaside.fbsbx.com", // Facebook
"static-cdn.jtvnw.net", // Twitch
"yt3.ggpht.com", // YouTube
"i.ytimg.com", // YouTube
"avatars.githubusercontent.com", // GitHub
"example.com", // Example domain from our mock data
"arweave.net", // Arweave
"storage.googleapis.com", // Fal AI image hosting (backup)
],
remotePatterns: [
{
protocol: 'https',
hostname: '*.fal.media',
port: '',
pathname: '/**',
protocol: "https",
hostname: "*.fal.media",
pathname: "/**",
},
{
protocol: "https",
hostname: "ipfs.decentralized-content.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "arweave.net",
pathname: "/**",
},
{ protocol: "https", hostname: "i.imgur.com", pathname: "/**" },
{ protocol: "https", hostname: "pbs.twimg.com", pathname: "/**" },
{ protocol: "https", hostname: "abs.twimg.com", pathname: "/**" },
{ protocol: "https", hostname: "cdn.discordapp.com", pathname: "/**" },
{ protocol: "https", hostname: "scontent.xx.fbcdn.net", pathname: "/**" },
{
protocol: "https",
hostname: "scontent.cdninstagram.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "instagram.fyvr4-1.fna.fbcdn.net",
pathname: "/**",
},
{
protocol: "https",
hostname: "platform-lookaside.fbsbx.com",
pathname: "/**",
},
{ protocol: "https", hostname: "static-cdn.jtvnw.net", pathname: "/**" },
{ protocol: "https", hostname: "yt3.ggpht.com", pathname: "/**" },
{ protocol: "https", hostname: "i.ytimg.com", pathname: "/**" },
{
protocol: 'https',
hostname: 'ipfs.decentralized-content.com',
port: '',
pathname: '/**',
protocol: "https",
hostname: "avatars.githubusercontent.com",
pathname: "/**",
},
{ protocol: "https", hostname: "example.com", pathname: "/**" },
{
protocol: 'https',
hostname: 'arweave.net',
port: '',
pathname: '/**',
protocol: "https",
hostname: "storage.googleapis.com",
pathname: "/**",
},
],
},
Expand Down
2 changes: 2 additions & 0 deletions providers/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import WagmiProvider from "./WagmiProvider";
import { MiniAppProvider } from "./MiniAppProvider";
import { ThemeProvider } from "./ThemeProvider";
import { OrganizationProvider } from "./OrganizationProvider";
import SandboxSetupOnLogin from "./SandboxSetupOnLogin";

const queryClient = new QueryClient();

Expand All @@ -30,6 +31,7 @@ const Providers = ({ children }: { children: React.ReactNode }) => (
<MiniAppProvider>
<UserProvider>
<OrganizationProvider>
<SandboxSetupOnLogin />
<FunnelReportProvider>
<ArtistProvider>
<SidebarExpansionProvider>
Expand Down
11 changes: 11 additions & 0 deletions providers/SandboxSetupOnLogin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import { useSandboxSetupOnLogin } from "@/hooks/useSandboxSetupOnLogin";

const SandboxSetupOnLogin = () => {
useSandboxSetupOnLogin();
return null;
};

export default SandboxSetupOnLogin;