Skip to content

Commit ed92ef3

Browse files
committed
Ads info banner. Encourage users to hide ads if they don't like them
1 parent 9469d3d commit ed92ef3

File tree

2 files changed

+114
-6
lines changed

2 files changed

+114
-6
lines changed

cli/src/chat.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from 'react'
1111
import { useShallow } from 'zustand/react/shallow'
1212

13-
import { getAdsEnabled } from './commands/ads'
13+
import { getAdsEnabled, handleAdsDisable } from './commands/ads'
1414
import { routeUserPrompt, addBashMessageToHistory } from './commands/router'
1515
import { AdBanner } from './components/ad-banner'
1616
import { BottomStatusLine } from './components/bottom-status-line'
@@ -162,6 +162,12 @@ export const Chat = ({
162162

163163
const { statusMessage } = useClipboard()
164164
const { ad } = useGravityAd()
165+
const [adsManuallyDisabled, setAdsManuallyDisabled] = useState(false)
166+
167+
const handleDisableAds = useCallback(() => {
168+
handleAdsDisable()
169+
setAdsManuallyDisabled(true)
170+
}, [])
165171

166172
// Fetch subscription data early - needed for session credits tracking
167173
const { data: subscriptionData } = useSubscriptionQuery({
@@ -1432,7 +1438,13 @@ export const Chat = ({
14321438
/>
14331439
)}
14341440

1435-
{ad && getAdsEnabled() && <AdBanner ad={ad} />}
1441+
{ad && !adsManuallyDisabled && getAdsEnabled() && (
1442+
<AdBanner
1443+
ad={ad}
1444+
onDisableAds={handleDisableAds}
1445+
isFreeMode={agentMode === 'FREE'}
1446+
/>
1447+
)}
14361448

14371449
{reviewMode ? (
14381450
<ReviewScreen

cli/src/components/ad-banner.tsx

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { TextAttributes } from '@opentui/core'
12
import open from 'open'
23
import React, { useCallback, useState } from 'react'
34

@@ -10,6 +11,8 @@ import type { AdResponse } from '../hooks/use-gravity-ad'
1011

1112
interface AdBannerProps {
1213
ad: AdResponse
14+
onDisableAds: () => void
15+
isFreeMode: boolean
1316
}
1417

1518
const extractDomain = (url: string): string => {
@@ -21,10 +24,14 @@ const extractDomain = (url: string): string => {
2124
}
2225
}
2326

24-
export const AdBanner: React.FC<AdBannerProps> = ({ ad }) => {
27+
export const AdBanner: React.FC<AdBannerProps> = ({ ad, onDisableAds, isFreeMode }) => {
2528
const theme = useTheme()
2629
const { separatorWidth, terminalWidth } = useTerminalDimensions()
2730
const [isLinkHovered, setIsLinkHovered] = useState(false)
31+
const [showInfoPanel, setShowInfoPanel] = useState(false)
32+
const [isAdLabelHovered, setIsAdLabelHovered] = useState(false)
33+
const [isHideHovered, setIsHideHovered] = useState(false)
34+
const [isCloseHovered, setIsCloseHovered] = useState(false)
2835

2936
const handleClick = useCallback(() => {
3037
if (ad.clickUrl) {
@@ -40,8 +47,8 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad }) => {
4047
const ctaText = ad.cta || ad.title || 'Learn more'
4148

4249
// Calculate available width for ad text
43-
// Account for: padding (2), "Ad" label with space (3)
44-
const maxTextWidth = separatorWidth - 5
50+
// Account for: padding (2), "Ad ?" label with space (5)
51+
const maxTextWidth = separatorWidth - 7
4552

4653
return (
4754
<box
@@ -72,7 +79,20 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad }) => {
7279
>
7380
{ad.adText}
7481
</text>
75-
<text style={{ fg: theme.muted, flexShrink: 0 }}>Ad</text>
82+
<Button
83+
onClick={() => setShowInfoPanel(true)}
84+
onMouseOver={() => setIsAdLabelHovered(true)}
85+
onMouseOut={() => setIsAdLabelHovered(false)}
86+
>
87+
<text
88+
style={{
89+
fg: isAdLabelHovered && !showInfoPanel ? theme.foreground : theme.muted,
90+
flexShrink: 0,
91+
}}
92+
>
93+
{isAdLabelHovered && !showInfoPanel ? 'Ad ?' : ' Ad'}
94+
</text>
95+
</Button>
7696
</box>
7797
{/* Bottom line: button, domain, credits */}
7898
<box
@@ -108,6 +128,82 @@ export const AdBanner: React.FC<AdBannerProps> = ({ ad }) => {
108128
<text style={{ fg: theme.muted }}>+{ad.credits} credits</text>
109129
)}
110130
</box>
131+
{/* Info panel: shown when Ad label is clicked, below the ad */}
132+
{showInfoPanel && (
133+
<box
134+
style={{
135+
width: '100%',
136+
flexDirection: 'column',
137+
gap: 0,
138+
}}
139+
>
140+
<text style={{ fg: theme.muted }}>{' ' + '┄'.repeat(separatorWidth - 2)}</text>
141+
<box
142+
style={{
143+
width: '100%',
144+
paddingLeft: 1,
145+
paddingRight: 1,
146+
flexDirection: 'row',
147+
justifyContent: 'space-between',
148+
alignItems: 'flex-start',
149+
}}
150+
>
151+
<text style={{ fg: theme.muted, flexShrink: 1 }}>
152+
Ads are optional and earn you credits on each impression. Feel free to hide them anytime.
153+
</text>
154+
<Button
155+
onClick={() => setShowInfoPanel(false)}
156+
onMouseOver={() => setIsCloseHovered(true)}
157+
onMouseOut={() => setIsCloseHovered(false)}
158+
>
159+
<text
160+
style={{
161+
fg: isCloseHovered ? theme.foreground : theme.muted,
162+
flexShrink: 0,
163+
}}
164+
>
165+
{' ✕'}
166+
</text>
167+
</Button>
168+
</box>
169+
<box
170+
style={{
171+
paddingLeft: 1,
172+
paddingRight: 1,
173+
flexDirection: 'row',
174+
alignItems: 'center',
175+
gap: 2,
176+
}}
177+
>
178+
{isFreeMode ? (
179+
<text style={{ fg: theme.muted }}>
180+
Ads are required in Free mode.
181+
</text>
182+
) : (
183+
<>
184+
<Button
185+
onClick={onDisableAds}
186+
onMouseOver={() => setIsHideHovered(true)}
187+
onMouseOut={() => setIsHideHovered(false)}
188+
>
189+
<text
190+
style={{
191+
fg: isHideHovered ? theme.link : theme.muted,
192+
attributes: TextAttributes.UNDERLINE,
193+
}}
194+
>
195+
Hide ads
196+
</text>
197+
</Button>
198+
<text style={{ fg: theme.muted }}>·</text>
199+
<text style={{ fg: theme.muted }}>
200+
Use /ads:enable to show again
201+
</text>
202+
</>
203+
)}
204+
</box>
205+
</box>
206+
)}
111207
</box>
112208
)
113209
}

0 commit comments

Comments
 (0)