Refactor keyword handling: Replace 'intent' with 'country' across backend and frontend

- Updated AutomationService to include estimated_word_count.
- Increased stage_1_batch_size from 20 to 50 in AutomationViewSet.
- Changed Keywords model to replace 'intent' property with 'country'.
- Adjusted ClusteringService to allow a maximum of 50 keywords for clustering.
- Modified admin and management commands to remove 'intent' and use 'country' instead.
- Updated serializers to reflect the change from 'intent' to 'country'.
- Adjusted views and filters to use 'country' instead of 'intent'.
- Updated frontend forms, filters, and pages to replace 'intent' with 'country'.
- Added migration to remove 'intent' field and add 'country' field to SeedKeyword model.
This commit is contained in:
IGNY8 VPS (Salman)
2025-12-17 07:37:36 +00:00
parent 9f826c92f8
commit 7ad06c6227
30 changed files with 240 additions and 205 deletions

View File

@@ -39,7 +39,7 @@ interface DashboardStats {
mapped: number;
unmapped: number;
byStatus: Record<string, number>;
byIntent: Record<string, number>;
byCountry: Record<string, number>;
};
clusters: {
total: number;
@@ -90,11 +90,11 @@ export default function PlannerDashboard() {
const unmappedKeywords = keywords.filter(k => !k.cluster || k.cluster.length === 0);
const keywordsByStatus: Record<string, number> = {};
const keywordsByIntent: Record<string, number> = {};
const keywordsByCountry: Record<string, number> = {};
keywords.forEach(k => {
keywordsByStatus[k.status || 'unknown'] = (keywordsByStatus[k.status || 'unknown'] || 0) + 1;
if (k.intent) {
keywordsByIntent[k.intent] = (keywordsByIntent[k.intent] || 0) + 1;
if (k.country) {
keywordsByCountry[k.country] = (keywordsByCountry[k.country] || 0) + 1;
}
});
@@ -135,7 +135,7 @@ export default function PlannerDashboard() {
mapped: mappedKeywords.length,
unmapped: unmappedKeywords.length,
byStatus: keywordsByStatus,
byIntent: keywordsByIntent
byCountry: keywordsByCountry
},
clusters: {
total: clusters.length,

View File

@@ -47,7 +47,7 @@ export default function KeywordOpportunities() {
// Filter state
const [searchTerm, setSearchTerm] = useState('');
const [intentFilter, setIntentFilter] = useState('');
const [countryFilter, setCountryFilter] = useState('');
const [difficultyFilter, setDifficultyFilter] = useState('');
const [volumeMin, setVolumeMin] = useState<number | ''>('');
const [volumeMax, setVolumeMax] = useState<number | ''>('');
@@ -119,7 +119,7 @@ export default function KeywordOpportunities() {
}
if (searchTerm) baseFilters.search = searchTerm;
if (intentFilter) baseFilters.intent = intentFilter;
if (countryFilter) baseFilters.country = countryFilter;
// Fetch ALL pages to get complete dataset
let allResults: SeedKeyword[] = [];
@@ -227,7 +227,7 @@ export default function KeywordOpportunities() {
} finally {
setLoading(false);
}
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, intentFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection]);
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection]);
// Load data on mount and when filters change (excluding search - handled separately)
useEffect(() => {
@@ -504,28 +504,27 @@ export default function KeywordOpportunities() {
},
},
{
key: 'intent',
label: 'Intent',
key: 'country',
label: 'Country',
sortable: true,
sortField: 'intent',
sortField: 'country',
render: (value: string) => {
const getIntentColor = (intent: string) => {
const lowerIntent = intent?.toLowerCase() || '';
if (lowerIntent === 'transactional' || lowerIntent === 'commercial') {
return 'success';
} else if (lowerIntent === 'navigational') {
return 'warning';
}
return 'info';
const countryNames: Record<string, string> = {
'US': 'United States',
'CA': 'Canada',
'GB': 'United Kingdom',
'AE': 'United Arab Emirates',
'AU': 'Australia',
'IN': 'India',
'PK': 'Pakistan',
};
return (
<Badge
color={getIntentColor(value)}
color="info"
size="sm"
variant={value?.toLowerCase() === 'informational' ? 'light' : undefined}
variant="light"
>
{value}
{value || '-'}
</Badge>
);
},
@@ -539,15 +538,18 @@ export default function KeywordOpportunities() {
placeholder: 'Search keywords...',
},
{
key: 'intent',
label: 'Intent',
key: 'country',
label: 'Country',
type: 'select',
options: [
{ value: '', label: 'All Intent' },
{ value: 'informational', label: 'Informational' },
{ value: 'navigational', label: 'Navigational' },
{ value: 'transactional', label: 'Transactional' },
{ value: 'commercial', label: 'Commercial' },
{ value: '', label: 'All Countries' },
{ value: 'US', label: 'United States' },
{ value: 'CA', label: 'Canada' },
{ value: 'GB', label: 'United Kingdom' },
{ value: 'AE', label: 'United Arab Emirates' },
{ value: 'AU', label: 'Australia' },
{ value: 'IN', label: 'India' },
{ value: 'PK', label: 'Pakistan' },
],
},
{
@@ -612,7 +614,7 @@ export default function KeywordOpportunities() {
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
intent: intentFilter,
country: countryFilter,
difficulty: difficultyFilter,
}}
onFilterChange={(key, value) => {
@@ -620,8 +622,8 @@ export default function KeywordOpportunities() {
if (key === 'search') {
setSearchTerm(stringValue);
} else if (key === 'intent') {
setIntentFilter(stringValue);
} else if (key === 'country') {
setCountryFilter(stringValue);
setCurrentPage(1);
} else if (key === 'difficulty') {
setDifficultyFilter(stringValue);

View File

@@ -52,7 +52,7 @@ export default function Keywords() {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [clusterFilter, setClusterFilter] = useState('');
const [intentFilter, setIntentFilter] = useState('');
const [countryFilter, setCountryFilter] = useState('');
const [difficultyFilter, setDifficultyFilter] = useState('');
const [volumeMin, setVolumeMin] = useState<number | ''>('');
const [volumeMax, setVolumeMax] = useState<number | ''>('');
@@ -81,7 +81,7 @@ export default function Keywords() {
keyword: '',
volume: null,
difficulty: null,
intent: 'informational',
country: 'US',
cluster_id: null,
status: 'new',
});
@@ -156,7 +156,7 @@ export default function Keywords() {
...(searchTerm && { search: searchTerm }),
...(statusFilter && { status: statusFilter }),
...(clusterFilter && { cluster_id: clusterFilter }),
...(intentFilter && { intent: intentFilter }),
...(countryFilter && { country: countryFilter }),
...(activeSector?.id && { sector_id: activeSector.id }),
page: currentPage,
page_size: pageSize || 10, // Ensure we always send a page_size
@@ -200,7 +200,7 @@ export default function Keywords() {
setShowContent(true);
setLoading(false);
}
}, [currentPage, statusFilter, clusterFilter, intentFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection, searchTerm, activeSite, activeSector, pageSize]);
}, [currentPage, statusFilter, clusterFilter, countryFilter, difficultyFilter, volumeMin, volumeMax, sortBy, sortDirection, searchTerm, activeSite, activeSector, pageSize]);
// Listen for site and sector changes and refresh data
useEffect(() => {
@@ -324,8 +324,8 @@ export default function Keywords() {
toast.error('Please select at least one keyword to cluster');
return;
}
if (ids.length > 20) {
toast.error('Maximum 20 keywords allowed for clustering');
if (ids.length > 50) {
toast.error('Maximum 50 keywords allowed for clustering');
return;
}
@@ -536,7 +536,7 @@ export default function Keywords() {
keyword: '',
volume: null,
difficulty: null,
intent: 'informational',
country: 'US',
cluster_id: null,
status: 'new',
});
@@ -605,8 +605,8 @@ export default function Keywords() {
setSearchTerm,
statusFilter,
setStatusFilter,
intentFilter,
setIntentFilter,
countryFilter,
setCountryFilter,
difficultyFilter,
setDifficultyFilter,
clusterFilter,
@@ -632,7 +632,7 @@ export default function Keywords() {
formData,
searchTerm,
statusFilter,
intentFilter,
countryFilter,
difficultyFilter,
clusterFilter,
volumeMin,
@@ -776,7 +776,7 @@ export default function Keywords() {
keyword: keyword.keyword,
volume: keyword.volume,
difficulty: keyword.difficulty,
intent: keyword.intent,
country: keyword.country,
cluster_id: keyword.cluster_id,
status: keyword.status,
});
@@ -807,7 +807,7 @@ export default function Keywords() {
filterValues={{
search: searchTerm,
status: statusFilter,
intent: intentFilter,
country: countryFilter,
difficulty: difficultyFilter,
cluster_id: clusterFilter,
volumeMin: volumeMin,
@@ -823,8 +823,8 @@ export default function Keywords() {
} else if (key === 'status') {
setStatusFilter(stringValue);
setCurrentPage(1);
} else if (key === 'intent') {
setIntentFilter(stringValue);
} else if (key === 'country') {
setCountryFilter(stringValue);
setCurrentPage(1);
} else if (key === 'difficulty') {
setDifficultyFilter(stringValue);
@@ -868,7 +868,7 @@ export default function Keywords() {
search: searchTerm,
status: statusFilter,
cluster_id: clusterFilter,
intent: intentFilter,
country: countryFilter,
difficulty: difficultyFilter,
};
await handleExport('csv', filterValues);
@@ -903,7 +903,7 @@ export default function Keywords() {
setSearchTerm('');
setStatusFilter('');
setClusterFilter('');
setIntentFilter('');
setCountryFilter('');
setDifficultyFilter('');
setVolumeMin('');
setVolumeMax('');

View File

@@ -87,7 +87,7 @@ export default function SeedKeywords() {
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Sector</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Volume</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Difficulty</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Intent</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Country</th>
</tr>
</thead>
<tbody>
@@ -109,7 +109,7 @@ export default function SeedKeywords() {
{keyword.difficulty}
</td>
<td className="py-3 px-4">
<Badge variant="light" color="primary">{keyword.intent_display}</Badge>
<Badge variant="light" color="info">{keyword.country_display}</Badge>
</td>
</tr>
))}

View File

@@ -58,7 +58,7 @@ export default function IndustriesSectorsKeywords() {
// Filter state
const [searchTerm, setSearchTerm] = useState('');
const [intentFilter, setIntentFilter] = useState('');
const [countryFilter, setCountryFilter] = useState('');
const [difficultyFilter, setDifficultyFilter] = useState('');
// Check if user is admin/superuser (role is 'admin' or 'developer')
@@ -153,7 +153,7 @@ export default function IndustriesSectorsKeywords() {
}
if (searchTerm) baseFilters.search = searchTerm;
if (intentFilter) baseFilters.intent = intentFilter;
if (countryFilter) baseFilters.country = countryFilter;
// Fetch ALL pages to get complete dataset
let allResults: SeedKeyword[] = [];
@@ -215,9 +215,9 @@ export default function IndustriesSectorsKeywords() {
} else if (sortBy === 'difficulty') {
aVal = a.difficulty;
bVal = b.difficulty;
} else if (sortBy === 'intent') {
aVal = a.intent.toLowerCase();
bVal = b.intent.toLowerCase();
} else if (sortBy === 'country') {
aVal = a.country.toLowerCase();
bVal = b.country.toLowerCase();
} else {
return 0;
}
@@ -249,7 +249,7 @@ export default function IndustriesSectorsKeywords() {
setTotalCount(0);
setTotalPages(1);
}
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, intentFilter, difficultyFilter, sortBy, sortDirection, toast]);
}, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, sortBy, sortDirection, toast]);
// Load data on mount and when filters change
useEffect(() => {
@@ -521,28 +521,18 @@ export default function IndustriesSectorsKeywords() {
},
},
{
key: 'intent',
label: 'Intent',
key: 'country',
label: 'Country',
sortable: true,
sortField: 'intent',
sortField: 'country',
render: (value: string) => {
const getIntentColor = (intent: string) => {
const lowerIntent = intent?.toLowerCase() || '';
if (lowerIntent === 'transactional' || lowerIntent === 'commercial') {
return 'success';
} else if (lowerIntent === 'navigational') {
return 'warning';
}
return 'info';
};
return (
<Badge
color={getIntentColor(value)}
color="info"
size="sm"
variant={value?.toLowerCase() === 'informational' ? 'light' : undefined}
variant="light"
>
{value}
{value || '-'}
</Badge>
);
},
@@ -584,15 +574,18 @@ export default function IndustriesSectorsKeywords() {
placeholder: 'Search keywords...',
},
{
key: 'intent',
label: 'Intent',
key: 'country',
label: 'Country',
type: 'select' as const,
options: [
{ value: '', label: 'All Intent' },
{ value: 'informational', label: 'Informational' },
{ value: 'navigational', label: 'Navigational' },
{ value: 'transactional', label: 'Transactional' },
{ value: 'commercial', label: 'Commercial' },
{ value: '', label: 'All Countries' },
{ value: 'US', label: 'United States' },
{ value: 'CA', label: 'Canada' },
{ value: 'GB', label: 'United Kingdom' },
{ value: 'AE', label: 'United Arab Emirates' },
{ value: 'AU', label: 'Australia' },
{ value: 'IN', label: 'India' },
{ value: 'PK', label: 'Pakistan' },
],
},
{
@@ -691,7 +684,7 @@ export default function IndustriesSectorsKeywords() {
filters={pageConfig.filters}
filterValues={{
search: searchTerm,
intent: intentFilter,
country: countryFilter,
difficulty: difficultyFilter,
}}
onFilterChange={(key, value) => {
@@ -699,8 +692,8 @@ export default function IndustriesSectorsKeywords() {
if (key === 'search') {
setSearchTerm(stringValue);
} else if (key === 'intent') {
setIntentFilter(stringValue);
} else if (key === 'country') {
setCountryFilter(stringValue);
setCurrentPage(1);
} else if (key === 'difficulty') {
setDifficultyFilter(stringValue);
@@ -762,7 +755,7 @@ export default function IndustriesSectorsKeywords() {
<div>
<Label htmlFor="import-file">Upload CSV File</Label>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
Expected columns: keyword, volume, difficulty, intent, industry_name, sector_name
Expected columns: keyword, volume, difficulty, country, industry_name, sector_name
</p>
<FileInput
accept=".csv"