feat: Refine keyword generation logic, enforce Serper API usage, and enhance search query construction with platform-specific templates.

This commit is contained in:
2026-02-03 22:52:13 +00:00
parent 358f2a42dd
commit f9222627ef
7 changed files with 274 additions and 209 deletions

View File

@@ -95,7 +95,6 @@ const GOAL_PRESETS: {
title: string
description: string
strategies: SearchStrategy[]
intensity: 'broad' | 'balanced' | 'targeted'
maxQueries: number
}[] = [
{
@@ -103,7 +102,6 @@ const GOAL_PRESETS: {
title: "High-intent leads",
description: "Shortlist people actively searching to buy or switch.",
strategies: ["direct-keywords", "competitor-alternative", "comparison", "recommendation"],
intensity: "targeted",
maxQueries: 30,
},
{
@@ -111,7 +109,6 @@ const GOAL_PRESETS: {
title: "Problem pain",
description: "Find people expressing frustration or blockers.",
strategies: ["problem-pain", "emotional-frustrated"],
intensity: "balanced",
maxQueries: 40,
},
{
@@ -119,7 +116,6 @@ const GOAL_PRESETS: {
title: "Market scan",
description: "Broader sweep to map demand and platforms.",
strategies: ["direct-keywords", "problem-pain", "how-to", "recommendation"],
intensity: "broad",
maxQueries: 50,
},
]
@@ -137,7 +133,6 @@ export default function OpportunitiesPage() {
'problem-pain',
'competitor-alternative'
])
const [intensity, setIntensity] = useState<'broad' | 'balanced' | 'targeted'>('balanced')
const [maxQueries, setMaxQueries] = useState(50)
const [goalPreset, setGoalPreset] = useState<string>('high-intent')
const [isSearching, setIsSearching] = useState(false)
@@ -253,9 +248,6 @@ export default function OpportunitiesPage() {
if (Array.isArray(parsed.strategies)) {
setStrategies(parsed.strategies)
}
if (parsed.intensity === 'broad' || parsed.intensity === 'balanced' || parsed.intensity === 'targeted') {
setIntensity(parsed.intensity)
}
if (typeof parsed.maxQueries === 'number') {
setMaxQueries(Math.min(Math.max(parsed.maxQueries, 10), 50))
}
@@ -275,7 +267,6 @@ export default function OpportunitiesPage() {
}
} else if (defaultPlatformsRef.current) {
setStrategies(['direct-keywords', 'problem-pain', 'competitor-alternative'])
setIntensity('balanced')
setMaxQueries(50)
setGoalPreset('high-intent')
setPlatforms(defaultPlatformsRef.current)
@@ -298,12 +289,11 @@ export default function OpportunitiesPage() {
const payload = {
goalPreset,
strategies,
intensity,
maxQueries,
platformIds: platforms.filter((platform) => platform.enabled).map((platform) => platform.id),
}
localStorage.setItem(key, JSON.stringify(payload))
}, [selectedProjectId, goalPreset, strategies, intensity, maxQueries, platforms])
}, [selectedProjectId, goalPreset, strategies, maxQueries, platforms])
useEffect(() => {
if (!analysis && latestAnalysis === null) {
@@ -330,7 +320,6 @@ export default function OpportunitiesPage() {
if (!preset) return
setGoalPreset(preset.id)
setStrategies(preset.strategies)
setIntensity(preset.intensity)
setMaxQueries(preset.maxQueries)
}
@@ -350,7 +339,6 @@ export default function OpportunitiesPage() {
searchTemplate: platform.searchTemplate ?? "",
})),
strategies,
intensity,
maxResults: Math.min(maxQueries, 50)
}
setLastSearchConfig(config as SearchConfig)
@@ -576,24 +564,12 @@ export default function OpportunitiesPage() {
<Separator />
{/* Depth + Queries */}
<div className="space-y-4">
<Label className="text-sm font-medium uppercase text-muted-foreground">Depth</Label>
<div className="space-y-3">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Broad</span>
<span>Targeted</span>
</div>
<Slider
value={[intensity === 'broad' ? 0 : intensity === 'balanced' ? 50 : 100]}
onValueChange={([v]) => setIntensity(v < 33 ? 'broad' : v < 66 ? 'balanced' : 'targeted')}
max={100}
step={50}
/>
</div>
{/* Queries */}
<div className="space-y-2">
<Label className="text-sm font-medium uppercase text-muted-foreground">Max Queries</Label>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium uppercase text-muted-foreground">Max Queries</Label>
<span className="text-xs text-muted-foreground">Max Queries</span>
<span className="text-xs text-muted-foreground">{maxQueries}</span>
</div>
<Slider
@@ -662,7 +638,6 @@ export default function OpportunitiesPage() {
</div>
<div className="flex flex-wrap gap-2 text-xs text-muted-foreground">
<Badge variant="outline">Goal: {GOAL_PRESETS.find((preset) => preset.id === goalPreset)?.title || "Custom"}</Badge>
<Badge variant="outline">Intensity: {intensity}</Badge>
<Badge variant="outline">Max queries: {maxQueries}</Badge>
</div>
{latestJob && (latestJob.status === "running" || latestJob.status === "pending") && (
@@ -698,7 +673,14 @@ export default function OpportunitiesPage() {
)}
{searchError && (
<Alert variant="destructive">
<AlertDescription>{searchError}</AlertDescription>
<AlertDescription>
{searchError}
{searchError.includes('SERPER_API_KEY') && (
<span className="block mt-2 text-xs text-muted-foreground">
Add `SERPER_API_KEY` to your environment and restart the app to enable search.
</span>
)}
</AlertDescription>
</Alert>
)}

View File

@@ -20,7 +20,6 @@ const searchSchema = z.object({
rateLimit: z.number()
})),
strategies: z.array(z.string()),
intensity: z.enum(['broad', 'balanced', 'targeted']),
maxResults: z.number().default(50)
})
})
@@ -41,6 +40,18 @@ export async function POST(request: NextRequest) {
const { projectId, config } = parsed
jobId = parsed.jobId
if (!process.env.SERPER_API_KEY) {
const errorMessage = "SERPER_API_KEY is not configured. Add it to your environment to run searches."
if (jobId) {
await fetchMutation(
api.searchJobs.update,
{ jobId: jobId as any, status: "failed", error: errorMessage },
{ token: await convexAuthNextjsToken() }
)
}
return NextResponse.json({ error: errorMessage }, { status: 400 })
}
const token = await convexAuthNextjsToken();
if (jobId) {
await fetchMutation(

View File

@@ -47,6 +47,13 @@ export async function POST(request: NextRequest) {
const body = await request.json()
const { analysis } = bodySchema.parse(body)
if (!process.env.SERPER_API_KEY) {
return NextResponse.json(
{ error: 'SERPER_API_KEY is not configured. Add it to your environment to run searches.' },
{ status: 400 }
)
}
console.log(`🔍 Finding opportunities for: ${analysis.productName}`)
// Sort queries by priority
@@ -103,15 +110,7 @@ export async function POST(request: NextRequest) {
}
async function searchGoogle(query: string, num: number): Promise<SearchResult[]> {
// Try Serper first
if (process.env.SERPER_API_KEY) {
try {
return await searchSerper(query, num)
} catch (e) {
console.error('Serper failed, falling back to direct')
}
}
return searchDirect(query, num)
return searchSerper(query, num)
}
async function searchSerper(query: string, num: number): Promise<SearchResult[]> {