Implement Stage 3: Enhance content metadata and validation features

- Added entity metadata fields to the Tasks model, including entity_type, taxonomy, and cluster_role.
- Updated CandidateEngine to prioritize content relevance based on cluster mappings.
- Introduced metadata completeness scoring in ContentAnalyzer.
- Enhanced validation services to check for entity type and mapping completeness.
- Updated frontend components to display and validate new metadata fields.
- Implemented API endpoints for content validation and metadata persistence.
- Migrated existing data to populate new metadata fields for Tasks and Content.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-19 19:21:30 +00:00
parent 38f6026e73
commit bae9ea47d8
33 changed files with 2388 additions and 73 deletions

View File

@@ -5,7 +5,7 @@
*/
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { SaveIcon, XIcon, EyeIcon, FileTextIcon, SettingsIcon, TagIcon } from 'lucide-react';
import { SaveIcon, XIcon, EyeIcon, FileTextIcon, SettingsIcon, TagIcon, CheckCircleIcon, XCircleIcon, AlertCircleIcon } from 'lucide-react';
import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
@@ -13,7 +13,7 @@ import Label from '../../components/form/Label';
import TextArea from '../../components/form/input/TextArea';
import SelectDropdown from '../../components/form/SelectDropdown';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { fetchAPI, fetchContentValidation, validateContent, ContentValidationResult } from '../../services/api';
interface Content {
id?: number;
@@ -40,7 +40,9 @@ export default function PostEditor() {
const toast = useToast();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [activeTab, setActiveTab] = useState<'content' | 'seo' | 'metadata'>('content');
const [activeTab, setActiveTab] = useState<'content' | 'seo' | 'metadata' | 'validation'>('content');
const [validationResult, setValidationResult] = useState<ContentValidationResult | null>(null);
const [validating, setValidating] = useState(false);
const [content, setContent] = useState<Content>({
title: '',
html_content: '',
@@ -64,12 +66,44 @@ export default function PostEditor() {
loadSite();
if (postId && postId !== 'new') {
loadPost();
loadValidation();
} else {
setLoading(false);
}
}
}, [siteId, postId]);
const loadValidation = async () => {
if (!postId || postId === 'new') return;
try {
const result = await fetchContentValidation(Number(postId));
setValidationResult(result);
} catch (error: any) {
console.error('Failed to load validation:', error);
}
};
const handleValidate = async () => {
if (!content.id) {
toast.error('Please save the content first before validating');
return;
}
try {
setValidating(true);
const result = await validateContent(content.id);
await loadValidation();
if (result.is_valid) {
toast.success('Content validation passed!');
} else {
toast.warning(`Validation found ${result.errors.length} issue(s)`);
}
} catch (error: any) {
toast.error(`Validation failed: ${error.message}`);
} finally {
setValidating(false);
}
};
const loadSite = async () => {
try {
const site = await fetchAPI(`/v1/auth/sites/${siteId}/`);
@@ -302,6 +336,28 @@ export default function PostEditor() {
<TagIcon className="w-4 h-4 inline mr-2" />
Metadata
</button>
{content.id && (
<button
type="button"
onClick={() => {
setActiveTab('validation');
loadValidation();
}}
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
activeTab === 'validation'
? 'border-brand-500 text-brand-600 dark:text-brand-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<CheckCircleIcon className="w-4 h-4 inline mr-2" />
Validation
{validationResult && !validationResult.is_valid && (
<span className="ml-2 px-2 py-0.5 text-xs bg-red-100 dark:bg-red-900 text-red-600 dark:text-red-400 rounded-full">
{validationResult.validation_errors.length}
</span>
)}
</button>
)}
</div>
</div>
@@ -514,6 +570,152 @@ export default function PostEditor() {
</div>
</Card>
)}
{/* Validation Tab - Stage 3 */}
{activeTab === 'validation' && content.id && (
<Card className="p-6">
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Content Validation
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Check if your content meets all requirements before publishing
</p>
</div>
<Button
variant="primary"
onClick={handleValidate}
disabled={validating}
>
{validating ? 'Validating...' : 'Run Validation'}
</Button>
</div>
{validationResult ? (
<div className="space-y-4">
{/* Validation Status */}
<div className={`p-4 rounded-lg ${
validationResult.is_valid
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
}`}>
<div className="flex items-center gap-2">
{validationResult.is_valid ? (
<CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
) : (
<XCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400" />
)}
<span className={`font-medium ${
validationResult.is_valid
? 'text-green-800 dark:text-green-300'
: 'text-red-800 dark:text-red-300'
}`}>
{validationResult.is_valid
? 'Content is valid and ready to publish'
: `Content has ${validationResult.validation_errors.length} validation error(s)`}
</span>
</div>
</div>
{/* Metadata Summary */}
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
Metadata Summary
</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600 dark:text-gray-400">Entity Type:</span>
<span className="ml-2 font-medium text-gray-900 dark:text-white">
{validationResult.metadata.entity_type || 'Not set'}
</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Cluster Mapping:</span>
<span className={`ml-2 font-medium ${
validationResult.metadata.has_cluster_mapping
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}>
{validationResult.metadata.has_cluster_mapping ? 'Yes' : 'No'}
</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Taxonomy Mapping:</span>
<span className={`ml-2 font-medium ${
validationResult.metadata.has_taxonomy_mapping
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`}>
{validationResult.metadata.has_taxonomy_mapping ? 'Yes' : 'No'}
</span>
</div>
</div>
</div>
{/* Validation Errors */}
{validationResult.validation_errors.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
Validation Errors
</h4>
<div className="space-y-2">
{validationResult.validation_errors.map((error, index) => (
<div
key={index}
className="flex items-start gap-2 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800"
>
<AlertCircleIcon className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-red-800 dark:text-red-300">
{error.field || error.code}
</div>
<div className="text-sm text-red-600 dark:text-red-400 mt-1">
{error.message}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Publish Errors */}
{validationResult.publish_errors && validationResult.publish_errors.length > 0 && (
<div>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
Publish Blockers
</h4>
<div className="space-y-2">
{validationResult.publish_errors.map((error, index) => (
<div
key={index}
className="flex items-start gap-2 p-3 bg-orange-50 dark:bg-orange-900/20 rounded-lg border border-orange-200 dark:border-orange-800"
>
<XCircleIcon className="w-5 h-5 text-orange-600 dark:text-orange-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<div className="text-sm font-medium text-orange-800 dark:text-orange-300">
{error.field || error.code}
</div>
<div className="text-sm text-orange-600 dark:text-orange-400 mt-1">
{error.message}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<p>Click "Run Validation" to check your content</p>
</div>
)}
</div>
</Card>
)}
</div>
</div>
);