diff --git a/cli/src/components/waiting-room-screen.tsx b/cli/src/components/waiting-room-screen.tsx index 2bbee6c71..d48d986d2 100644 --- a/cli/src/components/waiting-room-screen.tsx +++ b/cli/src/components/waiting-room-screen.tsx @@ -90,6 +90,7 @@ export const WaitingRoomScreen: React.FC = ({ forceStart: true, provider: 'gravity', fallbackProvider: 'carbon', + surface: 'waiting_room', }) useFreebuffCtrlCExit() diff --git a/cli/src/hooks/use-gravity-ad.ts b/cli/src/hooks/use-gravity-ad.ts index 36a18faae..ea6977864 100644 --- a/cli/src/hooks/use-gravity-ad.ts +++ b/cli/src/hooks/use-gravity-ad.ts @@ -35,6 +35,7 @@ export type AdVariant = 'banner' | 'choice' * same normalized response shape, so the rest of the hook is provider-agnostic. */ export type AdProvider = 'gravity' | 'carbon' +export type AdSurface = 'waiting_room' export type AdData = | { variant: 'banner'; ad: AdResponse } @@ -112,11 +113,14 @@ export const useGravityAd = (options?: { provider?: AdProvider /** Backup ad network to try when the primary returns no fill or errors. */ fallbackProvider?: AdProvider + /** Product surface requesting the ad. The server maps this to placements. */ + surface?: AdSurface }): GravityAdState => { const enabled = options?.enabled ?? true const forceStart = options?.forceStart ?? false const provider: AdProvider = options?.provider ?? 'gravity' const fallbackProvider = options?.fallbackProvider + const surface = options?.surface const [ad, setAd] = useState(null) const [adData, setAdData] = useState(null) const [isLoading, setIsLoading] = useState(false) @@ -299,6 +303,7 @@ export const useGravityAd = (options?: { messages: adMessages, sessionId: useChatStore.getState().chatSessionId, device: getDeviceInfo(), + ...(surface ? { surface } : {}), // Carbon requires a real browser-ish useragent for targeting/fraud // detection. Gravity ignores it. We source one centrally so every // provider that needs it sees the same value. @@ -430,7 +435,7 @@ export const useGravityAd = (options?: { clearInterval(id) ctrlRef.current.intervalId = null } - }, [shouldStart, shouldHideAds, provider, fallbackProvider]) + }, [shouldStart, shouldHideAds, provider, fallbackProvider, surface]) // Don't return ad when ads should be hidden const visible = shouldStart && !shouldHideAds diff --git a/web/src/app/api/v1/ads/_post.ts b/web/src/app/api/v1/ads/_post.ts index fc1fa07a5..a56846b05 100644 --- a/web/src/app/api/v1/ads/_post.ts +++ b/web/src/app/api/v1/ads/_post.ts @@ -35,12 +35,14 @@ const deviceSchema = z.object({ }) const providerSchema = z.enum(['gravity', 'carbon']).default('gravity') +const surfaceSchema = z.enum(['waiting_room']) const bodySchema = z.object({ provider: providerSchema.optional(), messages: z.array(messageSchema).optional().default([]), sessionId: z.string().optional(), device: deviceSchema.optional(), + surface: surfaceSchema.optional(), /** Browser/CLI useragent passed through to providers that require it. */ userAgent: z.string().optional(), }) @@ -136,6 +138,7 @@ export async function postAds(params: { clientIp, userAgent, device: parsedBody.device, + surface: parsedBody.surface, messages: parsedBody.messages, testMode: serverEnv.CB_ENVIRONMENT !== 'prod', logger, diff --git a/web/src/lib/ad-providers/gravity.ts b/web/src/lib/ad-providers/gravity.ts index ed9209cb0..4ae33b514 100644 --- a/web/src/lib/ad-providers/gravity.ts +++ b/web/src/lib/ad-providers/gravity.ts @@ -19,6 +19,12 @@ const CHOICE_PLACEMENT_IDS = [ 'choice-ad-3', 'choice-ad-4', ] +const WAITING_ROOM_PLACEMENT_IDS = [ + 'waiting-room-1', + 'waiting-room-2', + 'waiting-room-3', + 'waiting-room-4', +] type GravityRawAd = { adText: string @@ -105,16 +111,21 @@ export function createGravityProvider(config: { apiKey: string }): AdProvider { fetch, } = input - const variant = getGravityVariant(userId) + const variant = + input.surface === 'waiting_room' ? 'choice' : getGravityVariant(userId) const filteredMessages = prepareGravityMessages(messages) - const placements = - variant === 'choice' - ? CHOICE_PLACEMENT_IDS.map((id) => ({ - placement: 'below_response', - placement_id: id, - })) - : [{ placement: 'below_response', placement_id: BANNER_PLACEMENT_ID }] + const placementIds = + input.surface === 'waiting_room' + ? WAITING_ROOM_PLACEMENT_IDS + : variant === 'choice' + ? CHOICE_PLACEMENT_IDS + : [BANNER_PLACEMENT_ID] + + const placements = placementIds.map((id) => ({ + placement: 'below_response', + placement_id: id, + })) const deviceBody = clientIp ? { diff --git a/web/src/lib/ad-providers/types.ts b/web/src/lib/ad-providers/types.ts index 5b664332b..fb3284e2a 100644 --- a/web/src/lib/ad-providers/types.ts +++ b/web/src/lib/ad-providers/types.ts @@ -41,6 +41,8 @@ export type AdDeviceInfo = { locale?: string } +export type AdSurface = 'waiting_room' + export type FetchAdInput = { userId: string userEmail: string | null @@ -50,6 +52,8 @@ export type FetchAdInput = { /** Browser/CLI useragent string, passed through to upstream. */ userAgent?: string device?: AdDeviceInfo + /** Product surface requesting the ad. Providers may map this to placements. */ + surface?: AdSurface /** Last user + last preceding assistant message, if any. Used by Gravity. */ messages?: AdMessage[] /** Set in non-prod so providers can request test ads. */