initialised repo

This commit is contained in:
2026-02-02 15:58:45 +00:00
commit b060e7f008
46 changed files with 8574 additions and 0 deletions

256
lib/search-executor.ts Normal file
View File

@@ -0,0 +1,256 @@
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<SearchResult[]> {
const results: SearchResult[] = []
// Group by platform
const byPlatform = new Map<string, GeneratedQuery[]>()
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<SearchResult[]> {
// Use Serper API if available
if (process.env.SERPER_API_KEY) {
return searchWithSerper(query)
}
// Fallback to direct scraping (less reliable)
return searchDirect(query)
}
async function searchWithSerper(query: GeneratedQuery): Promise<SearchResult[]> {
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
}))
}
async function searchDirect(query: GeneratedQuery): Promise<SearchResult[]> {
// Simple direct search as fallback
const encodedQuery = encodeURIComponent(query.query)
const url = `https://www.google.com/search?q=${encodedQuery}&num=5`
try {
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
})
const html = await response.text()
const results: SearchResult[] = []
// Basic regex parsing
const resultBlocks = html.match(/<div class="g"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/g) || []
for (const block of resultBlocks.slice(0, 5)) {
const titleMatch = block.match(/<h3[^>]*>(.*?)<\/h3>/)
const linkMatch = block.match(/<a href="([^"]+)"/)
const snippetMatch = block.match(/<div class="VwiC3b[^"]*"[^>]*>(.*?)<\/div>/)
if (titleMatch && linkMatch) {
results.push({
title: titleMatch[1].replace(/<[^>]+>/g, ''),
url: linkMatch[1],
snippet: snippetMatch ? snippetMatch[1].replace(/<[^>]+>/g, '') : '',
platform: query.platform
})
}
}
return results
} catch (err) {
console.error('Direct search failed:', err)
return []
}
}
export function scoreOpportunities(
results: SearchResult[],
analysis: EnhancedProductAnalysis
): Opportunity[] {
const opportunities: Opportunity[] = []
const seen = new Set<string>()
for (const result of results) {
if (seen.has(result.url)) continue
seen.add(result.url)
const scored = scoreSingleOpportunity(result, analysis)
if (scored.relevanceScore >= 0.3) {
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<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}