256 lines
8.4 KiB
TypeScript
256 lines
8.4 KiB
TypeScript
import type {
|
|
EnhancedProductAnalysis,
|
|
SearchConfig,
|
|
GeneratedQuery,
|
|
SearchStrategy,
|
|
PlatformId
|
|
} from './types'
|
|
|
|
const DEFAULT_PLATFORMS: Record<PlatformId, { name: string; rateLimit: number }> = {
|
|
reddit: { name: 'Reddit', rateLimit: 30 },
|
|
twitter: { name: 'X/Twitter', rateLimit: 20 },
|
|
hackernews: { name: 'Hacker News', rateLimit: 30 },
|
|
indiehackers: { name: 'Indie Hackers', rateLimit: 20 },
|
|
quora: { name: 'Quora', rateLimit: 20 },
|
|
stackoverflow: { name: 'Stack Overflow', rateLimit: 30 },
|
|
linkedin: { name: 'LinkedIn', rateLimit: 15 }
|
|
}
|
|
|
|
export function getDefaultPlatforms(): Record<PlatformId, { name: string; icon: string; rateLimit: number; enabled: boolean }> {
|
|
return {
|
|
reddit: { name: 'Reddit', icon: 'MessageSquare', rateLimit: 30, enabled: true },
|
|
twitter: { name: 'X/Twitter', icon: 'Twitter', rateLimit: 20, enabled: true },
|
|
hackernews: { name: 'Hacker News', icon: 'HackerIcon', rateLimit: 30, enabled: true },
|
|
indiehackers: { name: 'Indie Hackers', icon: 'Users', rateLimit: 20, enabled: false },
|
|
quora: { name: 'Quora', icon: 'HelpCircle', rateLimit: 20, enabled: false },
|
|
stackoverflow: { name: 'Stack Overflow', rateLimit: 30, enabled: false },
|
|
linkedin: { name: 'LinkedIn', rateLimit: 15, enabled: false }
|
|
}
|
|
}
|
|
|
|
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.id)
|
|
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: PlatformId
|
|
): 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: PlatformId): GeneratedQuery[] {
|
|
const keywords = analysis.keywords
|
|
.filter(k => k.type === 'product' || k.type === 'feature')
|
|
.slice(0, 8)
|
|
|
|
const templates: Record<PlatformId, string[]> = {
|
|
reddit: ['("looking for" OR "need" OR "recommendation")', '("what do you use" OR "suggestion")'],
|
|
twitter: ['("looking for" OR "need")', '("any recommendations" OR "suggestions")'],
|
|
hackernews: ['("Ask HN")'],
|
|
indiehackers: ['("looking for" OR "need")'],
|
|
quora: ['("what is the best" OR "how do I choose")'],
|
|
stackoverflow: ['("best tool for" OR "recommendation")'],
|
|
linkedin: ['("looking for" OR "seeking")']
|
|
}
|
|
|
|
return keywords.flatMap(kw =>
|
|
(templates[platform] || templates.reddit).map(template => ({
|
|
query: buildPlatformQuery(platform, `"${kw.term}" ${template}`),
|
|
platform,
|
|
strategy: 'direct-keywords' as SearchStrategy,
|
|
priority: 3,
|
|
expectedIntent: 'looking'
|
|
}))
|
|
)
|
|
}
|
|
|
|
function buildProblemQueries(analysis: EnhancedProductAnalysis, platform: PlatformId): GeneratedQuery[] {
|
|
const highSeverityProblems = analysis.problemsSolved
|
|
.filter(p => p.severity === 'high')
|
|
.slice(0, 5)
|
|
|
|
return highSeverityProblems.flatMap(problem => [
|
|
{
|
|
query: buildPlatformQuery(platform, `"${problem.problem}" ("how to" OR "fix" OR "solve")`),
|
|
platform,
|
|
strategy: 'problem-pain',
|
|
priority: 5,
|
|
expectedIntent: 'frustrated'
|
|
},
|
|
...problem.searchTerms.slice(0, 2).map(term => ({
|
|
query: buildPlatformQuery(platform, `"${term}"`),
|
|
platform,
|
|
strategy: 'problem-pain',
|
|
priority: 4,
|
|
expectedIntent: 'frustrated'
|
|
}))
|
|
])
|
|
}
|
|
|
|
function buildCompetitorQueries(analysis: EnhancedProductAnalysis, platform: PlatformId): GeneratedQuery[] {
|
|
const competitors = analysis.competitors.slice(0, 5)
|
|
|
|
return competitors.flatMap(comp => [
|
|
{
|
|
query: buildPlatformQuery(platform, `"${comp.name}" ("alternative" OR "switching from" OR "moving away")`),
|
|
platform,
|
|
strategy: 'competitor-alternative',
|
|
priority: 5,
|
|
expectedIntent: 'comparing'
|
|
},
|
|
{
|
|
query: buildPlatformQuery(platform, `"${comp.name}" ("vs" OR "versus" OR "compared to" OR "better than")`),
|
|
platform,
|
|
strategy: 'competitor-alternative',
|
|
priority: 4,
|
|
expectedIntent: 'comparing'
|
|
},
|
|
{
|
|
query: buildPlatformQuery(platform, `"${comp.name}" ("frustrated" OR "disappointed" OR "problems with")`),
|
|
platform,
|
|
strategy: 'competitor-alternative',
|
|
priority: 5,
|
|
expectedIntent: 'frustrated'
|
|
}
|
|
])
|
|
}
|
|
|
|
function buildHowToQueries(analysis: EnhancedProductAnalysis, platform: PlatformId): 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" "${kw.term}" ("tutorial" OR "guide" OR "help")`),
|
|
platform,
|
|
strategy: 'how-to',
|
|
priority: 2,
|
|
expectedIntent: 'learning'
|
|
}))
|
|
}
|
|
|
|
function buildEmotionalQueries(analysis: EnhancedProductAnalysis, platform: PlatformId): 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, `"${kw.term}" ${term}`),
|
|
platform,
|
|
strategy: 'emotional-frustrated',
|
|
priority: 4,
|
|
expectedIntent: 'frustrated'
|
|
}))
|
|
)
|
|
}
|
|
|
|
function buildComparisonQueries(analysis: EnhancedProductAnalysis, platform: PlatformId): GeneratedQuery[] {
|
|
const keywords = analysis.keywords
|
|
.filter(k => k.type === 'product' || k.type === 'differentiator')
|
|
.slice(0, 4)
|
|
|
|
return keywords.map(kw => ({
|
|
query: buildPlatformQuery(platform, `"${kw.term}" ("vs" OR "versus" OR "or" OR "comparison")`),
|
|
platform,
|
|
strategy: 'comparison',
|
|
priority: 3,
|
|
expectedIntent: 'comparing'
|
|
}))
|
|
}
|
|
|
|
function buildRecommendationQueries(analysis: EnhancedProductAnalysis, platform: PlatformId): GeneratedQuery[] {
|
|
const keywords = analysis.keywords
|
|
.filter(k => k.type === 'product' || k.type === 'feature')
|
|
.slice(0, 5)
|
|
|
|
return keywords.map(kw => ({
|
|
query: buildPlatformQuery(platform, `("what do you use" OR "recommendation" OR "suggest") "${kw.term}"`),
|
|
platform,
|
|
strategy: 'recommendation',
|
|
priority: 3,
|
|
expectedIntent: 'recommending'
|
|
}))
|
|
}
|
|
|
|
function buildPlatformQuery(platform: PlatformId, query: string): string {
|
|
const siteOperators: 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'
|
|
}
|
|
|
|
return `${siteOperators[platform]} ${query}`
|
|
}
|
|
|
|
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)
|
|
}
|