Implement Stage 3: Enhance content generation and metadata features

- Updated AI prompts to include metadata context, cluster roles, and product attributes for improved content generation.
- Enhanced GenerateContentFunction to incorporate taxonomy and keyword objects for richer context.
- Introduced new metadata fields in frontend components for better content organization and filtering.
- Added cluster match, taxonomy match, and relevance score to LinkResults for improved link management.
- Implemented metadata completeness scoring and recommended actions in AnalysisPreview for better content optimization.
- Updated API services to support new metadata structures and site progress tracking.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-19 20:07:05 +00:00
parent bae9ea47d8
commit 746a51715f
14 changed files with 892 additions and 23 deletions

View File

@@ -108,6 +108,74 @@ export default function AnalysisPreview() {
{/* Scores */}
<OptimizationScores scores={scores} />
{/* Stage 3: Cluster Dimension Scorecards */}
{(scores.has_cluster_mapping || scores.has_taxonomy_mapping || scores.has_attributes) && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Metadata Coverage</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className={`p-4 rounded-lg border-2 ${
scores.has_cluster_mapping
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
: 'border-red-500 bg-red-50 dark:bg-red-900/20'
}`}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Cluster Mapping</span>
{scores.has_cluster_mapping ? (
<span className="text-green-600 dark:text-green-400"></span>
) : (
<span className="text-red-600 dark:text-red-400"></span>
)}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
{scores.has_cluster_mapping
? 'Content is mapped to cluster'
: 'Missing cluster mapping'}
</div>
</div>
<div className={`p-4 rounded-lg border-2 ${
scores.has_taxonomy_mapping
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
: 'border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20'
}`}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Taxonomy Mapping</span>
{scores.has_taxonomy_mapping ? (
<span className="text-green-600 dark:text-green-400"></span>
) : (
<span className="text-yellow-600 dark:text-yellow-400"></span>
)}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
{scores.has_taxonomy_mapping
? 'Content is mapped to taxonomy'
: 'Taxonomy mapping recommended'}
</div>
</div>
<div className={`p-4 rounded-lg border-2 ${
scores.has_attributes
? 'border-green-500 bg-green-50 dark:bg-green-900/20'
: 'border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20'
}`}>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Attributes</span>
{scores.has_attributes ? (
<span className="text-green-600 dark:text-green-400"></span>
) : (
<span className="text-yellow-600 dark:text-yellow-400"></span>
)}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
{scores.has_attributes
? 'Content has attributes'
: 'Attributes recommended for products/services'}
</div>
</div>
</div>
</div>
)}
{/* Score Details */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Score Details</h3>
@@ -140,8 +208,78 @@ export default function AnalysisPreview() {
{scores.internal_links_count || 0}
</span>
</div>
{/* Stage 3: Metadata completeness score */}
{scores.metadata_completeness_score !== undefined && (
<div>
<span className="text-sm text-gray-600 dark:text-gray-400">Metadata Completeness:</span>
<span className="ml-2 font-medium text-gray-900 dark:text-white">
{scores.metadata_completeness_score.toFixed(1)}%
</span>
</div>
)}
</div>
</div>
{/* Stage 3: Next Action Cards */}
{(!scores.has_cluster_mapping || !scores.has_taxonomy_mapping || !scores.has_attributes || (scores.metadata_completeness_score !== undefined && scores.metadata_completeness_score < 80)) && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">Recommended Actions</h3>
<div className="space-y-3">
{!scores.has_cluster_mapping && (
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<div className="flex items-start gap-2">
<span className="text-blue-600 dark:text-blue-400 font-bold">1.</span>
<div className="flex-1">
<div className="font-medium text-blue-900 dark:text-blue-300">Map Content to Cluster</div>
<div className="text-sm text-blue-700 dark:text-blue-400 mt-1">
Assign this content to a keyword cluster to improve internal linking and SEO structure.
</div>
</div>
</div>
</div>
)}
{!scores.has_taxonomy_mapping && (
<div className="p-3 bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg">
<div className="flex items-start gap-2">
<span className="text-purple-600 dark:text-purple-400 font-bold">2.</span>
<div className="flex-1">
<div className="font-medium text-purple-900 dark:text-purple-300">Add Taxonomy Mapping</div>
<div className="text-sm text-purple-700 dark:text-purple-400 mt-1">
Categorize this content with a taxonomy for better organization and navigation.
</div>
</div>
</div>
</div>
)}
{!scores.has_attributes && (content?.entity_type === 'product' || content?.entity_type === 'service') && (
<div className="p-3 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg">
<div className="flex items-start gap-2">
<span className="text-orange-600 dark:text-orange-400 font-bold">3.</span>
<div className="flex-1">
<div className="font-medium text-orange-900 dark:text-orange-300">Add Product/Service Attributes</div>
<div className="text-sm text-orange-700 dark:text-orange-400 mt-1">
Add attributes like specifications, features, or modifiers to enhance content completeness.
</div>
</div>
</div>
</div>
)}
{scores.metadata_completeness_score !== undefined && scores.metadata_completeness_score < 80 && (
<div className="p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<div className="flex items-start gap-2">
<span className="text-amber-600 dark:text-amber-400 font-bold">4.</span>
<div className="flex-1">
<div className="font-medium text-amber-900 dark:text-amber-300">Improve Metadata Completeness</div>
<div className="text-sm text-amber-700 dark:text-amber-400 mt-1">
Current score: {scores.metadata_completeness_score.toFixed(1)}%. Complete missing metadata fields to reach 80%+.
</div>
</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
) : (
<div className="text-center py-12">

View File

@@ -45,6 +45,7 @@ export default function Ideas() {
const [clusterFilter, setClusterFilter] = useState('');
const [structureFilter, setStructureFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [entityTypeFilter, setEntityTypeFilter] = useState(''); // Stage 3: Entity type filter
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state
@@ -101,6 +102,7 @@ export default function Ideas() {
...(clusterFilter && { keyword_cluster_id: clusterFilter }),
...(structureFilter && { content_structure: structureFilter }),
...(typeFilter && { content_type: typeFilter }),
...(entityTypeFilter && { site_entity_type: entityTypeFilter }), // Stage 3: Entity type filter
page: currentPage,
page_size: pageSize,
ordering,
@@ -121,7 +123,7 @@ export default function Ideas() {
setShowContent(true);
setLoading(false);
}
}, [currentPage, statusFilter, clusterFilter, structureFilter, typeFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize]);
}, [currentPage, statusFilter, clusterFilter, structureFilter, typeFilter, entityTypeFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize]);
useEffect(() => {
loadIdeas();
@@ -311,6 +313,7 @@ export default function Ideas() {
keyword_cluster_id: clusterFilter,
content_structure: structureFilter,
content_type: typeFilter,
site_entity_type: entityTypeFilter, // Stage 3: Entity type filter
}}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
@@ -318,12 +321,19 @@ export default function Ideas() {
setSearchTerm(stringValue);
} else if (key === 'status') {
setStatusFilter(stringValue);
setCurrentPage(1);
} else if (key === 'keyword_cluster_id') {
setClusterFilter(stringValue);
setCurrentPage(1);
} else if (key === 'content_structure') {
setStructureFilter(stringValue);
setCurrentPage(1);
} else if (key === 'content_type') {
setTypeFilter(stringValue);
setCurrentPage(1);
} else if (key === 'site_entity_type') { // Stage 3: Entity type filter
setEntityTypeFilter(stringValue);
setCurrentPage(1);
}
setCurrentPage(1);
}}

View File

@@ -17,7 +17,8 @@ import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { fetchAPI, fetchSiteBlueprints } from '../../services/api';
import SiteProgressWidget from '../../components/sites/SiteProgressWidget';
interface Site {
id: number;
@@ -51,6 +52,7 @@ export default function SiteDashboard() {
const toast = useToast();
const [site, setSite] = useState<Site | null>(null);
const [stats, setStats] = useState<SiteStats | null>(null);
const [blueprints, setBlueprints] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
@@ -62,9 +64,10 @@ export default function SiteDashboard() {
const loadSiteData = async () => {
try {
setLoading(true);
const [siteData, statsData] = await Promise.all([
const [siteData, statsData, blueprintsData] = await Promise.all([
fetchAPI(`/v1/auth/sites/${siteId}/`),
fetchSiteStats(),
fetchSiteBlueprints({ site_id: Number(siteId) }),
]);
if (siteData) {
@@ -74,6 +77,10 @@ export default function SiteDashboard() {
if (statsData) {
setStats(statsData);
}
if (blueprintsData && blueprintsData.results) {
setBlueprints(blueprintsData.results);
}
} catch (error: any) {
toast.error(`Failed to load site data: ${error.message}`);
} finally {
@@ -207,6 +214,19 @@ export default function SiteDashboard() {
</div>
</div>
{/* Stage 3: Site Progress Widget */}
{blueprints.length > 0 && (
<div className="mb-6">
{blueprints.map((blueprint) => (
<SiteProgressWidget
key={blueprint.id}
blueprintId={blueprint.id}
siteId={Number(siteId)}
/>
))}
</div>
)}
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
{statCards.map((stat, index) => (

View File

@@ -32,6 +32,13 @@ interface Content {
sector: number;
word_count?: number;
metadata?: Record<string, any>;
// Stage 3: Metadata fields
entity_type?: string | null;
cluster_name?: string | null;
cluster_id?: number | null;
taxonomy_name?: string | null;
taxonomy_id?: number | null;
cluster_role?: string | null;
}
export default function PostEditor() {
@@ -269,6 +276,10 @@ export default function PostEditor() {
<div className="p-6">
<PageMeta title={content.id ? 'Edit Post' : 'New Post'} />
<div className="flex gap-6">
{/* Main Content Area */}
<div className="flex-1">
<div className="mb-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
@@ -289,10 +300,11 @@ export default function PostEditor() {
<Button
variant="primary"
onClick={handleSave}
disabled={saving}
disabled={saving || (content.status === 'publish' && validationResult && !validationResult.is_valid)}
title={content.status === 'publish' && validationResult && !validationResult.is_valid ? 'Please fix validation errors before publishing' : undefined}
>
<SaveIcon className="w-4 h-4 mr-2" />
{saving ? 'Saving...' : 'Save Post'}
{saving ? 'Saving...' : content.status === 'publish' ? 'Publish' : 'Save Post'}
</Button>
</div>
</div>
@@ -717,6 +729,103 @@ export default function PostEditor() {
</Card>
)}
</div>
</div>
{/* Stage 3: Sidebar with Metadata Summary */}
{content.id && (
<div className="w-80 flex-shrink-0">
<Card className="p-4 sticky top-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Content Metadata
</h3>
<div className="space-y-4">
{/* Entity Type */}
{content.entity_type && (
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Entity Type
</div>
<div className="text-sm text-gray-900 dark:text-white">
{content.entity_type ? content.entity_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()) : '-'}
</div>
</div>
)}
{/* Cluster */}
{content.cluster_name && (
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Cluster
</div>
<div className="text-sm text-gray-900 dark:text-white">
{content.cluster_name}
{content.cluster_role && (
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400">
({content.cluster_role})
</span>
)}
</div>
</div>
)}
{/* Taxonomy */}
{content.taxonomy_name && (
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Taxonomy
</div>
<div className="text-sm text-gray-900 dark:text-white">
{content.taxonomy_name}
</div>
</div>
)}
{/* Validation Status */}
{validationResult && (
<div>
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
Validation Status
</div>
<div className={`text-sm font-medium ${
validationResult.is_valid
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}>
{validationResult.is_valid ? '✓ Valid' : `${validationResult.validation_errors.length} error(s)`}
</div>
</div>
)}
{/* Quick Links */}
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
Quick Actions
</div>
<div className="space-y-1">
{content.cluster_id && (
<button
onClick={() => navigate(`/planner/clusters/${content.cluster_id}`)}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline w-full text-left"
>
View Cluster
</button>
)}
{content.taxonomy_id && (
<button
onClick={() => navigate(`/sites/builder?taxonomy=${content.taxonomy_id}`)}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline w-full text-left"
>
View Taxonomy
</button>
)}
</div>
</div>
</div>
</Card>
</div>
)}
</div>
</div>
);
}

View File

@@ -48,6 +48,7 @@ export default function Tasks() {
const [structureFilter, setStructureFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [sourceFilter, setSourceFilter] = useState('');
const [entityTypeFilter, setEntityTypeFilter] = useState(''); // Stage 3: Entity type filter
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state
@@ -143,6 +144,7 @@ export default function Tasks() {
...(clusterFilter && { cluster_id: clusterFilter }),
...(structureFilter && { content_structure: structureFilter }),
...(typeFilter && { content_type: typeFilter }),
...(entityTypeFilter && { entity_type: entityTypeFilter }), // Stage 3: Entity type filter
page: currentPage,
page_size: pageSize,
ordering,
@@ -163,7 +165,7 @@ export default function Tasks() {
setShowContent(true);
setLoading(false);
}
}, [currentPage, statusFilter, clusterFilter, structureFilter, typeFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize]);
}, [currentPage, statusFilter, clusterFilter, structureFilter, typeFilter, entityTypeFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize]);
useEffect(() => {
loadTasks();
@@ -512,7 +514,7 @@ export default function Tasks() {
setTypeFilter,
setCurrentPage,
});
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, sourceFilter]);
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, sourceFilter, entityTypeFilter]);
// Calculate header metrics
const headerMetrics = useMemo(() => {
@@ -576,6 +578,7 @@ export default function Tasks() {
content_structure: structureFilter,
content_type: typeFilter,
source: sourceFilter,
entity_type: entityTypeFilter, // Stage 3: Entity type filter
}}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
@@ -591,6 +594,8 @@ export default function Tasks() {
setTypeFilter(stringValue);
} else if (key === 'source') {
setSourceFilter(stringValue);
} else if (key === 'entity_type') { // Stage 3: Entity type filter
setEntityTypeFilter(stringValue);
}
setCurrentPage(1);
}}