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

Binary file not shown.

View File

@@ -63,9 +63,10 @@ class GenerateContentFunction(BaseAIFunction):
queryset = queryset.filter(account=account)
# Preload all relationships to avoid N+1 queries
# Stage 3: Include taxonomy and keyword_objects for metadata
tasks = list(queryset.select_related(
'account', 'site', 'sector', 'cluster', 'idea'
))
'account', 'site', 'sector', 'cluster', 'idea', 'taxonomy'
).prefetch_related('keyword_objects'))
if not tasks:
raise ValueError("No tasks found")
@@ -125,12 +126,54 @@ class GenerateContentFunction(BaseAIFunction):
cluster_data += f"Description: {task.cluster.description}\n"
cluster_data += f"Status: {task.cluster.status or 'active'}\n"
# Stage 3: Build cluster role context
cluster_role_data = ''
if hasattr(task, 'cluster_role') and task.cluster_role:
role_descriptions = {
'hub': 'Hub Page - Main authoritative resource for this topic cluster. Should be comprehensive, overview-focused, and link to supporting content.',
'supporting': 'Supporting Page - Detailed content that supports the hub page. Focus on specific aspects, use cases, or subtopics.',
'attribute': 'Attribute Page - Content focused on specific attributes, features, or specifications. Include detailed comparisons and specifications.',
}
role_desc = role_descriptions.get(task.cluster_role, f'Role: {task.cluster_role}')
cluster_role_data = f"Cluster Role: {role_desc}\n"
# Stage 3: Build taxonomy context
taxonomy_data = ''
if hasattr(task, 'taxonomy') and task.taxonomy:
taxonomy_data = f"Taxonomy: {task.taxonomy.name or ''}\n"
if task.taxonomy.taxonomy_type:
taxonomy_data += f"Taxonomy Type: {task.taxonomy.get_taxonomy_type_display() or task.taxonomy.taxonomy_type}\n"
if task.taxonomy.description:
taxonomy_data += f"Description: {task.taxonomy.description}\n"
# Stage 3: Build attributes context from keywords
attributes_data = ''
if hasattr(task, 'keyword_objects') and task.keyword_objects.exists():
attribute_list = []
for keyword in task.keyword_objects.all():
if hasattr(keyword, 'attribute_values') and keyword.attribute_values:
if isinstance(keyword.attribute_values, dict):
for attr_name, attr_value in keyword.attribute_values.items():
attribute_list.append(f"{attr_name}: {attr_value}")
elif isinstance(keyword.attribute_values, list):
for attr_item in keyword.attribute_values:
if isinstance(attr_item, dict):
for attr_name, attr_value in attr_item.items():
attribute_list.append(f"{attr_name}: {attr_value}")
else:
attribute_list.append(str(attr_item))
if attribute_list:
attributes_data = "Product/Service Attributes:\n"
attributes_data += "\n".join(f"- {attr}" for attr in attribute_list) + "\n"
# Build keywords string
keywords_data = task.keywords or ''
if not keywords_data and task.idea:
keywords_data = task.idea.target_keywords or ''
# Get prompt from registry with context
# Stage 3: Include cluster_role, taxonomy, and attributes in context
prompt = PromptRegistry.get_prompt(
function_name='generate_content',
account=account,
@@ -138,6 +181,9 @@ class GenerateContentFunction(BaseAIFunction):
context={
'IDEA': idea_data,
'CLUSTER': cluster_data,
'CLUSTER_ROLE': cluster_role_data,
'TAXONOMY': taxonomy_data,
'ATTRIBUTES': attributes_data,
'KEYWORDS': keywords_data,
}
)

View File

@@ -147,7 +147,7 @@ Output JSON Example:
]
}""",
'content_generation': """You are an editorial content strategist. Your task is to generate a complete JSON response object that includes all the fields listed below, based on the provided content idea, keyword cluster, and keyword list.
'content_generation': """You are an editorial content strategist. Your task is to generate a complete JSON response object that includes all the fields listed below, based on the provided content idea, keyword cluster, keyword list, and metadata context.
Only the `content` field should contain HTML inside JSON object.
@@ -217,7 +217,28 @@ KEYWORD & SEO RULES
- Don't repeat heading in opening sentence
- Vary sentence structure and length
===========================
STAGE 3: METADATA CONTEXT (NEW)
===========================
**Cluster Role:**
[IGNY8_CLUSTER_ROLE]
- If role is "hub": Create comprehensive, authoritative content that serves as the main resource for this topic cluster. Include overview sections, key concepts, and links to related topics.
- If role is "supporting": Create detailed, focused content that supports the hub page. Dive deep into specific aspects, use cases, or subtopics.
- If role is "attribute": Create content focused on specific attributes, features, or specifications. Include detailed comparisons, specifications, or attribute-focused information.
**Taxonomy Context:**
[IGNY8_TAXONOMY]
- Use taxonomy information to structure categories and tags appropriately.
- Align content with taxonomy hierarchy and relationships.
- Ensure content fits within the defined taxonomy structure.
**Product/Service Attributes:**
[IGNY8_ATTRIBUTES]
- If attributes are provided (e.g., product specs, service modifiers), incorporate them naturally into the content.
- For product content: Include specifications, features, dimensions, materials, etc. as relevant.
- For service content: Include service tiers, pricing modifiers, availability, etc. as relevant.
- Present attributes in a user-friendly format (tables, lists, or integrated into narrative).
===========================
INPUT VARIABLES

View File

@@ -5,6 +5,9 @@ interface Link {
anchor_text: string;
target_content_id: number;
target_url?: string;
cluster_match?: boolean; // Stage 3: Cluster match flag
taxonomy_match?: boolean; // Stage 3: Taxonomy match flag
relevance_score?: number; // Stage 3: Relevance score
}
interface LinkResultsProps {
@@ -39,6 +42,58 @@ export const LinkResults: React.FC<LinkResultsProps> = ({
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Added Links:</h4>
{/* Stage 3: Group links by cluster match */}
{links.some(l => l.cluster_match) && (
<div className="mb-4">
<div className="text-xs font-semibold text-blue-600 dark:text-blue-400 mb-2">
Cluster Matches (High Priority)
</div>
<ul className="space-y-2">
{links.filter(l => l.cluster_match).map((link, index) => (
<li key={`cluster-${index}`} className="flex items-center gap-2 text-sm pl-2 border-l-2 border-blue-500">
<span className="text-gray-600 dark:text-gray-400">"{link.anchor_text}"</span>
<span className="text-gray-400"></span>
<span className="text-blue-600 dark:text-blue-400">
Content #{link.target_content_id}
</span>
{link.relevance_score && (
<span className="text-xs text-gray-500 dark:text-gray-400">
(Score: {link.relevance_score})
</span>
)}
</li>
))}
</ul>
</div>
)}
{/* Other links */}
{links.filter(l => !l.cluster_match || l.cluster_match === false).length > 0 && (
<div>
<div className="text-xs font-semibold text-gray-600 dark:text-gray-400 mb-2">
Other Links
</div>
<ul className="space-y-2">
{links.filter(l => !l.cluster_match || l.cluster_match === false).map((link, index) => (
<li key={`other-${index}`} className="flex items-center gap-2 text-sm">
<span className="text-gray-600 dark:text-gray-400">"{link.anchor_text}"</span>
<span className="text-gray-400"></span>
<span className="text-blue-600 dark:text-blue-400">
Content #{link.target_content_id}
</span>
{link.relevance_score && (
<span className="text-xs text-gray-500 dark:text-gray-400">
(Score: {link.relevance_score})
</span>
)}
</li>
))}
</ul>
</div>
)}
{/* Fallback if no grouping */}
{!links.some(l => l.cluster_match !== undefined) && (
<ul className="space-y-2">
{links.map((link, index) => (
<li key={index} className="flex items-center gap-2 text-sm">
@@ -50,6 +105,7 @@ export const LinkResults: React.FC<LinkResultsProps> = ({
</li>
))}
</ul>
)}
</div>
</div>
) : (

View File

@@ -0,0 +1,262 @@
/**
* Site Progress Widget - Stage 3
* Displays cluster-level completion bars for hub/supporting/attribute pages
*/
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card } from '../ui/card';
import Badge from '../ui/badge/Badge';
import { fetchSiteProgress, SiteProgress } from '../../services/api';
import { CheckCircleIcon, XCircleIcon, AlertCircleIcon, ArrowRightIcon } from 'lucide-react';
interface SiteProgressWidgetProps {
blueprintId: number;
siteId?: number;
}
export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgressWidgetProps) {
const navigate = useNavigate();
const [progress, setProgress] = useState<SiteProgress | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadProgress();
}, [blueprintId]);
const loadProgress = async () => {
try {
setLoading(true);
const data = await fetchSiteProgress(blueprintId);
setProgress(data);
} catch (error: any) {
console.error('Failed to load site progress:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<Card className="p-4">
<div className="text-center py-4 text-gray-500 dark:text-gray-400">
Loading progress...
</div>
</Card>
);
}
if (!progress) {
return null;
}
const getStatusColor = (status: string) => {
switch (status) {
case 'complete':
return 'success';
case 'blocked':
return 'error';
default:
return 'warning';
}
};
const getRoleColor = (role: string) => {
switch (role) {
case 'hub':
return 'primary';
case 'supporting':
return 'success';
case 'attribute':
return 'warning';
default:
return 'info';
}
};
return (
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Site Progress: {progress.blueprint_name}
</h3>
<Badge color={getStatusColor(progress.overall_status)} size="sm">
{progress.overall_status ? progress.overall_status.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'Unknown'}
</Badge>
</div>
{/* Overall Stats */}
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="text-center p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{progress.cluster_coverage.covered_clusters}/{progress.cluster_coverage.total_clusters}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 mt-1">Clusters Covered</div>
</div>
<div className="text-center p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{progress.taxonomy_coverage.defined_taxonomies}/{progress.taxonomy_coverage.total_taxonomies}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 mt-1">Taxonomies Defined</div>
</div>
<div className="text-center p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="text-2xl font-bold text-gray-900 dark:text-white">
{progress.validation_flags.all_pages_generated ? '✓' : '✗'}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 mt-1">All Pages Generated</div>
</div>
</div>
{/* Cluster Progress */}
<div className="space-y-4">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
Cluster Coverage
</h4>
{progress.cluster_coverage.details.map((cluster) => {
const totalPages = cluster.hub_pages + cluster.supporting_pages + cluster.attribute_pages;
const completionPercent = totalPages > 0 ? Math.min(100, (cluster.content_count / totalPages) * 100) : 0;
return (
<div
key={cluster.cluster_id}
className={`p-4 rounded-lg border-2 ${
cluster.is_complete
? 'border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20'
: 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h5 className="font-medium text-gray-900 dark:text-white">
{cluster.cluster_name}
</h5>
<Badge color={getRoleColor(cluster.role)} size="sm" variant="light">
{cluster.role}
</Badge>
{cluster.is_complete ? (
<CheckCircleIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
) : (
<AlertCircleIcon className="w-4 h-4 text-amber-600 dark:text-amber-400" />
)}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
{cluster.content_count} content / {totalPages} pages
</div>
</div>
<button
onClick={() => navigate(`/planner/clusters/${cluster.cluster_id}`)}
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
>
View <ArrowRightIcon className="w-3 h-3" />
</button>
</div>
{/* Progress Bar */}
<div className="mt-2">
<div className="flex items-center justify-between text-xs text-gray-600 dark:text-gray-400 mb-1">
<span>Completion</span>
<span>{completionPercent.toFixed(0)}%</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className={`h-2 rounded-full ${
cluster.is_complete
? 'bg-green-500 dark:bg-green-400'
: 'bg-blue-500 dark:bg-blue-400'
}`}
style={{ width: `${completionPercent}%` }}
/>
</div>
</div>
{/* Page Type Breakdown */}
<div className="grid grid-cols-3 gap-2 mt-3 text-xs">
<div className="text-center p-2 bg-white dark:bg-gray-700 rounded">
<div className="font-medium text-gray-900 dark:text-white">{cluster.hub_pages}</div>
<div className="text-gray-600 dark:text-gray-400">Hub</div>
</div>
<div className="text-center p-2 bg-white dark:bg-gray-700 rounded">
<div className="font-medium text-gray-900 dark:text-white">{cluster.supporting_pages}</div>
<div className="text-gray-600 dark:text-gray-400">Supporting</div>
</div>
<div className="text-center p-2 bg-white dark:bg-gray-700 rounded">
<div className="font-medium text-gray-900 dark:text-white">{cluster.attribute_pages}</div>
<div className="text-gray-600 dark:text-gray-400">Attribute</div>
</div>
</div>
{/* Validation Messages */}
{cluster.validation_messages && cluster.validation_messages.length > 0 && (
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div className="text-xs font-medium text-red-600 dark:text-red-400 mb-1">
Issues:
</div>
<ul className="text-xs text-gray-600 dark:text-gray-400 space-y-1">
{cluster.validation_messages.map((msg, idx) => (
<li key={idx} className="flex items-start gap-1">
<XCircleIcon className="w-3 h-3 text-red-500 mt-0.5 flex-shrink-0" />
<span>{msg}</span>
</li>
))}
</ul>
</div>
)}
</div>
);
})}
</div>
{/* Validation Flags Summary */}
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
Validation Status
</h4>
<div className="grid grid-cols-2 gap-3">
<div className="flex items-center gap-2">
{progress.validation_flags.clusters_attached ? (
<CheckCircleIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
) : (
<XCircleIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
)}
<span className="text-sm text-gray-700 dark:text-gray-300">Clusters Attached</span>
</div>
<div className="flex items-center gap-2">
{progress.validation_flags.taxonomies_defined ? (
<CheckCircleIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
) : (
<XCircleIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
)}
<span className="text-sm text-gray-700 dark:text-gray-300">Taxonomies Defined</span>
</div>
<div className="flex items-center gap-2">
{progress.validation_flags.sitemap_generated ? (
<CheckCircleIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
) : (
<XCircleIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
)}
<span className="text-sm text-gray-700 dark:text-gray-300">Sitemap Generated</span>
</div>
<div className="flex items-center gap-2">
{progress.validation_flags.all_pages_generated ? (
<CheckCircleIcon className="w-4 h-4 text-green-600 dark:text-green-400" />
) : (
<XCircleIcon className="w-4 h-4 text-red-600 dark:text-red-400" />
)}
<span className="text-sm text-gray-700 dark:text-gray-300">All Pages Generated</span>
</div>
</div>
</div>
{/* Deep Link to Blueprint */}
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
onClick={() => navigate(`/sites/builder/workflow/${blueprintId}`)}
className="w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors"
>
Continue Site Builder Workflow
</button>
</div>
</Card>
);
}

View File

@@ -156,6 +156,58 @@ export const createIdeasPageConfig = (
width: '200px',
render: (_value: string, row: ContentIdea) => row.keyword_cluster_name || '-',
},
// Stage 3: Metadata columns
{
key: 'site_entity_type',
label: 'Entity Type',
sortable: true,
sortField: 'site_entity_type',
width: '120px',
defaultVisible: true,
render: (value: string, row: ContentIdea) => {
const entityType = value || (row as any).site_entity_type;
if (!entityType) {
return <span className="text-gray-400 dark:text-gray-500">-</span>;
}
const typeLabels: Record<string, string> = {
'blog_post': 'Blog Post',
'article': 'Article',
'product': 'Product',
'service': 'Service',
'taxonomy': 'Taxonomy',
'page': 'Page',
};
return (
<Badge color="info" size="sm" variant="light">
{typeLabels[entityType] || entityType}
</Badge>
);
},
},
{
key: 'cluster_role',
label: 'Role',
sortable: true,
sortField: 'cluster_role',
width: '100px',
defaultVisible: false,
render: (value: string, row: ContentIdea) => {
const role = value || (row as any).cluster_role;
if (!role) {
return <span className="text-gray-400 dark:text-gray-500">-</span>;
}
const roleColors: Record<string, 'primary' | 'success' | 'warning'> = {
'hub': 'primary',
'supporting': 'success',
'attribute': 'warning',
};
return (
<Badge color={roleColors[role] || 'primary'} size="sm" variant="light">
{role.charAt(0).toUpperCase() + role.slice(1)}
</Badge>
);
},
},
{
...statusColumn,
sortable: true,
@@ -242,6 +294,21 @@ export const createIdeasPageConfig = (
{ value: 'tutorial', label: 'Tutorial' },
],
},
// Stage 3: Entity type filter
{
key: 'site_entity_type',
label: 'Entity Type',
type: 'select',
options: [
{ value: '', label: 'All Entity Types' },
{ value: 'blog_post', label: 'Blog Post' },
{ value: 'article', label: 'Article' },
{ value: 'product', label: 'Product' },
{ value: 'service', label: 'Service' },
{ value: 'taxonomy', label: 'Taxonomy' },
{ value: 'page', label: 'Page' },
],
},
{
key: 'keyword_cluster_id',
label: 'Cluster',

View File

@@ -107,7 +107,7 @@ export const createTasksPageConfig = (
toggleContentLabel: 'Idea & Content Outline',
render: (value: string, row: Task) => {
const isSiteBuilder = value?.startsWith('[Site Builder]');
const displayTitle = isSiteBuilder ? value.replace('[Site Builder] ', '') : value;
const displayTitle = isSiteBuilder && value ? value.replace('[Site Builder] ', '') : (value || 'Untitled');
return (
<div className="flex items-center gap-2">
@@ -140,6 +140,76 @@ export const createTasksPageConfig = (
width: '200px',
render: (_value: string, row: Task) => row.cluster_name || '-',
},
// Stage 3: Metadata columns
{
key: 'entity_type',
label: 'Entity Type',
sortable: true,
sortField: 'entity_type',
width: '120px',
defaultVisible: true,
render: (value: string, row: Task) => {
const entityType = value || row.entity_type;
if (!entityType) {
return <span className="text-gray-400 dark:text-gray-500">-</span>;
}
const typeLabels: Record<string, string> = {
'blog_post': 'Blog Post',
'article': 'Article',
'product': 'Product',
'service': 'Service',
'taxonomy': 'Taxonomy',
'page': 'Page',
};
return (
<Badge color="info" size="sm" variant="light">
{typeLabels[entityType] || entityType}
</Badge>
);
},
},
{
key: 'cluster_role',
label: 'Role',
sortable: true,
sortField: 'cluster_role',
width: '100px',
defaultVisible: false,
render: (value: string, row: Task) => {
const role = value || row.cluster_role;
if (!role) {
return <span className="text-gray-400 dark:text-gray-500">-</span>;
}
const roleColors: Record<string, 'primary' | 'success' | 'warning'> = {
'hub': 'primary',
'supporting': 'success',
'attribute': 'warning',
};
return (
<Badge color={roleColors[role] || 'primary'} size="sm" variant="light">
{role.charAt(0).toUpperCase() + role.slice(1)}
</Badge>
);
},
},
{
key: 'taxonomy_name',
label: 'Taxonomy',
sortable: false,
width: '150px',
defaultVisible: false,
render: (_value: string, row: Task) => {
const taxonomyName = row.taxonomy_name;
if (!taxonomyName) {
return <span className="text-gray-400 dark:text-gray-500">-</span>;
}
return (
<Badge color="purple" size="sm" variant="light">
{taxonomyName}
</Badge>
);
},
},
{
key: 'content_structure',
label: 'Structure',
@@ -339,6 +409,21 @@ export const createTasksPageConfig = (
})(),
dynamicOptions: 'clusters',
},
// Stage 3: Entity type filter
{
key: 'entity_type',
label: 'Entity Type',
type: 'select',
options: [
{ value: '', label: 'All Entity Types' },
{ value: 'blog_post', label: 'Blog Post' },
{ value: 'article', label: 'Article' },
{ value: 'product', label: 'Product' },
{ value: 'service', label: 'Service' },
{ value: 'taxonomy', label: 'Taxonomy' },
{ value: 'page', label: 'Page' },
],
},
],
formFields: (clusters: Array<{ id: number; name: string }>) => [
{

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>
@@ -718,6 +730,103 @@ export default function PostEditor() {
)}
</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);
}}

View File

@@ -777,6 +777,11 @@ export interface ContentIdea {
estimated_word_count: number;
created_at: string;
updated_at: string;
// Stage 3: Metadata fields
site_entity_type?: string | null;
cluster_role?: string | null;
taxonomy_id?: number | null;
taxonomy_name?: string | null;
}
export interface ContentIdeaCreateData {
@@ -887,6 +892,7 @@ export interface TasksFilters {
cluster_id?: string;
content_type?: string;
content_structure?: string;
entity_type?: string; // Stage 3: Entity type filter
page?: number;
page_size?: number;
ordering?: string;
@@ -927,6 +933,11 @@ export interface Task {
post_url?: string | null;
created_at: string;
updated_at: string;
// Stage 3: Metadata fields
entity_type?: string | null;
taxonomy_id?: number | null;
taxonomy_name?: string | null;
cluster_role?: string | null;
}
export interface TaskCreateData {
@@ -2103,6 +2114,45 @@ export async function fetchSiteBlueprintById(id: number): Promise<SiteBlueprint>
return fetchAPI(`/v1/site-builder/blueprints/${id}/`);
}
// Stage 3: Site Progress API
export interface SiteProgress {
blueprint_id: number;
blueprint_name: string;
overall_status: 'in_progress' | 'complete' | 'blocked';
cluster_coverage: {
total_clusters: number;
covered_clusters: number;
details: Array<{
cluster_id: number;
cluster_name: string;
role: string;
coverage_status: string;
validation_messages: string[];
tasks_count: number;
content_count: number;
hub_pages: number;
supporting_pages: number;
attribute_pages: number;
is_complete: boolean;
}>;
};
taxonomy_coverage: {
total_taxonomies: number;
defined_taxonomies: number;
details: any[];
};
validation_flags: {
clusters_attached: boolean;
taxonomies_defined: boolean;
sitemap_generated: boolean;
all_pages_generated: boolean;
};
}
export async function fetchSiteProgress(blueprintId: number): Promise<SiteProgress> {
return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/progress/`);
}
export async function createSiteBlueprint(data: Partial<SiteBlueprint>): Promise<SiteBlueprint> {
return fetchAPI('/v1/site-builder/blueprints/', {
method: 'POST',

View File

@@ -25,7 +25,7 @@ Propagate the new metadata (clusters, taxonomies, entity types, attributes) thro
| **Ideas → Tasks** | ✅ Complete | `backend/igny8_core/modules/planner/views.py` | `bulk_queue_to_writer()` inherits `entity_type`, `taxonomy`, `cluster_role` from Ideas |
| **PageBlueprint → Tasks** | ✅ Complete | `backend/igny8_core/business/site_building/services/page_generation_service.py` | `_create_task_from_page()` sets metadata from blueprint |
| **Tasks → Content** | ✅ Complete | `backend/igny8_core/business/content/services/metadata_mapping_service.py` | `MetadataMappingService` persists cluster/taxonomy/attribute mappings |
| **AI Prompts** | ⚠️ **Pending** | `backend/igny8_core/ai/prompts.py` | Prompts need to include cluster role, taxonomy context, product attributes |
| **AI Prompts** | **Complete** | `backend/igny8_core/ai/prompts.py` | Updated content generation prompt to include cluster role, taxonomy context, product attributes |
### 3. Validation Services