initialised repo
This commit is contained in:
139
app/api/opportunities/route.ts
Normal file
139
app/api/opportunities/route.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { generateSearchQueries, getDefaultPlatforms } from '@/lib/query-generator'
|
||||
import { executeSearches, scoreOpportunities } from '@/lib/search-executor'
|
||||
import type { EnhancedProductAnalysis, SearchConfig, PlatformConfig } from '@/lib/types'
|
||||
|
||||
const searchSchema = z.object({
|
||||
analysis: z.object({
|
||||
productName: z.string(),
|
||||
tagline: z.string(),
|
||||
description: z.string(),
|
||||
features: z.array(z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
benefits: z.array(z.string()),
|
||||
useCases: z.array(z.string())
|
||||
})),
|
||||
problemsSolved: z.array(z.object({
|
||||
problem: z.string(),
|
||||
severity: z.enum(['high', 'medium', 'low']),
|
||||
currentWorkarounds: z.array(z.string()),
|
||||
emotionalImpact: z.string(),
|
||||
searchTerms: z.array(z.string())
|
||||
})),
|
||||
keywords: z.array(z.object({
|
||||
term: z.string(),
|
||||
type: z.string(),
|
||||
searchVolume: z.string(),
|
||||
intent: z.string(),
|
||||
funnel: z.string(),
|
||||
emotionalIntensity: z.string()
|
||||
})),
|
||||
competitors: z.array(z.object({
|
||||
name: z.string(),
|
||||
differentiator: z.string(),
|
||||
theirStrength: z.string(),
|
||||
switchTrigger: z.string(),
|
||||
theirWeakness: z.string()
|
||||
}))
|
||||
}),
|
||||
config: z.object({
|
||||
platforms: z.array(z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
icon: z.string(),
|
||||
enabled: z.boolean(),
|
||||
searchTemplate: z.string(),
|
||||
rateLimit: z.number()
|
||||
})),
|
||||
strategies: z.array(z.string()),
|
||||
intensity: z.enum(['broad', 'balanced', 'targeted']),
|
||||
maxResults: z.number().default(50)
|
||||
})
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { analysis, config } = searchSchema.parse(body)
|
||||
|
||||
console.log('🔍 Starting opportunity search...')
|
||||
console.log(` Product: ${analysis.productName}`)
|
||||
console.log(` Platforms: ${config.platforms.filter(p => p.enabled).map(p => p.name).join(', ')}`)
|
||||
console.log(` Strategies: ${config.strategies.join(', ')}`)
|
||||
|
||||
// Generate queries
|
||||
console.log(' Generating search queries...')
|
||||
const queries = generateSearchQueries(analysis as EnhancedProductAnalysis, config as SearchConfig)
|
||||
console.log(` ✓ Generated ${queries.length} queries`)
|
||||
|
||||
// Execute searches
|
||||
console.log(' Executing searches...')
|
||||
const searchResults = await executeSearches(queries)
|
||||
console.log(` ✓ Found ${searchResults.length} raw results`)
|
||||
|
||||
// Score and rank
|
||||
console.log(' Scoring opportunities...')
|
||||
const opportunities = scoreOpportunities(searchResults, analysis as EnhancedProductAnalysis)
|
||||
console.log(` ✓ Scored ${opportunities.length} opportunities`)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
opportunities: opportunities.slice(0, 50),
|
||||
stats: {
|
||||
queriesGenerated: queries.length,
|
||||
rawResults: searchResults.length,
|
||||
opportunitiesFound: opportunities.length,
|
||||
highRelevance: opportunities.filter(o => o.relevanceScore >= 0.7).length,
|
||||
averageScore: opportunities.length > 0
|
||||
? opportunities.reduce((a, o) => a + o.relevanceScore, 0) / opportunities.length
|
||||
: 0
|
||||
},
|
||||
queries: queries.map(q => ({
|
||||
query: q.query,
|
||||
platform: q.platform,
|
||||
strategy: q.strategy,
|
||||
priority: q.priority
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Opportunity search error:', error)
|
||||
|
||||
if (error.name === 'ZodError') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request format', details: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Failed to search for opportunities' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get default configuration
|
||||
export async function GET() {
|
||||
const defaultPlatforms = getDefaultPlatforms()
|
||||
|
||||
return NextResponse.json({
|
||||
platforms: Object.entries(defaultPlatforms).map(([id, config]) => ({
|
||||
id,
|
||||
...config
|
||||
})),
|
||||
strategies: [
|
||||
{ id: 'direct-keywords', name: 'Direct Keywords', description: 'Search for people looking for your product category' },
|
||||
{ id: 'problem-pain', name: 'Problem/Pain', description: 'Find people experiencing problems you solve' },
|
||||
{ id: 'competitor-alternative', name: 'Competitor Alternatives', description: 'People looking to switch from competitors' },
|
||||
{ id: 'how-to', name: 'How-To/Tutorials', description: 'People learning about solutions' },
|
||||
{ id: 'emotional-frustrated', name: 'Frustration Posts', description: 'Emotional posts about pain points' },
|
||||
{ id: 'comparison', name: 'Comparisons', description: '"X vs Y" comparison posts' },
|
||||
{ id: 'recommendation', name: 'Recommendations', description: '"What do you use" recommendation requests' }
|
||||
]
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user