import type { GeneratedQuery, Opportunity, EnhancedProductAnalysis } from './types' interface SearchResult { title: string url: string snippet: string platform: string raw?: any } export async function executeSearches( queries: GeneratedQuery[], onProgress?: (progress: { current: number; total: number; platform: string }) => void ): Promise { const results: SearchResult[] = [] // Group by platform const byPlatform = new Map() queries.forEach(q => { if (!byPlatform.has(q.platform)) byPlatform.set(q.platform, []) byPlatform.get(q.platform)!.push(q) }) let completed = 0 for (const [platform, platformQueries] of byPlatform) { console.log(`Searching ${platform}: ${platformQueries.length} queries`) for (const query of platformQueries) { try { const searchResults = await executeSingleSearch(query) results.push(...searchResults) completed++ onProgress?.({ current: completed, total: queries.length, platform }) // Rate limiting - 1 second between requests await delay(1000) } catch (err) { console.error(`Search failed for ${platform}:`, err) } } } return results } async function executeSingleSearch(query: GeneratedQuery): Promise { if (!process.env.SERPER_API_KEY) { throw new Error('SERPER_API_KEY is not configured.') } return searchWithSerper(query) } async function searchWithSerper(query: GeneratedQuery): Promise { const response = await fetch('https://google.serper.dev/search', { method: 'POST', headers: { 'X-API-KEY': process.env.SERPER_API_KEY!, 'Content-Type': 'application/json' }, body: JSON.stringify({ q: query.query, num: 5, gl: 'us', hl: 'en' }) }) if (!response.ok) { throw new Error(`Serper API error: ${response.status}`) } const data = await response.json() return (data.organic || []).map((r: any) => ({ title: r.title, url: r.link, snippet: r.snippet, platform: query.platform, raw: r })) } export function scoreOpportunities( results: SearchResult[], analysis: EnhancedProductAnalysis ): Opportunity[] { const opportunities: Opportunity[] = [] const seen = new Set() for (const result of results) { if (seen.has(result.url)) continue seen.add(result.url) const scored = scoreSingleOpportunity(result, analysis) opportunities.push(scored) } return opportunities.sort((a, b) => b.relevanceScore - a.relevanceScore) } function scoreSingleOpportunity( result: SearchResult, analysis: EnhancedProductAnalysis ): Opportunity { const content = (result.title + ' ' + result.snippet).toLowerCase() // 1. Keyword matching (max 30 points) const matchedKeywords = analysis.keywords.filter(k => content.includes(k.term.toLowerCase()) ) const keywordScore = Math.min(matchedKeywords.length * 5, 30) // 2. Problem matching (max 25 points) const matchedProblems = analysis.problemsSolved.filter(p => content.includes(p.problem.toLowerCase()) || p.searchTerms.some(t => content.includes(t.toLowerCase())) ) const problemScore = matchedProblems.reduce((sum, p) => sum + (p.severity === 'high' ? 10 : p.severity === 'medium' ? 5 : 2), 0 ) const cappedProblemScore = Math.min(problemScore, 25) // 3. Emotional intensity (max 20 points) const emotionalTerms = [ 'frustrated', 'hate', 'terrible', 'sucks', 'awful', 'nightmare', 'desperate', 'urgent', 'please help', 'dying', 'killing me', 'fed up', 'tired of', 'annoying', 'painful' ] const emotionalMatches = emotionalTerms.filter(t => content.includes(t)) const emotionalScore = Math.min(emotionalMatches.length * 5, 20) // 4. Competitor mention (max 15 points) const competitorMentioned = analysis.competitors.some(c => content.includes(c.name.toLowerCase()) ) const competitorScore = competitorMentioned ? 15 : 0 // Detect intent let intent: Opportunity['intent'] = 'looking' if (content.includes('frustrated') || content.includes('hate') || emotionalMatches.length > 0) { intent = 'frustrated' } else if (content.includes('vs') || content.includes('compare') || content.includes('alternative')) { intent = 'comparing' } else if (content.includes('how to') || content.includes('tutorial')) { intent = 'learning' } else if (content.includes('recommend') || content.includes('suggest')) { intent = 'recommending' } // Generate approach const problemContext = matchedProblems[0]?.problem || 'their current challenges' const suggestedApproach = generateApproach(intent, analysis.productName, problemContext) const softPitch = intent === 'frustrated' || intent === 'learning' const totalScore = (keywordScore + cappedProblemScore + emotionalScore + competitorScore) / 90 return { id: Math.random().toString(36).substring(2, 15), title: result.title, url: result.url, snippet: result.snippet.slice(0, 300), platform: result.platform, source: result.platform, relevanceScore: Math.min(totalScore, 1), emotionalIntensity: emotionalScore > 10 ? 'high' : emotionalScore > 5 ? 'medium' : 'low', intent, matchedKeywords: matchedKeywords.map(k => k.term), matchedProblems: matchedProblems.map(p => p.problem), suggestedApproach, softPitch, status: 'new', scoringBreakdown: { keywordMatches: keywordScore, problemMatches: cappedProblemScore, emotionalIntensity: emotionalScore, competitorMention: competitorScore, recency: 0, engagement: 0 } } } function generateApproach( intent: Opportunity['intent'], productName: string, problemContext: string ): string { switch (intent) { case 'frustrated': return `Empathize with their frustration about ${problemContext}. Share how ${productName} helps teams overcome this specific pain point.` case 'comparing': return `They're evaluating options. Highlight ${productName}'s unique approach to ${problemContext}. Be honest about trade-offs.` case 'looking': return `They're actively searching. Introduce ${productName} as purpose-built for ${problemContext}. Mention 2-3 specific features.` case 'learning': return `Provide genuine help with ${problemContext}. Mention ${productName} as the solution that worked for your team.` case 'recommending': return `Share a genuine recommendation for ${productName}. Focus on how it transformed ${problemContext} for your team.` } } function delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) }