@@ -101,8 +101,8 @@ export async function POST(req: NextRequest) {
101101
102102 console . log ( 'Sending webhook request with query params:' , chatInputData ) ;
103103
104- // Make request to Railway webhook using GET with query parameters
105- const webhookUrl = `https://primary-production-20a3.up.railway.app /webhook/dc520c2d-e515-4e54-b838-fc728c2a24ea ?chatInput=${ chatInputParam } ` ;
104+ // Make request to new webhook endpoint using GET with query parameters
105+ const webhookUrl = `https://automations.ideacharge.com /webhook/keywordresearchair ?chatInput=${ chatInputParam } ` ;
106106
107107 const webhookResponse = await fetch ( webhookUrl , {
108108 method : 'GET' ,
@@ -121,9 +121,9 @@ export async function POST(req: NextRequest) {
121121 }
122122
123123 const keywordResults = await webhookResponse . json ( ) ;
124- console . log ( 'Keyword Results:' , webhookResponse ) ;
124+ console . log ( 'Keyword Results:' , JSON . stringify ( keywordResults , null , 2 ) ) ;
125125
126- // Validate response structure - webhook returns array with output object
126+ // Validate response structure - webhook returns array of keyword objects with results/keywords properties
127127 if ( ! keywordResults || ! Array . isArray ( keywordResults ) || keywordResults . length === 0 ) {
128128 console . error ( 'Invalid webhook response structure:' , keywordResults ) ;
129129 return NextResponse . json (
@@ -132,11 +132,14 @@ export async function POST(req: NextRequest) {
132132 ) ;
133133 }
134134
135- const outputData = keywordResults [ 0 ] ?. output ;
136- if ( ! outputData || ! outputData . keywords ) {
137- console . error ( 'Invalid webhook response - missing output.keywords:' , keywordResults ) ;
135+ // Filter out metadata objects and get actual keyword data
136+ const keywordData = keywordResults . filter ( item => item . keywords && item . results ) ;
137+ const metadata = keywordResults . find ( item => item . metadata ) ;
138+
139+ if ( keywordData . length === 0 ) {
140+ console . error ( 'Invalid webhook response - no keyword data found:' , keywordResults ) ;
138141 return NextResponse . json (
139- { error : 'Invalid response from keyword analysis service - missing keywords data' } ,
142+ { error : 'Invalid response from keyword analysis service - no keywords data found ' } ,
140143 { status : 500 , headers : corsHeaders }
141144 ) ;
142145 }
@@ -145,16 +148,31 @@ export async function POST(req: NextRequest) {
145148 const analysisId = crypto . randomUUID ( ) ;
146149
147150 // Define types for keyword processing
151+ interface MonthlySearchVolume {
152+ month : string ;
153+ year : string ;
154+ monthlySearches : string ;
155+ }
156+
157+
158+
148159 interface ProcessedKeyword {
149160 conversational_keyword : string ;
150161 intent : string | null ;
151162 google_seed_keyword : string | null ;
152163 category : string | null ;
153164 search_volume : number ;
154165 competition_index : number ;
155- low_cpc : string ;
166+ competition : string | null ;
167+ low_cpc : string | null ;
168+ high_cpc : string | null ;
169+ low_cpc_usd : string | null ;
170+ high_cpc_usd : string | null ;
171+ trend_3m : string ;
156172 trend_6m : string ;
173+ trend_11m : string ;
157174 relevance_score : number ;
175+ monthly_search_volumes : MonthlySearchVolume [ ] ;
158176 }
159177
160178 interface KeywordStats {
@@ -176,55 +194,123 @@ export async function POST(req: NextRequest) {
176194 category_distribution : { }
177195 } ;
178196
179- if ( outputData . keywords && typeof outputData . keywords === 'object' ) {
180- // Convert object with numbered keys to array
181- keywordsArray = Object . values ( outputData . keywords ) . map ( ( keywordData ) => {
182- const data = keywordData as Record < string , unknown > ;
183- return {
184- conversational_keyword : String ( data . conversational_keyword || '' ) ,
185- intent : data . intent ? String ( data . intent ) : null ,
186- google_seed_keyword : data . google_seed_keyword ? String ( data . google_seed_keyword ) : null ,
187- category : data . category ? String ( data . category ) : null ,
188- search_volume : parseInt ( String ( data . search_volume || 0 ) ) || 0 ,
189- competition_index : parseFloat ( String ( data . competition_index || 0 ) ) || 0 ,
190- low_cpc : String ( data . low_cpc || '$0.00' ) ,
191- trend_6m : String ( data . trend_6m || '0%' ) ,
192- relevance_score : parseFloat ( String ( data . relevance_score || 0 ) ) || 0 ,
193- } ;
194- } ) ;
195-
196- // Get top 10 keywords by relevance score for quick dashboard access
197- topKeywords = keywordsArray
198- . sort ( ( a , b ) => b . relevance_score - a . relevance_score )
199- . slice ( 0 , 10 ) ;
200-
201- // Calculate quick stats
202- const totalKeywords = keywordsArray . length ;
203- const avgRelevance = keywordsArray . reduce ( ( sum , kw ) => sum + kw . relevance_score , 0 ) / totalKeywords ;
204- const highVolumeCount = keywordsArray . filter ( kw => kw . search_volume > 1000 ) . length ;
205- const intentCounts = keywordsArray . reduce ( ( acc : Record < string , number > , kw ) => {
206- const intent = kw . intent || 'unknown' ;
207- acc [ intent ] = ( acc [ intent ] || 0 ) + 1 ;
208- return acc ;
209- } , { } ) ;
210- const categoryCounts = keywordsArray . reduce ( ( acc : Record < string , number > , kw ) => {
211- const category = kw . category || 'unknown' ;
212- acc [ category ] = ( acc [ category ] || 0 ) + 1 ;
213- return acc ;
214- } , { } ) ;
215-
216- stats = {
217- total_keywords : totalKeywords ,
218- avg_relevance_score : Math . round ( avgRelevance * 10 ) / 10 ,
219- high_volume_count : highVolumeCount ,
220- intent_distribution : intentCounts ,
221- category_distribution : categoryCounts
197+ // Process each keyword from the new structure
198+ keywordsArray = keywordData . map ( ( item : Record < string , unknown > ) => {
199+ const results = ( item . results || { } ) as Record < string , unknown > ;
200+ const keywords = ( item . keywords || { } ) as Record < string , unknown > ;
201+ const keywordMetrics = ( results . keywordMetrics || { } ) as Record < string , unknown > ;
202+
203+ // Extract search volume from avgMonthlySearches
204+ const avgMonthlySearches = parseInt ( String ( keywordMetrics . avgMonthlySearches || '0' ) ) || 0 ;
205+
206+ // Extract competition index
207+ const competitionIndex = parseFloat ( String ( keywordMetrics . competitionIndex || '0' ) ) || 0 ;
208+
209+ // Extract trends
210+ const trends = ( keywordMetrics . trends || { } ) as Record < string , unknown > ;
211+
212+ return {
213+ conversational_keyword : String ( keywords . conversational_keyword || '' ) ,
214+ intent : keywords . intent ? String ( keywords . intent ) : null ,
215+ google_seed_keyword : String ( keywords . google_seed_keyword || results . text || '' ) ,
216+ category : keywords . category ? String ( keywords . category ) : null ,
217+ search_volume : avgMonthlySearches ,
218+ competition_index : competitionIndex ,
219+ competition : keywordMetrics . competition ? String ( keywordMetrics . competition ) : null ,
220+ low_cpc : keywordMetrics . lowTopOfPageBid ? String ( keywordMetrics . lowTopOfPageBid ) : null ,
221+ high_cpc : keywordMetrics . highTopOfPageBid ? String ( keywordMetrics . highTopOfPageBid ) : null ,
222+ low_cpc_usd : keywordMetrics . lowTopOfPageBidUSD ? String ( keywordMetrics . lowTopOfPageBidUSD ) : null ,
223+ high_cpc_usd : keywordMetrics . highTopOfPageBidUSD ? String ( keywordMetrics . highTopOfPageBidUSD ) : null ,
224+ trend_3m : String ( trends [ '3m_trend' ] || '0%' ) ,
225+ trend_6m : String ( trends [ '6m_trend' ] || '0%' ) ,
226+ trend_11m : String ( trends [ '11m_trend' ] || '0%' ) ,
227+ relevance_score : parseFloat ( String ( keywords . relevance_score || 0 ) ) || 0 ,
228+ monthly_search_volumes : ( keywordMetrics . monthlySearchVolumes || [ ] ) as MonthlySearchVolume [ ] ,
222229 } ;
223- }
230+ } ) ;
231+
232+ // Get top 10 keywords by relevance score for quick dashboard access
233+ topKeywords = keywordsArray
234+ . sort ( ( a , b ) => b . relevance_score - a . relevance_score )
235+ . slice ( 0 , 10 ) ;
224236
237+ // Calculate quick stats
225238 const totalKeywords = keywordsArray . length ;
239+ const avgRelevance = keywordsArray . reduce ( ( sum , kw ) => sum + kw . relevance_score , 0 ) / totalKeywords ;
240+ const highVolumeCount = keywordsArray . filter ( kw => kw . search_volume > 1000 ) . length ;
241+ const intentCounts = keywordsArray . reduce ( ( acc : Record < string , number > , kw ) => {
242+ const intent = kw . intent || 'unknown' ;
243+ acc [ intent ] = ( acc [ intent ] || 0 ) + 1 ;
244+ return acc ;
245+ } , { } ) ;
246+ const categoryCounts = keywordsArray . reduce ( ( acc : Record < string , number > , kw ) => {
247+ const category = kw . category || 'unknown' ;
248+ acc [ category ] = ( acc [ category ] || 0 ) + 1 ;
249+ return acc ;
250+ } , { } ) ;
251+
252+ stats = {
253+ total_keywords : totalKeywords ,
254+ avg_relevance_score : Math . round ( avgRelevance * 10 ) / 10 ,
255+ high_volume_count : highVolumeCount ,
256+ intent_distribution : intentCounts ,
257+ category_distribution : categoryCounts
258+ } ;
259+
260+ // Calculate additional metrics for new database columns
261+ const totalMonthlySearches = keywordsArray . reduce ( ( sum , kw ) => sum + kw . search_volume , 0 ) ;
262+ const avgSearchVolume = Math . round ( totalMonthlySearches / totalKeywords ) || 0 ;
263+ const highCompetitionCount = keywordsArray . filter ( kw => kw . competition_index > 50 ) . length ;
264+ const avgCompetitionIndex = keywordsArray . reduce ( ( sum , kw ) => sum + kw . competition_index , 0 ) / totalKeywords ;
265+
266+ // Analyze competition distribution
267+ const competitionDistribution = keywordsArray . reduce ( ( acc : Record < string , number > , kw ) => {
268+ const competition = kw . competition || 'UNKNOWN' ;
269+ acc [ competition ] = ( acc [ competition ] || 0 ) + 1 ;
270+ return acc ;
271+ } , { } ) ;
272+
273+ // Analyze CPC data
274+ const validCpcs = keywordsArray . filter ( kw => kw . low_cpc && kw . low_cpc !== '$0.00' ) ;
275+ const validCpcsUsd = keywordsArray . filter ( kw => kw . low_cpc_usd && kw . low_cpc_usd !== '$0.00' ) ;
276+ const cpcAnalysis = {
277+ has_cpc_data : validCpcs . length > 0 || validCpcsUsd . length > 0 ,
278+ keywords_with_cpc : validCpcs . length ,
279+ keywords_with_cpc_usd : validCpcsUsd . length ,
280+ avg_low_cpc : validCpcs . length > 0 ?
281+ validCpcs . reduce ( ( sum , kw ) => sum + parseFloat ( kw . low_cpc ?. replace ( '$' , '' ) || '0' ) , 0 ) / validCpcs . length : 0 ,
282+ avg_high_cpc : validCpcs . length > 0 ?
283+ validCpcs . reduce ( ( sum , kw ) => sum + parseFloat ( kw . high_cpc ?. replace ( '$' , '' ) || '0' ) , 0 ) / validCpcs . length : 0 ,
284+ avg_low_cpc_usd : validCpcsUsd . length > 0 ?
285+ validCpcsUsd . reduce ( ( sum , kw ) => sum + parseFloat ( kw . low_cpc_usd ?. replace ( '$' , '' ) || '0' ) , 0 ) / validCpcsUsd . length : 0 ,
286+ avg_high_cpc_usd : validCpcsUsd . length > 0 ?
287+ validCpcsUsd . reduce ( ( sum , kw ) => sum + parseFloat ( kw . high_cpc_usd ?. replace ( '$' , '' ) || '0' ) , 0 ) / validCpcsUsd . length : 0 ,
288+ max_cpc_usd : validCpcsUsd . length > 0 ?
289+ Math . max ( ...validCpcsUsd . map ( kw => parseFloat ( kw . high_cpc_usd ?. replace ( '$' , '' ) || '0' ) ) ) : 0 ,
290+ min_cpc_usd : validCpcsUsd . length > 0 ?
291+ Math . min ( ...validCpcsUsd . map ( kw => parseFloat ( kw . low_cpc_usd ?. replace ( '$' , '' ) || '0' ) ) ) : 0
292+ } ;
226293
227- // Save the analysis session with embedded keywords data
294+ // Analyze trends
295+ const trendAnalysis = {
296+ positive_3m_trends : keywordsArray . filter ( kw => kw . trend_3m . includes ( '+' ) ) . length ,
297+ negative_3m_trends : keywordsArray . filter ( kw => kw . trend_3m . includes ( '-' ) ) . length ,
298+ positive_6m_trends : keywordsArray . filter ( kw => kw . trend_6m . includes ( '+' ) ) . length ,
299+ negative_6m_trends : keywordsArray . filter ( kw => kw . trend_6m . includes ( '-' ) ) . length ,
300+ positive_11m_trends : keywordsArray . filter ( kw => kw . trend_11m . includes ( '+' ) ) . length ,
301+ negative_11m_trends : keywordsArray . filter ( kw => kw . trend_11m . includes ( '-' ) ) . length ,
302+ } ;
303+
304+ // Calculate high-value keywords (>$5 USD CPC)
305+ const highValueKeywordsCount = keywordsArray . filter ( kw => {
306+ const highCpcUsd = parseFloat ( kw . high_cpc_usd ?. replace ( '$' , '' ) || '0' ) ;
307+ return highCpcUsd > 5 ;
308+ } ) . length ;
309+
310+ // Generate analysis summary based on the results
311+ const analysisSummary = `Analyzed ${ totalKeywords } keywords with an average relevance score of ${ stats . avg_relevance_score } . Found ${ stats . high_volume_count } high-volume keywords (>1000 monthly searches). Average search volume: ${ avgSearchVolume } /month. ${ validCpcsUsd . length > 0 ? `Average CPC: $${ cpcAnalysis . avg_low_cpc_usd . toFixed ( 2 ) } -$${ cpcAnalysis . avg_high_cpc_usd . toFixed ( 2 ) } USD. ` : '' } Top intents: ${ Object . keys ( stats . intent_distribution ) . slice ( 0 , 3 ) . join ( ', ' ) } .` ;
312+
313+ // Save the analysis session with embedded keywords data and new metrics
228314 const { error : sessionError } = await supabase
229315 . from ( 'keyword_analysis_sessions' )
230316 . insert ( {
@@ -234,12 +320,28 @@ export async function POST(req: NextRequest) {
234320 website : website ,
235321 keyword_input : keyword ,
236322 total_keywords : totalKeywords ,
237- analysis_summary : outputData . summary || '' ,
323+ analysis_summary : analysisSummary ,
238324 keywords_data : keywordsArray ,
239325 top_keywords : topKeywords ,
240326 stats : stats ,
241- language : language ,
242- location : location ,
327+ language : metadata ?. language || language || 'en' ,
328+ location : metadata ?. country || location || 'Global' ,
329+ // New calculated metrics
330+ avg_search_volume : avgSearchVolume ,
331+ total_monthly_searches : totalMonthlySearches ,
332+ high_competition_count : highCompetitionCount ,
333+ avg_competition_index : Math . round ( avgCompetitionIndex * 100 ) / 100 ,
334+ trend_analysis : trendAnalysis ,
335+ response_metadata : metadata || { language : language || 'en' , country : location || 'Global' } ,
336+ competition_distribution : competitionDistribution ,
337+ cpc_analysis : cpcAnalysis ,
338+ // Enhanced CPC metrics
339+ avg_low_cpc_usd : Math . round ( cpcAnalysis . avg_low_cpc_usd * 100 ) / 100 ,
340+ avg_high_cpc_usd : Math . round ( cpcAnalysis . avg_high_cpc_usd * 100 ) / 100 ,
341+ max_cpc_usd : Math . round ( cpcAnalysis . max_cpc_usd * 100 ) / 100 ,
342+ min_cpc_usd : Math . round ( cpcAnalysis . min_cpc_usd * 100 ) / 100 ,
343+ keywords_with_cpc_data : validCpcsUsd . length ,
344+ high_value_keywords_count : highValueKeywordsCount ,
243345 } ) ;
244346
245347 if ( sessionError ) {
@@ -276,7 +378,12 @@ export async function POST(req: NextRequest) {
276378
277379 return NextResponse . json ( {
278380 success : true ,
279- data : outputData ,
381+ data : {
382+ keywords : keywordsArray ,
383+ summary : analysisSummary ,
384+ stats : stats ,
385+ metadata : metadata || { language : language || 'en' , country : location || 'Global' }
386+ } ,
280387 keyword_id : analysisId // Include the analysis ID for redirect
281388 } , { headers : corsHeaders } ) ;
282389
0 commit comments