From 746a51715f9d486c9844b173743369fb019330ca Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Wed, 19 Nov 2025 20:07:05 +0000 Subject: [PATCH] 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. --- backend/celerybeat-schedule | Bin 16384 -> 16384 bytes .../ai/functions/generate_content.py | 50 +++- backend/igny8_core/ai/prompts.py | 23 +- .../src/components/linker/LinkResults.tsx | 78 +++++- .../components/sites/SiteProgressWidget.tsx | 262 ++++++++++++++++++ frontend/src/config/pages/ideas.config.tsx | 67 +++++ frontend/src/config/pages/tasks.config.tsx | 87 +++++- .../src/pages/Optimizer/AnalysisPreview.tsx | 138 +++++++++ frontend/src/pages/Planner/Ideas.tsx | 12 +- frontend/src/pages/Sites/Dashboard.tsx | 24 +- frontend/src/pages/Sites/PostEditor.tsx | 113 +++++++- frontend/src/pages/Writer/Tasks.tsx | 9 +- frontend/src/services/api.ts | 50 ++++ .../refactor-stage-3-completion-status.md | 2 +- 14 files changed, 892 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/sites/SiteProgressWidget.tsx diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 05c10a6cd556f91db9fec8d7888417c375730c17..765162fdbc9a47c9a2dbf11c3e007e69dbf874b3 100644 GIT binary patch delta 35 pcmZo@U~Fh$++b=ZAi@9yMN=|F+ol9f@y%nLY+$0iIm2uQCjhrh3XK2& delta 35 rcmZo@U~Fh$++b=ZAk5Fezz{SgL$qy5&=lWP#>oaI%9}IHW^e)kwYUm$ diff --git a/backend/igny8_core/ai/functions/generate_content.py b/backend/igny8_core/ai/functions/generate_content.py index ce0eda0f..a4cef40f 100644 --- a/backend/igny8_core/ai/functions/generate_content.py +++ b/backend/igny8_core/ai/functions/generate_content.py @@ -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, } ) diff --git a/backend/igny8_core/ai/prompts.py b/backend/igny8_core/ai/prompts.py index 6037d5ef..52e647f4 100644 --- a/backend/igny8_core/ai/prompts.py +++ b/backend/igny8_core/ai/prompts.py @@ -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 diff --git a/frontend/src/components/linker/LinkResults.tsx b/frontend/src/components/linker/LinkResults.tsx index 9f18c231..acf6b30c 100644 --- a/frontend/src/components/linker/LinkResults.tsx +++ b/frontend/src/components/linker/LinkResults.tsx @@ -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,17 +42,70 @@ export const LinkResults: React.FC = ({

Added Links:

-
    - {links.map((link, index) => ( -
  • - "{link.anchor_text}" - - - Content #{link.target_content_id} - -
  • - ))} -
+ {/* Stage 3: Group links by cluster match */} + {links.some(l => l.cluster_match) && ( +
+
+ Cluster Matches (High Priority) +
+
    + {links.filter(l => l.cluster_match).map((link, index) => ( +
  • + "{link.anchor_text}" + + + Content #{link.target_content_id} + + {link.relevance_score && ( + + (Score: {link.relevance_score}) + + )} +
  • + ))} +
+
+ )} + + {/* Other links */} + {links.filter(l => !l.cluster_match || l.cluster_match === false).length > 0 && ( +
+
+ Other Links +
+
    + {links.filter(l => !l.cluster_match || l.cluster_match === false).map((link, index) => ( +
  • + "{link.anchor_text}" + + + Content #{link.target_content_id} + + {link.relevance_score && ( + + (Score: {link.relevance_score}) + + )} +
  • + ))} +
+
+ )} + + {/* Fallback if no grouping */} + {!links.some(l => l.cluster_match !== undefined) && ( +
    + {links.map((link, index) => ( +
  • + "{link.anchor_text}" + + + Content #{link.target_content_id} + +
  • + ))} +
+ )}
) : ( diff --git a/frontend/src/components/sites/SiteProgressWidget.tsx b/frontend/src/components/sites/SiteProgressWidget.tsx new file mode 100644 index 00000000..d56cd47d --- /dev/null +++ b/frontend/src/components/sites/SiteProgressWidget.tsx @@ -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(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 ( + +
+ Loading progress... +
+
+ ); + } + + 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 ( + +
+

+ Site Progress: {progress.blueprint_name} +

+ + {progress.overall_status ? progress.overall_status.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()) : 'Unknown'} + +
+ + {/* Overall Stats */} +
+
+
+ {progress.cluster_coverage.covered_clusters}/{progress.cluster_coverage.total_clusters} +
+
Clusters Covered
+
+
+
+ {progress.taxonomy_coverage.defined_taxonomies}/{progress.taxonomy_coverage.total_taxonomies} +
+
Taxonomies Defined
+
+
+
+ {progress.validation_flags.all_pages_generated ? '✓' : '✗'} +
+
All Pages Generated
+
+
+ + {/* Cluster Progress */} +
+

+ Cluster Coverage +

+ {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 ( +
+
+
+
+
+ {cluster.cluster_name} +
+ + {cluster.role} + + {cluster.is_complete ? ( + + ) : ( + + )} +
+
+ {cluster.content_count} content / {totalPages} pages +
+
+ +
+ + {/* Progress Bar */} +
+
+ Completion + {completionPercent.toFixed(0)}% +
+
+
+
+
+ + {/* Page Type Breakdown */} +
+
+
{cluster.hub_pages}
+
Hub
+
+
+
{cluster.supporting_pages}
+
Supporting
+
+
+
{cluster.attribute_pages}
+
Attribute
+
+
+ + {/* Validation Messages */} + {cluster.validation_messages && cluster.validation_messages.length > 0 && ( +
+
+ Issues: +
+
    + {cluster.validation_messages.map((msg, idx) => ( +
  • + + {msg} +
  • + ))} +
+
+ )} +
+ ); + })} +
+ + {/* Validation Flags Summary */} +
+

+ Validation Status +

+
+
+ {progress.validation_flags.clusters_attached ? ( + + ) : ( + + )} + Clusters Attached +
+
+ {progress.validation_flags.taxonomies_defined ? ( + + ) : ( + + )} + Taxonomies Defined +
+
+ {progress.validation_flags.sitemap_generated ? ( + + ) : ( + + )} + Sitemap Generated +
+
+ {progress.validation_flags.all_pages_generated ? ( + + ) : ( + + )} + All Pages Generated +
+
+
+ + {/* Deep Link to Blueprint */} +
+ +
+ + ); +} + diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index 958d62bd..0c6eac65 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -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 -; + } + const typeLabels: Record = { + 'blog_post': 'Blog Post', + 'article': 'Article', + 'product': 'Product', + 'service': 'Service', + 'taxonomy': 'Taxonomy', + 'page': 'Page', + }; + return ( + + {typeLabels[entityType] || entityType} + + ); + }, + }, + { + 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 -; + } + const roleColors: Record = { + 'hub': 'primary', + 'supporting': 'success', + 'attribute': 'warning', + }; + return ( + + {role.charAt(0).toUpperCase() + role.slice(1)} + + ); + }, + }, { ...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', diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index 61011647..1bba0784 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -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 (
@@ -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 -; + } + const typeLabels: Record = { + 'blog_post': 'Blog Post', + 'article': 'Article', + 'product': 'Product', + 'service': 'Service', + 'taxonomy': 'Taxonomy', + 'page': 'Page', + }; + return ( + + {typeLabels[entityType] || entityType} + + ); + }, + }, + { + 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 -; + } + const roleColors: Record = { + 'hub': 'primary', + 'supporting': 'success', + 'attribute': 'warning', + }; + return ( + + {role.charAt(0).toUpperCase() + role.slice(1)} + + ); + }, + }, + { + key: 'taxonomy_name', + label: 'Taxonomy', + sortable: false, + width: '150px', + defaultVisible: false, + render: (_value: string, row: Task) => { + const taxonomyName = row.taxonomy_name; + if (!taxonomyName) { + return -; + } + return ( + + {taxonomyName} + + ); + }, + }, { 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 }>) => [ { diff --git a/frontend/src/pages/Optimizer/AnalysisPreview.tsx b/frontend/src/pages/Optimizer/AnalysisPreview.tsx index 22b063aa..ebbdb795 100644 --- a/frontend/src/pages/Optimizer/AnalysisPreview.tsx +++ b/frontend/src/pages/Optimizer/AnalysisPreview.tsx @@ -108,6 +108,74 @@ export default function AnalysisPreview() { {/* Scores */} + {/* Stage 3: Cluster Dimension Scorecards */} + {(scores.has_cluster_mapping || scores.has_taxonomy_mapping || scores.has_attributes) && ( +
+

Metadata Coverage

+
+
+
+ Cluster Mapping + {scores.has_cluster_mapping ? ( + + ) : ( + + )} +
+
+ {scores.has_cluster_mapping + ? 'Content is mapped to cluster' + : 'Missing cluster mapping'} +
+
+ +
+
+ Taxonomy Mapping + {scores.has_taxonomy_mapping ? ( + + ) : ( + + )} +
+
+ {scores.has_taxonomy_mapping + ? 'Content is mapped to taxonomy' + : 'Taxonomy mapping recommended'} +
+
+ +
+
+ Attributes + {scores.has_attributes ? ( + + ) : ( + + )} +
+
+ {scores.has_attributes + ? 'Content has attributes' + : 'Attributes recommended for products/services'} +
+
+
+
+ )} + {/* Score Details */}

Score Details

@@ -140,8 +208,78 @@ export default function AnalysisPreview() { {scores.internal_links_count || 0}
+ {/* Stage 3: Metadata completeness score */} + {scores.metadata_completeness_score !== undefined && ( +
+ Metadata Completeness: + + {scores.metadata_completeness_score.toFixed(1)}% + +
+ )}
+ + {/* 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)) && ( +
+

Recommended Actions

+
+ {!scores.has_cluster_mapping && ( +
+
+ 1. +
+
Map Content to Cluster
+
+ Assign this content to a keyword cluster to improve internal linking and SEO structure. +
+
+
+
+ )} + {!scores.has_taxonomy_mapping && ( +
+
+ 2. +
+
Add Taxonomy Mapping
+
+ Categorize this content with a taxonomy for better organization and navigation. +
+
+
+
+ )} + {!scores.has_attributes && (content?.entity_type === 'product' || content?.entity_type === 'service') && ( +
+
+ 3. +
+
Add Product/Service Attributes
+
+ Add attributes like specifications, features, or modifiers to enhance content completeness. +
+
+
+
+ )} + {scores.metadata_completeness_score !== undefined && scores.metadata_completeness_score < 80 && ( +
+
+ 4. +
+
Improve Metadata Completeness
+
+ Current score: {scores.metadata_completeness_score.toFixed(1)}%. Complete missing metadata fields to reach 80%+. +
+
+
+
+ )} +
+
+ )} ) : (
diff --git a/frontend/src/pages/Planner/Ideas.tsx b/frontend/src/pages/Planner/Ideas.tsx index ee81ec15..088c0c33 100644 --- a/frontend/src/pages/Planner/Ideas.tsx +++ b/frontend/src/pages/Planner/Ideas.tsx @@ -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([]); // 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); }} diff --git a/frontend/src/pages/Sites/Dashboard.tsx b/frontend/src/pages/Sites/Dashboard.tsx index feb73f83..5c0d14f8 100644 --- a/frontend/src/pages/Sites/Dashboard.tsx +++ b/frontend/src/pages/Sites/Dashboard.tsx @@ -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(null); const [stats, setStats] = useState(null); + const [blueprints, setBlueprints] = useState([]); 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() {
+ {/* Stage 3: Site Progress Widget */} + {blueprints.length > 0 && ( +
+ {blueprints.map((blueprint) => ( + + ))} +
+ )} + {/* Stats Grid */}
{statCards.map((stat, index) => ( diff --git a/frontend/src/pages/Sites/PostEditor.tsx b/frontend/src/pages/Sites/PostEditor.tsx index b252cbc7..d51bfaca 100644 --- a/frontend/src/pages/Sites/PostEditor.tsx +++ b/frontend/src/pages/Sites/PostEditor.tsx @@ -32,6 +32,13 @@ interface Content { sector: number; word_count?: number; metadata?: Record; + // 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() {
+
+ {/* Main Content Area */} +
+

@@ -289,10 +300,11 @@ export default function PostEditor() {

@@ -717,6 +729,103 @@ export default function PostEditor() { )}
+
+ + {/* Stage 3: Sidebar with Metadata Summary */} + {content.id && ( +
+ +

+ Content Metadata +

+ +
+ {/* Entity Type */} + {content.entity_type && ( +
+
+ Entity Type +
+
+ {content.entity_type ? content.entity_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()) : '-'} +
+
+ )} + + {/* Cluster */} + {content.cluster_name && ( +
+
+ Cluster +
+
+ {content.cluster_name} + {content.cluster_role && ( + + ({content.cluster_role}) + + )} +
+
+ )} + + {/* Taxonomy */} + {content.taxonomy_name && ( +
+
+ Taxonomy +
+
+ {content.taxonomy_name} +
+
+ )} + + {/* Validation Status */} + {validationResult && ( +
+
+ Validation Status +
+
+ {validationResult.is_valid ? '✓ Valid' : `✗ ${validationResult.validation_errors.length} error(s)`} +
+
+ )} + + {/* Quick Links */} +
+
+ Quick Actions +
+
+ {content.cluster_id && ( + + )} + {content.taxonomy_id && ( + + )} +
+
+
+
+
+ )} +
); } diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index eb877fdc..f68753c5 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -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([]); // 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); }} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 11a791d0..8f8d0570 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -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 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 { + return fetchAPI(`/v1/site-builder/blueprints/${blueprintId}/progress/`); +} + export async function createSiteBlueprint(data: Partial): Promise { return fetchAPI('/v1/site-builder/blueprints/', { method: 'POST', diff --git a/refactor-plan/refactor-stage-3-completion-status.md b/refactor-plan/refactor-stage-3-completion-status.md index cdd2acf7..46c17a40 100644 --- a/refactor-plan/refactor-stage-3-completion-status.md +++ b/refactor-plan/refactor-stage-3-completion-status.md @@ -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