Files
SanatiLeads/lib/query-generator.ts

351 lines
10 KiB
TypeScript

import type {
EnhancedProductAnalysis,
SearchConfig,
GeneratedQuery,
SearchStrategy,
PlatformId
} from './types'
export function getDefaultPlatforms(): Record<PlatformId, { name: string; icon: string; rateLimit: number; enabled: boolean }> {
return {
reddit: {
name: 'Reddit',
icon: 'MessageSquare',
rateLimit: 30,
enabled: true,
searchTemplate: '{site} {term} {intent}',
},
twitter: {
name: 'X/Twitter',
icon: 'Twitter',
rateLimit: 20,
enabled: true,
searchTemplate: '{site} {term} {intent}',
},
hackernews: {
name: 'Hacker News',
icon: 'HackerIcon',
rateLimit: 30,
enabled: true,
searchTemplate: '{site} ("Ask HN" OR "Show HN") {term} {intent}',
},
indiehackers: {
name: 'Indie Hackers',
icon: 'Users',
rateLimit: 20,
enabled: false,
searchTemplate: '{site} {term} {intent}',
},
quora: {
name: 'Quora',
icon: 'HelpCircle',
rateLimit: 20,
enabled: false,
searchTemplate: '{site} {term} {intent}',
},
stackoverflow: {
name: 'Stack Overflow',
icon: 'Filter',
rateLimit: 30,
enabled: false,
searchTemplate: '{site} {term} {intent}',
},
linkedin: {
name: 'LinkedIn',
icon: 'Globe',
rateLimit: 15,
enabled: false,
searchTemplate: '{site} {term} {intent}',
}
}
}
export function generateSearchQueries(
analysis: EnhancedProductAnalysis,
config: SearchConfig
): GeneratedQuery[] {
const queries: GeneratedQuery[] = []
const enabledPlatforms = config.platforms.filter(p => p.enabled)
enabledPlatforms.forEach(platform => {
config.strategies.forEach(strategy => {
const strategyQueries = buildStrategyQueries(strategy, analysis, platform)
queries.push(...strategyQueries)
})
})
const deduped = sortAndDedupeQueries(queries)
const limited = deduped.slice(0, config.maxResults || 50)
console.info(
`[opportunities] queries: generated=${queries.length} deduped=${deduped.length} limited=${limited.length}`
)
return limited
}
function buildStrategyQueries(
strategy: SearchStrategy,
analysis: EnhancedProductAnalysis,
platform: { id: PlatformId; searchTemplate?: string }
): GeneratedQuery[] {
switch (strategy) {
case 'direct-keywords':
return buildDirectKeywordQueries(analysis, platform)
case 'problem-pain':
return buildProblemQueries(analysis, platform)
case 'competitor-alternative':
return buildCompetitorQueries(analysis, platform)
case 'how-to':
return buildHowToQueries(analysis, platform)
case 'emotional-frustrated':
return buildEmotionalQueries(analysis, platform)
case 'comparison':
return buildComparisonQueries(analysis, platform)
case 'recommendation':
return buildRecommendationQueries(analysis, platform)
default:
return []
}
}
function buildDirectKeywordQueries(
analysis: EnhancedProductAnalysis,
platform: { id: PlatformId; searchTemplate?: string }
): GeneratedQuery[] {
const keywords = analysis.keywords
.filter(k => k.type === 'product' || k.type === 'feature' || k.type === 'solution')
.slice(0, 8)
const intentPhrases = [
'("looking for" OR "need" OR "recommendation")',
'("what do you use" OR "suggestions")',
'("any alternatives" OR "best tool")',
]
return keywords.flatMap((kw) =>
intentPhrases.map((intent) => ({
query: buildPlatformQuery(platform, quoteTerm(kw.term), intent),
platform: platform.id,
strategy: 'direct-keywords' as SearchStrategy,
priority: 3,
expectedIntent: 'looking'
}))
)
}
function buildProblemQueries(
analysis: EnhancedProductAnalysis,
platform: { id: PlatformId; searchTemplate?: string }
): GeneratedQuery[] {
const problems = analysis.problemsSolved
.filter((p) => p.severity === 'high' || p.severity === 'medium')
.slice(0, 6)
const intentPhrases = [
'("how do I" OR "how to" OR "fix")',
'("frustrated" OR "stuck" OR "struggling")',
'("best way" OR "recommendation")',
]
return problems.flatMap((problem) => {
const terms = problem.searchTerms && problem.searchTerms.length > 0
? problem.searchTerms.slice(0, 3)
: [problem.problem]
return terms.flatMap((term) =>
intentPhrases.map((intent) => ({
query: buildPlatformQuery(platform, quoteTerm(term), intent),
platform: platform.id,
strategy: 'problem-pain' as SearchStrategy,
priority: 5,
expectedIntent: 'frustrated',
}))
)
})
}
function buildCompetitorQueries(
analysis: EnhancedProductAnalysis,
platform: { id: PlatformId; searchTemplate?: string }
): GeneratedQuery[] {
const competitors = analysis.competitors
.filter((comp) => comp.name && comp.name.length > 2)
.slice(0, 6)
const switchIntent = '("switching" OR "moving from" OR "left" OR "canceling")'
const compareIntent = '("vs" OR "versus" OR "compared to" OR "better than")'
const painIntent = '("frustrated" OR "issues with" OR "problems with")'
return competitors.flatMap((comp) => [
{
query: buildPlatformQuery(platform, quoteTerm(comp.name), switchIntent),
platform: platform.id,
strategy: 'competitor-alternative' as SearchStrategy,
priority: 5,
expectedIntent: 'comparing',
},
{
query: buildPlatformQuery(platform, quoteTerm(comp.name), compareIntent),
platform: platform.id,
strategy: 'competitor-alternative' as SearchStrategy,
priority: 4,
expectedIntent: 'comparing',
},
{
query: buildPlatformQuery(platform, quoteTerm(comp.name), painIntent),
platform: platform.id,
strategy: 'competitor-alternative' as SearchStrategy,
priority: 5,
expectedIntent: 'frustrated',
},
])
}
function buildHowToQueries(
analysis: EnhancedProductAnalysis,
platform: { id: PlatformId; searchTemplate?: string }
): GeneratedQuery[] {
const keywords = analysis.keywords
.filter(k => k.type === 'feature' || k.type === 'solution')
.slice(0, 6)
return keywords.map(kw => ({
query: buildPlatformQuery(
platform,
`"how to" ${quoteTerm(kw.term)}`,
'("tutorial" OR "guide" OR "help")'
),
platform: platform.id,
strategy: 'how-to',
priority: 2,
expectedIntent: 'learning'
}))
}
function buildEmotionalQueries(
analysis: EnhancedProductAnalysis,
platform: { id: PlatformId; searchTemplate?: string }
): GeneratedQuery[] {
const keywords = analysis.keywords
.filter(k => k.type === 'product' || k.type === 'problem')
.slice(0, 5)
const emotionalTerms = ['frustrated', 'hate', 'sucks', 'terrible', 'annoying', 'fed up', 'tired of']
return keywords.flatMap(kw =>
emotionalTerms.slice(0, 3).map(term => ({
query: buildPlatformQuery(platform, quoteTerm(kw.term), term),
platform: platform.id,
strategy: 'emotional-frustrated',
priority: 4,
expectedIntent: 'frustrated'
}))
)
}
function buildComparisonQueries(
analysis: EnhancedProductAnalysis,
platform: { id: PlatformId; searchTemplate?: string }
): GeneratedQuery[] {
const keywords = analysis.keywords
.filter(k => k.type === 'product' || k.type === 'differentiator')
.slice(0, 4)
return keywords.map(kw => ({
query: buildPlatformQuery(platform, quoteTerm(kw.term), '("vs" OR "versus" OR "or" OR "comparison")'),
platform: platform.id,
strategy: 'comparison',
priority: 3,
expectedIntent: 'comparing'
}))
}
function buildRecommendationQueries(
analysis: EnhancedProductAnalysis,
platform: { id: PlatformId; searchTemplate?: string }
): GeneratedQuery[] {
const keywords = analysis.keywords
.filter(k => k.type === 'product' || k.type === 'feature')
.slice(0, 5)
return keywords.map(kw => ({
query: buildPlatformQuery(platform, quoteTerm(kw.term), '("what do you use" OR "recommendation" OR "suggest")'),
platform: platform.id,
strategy: 'recommendation',
priority: 3,
expectedIntent: 'recommending'
}))
}
const SITE_OPERATORS: Record<PlatformId, string> = {
reddit: 'site:reddit.com',
twitter: 'site:twitter.com OR site:x.com',
hackernews: 'site:news.ycombinator.com',
indiehackers: 'site:indiehackers.com',
quora: 'site:quora.com',
stackoverflow: 'site:stackoverflow.com',
linkedin: 'site:linkedin.com',
}
const DEFAULT_TEMPLATES: Record<PlatformId, string> = {
reddit: '{site} {term} {intent}',
twitter: '{site} {term} {intent}',
hackernews: '{site} ("Ask HN" OR "Show HN") {term} {intent}',
indiehackers: '{site} {term} {intent}',
quora: '{site} {term} {intent}',
stackoverflow: '{site} {term} {intent}',
linkedin: '{site} {term} {intent}',
}
function applyTemplate(template: string, vars: Record<string, string>): string {
return template.replace(/\{(\w+)\}/g, (_match, key) => vars[key] || '')
}
function buildPlatformQuery(
platform: { id: PlatformId; searchTemplate?: string },
term: string,
intent: string
): string {
const template = (platform.searchTemplate && platform.searchTemplate.trim().length > 0)
? platform.searchTemplate
: DEFAULT_TEMPLATES[platform.id]
const raw = applyTemplate(template, {
site: SITE_OPERATORS[platform.id],
term,
intent,
})
return raw.replace(/\s+/g, ' ').trim()
}
function quoteTerm(term: string): string {
const cleaned = term.replace(/["]+/g, '').trim()
return cleaned.includes(' ') ? `"${cleaned}"` : `"${cleaned}"`
}
function sortAndDedupeQueries(queries: GeneratedQuery[]): GeneratedQuery[] {
// Sort by priority (high first)
const sorted = queries.sort((a, b) => b.priority - a.priority)
// Deduplicate by query string
const seen = new Set<string>()
return sorted.filter(q => {
const normalized = q.query.toLowerCase().replace(/\s+/g, ' ')
if (seen.has(normalized)) return false
seen.add(normalized)
return true
})
}
export function estimateSearchTime(queryCount: number, platforms: PlatformId[]): number {
const avgRateLimit = 25 // requests per minute
return Math.ceil(queryCount / avgRateLimit)
}