diff --git a/app/api/agent/stream/route.ts b/app/api/agent/stream/route.ts index 79a3f0c..62ae5be 100644 --- a/app/api/agent/stream/route.ts +++ b/app/api/agent/stream/route.ts @@ -8,7 +8,9 @@ export const maxDuration = 600; function sseEncode(event: string, data: unknown): Uint8Array { const encoder = new TextEncoder(); - return encoder.encode(`event: ${event}\n` + `data: ${JSON.stringify(data)}\n\n`); + return encoder.encode( + `event: ${event}\n` + `data: ${JSON.stringify(data)}\n\n` + ); } function sseComment(comment: string): Uint8Array { @@ -18,7 +20,11 @@ function sseComment(comment: string): Uint8Array { export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const [sessionId, goal] = [searchParams.get("sessionId"), searchParams.get("goal")]; + const [sessionId, goal, fromChat] = [ + searchParams.get("sessionId"), + searchParams.get("goal"), + searchParams.get("fromChat") === "true", + ]; if (!sessionId || !goal) { return new Response( @@ -39,11 +45,14 @@ export async function GET(request: Request) { }, 15000); let timeoutTimer: ReturnType | undefined; - timeoutTimer = setTimeout(async () => { - console.log(`[SSE] Timeout reached for session ${sessionId}`); - send("error", { message: "Agent run timed out after 10 minutes" }); - await cleanup(); - }, 10 * 60 * 1000); + timeoutTimer = setTimeout( + async () => { + console.log(`[SSE] Timeout reached for session ${sessionId}`); + send("error", { message: "Agent run timed out after 10 minutes" }); + await cleanup(); + }, + 10 * 60 * 1000 + ); let closed = false; @@ -52,7 +61,10 @@ export async function GET(request: Request) { try { controller.enqueue(chunk); } catch (err) { - console.error(`[SSE] enqueue error`, err instanceof Error ? err.message : String(err)); + console.error( + `[SSE] enqueue error`, + err instanceof Error ? err.message : String(err) + ); } }; @@ -61,7 +73,10 @@ export async function GET(request: Request) { try { safeEnqueue(sseEncode(event, data)); } catch (err) { - console.error(`[SSE] send error`, err instanceof Error ? err.message : String(err)); + console.error( + `[SSE] send error`, + err instanceof Error ? err.message : String(err) + ); } }; @@ -89,11 +104,14 @@ export async function GET(request: Request) { }, 15000); // Hard timeout at 10 minutes - timeoutTimer = setTimeout(async () => { - console.log(`[SSE] Timeout reached for session ${sessionId}`); - send("error", { message: "Agent run timed out after 10 minutes" }); - await cleanup(); - }, 10 * 60 * 1000); + timeoutTimer = setTimeout( + async () => { + console.log(`[SSE] Timeout reached for session ${sessionId}`); + send("error", { message: "Agent run timed out after 10 minutes" }); + await cleanup(); + }, + 10 * 60 * 1000 + ); console.log(`[SSE] Starting Stagehand agent run`, { sessionId, @@ -101,7 +119,9 @@ export async function GET(request: Request) { hasInstructions: true, }); - const logger = createStagehandUserLogger(send, { forwardStepEvents: false }); + const logger = createStagehandUserLogger(send, { + forwardStepEvents: false, + }); const stagehand = new Stagehand({ env: "BROWSERBASE", @@ -118,6 +138,7 @@ export async function GET(request: Request) { width: 1288, height: 711, }, + solveCaptchas: !fromChat, // false if session is from a search param, true otherwise }, }, useAPI: false, @@ -140,7 +161,7 @@ export async function GET(request: Request) { }); const agent = stagehand.agent({ - provider: "google", + provider: "google", model: "computer-use-preview-10-2025", options: { apiKey: process.env.GOOGLE_API_KEY, @@ -149,24 +170,24 @@ export async function GET(request: Request) { }); const result = await agent.execute({ - instruction: goal, - autoScreenshot: true, - waitBetweenActions: 200, - maxSteps: 100, + instruction: goal, + autoScreenshot: true, + waitBetweenActions: 200, + maxSteps: 100, }); try { - console.log(`[SSE] metrics snapshot`, stagehand.metrics); - send("metrics", stagehand.metrics); + console.log(`[SSE] metrics snapshot`, stagehand.metrics); + send("metrics", stagehand.metrics); } catch {} - const finalMessage = logger.getLastReasoning(); - console.log(`[SSE] done`, { - success: result.success, - completed: result.completed, - finalMessage: finalMessage - }); - send("done", { ...result, finalMessage }); + const finalMessage = logger.getLastReasoning(); + console.log(`[SSE] done`, { + success: result.success, + completed: result.completed, + finalMessage: finalMessage, + }); + send("done", { ...result, finalMessage }); await cleanup(stagehand); } catch (error) { diff --git a/app/api/session/route.ts b/app/api/session/route.ts index c640ebf..de60c05 100644 --- a/app/api/session/route.ts +++ b/app/api/session/route.ts @@ -7,70 +7,107 @@ type BrowserbaseRegion = | "eu-central-1" | "ap-southeast-1"; -// Exact timezone matches for east coast cities -const exactTimezoneMap: Record = { - "America/New_York": "us-east-1", - "America/Detroit": "us-east-1", - "America/Toronto": "us-east-1", - "America/Montreal": "us-east-1", - "America/Boston": "us-east-1", - "America/Chicago": "us-east-1", +// Timezone abbreviation to region mapping +const timezoneAbbreviationMap: Record = { + // US East Coast + EST: "us-east-1", + EDT: "us-east-1", + + // US West Coast + PST: "us-west-2", + PDT: "us-west-2", + + // US Mountain/Central - route to appropriate region + MST: "us-west-2", + MDT: "us-west-2", + CST: "us-east-1", + CDT: "us-east-1", + + // Europe + GMT: "eu-central-1", + BST: "eu-central-1", + CET: "eu-central-1", + CEST: "eu-central-1", + EET: "eu-central-1", + EEST: "eu-central-1", + WET: "eu-central-1", + WEST: "eu-central-1", + + // Asia-Pacific + JST: "ap-southeast-1", // Japan Standard Time + // Note: CST conflicts between US Central and China Standard - US takes priority + KST: "ap-southeast-1", // Korea Standard Time + IST: "ap-southeast-1", + AEST: "ap-southeast-1", + AEDT: "ap-southeast-1", + AWST: "ap-southeast-1", + NZST: "ap-southeast-1", + NZDT: "ap-southeast-1", }; -// Prefix-based region mapping -const prefixToRegion: Record = { - America: "us-west-2", - US: "us-west-2", - Canada: "us-west-2", - Europe: "eu-central-1", - Africa: "eu-central-1", - Asia: "ap-southeast-1", - Australia: "ap-southeast-1", - Pacific: "ap-southeast-1", +// Probability distributions for region routing +const distributions: Record< + BrowserbaseRegion, + Record +> = { + "us-west-2": { + "us-west-2": 100, + "us-east-1": 0, + "eu-central-1": 0, + "ap-southeast-1": 0, + }, + "us-east-1": { + "us-east-1": 100, + "us-west-2": 0, + "eu-central-1": 0, + "ap-southeast-1": 0, + }, + "eu-central-1": { + "eu-central-1": 100, + "us-east-1": 0, + "us-west-2": 0, + "ap-southeast-1": 0, + }, + "ap-southeast-1": { + "ap-southeast-1": 100, + "eu-central-1": 0, + "us-west-2": 0, + "us-east-1": 0, + }, }; -// Offset ranges to regions (inclusive bounds) -const offsetRanges: { - min: number; - max: number; - region: BrowserbaseRegion; -}[] = [ - { min: -24, max: -4, region: "us-west-2" }, // UTC-24 to UTC-4 - { min: -3, max: 4, region: "eu-central-1" }, // UTC-3 to UTC+4 - { min: 5, max: 24, region: "ap-southeast-1" }, // UTC+5 to UTC+24 -]; - -function getClosestRegion(timezone?: string): BrowserbaseRegion { - try { - if (!timezone) { - return "us-west-2"; // Default if no timezone provided +function selectRegionWithProbability( + baseRegion: BrowserbaseRegion +): BrowserbaseRegion { + const distribution = distributions[baseRegion]; + const random = Math.random() * 100; // Generate random number between 0-100 + + let cumulativeProbability = 0; + for (const [region, probability] of Object.entries(distribution)) { + cumulativeProbability += probability; + if (random < cumulativeProbability) { + return region as BrowserbaseRegion; } + } - // Check exact matches first - if (timezone in exactTimezoneMap) { - return exactTimezoneMap[timezone]; - } + // Fallback to base region if something goes wrong + return baseRegion; +} - // Check prefix matches - const prefix = timezone.split("/")[0]; - if (prefix in prefixToRegion) { - return prefixToRegion[prefix]; +function getRegionFromTimezoneAbbr(timezoneAbbr?: string): BrowserbaseRegion { + try { + if (!timezoneAbbr) { + return "us-west-2"; // Default if no timezone provided } - // Use offset-based fallback - const date = new Date(); - // Create a date formatter for the given timezone - const formatter = new Intl.DateTimeFormat("en-US", { timeZone: timezone }); - // Get the timezone offset in minutes - const timeString = formatter.format(date); - const testDate = new Date(timeString); - const hourOffset = (testDate.getTime() - date.getTime()) / (1000 * 60 * 60); - - const matchingRange = offsetRanges.find( - (range) => hourOffset >= range.min && hourOffset <= range.max - ); + // Direct lookup from timezone abbreviation + const region = timezoneAbbreviationMap[timezoneAbbr.toUpperCase()]; + if (region) { + return region; + } - return matchingRange?.region ?? "us-west-2"; + // Fallback to us-west-2 for unknown abbreviations + return "us-west-2"; } catch { return "us-west-2"; } @@ -81,26 +118,32 @@ async function createSession(timezone?: string) { apiKey: process.env.BROWSERBASE_API_KEY!, }); - const browserSettings: Browserbase.Sessions.SessionCreateParams.BrowserSettings = { - viewport: { - width: 2560, - height: 1440, - }, - //@ts-expect-error - not present in the types, but valid - os: "windows", - blockAds: true, - advancedStealth: true - }; - - console.log("timezone ", timezone); - console.log("getClosestRegion(timezone)", getClosestRegion(timezone)); + const browserSettings: Browserbase.Sessions.SessionCreateParams.BrowserSettings = + { + viewport: { + width: 2560, + height: 1440, + }, + // @ts-expect-error - os is not a valid property + os: "windows", + blockAds: true, + advancedStealth: true, + }; + + // Use timezone abbreviation to determine region + const closestRegion = getRegionFromTimezoneAbbr(timezone); + console.log("timezone abbreviation:", timezone); + console.log("mapped to region:", closestRegion); + + const finalRegion = selectRegionWithProbability(closestRegion); + console.log("finalRegion after probability routing", finalRegion); const session = await bb.sessions.create({ projectId: process.env.BROWSERBASE_PROJECT_ID!, proxies: true, browserSettings, keepAlive: true, - region: getClosestRegion(timezone), + region: finalRegion, }); return { session, diff --git a/app/components/BrowserSessionContainer.tsx b/app/components/BrowserSessionContainer.tsx index 072d2c7..68eaba2 100644 --- a/app/components/BrowserSessionContainer.tsx +++ b/app/components/BrowserSessionContainer.tsx @@ -11,6 +11,7 @@ interface BrowserSessionContainerProps { isCompleted: boolean; initialMessage: string | undefined; sessionTime?: number; + isFromSearchParam?: boolean; onStop?: () => void; onRestart?: () => void; } @@ -104,6 +105,7 @@ const BrowserSessionContainer: React.FC = ({ isCompleted, initialMessage, sessionTime = 0, + isFromSearchParam = false, onStop = () => {}, onRestart = () => {}, }) => { @@ -192,7 +194,7 @@ const BrowserSessionContainer: React.FC = ({ sessionUrl ? (