Skip to content

Commit fc1690e

Browse files
committed
feat(frontend): Update playground to use backend rate limits
- Fetch limits on mount from GET /playground/limits - Include credentials for session cookie tracking - Use backend response as source of truth for remaining count - Handle 429 rate limit errors with user-friendly message - Remove client-side only tracking (was bypassable on refresh) - Update both LandingPage.tsx and Playground.tsx Part of #93
1 parent d2872bd commit fc1690e

2 files changed

Lines changed: 78 additions & 30 deletions

File tree

frontend/src/pages/LandingPage.tsx

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,24 @@ export function LandingPage() {
106106
const [results, setResults] = useState<SearchResult[]>([])
107107
const [loading, setLoading] = useState(false)
108108
const [searchTime, setSearchTime] = useState<number | null>(null)
109-
const [searchCount, setSearchCount] = useState(0)
109+
const [remaining, setRemaining] = useState(50) // Will be updated from backend
110+
const [limit, setLimit] = useState(50) // Total limit from backend
110111
const [hasSearched, setHasSearched] = useState(false)
111112
const [availableRepos, setAvailableRepos] = useState<string[]>([])
113+
const [rateLimitError, setRateLimitError] = useState<string | null>(null)
112114

113-
const FREE_LIMIT = 5
114-
const remaining = FREE_LIMIT - searchCount
115+
// Fetch rate limit status on mount (backend is source of truth)
116+
useEffect(() => {
117+
fetch(`${API_URL}/playground/limits`, {
118+
credentials: 'include', // Send cookies for session tracking
119+
})
120+
.then(res => res.json())
121+
.then(data => {
122+
setRemaining(data.remaining ?? 50)
123+
setLimit(data.limit ?? 50)
124+
})
125+
.catch(console.error)
126+
}, [])
115127

116128
useEffect(() => {
117129
fetch(`${API_URL}/playground/repos`)
@@ -125,23 +137,36 @@ export function LandingPage() {
125137

126138
const handleSearch = async (searchQuery?: string) => {
127139
const q = searchQuery || query
128-
if (!q.trim() || loading || searchCount >= FREE_LIMIT) return
140+
if (!q.trim() || loading || remaining <= 0) return
129141

130142
setLoading(true)
131143
setHasSearched(true)
144+
setRateLimitError(null)
132145
const startTime = Date.now()
133146

134147
try {
135148
const response = await fetch(`${API_URL}/playground/search`, {
136149
method: 'POST',
137150
headers: { 'Content-Type': 'application/json' },
151+
credentials: 'include', // Send cookies for session tracking
138152
body: JSON.stringify({ query: q, demo_repo: selectedRepo, max_results: 10 })
139153
})
140154
const data = await response.json()
155+
141156
if (response.ok) {
142157
setResults(data.results || [])
143-
setSearchTime(Date.now() - startTime)
144-
setSearchCount(prev => prev + 1)
158+
setSearchTime(data.search_time_ms || (Date.now() - startTime))
159+
// Update remaining from backend (source of truth)
160+
if (typeof data.remaining_searches === 'number') {
161+
setRemaining(data.remaining_searches)
162+
}
163+
if (typeof data.limit === 'number') {
164+
setLimit(data.limit)
165+
}
166+
} else if (response.status === 429) {
167+
// Rate limit exceeded
168+
setRateLimitError(data.detail?.message || 'Daily limit reached. Sign up for unlimited searches!')
169+
setRemaining(0)
145170
}
146171
} catch (error) {
147172
console.error('Search error:', error)
@@ -240,7 +265,7 @@ export function LandingPage() {
240265
</div>
241266
<Button
242267
type="submit"
243-
disabled={loading || !query.trim() || searchCount >= FREE_LIMIT}
268+
disabled={loading || !query.trim() || remaining <= 0}
244269
className="px-6 py-3 h-auto bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 rounded-xl disabled:opacity-50 shrink-0"
245270
>
246271
{loading ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : 'Search'}
@@ -281,15 +306,17 @@ export function LandingPage() {
281306
<span className="text-gray-400"><span className="text-white font-semibold">{results.length}</span> results</span>
282307
{searchTime && <><span className="text-gray-700"></span><span className="font-mono text-green-400">{searchTime}ms</span></>}
283308
</div>
284-
{remaining > 0 && remaining < FREE_LIMIT && (
309+
{remaining > 0 && remaining < limit && (
285310
<div className="text-sm text-gray-500">{remaining} remaining</div>
286311
)}
287312
</div>
288313

289-
{searchCount >= FREE_LIMIT && (
314+
{(remaining <= 0 || rateLimitError) && (
290315
<Card className="bg-gradient-to-r from-blue-600/20 to-purple-600/20 border-blue-500/30 p-6 mb-6">
291-
<h3 className="text-lg font-semibold mb-2">You've used all free searches</h3>
292-
<p className="text-gray-300 mb-4">Sign up to get unlimited searches and index your own repos.</p>
316+
<h3 className="text-lg font-semibold mb-2">You've reached today's limit</h3>
317+
<p className="text-gray-300 mb-4">
318+
{rateLimitError || 'Sign up to get unlimited searches and index your own repos.'}
319+
</p>
293320
<Button onClick={() => navigate('/signup')} className="bg-white text-black hover:bg-gray-100">Get started — it's free</Button>
294321
</Card>
295322
)}

frontend/src/pages/Playground.tsx

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react'
1+
import { useState, useEffect } from 'react'
22
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
33
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
44
import { API_URL } from '../config/api'
@@ -29,28 +29,38 @@ export function Playground({ onSignupClick }: PlaygroundProps) {
2929
const [results, setResults] = useState<SearchResult[]>([])
3030
const [loading, setLoading] = useState(false)
3131
const [searchTime, setSearchTime] = useState<number | null>(null)
32-
const [searchCount, setSearchCount] = useState(0)
32+
const [remaining, setRemaining] = useState(50) // Will be updated from backend
33+
const [limit, setLimit] = useState(50) // Total limit from backend
3334
const [hasSearched, setHasSearched] = useState(false)
35+
const [rateLimitError, setRateLimitError] = useState<string | null>(null)
3436

35-
const FREE_SEARCH_LIMIT = 5
37+
// Fetch rate limit status on mount (backend is source of truth)
38+
useEffect(() => {
39+
fetch(`${API_URL}/playground/limits`, {
40+
credentials: 'include', // Send cookies for session tracking
41+
})
42+
.then(res => res.json())
43+
.then(data => {
44+
setRemaining(data.remaining ?? 50)
45+
setLimit(data.limit ?? 50)
46+
})
47+
.catch(console.error)
48+
}, [])
3649

3750
const handleSearch = async (searchQuery?: string) => {
3851
const q = searchQuery || query
39-
if (!q.trim()) return
40-
41-
if (searchCount >= FREE_SEARCH_LIMIT) {
42-
// Show signup prompt
43-
return
44-
}
52+
if (!q.trim() || loading || remaining <= 0) return
4553

4654
setLoading(true)
4755
setHasSearched(true)
56+
setRateLimitError(null)
4857
const startTime = Date.now()
4958

5059
try {
5160
const response = await fetch(`${API_URL}/playground/search`, {
5261
method: 'POST',
5362
headers: { 'Content-Type': 'application/json' },
63+
credentials: 'include', // Send cookies for session tracking
5464
body: JSON.stringify({
5565
query: q,
5666
demo_repo: selectedRepo,
@@ -59,18 +69,29 @@ export function Playground({ onSignupClick }: PlaygroundProps) {
5969
})
6070

6171
const data = await response.json()
62-
setResults(data.results || [])
63-
setSearchTime(Date.now() - startTime)
64-
setSearchCount(prev => prev + 1)
72+
73+
if (response.ok) {
74+
setResults(data.results || [])
75+
setSearchTime(data.search_time_ms || (Date.now() - startTime))
76+
// Update remaining from backend (source of truth)
77+
if (typeof data.remaining_searches === 'number') {
78+
setRemaining(data.remaining_searches)
79+
}
80+
if (typeof data.limit === 'number') {
81+
setLimit(data.limit)
82+
}
83+
} else if (response.status === 429) {
84+
// Rate limit exceeded
85+
setRateLimitError(data.detail?.message || 'Daily limit reached. Sign up for unlimited searches!')
86+
setRemaining(0)
87+
}
6588
} catch (error) {
6689
console.error('Search error:', error)
6790
} finally {
6891
setLoading(false)
6992
}
7093
}
7194

72-
const remainingSearches = FREE_SEARCH_LIMIT - searchCount
73-
7495
return (
7596
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-white">
7697
{/* Minimal Nav */}
@@ -171,9 +192,9 @@ export function Playground({ onSignupClick }: PlaygroundProps) {
171192
)}
172193

173194
{/* Remaining searches indicator */}
174-
{searchCount > 0 && remainingSearches > 0 && (
195+
{hasSearched && remaining > 0 && remaining < limit && (
175196
<div className="text-center mt-4 text-sm text-gray-500">
176-
{remainingSearches} free {remainingSearches === 1 ? 'search' : 'searches'} remaining •{' '}
197+
{remaining} free {remaining === 1 ? 'search' : 'searches'} remaining •{' '}
177198
<button onClick={onSignupClick} className="text-blue-600 hover:underline">
178199
Sign up for unlimited
179200
</button>
@@ -194,11 +215,11 @@ export function Playground({ onSignupClick }: PlaygroundProps) {
194215
)}
195216

196217
{/* Limit Reached Banner */}
197-
{searchCount >= FREE_SEARCH_LIMIT && (
218+
{(remaining <= 0 || rateLimitError) && (
198219
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 rounded-2xl p-6 mb-6 text-white">
199-
<h3 className="text-lg font-semibold mb-2">You've used all free searches</h3>
220+
<h3 className="text-lg font-semibold mb-2">You've reached today's limit</h3>
200221
<p className="text-blue-100 mb-4">
201-
Sign up to get unlimited searches, index your own repos, and more.
222+
{rateLimitError || 'Sign up to get unlimited searches, index your own repos, and more.'}
202223
</p>
203224
<button
204225
onClick={onSignupClick}

0 commit comments

Comments
 (0)