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:
@@ -121,6 +121,9 @@ export default function LinkerContentList() {
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Source
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Cluster
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Links
|
||||
</th>
|
||||
@@ -147,6 +150,20 @@ export default function LinkerContentList() {
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<SourceBadge source={(item.source as ContentSource) || 'igny8'} />
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||
{item.cluster_name ? (
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
{item.cluster_name}
|
||||
{item.cluster_role && (
|
||||
<span className="ml-1 text-blue-600 dark:text-blue-400">
|
||||
({item.cluster_role})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
{item.internal_links?.length || 0}
|
||||
</td>
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
} from "../../services/api";
|
||||
import { useSiteStore } from "../../store/siteStore";
|
||||
import { useSectorStore } from "../../store/sectorStore";
|
||||
import { Link } from "react-router";
|
||||
import Alert from "../../components/ui/alert/Alert";
|
||||
|
||||
interface DashboardStats {
|
||||
|
||||
@@ -1,21 +1,61 @@
|
||||
/**
|
||||
* Step 1: Business Details
|
||||
* Site type selection, hosting detection, brand inputs
|
||||
*
|
||||
* Supports both:
|
||||
* - Stage 1 Wizard: data, onChange, metadata, selectedSectors
|
||||
* - Stage 2 Workflow: blueprintId
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useBuilderWorkflowStore } from '../../../../store/builderWorkflowStore';
|
||||
import { fetchSiteBlueprintById, updateSiteBlueprint, SiteBlueprint } from '../../../../services/api';
|
||||
import { Card, CardDescription, CardTitle } from '../../../../components/ui/card';
|
||||
import ButtonWithTooltip from '../../../../components/ui/button/ButtonWithTooltip';
|
||||
import Input from '../../../../components/ui/input/Input';
|
||||
import Input from '../../../../components/form/input/InputField';
|
||||
import Alert from '../../../../components/ui/alert/Alert';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import type { BuilderFormData, SiteBuilderMetadata } from '../../../../types/siteBuilder';
|
||||
|
||||
interface BusinessDetailsStepProps {
|
||||
blueprintId: number;
|
||||
// Stage 1 Wizard props
|
||||
interface Stage1Props {
|
||||
data: BuilderFormData;
|
||||
onChange: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
|
||||
metadata?: SiteBuilderMetadata;
|
||||
selectedSectors?: Array<{ id: number; name: string }>;
|
||||
blueprintId?: never;
|
||||
}
|
||||
|
||||
export default function BusinessDetailsStep({ blueprintId }: BusinessDetailsStepProps) {
|
||||
// Stage 2 Workflow props
|
||||
interface Stage2Props {
|
||||
blueprintId: number;
|
||||
data?: never;
|
||||
onChange?: never;
|
||||
metadata?: never;
|
||||
selectedSectors?: never;
|
||||
}
|
||||
|
||||
type BusinessDetailsStepProps = Stage1Props | Stage2Props;
|
||||
|
||||
export function BusinessDetailsStep(props: BusinessDetailsStepProps) {
|
||||
// Check if this is Stage 2 (has blueprintId)
|
||||
const isStage2 = 'blueprintId' in props && props.blueprintId !== undefined;
|
||||
|
||||
// Stage 2 implementation
|
||||
if (isStage2) {
|
||||
return <BusinessDetailsStepStage2 blueprintId={props.blueprintId} />;
|
||||
}
|
||||
|
||||
// Stage 1 implementation
|
||||
return <BusinessDetailsStepStage1
|
||||
data={props.data}
|
||||
onChange={props.onChange}
|
||||
metadata={props.metadata}
|
||||
selectedSectors={props.selectedSectors}
|
||||
/>;
|
||||
}
|
||||
|
||||
// Stage 2 Workflow Component
|
||||
function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
|
||||
const { context, completeStep, loading } = useBuilderWorkflowStore();
|
||||
const [blueprint, setBlueprint] = useState<SiteBlueprint | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -154,3 +194,88 @@ export default function BusinessDetailsStep({ blueprintId }: BusinessDetailsStep
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Stage 1 Wizard Component
|
||||
function BusinessDetailsStepStage1({
|
||||
data,
|
||||
onChange,
|
||||
metadata,
|
||||
selectedSectors
|
||||
}: {
|
||||
data: BuilderFormData;
|
||||
onChange: <K extends keyof BuilderFormData>(key: K, value: BuilderFormData[K]) => void;
|
||||
metadata?: SiteBuilderMetadata;
|
||||
selectedSectors?: Array<{ id: number; name: string }>;
|
||||
}) {
|
||||
return (
|
||||
<Card variant="surface" padding="lg">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
|
||||
Business context
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Business details
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
These details help the AI understand what kind of site we are building.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Site name</label>
|
||||
<Input
|
||||
value={data.siteName}
|
||||
onChange={(e) => onChange('siteName', e.target.value)}
|
||||
placeholder="Acme Robotics"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Business type</label>
|
||||
<Input
|
||||
value={data.businessType}
|
||||
onChange={(e) => onChange('businessType', e.target.value)}
|
||||
placeholder="B2B SaaS platform"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Industry</label>
|
||||
<Input
|
||||
value={data.industry}
|
||||
onChange={(e) => onChange('industry', e.target.value)}
|
||||
placeholder="Supply chain automation"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Target audience</label>
|
||||
<Input
|
||||
value={data.targetAudience}
|
||||
onChange={(e) => onChange('targetAudience', e.target.value)}
|
||||
placeholder="Operations leaders at fast-scaling eCommerce brands"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Hosting preference</label>
|
||||
<select
|
||||
value={data.hostingType}
|
||||
onChange={(e) => onChange('hostingType', e.target.value as BuilderFormData['hostingType'])}
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
>
|
||||
<option value="igny8_sites">IGNY8 Sites</option>
|
||||
<option value="wordpress">WordPress</option>
|
||||
<option value="shopify">Shopify</option>
|
||||
<option value="multi">Multiple destinations</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Also export as default for WorkflowWizard compatibility
|
||||
export default BusinessDetailsStep;
|
||||
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
} from '../../../../services/api';
|
||||
import { Card, CardDescription, CardTitle } from '../../../../components/ui/card';
|
||||
import ButtonWithTooltip from '../../../../components/ui/button/ButtonWithTooltip';
|
||||
import Button from '../../../../components/ui/button/Button';
|
||||
import Alert from '../../../../components/ui/alert/Alert';
|
||||
import Input from '../../../../components/ui/input/Input';
|
||||
import Input from '../../../../components/form/input/InputField';
|
||||
import Checkbox from '../../../../components/form/input/Checkbox';
|
||||
import SelectDropdown from '../../../../components/form/SelectDropdown';
|
||||
import {
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
import { siteBuilderApi } from '../../../../services/siteBuilder.api';
|
||||
import { Card, CardDescription, CardTitle } from '../../../../components/ui/card';
|
||||
import ButtonWithTooltip from '../../../../components/ui/button/ButtonWithTooltip';
|
||||
import Button from '../../../../components/ui/button/Button';
|
||||
import Alert from '../../../../components/ui/alert/Alert';
|
||||
import Checkbox from '../../../../components/form/input/Checkbox';
|
||||
import Input from '../../../../components/ui/input/Input';
|
||||
import { useToast } from '../../../../hooks/useToast';
|
||||
import Input from '../../../../components/form/input/InputField';
|
||||
import { useToast } from '../../../../components/ui/toast/ToastContainer';
|
||||
|
||||
interface IdeasHandoffStepProps {
|
||||
blueprintId: number;
|
||||
|
||||
@@ -12,9 +12,10 @@ import {
|
||||
import { siteBuilderApi } from '../../../../services/siteBuilder.api';
|
||||
import { Card, CardDescription, CardTitle } from '../../../../components/ui/card';
|
||||
import ButtonWithTooltip from '../../../../components/ui/button/ButtonWithTooltip';
|
||||
import Button from '../../../../components/ui/button/Button';
|
||||
import Alert from '../../../../components/ui/alert/Alert';
|
||||
import Input from '../../../../components/ui/input/Input';
|
||||
import { useToast } from '../../../../hooks/useToast';
|
||||
import Input from '../../../../components/form/input/InputField';
|
||||
import { useToast } from '../../../../components/ui/toast/ToastContainer';
|
||||
|
||||
interface SitemapReviewStepProps {
|
||||
blueprintId: number;
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Card, CardDescription, CardTitle } from '../../../../components/ui/card
|
||||
import ButtonWithTooltip from '../../../../components/ui/button/ButtonWithTooltip';
|
||||
import Button from '../../../../components/ui/button/Button';
|
||||
import Alert from '../../../../components/ui/alert/Alert';
|
||||
import Input from '../../../../components/ui/input/Input';
|
||||
import Input from '../../../../components/form/input/InputField';
|
||||
import SelectDropdown from '../../../../components/form/SelectDropdown';
|
||||
import {
|
||||
Table,
|
||||
|
||||
@@ -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