Skip to content

Commit c777556

Browse files
committed
1 parent 38718ad commit c777556

File tree

10 files changed

+1121
-315
lines changed

10 files changed

+1121
-315
lines changed

src/app/api/keywords-analysis/route.ts

Lines changed: 165 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/app/api/onboarding-autofill/route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,21 @@ export async function POST(request: NextRequest) {
3434
}
3535

3636
const data = await response.json();
37+
38+
// The external API returns an array with an object containing output
39+
// Format: [{"output": {...}}]
40+
if (!Array.isArray(data) || data.length === 0 || !data[0].output) {
41+
console.error("Unexpected response format:", data);
42+
return NextResponse.json(
43+
{ error: "Invalid response format from external API" },
44+
{ status: 500 }
45+
);
46+
}
3747

3848
// Return the autofill data
3949
return NextResponse.json({
4050
success: true,
41-
data: data.output,
51+
data: data[0].output,
4252
});
4353
} catch (error) {
4454
console.error("Onboarding autofill error:", error);

0 commit comments

Comments
 (0)