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