Skip to content

Commit 850ac21

Browse files
committed
Refactor and simplify ads client. Add more logging
1 parent 07b184b commit 850ac21

File tree

3 files changed

+120
-131
lines changed

3 files changed

+120
-131
lines changed

cli/src/hooks/use-gravity-ad.ts

Lines changed: 79 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { useChatStore } from '../state/chat-store'
66
import { getAuthToken } from '../utils/auth'
77
import { logger } from '../utils/logger'
88

9-
const AD_DISPLAY_DURATION_MS = 60 * 1000 // 60 seconds per ad
10-
const PREFETCH_BEFORE_MS = 5 * 1000 // Fetch next ad 5 seconds before swap
9+
const AD_ROTATION_INTERVAL_MS = 60 * 1000 // 60 seconds per ad
1110
const MAX_ADS_AFTER_ACTIVITY = 3 // Show up to 3 ads after last activity, then stop
1211

1312
// Ad response type (matches Gravity API response, credits added after impression)
@@ -31,41 +30,29 @@ export type GravityAdState = {
3130
* Hook for fetching and rotating Gravity ads.
3231
*
3332
* Behavior:
33+
* - Ads only start after the user sends their first message
3434
* - Ads rotate every 60 seconds
35-
* - Next ad is pre-fetched 5 seconds before display for instant swap
3635
* - After 3 ads without user activity, rotation stops
3736
* - Any user activity resets the counter and resumes rotation
3837
*/
3938
export const useGravityAd = (): GravityAdState => {
4039
const [ad, setAd] = useState<AdResponse | null>(null)
4140
const [isLoading, setIsLoading] = useState(false)
42-
const [shouldShowAd, setShouldShowAd] = useState(false)
41+
const [isActive, setIsActive] = useState(false)
4342
const impressionFiredRef = useRef<Set<string>>(new Set())
4443

45-
// Pre-fetched next ad ready to display
46-
const nextAdRef = useRef<AdResponse | null>(null)
47-
4844
// Counter: how many ads shown since last user activity
4945
const adsShownRef = useRef<number>(0)
5046

5147
// Is rotation currently paused (shown 3 ads without activity)?
5248
const isPausedRef = useRef<boolean>(false)
5349

54-
// Timers
55-
const prefetchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
56-
const swapTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
57-
58-
// Has the first ad been fetched?
59-
const isStartedRef = useRef<boolean>(false)
50+
// Rotation timer
51+
const rotationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
6052

6153
// Fire impression via web API when ad changes (grants credits)
62-
// Only fire impressions when ad is actually being shown
6354
useEffect(() => {
64-
if (
65-
shouldShowAd &&
66-
ad?.impUrl &&
67-
!impressionFiredRef.current.has(ad.impUrl)
68-
) {
55+
if (isActive && ad?.impUrl && !impressionFiredRef.current.has(ad.impUrl)) {
6956
const currentImpUrl = ad.impUrl
7057
impressionFiredRef.current.add(currentImpUrl)
7158
logger.info(
@@ -79,8 +66,6 @@ export const useGravityAd = (): GravityAdState => {
7966
return
8067
}
8168

82-
// Call our web API to fire impression and grant credits
83-
// Only send impUrl - server looks up trusted ad data from database
8469
fetch(`${WEBSITE_URL}/api/v1/ads/impression`, {
8570
method: 'POST',
8671
headers: {
@@ -98,7 +83,6 @@ export const useGravityAd = (): GravityAdState => {
9883
{ creditsGranted: data.creditsGranted },
9984
'[gravity] Ad impression credits granted',
10085
)
101-
// Update ad with credits from impression response
10286
setAd((currentAd) =>
10387
currentAd?.impUrl === currentImpUrl
10488
? { ...currentAd, credits: data.creditsGranted }
@@ -110,22 +94,17 @@ export const useGravityAd = (): GravityAdState => {
11094
logger.debug({ err }, '[gravity] Failed to record ad impression')
11195
})
11296
}
113-
}, [ad, shouldShowAd])
97+
}, [ad, isActive])
11498

115-
// Clear all timers
116-
const clearTimers = useCallback(() => {
117-
if (prefetchTimerRef.current) {
118-
clearTimeout(prefetchTimerRef.current)
119-
prefetchTimerRef.current = null
120-
}
121-
if (swapTimerRef.current) {
122-
clearTimeout(swapTimerRef.current)
123-
swapTimerRef.current = null
99+
const clearTimer = useCallback(() => {
100+
if (rotationTimerRef.current) {
101+
clearTimeout(rotationTimerRef.current)
102+
rotationTimerRef.current = null
124103
}
125104
}, [])
126105

127-
// Fetch an ad via web API and return it (for pre-fetching)
128-
const fetchAdAsync = useCallback(async (): Promise<AdResponse | null> => {
106+
// Fetch an ad via web API
107+
const fetchAd = useCallback(async (): Promise<AdResponse | null> => {
129108
if (!getAdsEnabled()) return null
130109

131110
const authToken = getAuthToken()
@@ -134,11 +113,30 @@ export const useGravityAd = (): GravityAdState => {
134113
return null
135114
}
136115

116+
// Get message history from runState (populated after LLM responds)
137117
const currentRunState = useChatStore.getState().runState
138118
const messageHistory =
139119
currentRunState?.sessionState?.mainAgentState?.messageHistory ?? []
140120
const adMessages = convertToAdMessages(messageHistory)
141121

122+
// Also check UI messages for the latest user message
123+
// (UI messages update immediately, runState.messageHistory updates after LLM responds)
124+
const uiMessages = useChatStore.getState().messages
125+
const latestUserMessage = [...uiMessages]
126+
.reverse()
127+
.find((msg) => msg.variant === 'user')
128+
129+
// If the latest UI user message isn't in our converted history, append it
130+
// This ensures we always include the most recent user message even before LLM responds
131+
if (latestUserMessage?.content) {
132+
const lastAdUserMessage = [...adMessages]
133+
.reverse()
134+
.find((m) => m.role === 'user')
135+
if (lastAdUserMessage?.content !== latestUserMessage.content) {
136+
adMessages.push({ role: 'user', content: latestUserMessage.content })
137+
}
138+
}
139+
142140
try {
143141
const response = await fetch(`${WEBSITE_URL}/api/v1/ads`, {
144142
method: 'POST',
@@ -161,9 +159,7 @@ export const useGravityAd = (): GravityAdState => {
161159
const ad = data.ad as AdResponse | null
162160

163161
logger.info(
164-
{
165-
ad,
166-
},
162+
{ ad, request: { messages: adMessages } },
167163
'[gravity] Received ad response',
168164
)
169165
return ad
@@ -173,27 +169,19 @@ export const useGravityAd = (): GravityAdState => {
173169
}
174170
}, [])
175171

176-
// Schedule the next ad cycle
177-
const scheduleNextCycle = useCallback(() => {
178-
clearTimers()
172+
// Schedule ad rotation
173+
const scheduleRotation = useCallback(() => {
174+
clearTimer()
179175

180176
if (!getAdsEnabled() || isPausedRef.current) {
181177
logger.debug(
182178
{ isPaused: isPausedRef.current },
183-
'[gravity] Not scheduling next cycle',
179+
'[gravity] Not scheduling rotation',
184180
)
185181
return
186182
}
187183

188-
// Schedule pre-fetch (55 seconds from now)
189-
prefetchTimerRef.current = setTimeout(async () => {
190-
logger.debug('[gravity] Pre-fetching next ad')
191-
nextAdRef.current = await fetchAdAsync()
192-
}, AD_DISPLAY_DURATION_MS - PREFETCH_BEFORE_MS)
193-
194-
// Schedule swap (60 seconds from now)
195-
swapTimerRef.current = setTimeout(() => {
196-
// Increment counter and check if we should pause
184+
rotationTimerRef.current = setTimeout(async () => {
197185
adsShownRef.current += 1
198186
logger.info(
199187
{ adsShown: adsShownRef.current, max: MAX_ADS_AFTER_ACTIVITY },
@@ -203,68 +191,43 @@ export const useGravityAd = (): GravityAdState => {
203191
if (adsShownRef.current >= MAX_ADS_AFTER_ACTIVITY) {
204192
logger.info('[gravity] Max ads shown, pausing rotation')
205193
isPausedRef.current = true
206-
// Keep showing the current ad, just stop rotating
207194
return
208195
}
209196

210-
// Swap to pre-fetched ad (or keep current if fetch failed)
211-
if (nextAdRef.current) {
212-
setAd(nextAdRef.current)
213-
nextAdRef.current = null
197+
const newAd = await fetchAd()
198+
if (newAd) {
199+
setAd(newAd)
214200
}
215201

216-
// Schedule next cycle
217-
scheduleNextCycle()
218-
}, AD_DISPLAY_DURATION_MS)
219-
}, [clearTimers, fetchAdAsync])
202+
scheduleRotation()
203+
}, AD_ROTATION_INTERVAL_MS)
204+
}, [clearTimer, fetchAd])
220205

221206
// Report user activity - resets counter and resumes rotation if paused
222207
const reportActivity = useCallback(() => {
223208
const wasPaused = isPausedRef.current
224-
225-
// Reset counter
226209
adsShownRef.current = 0
227210

228211
if (wasPaused) {
229212
logger.info('[gravity] User active, resuming ad rotation')
230213
isPausedRef.current = false
231-
// Restart the cycle from current ad
232-
scheduleNextCycle()
233-
}
234-
}, [scheduleNextCycle])
235-
236-
// Prefetch ad on startup (before any messages are sent)
237-
useEffect(() => {
238-
const adsEnabled = getAdsEnabled()
239-
const hasAuth = !!getAuthToken()
240-
241-
if (adsEnabled && hasAuth && !isStartedRef.current) {
242-
logger.info('[gravity] Prefetching ad on startup')
243-
isStartedRef.current = true
244-
setIsLoading(true)
245-
246-
// Prefetch first ad immediately
247-
fetchAdAsync().then((firstAd) => {
248-
setAd(firstAd)
249-
setIsLoading(false)
250-
scheduleNextCycle()
251-
})
214+
scheduleRotation()
252215
}
253-
}, [fetchAdAsync, scheduleNextCycle])
216+
}, [scheduleRotation])
254217

255218
// Subscribe to UI messages to detect first user message
256-
// Only show ads after the user has sent at least one message (clean startup UX)
257-
// We use UI messages instead of runState.messageHistory because UI messages
258-
// update immediately when the user sends a message
219+
// We use UI messages (not runState.messageHistory) because UI messages
220+
// update immediately when the user sends a message, allowing us to fetch
221+
// ads sooner rather than waiting for the assistant to respond
259222
useEffect(() => {
260-
if (shouldShowAd || !getAdsEnabled()) {
223+
if (isActive || !getAdsEnabled()) {
261224
return
262225
}
263226

264227
// Check initial state
265228
const initialMessages = useChatStore.getState().messages
266229
if (initialMessages.some((msg) => msg.variant === 'user')) {
267-
setShouldShowAd(true)
230+
setIsActive(true)
268231
return
269232
}
270233

@@ -275,21 +238,35 @@ export const useGravityAd = (): GravityAdState => {
275238

276239
if (hasUserMessage) {
277240
unsubscribe()
278-
logger.info('[gravity] First user message detected, showing ads')
279-
setShouldShowAd(true)
241+
logger.info('[gravity] First user message detected, starting ads')
242+
setIsActive(true)
280243
}
281244
})
282245

283246
return unsubscribe
284-
}, [shouldShowAd])
247+
}, [isActive])
248+
249+
// Fetch first ad and start rotation when becoming active
250+
useEffect(() => {
251+
if (!isActive) return
252+
253+
setIsLoading(true)
254+
fetchAd().then((firstAd) => {
255+
if (firstAd) {
256+
setAd(firstAd)
257+
}
258+
// Always start rotation, even if first fetch returned null
259+
scheduleRotation()
260+
setIsLoading(false)
261+
})
262+
}, [isActive, fetchAd, scheduleRotation])
285263

286-
// Clear timers only on unmount
264+
// Cleanup timer on unmount
287265
useEffect(() => {
288-
return () => clearTimers()
289-
}, [clearTimers])
266+
return () => clearTimer()
267+
}, [clearTimer])
290268

291-
// Only return the ad if we should show it (after first user message)
292-
return { ad: shouldShowAd ? ad : null, isLoading, reportActivity }
269+
return { ad: isActive ? ad : null, isLoading, reportActivity }
293270
}
294271

295272
type AdMessage = { role: 'user' | 'assistant'; content: string }
@@ -303,13 +280,17 @@ const convertToAdMessages = (messages: Message[]): AdMessage[] => {
303280
.filter(
304281
(message) => message.role === 'assistant' || message.role === 'user',
305282
)
283+
.filter((message) => !message.tags?.includes('USER_PROMPT'))
306284
.map((message) => ({
307285
role: message.role,
308286
content: message.content
309287
.filter((c) => c.type === 'text')
310-
.map((c) => c.text)
311-
.join('\n\n'),
288+
.map((c) => c.text.trim())
289+
.filter((c) => c !== '')
290+
.join('\n\n')
291+
.trim(),
312292
}))
293+
.filter((message) => message.content !== '')
313294

314295
return adMessages
315296
}

0 commit comments

Comments
 (0)