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:
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user