stage 4-1
This commit is contained in:
@@ -23,6 +23,24 @@ export const LinkResults: React.FC<LinkResultsProps> = ({
|
||||
linksAdded,
|
||||
linkerVersion,
|
||||
}) => {
|
||||
if (!links || links.length === 0) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Linking Results</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<PlugInIcon className="w-5 h-5 text-blue-500" />
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">Version {linkerVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<PlugInIcon className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">No links found to add</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@@ -51,14 +69,14 @@ export const LinkResults: React.FC<LinkResultsProps> = ({
|
||||
<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-600 dark:text-gray-400">"{link.anchor_text || 'Untitled'}"</span>
|
||||
<span className="text-gray-400">→</span>
|
||||
<span className="text-blue-600 dark:text-blue-400">
|
||||
Content #{link.target_content_id}
|
||||
Content #{link.target_content_id || 'N/A'}
|
||||
</span>
|
||||
{link.relevance_score && (
|
||||
{link.relevance_score !== undefined && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
(Score: {link.relevance_score})
|
||||
(Score: {link.relevance_score.toFixed(1)})
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
@@ -76,14 +94,14 @@ export const LinkResults: React.FC<LinkResultsProps> = ({
|
||||
<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-600 dark:text-gray-400">"{link.anchor_text || 'Untitled'}"</span>
|
||||
<span className="text-gray-400">→</span>
|
||||
<span className="text-blue-600 dark:text-blue-400">
|
||||
Content #{link.target_content_id}
|
||||
Content #{link.target_content_id || 'N/A'}
|
||||
</span>
|
||||
{link.relevance_score && (
|
||||
{link.relevance_score !== undefined && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
(Score: {link.relevance_score})
|
||||
(Score: {link.relevance_score.toFixed(1)})
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@@ -18,6 +18,8 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
|
||||
const navigate = useNavigate();
|
||||
const [progress, setProgress] = useState<SiteProgress | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
loadProgress();
|
||||
@@ -26,27 +28,63 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
|
||||
const loadProgress = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await fetchSiteProgress(blueprintId);
|
||||
setProgress(data);
|
||||
setRetryCount(0); // Reset retry count on success
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load site progress:', error);
|
||||
setError(error.message || 'Failed to load site progress. Please try again.');
|
||||
setProgress(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
setRetryCount(prev => prev + 1);
|
||||
loadProgress();
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="text-center py-4 text-gray-500 dark:text-gray-400">
|
||||
Loading progress...
|
||||
<div className="text-center py-4">
|
||||
<div className="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mb-2"></div>
|
||||
<div className="text-gray-500 dark:text-gray-400">Loading progress...</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && !progress) {
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="text-center py-4">
|
||||
<AlertCircleIcon className="w-8 h-8 text-red-500 mx-auto mb-2" />
|
||||
<div className="text-sm text-red-600 dark:text-red-400 mb-3">{error}</div>
|
||||
<button
|
||||
onClick={handleRetry}
|
||||
disabled={retryCount >= 3}
|
||||
className="px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
aria-label="Retry loading site progress"
|
||||
>
|
||||
{retryCount >= 3 ? 'Max retries reached' : 'Retry'}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!progress) {
|
||||
return null;
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="text-center py-4 text-gray-500 dark:text-gray-400">
|
||||
<AlertCircleIcon className="w-6 h-6 mx-auto mb-2 opacity-50" />
|
||||
<p>No progress data available</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
@@ -76,16 +114,16 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
|
||||
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">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white" id="site-progress-title">
|
||||
Site Progress: {progress.blueprint_name}
|
||||
</h3>
|
||||
<Badge color={getStatusColor(progress.overall_status)} size="sm">
|
||||
<Badge color={getStatusColor(progress.overall_status)} size="sm" aria-label={`Status: ${progress.overall_status || 'Unknown'}`}>
|
||||
{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="grid grid-cols-1 sm: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}
|
||||
@@ -107,11 +145,12 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
|
||||
</div>
|
||||
|
||||
{/* Cluster Progress */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<div className="space-y-4" aria-labelledby="cluster-coverage-title">
|
||||
<h4 id="cluster-coverage-title" className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Cluster Coverage
|
||||
</h4>
|
||||
{progress.cluster_coverage.details.map((cluster) => {
|
||||
{progress.cluster_coverage.details && progress.cluster_coverage.details.length > 0 ? (
|
||||
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;
|
||||
|
||||
@@ -145,7 +184,8 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
|
||||
</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"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 transition-colors"
|
||||
aria-label={`View cluster ${cluster.cluster_name}`}
|
||||
>
|
||||
View <ArrowRightIcon className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -203,15 +243,21 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
|
||||
<AlertCircleIcon className="w-6 h-6 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No clusters found. Attach clusters to get started.</p>
|
||||
</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">
|
||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700" aria-labelledby="validation-status-title">
|
||||
<h4 id="validation-status-title" 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="grid grid-cols-1 sm: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" />
|
||||
@@ -251,11 +297,24 @@ export default function SiteProgressWidget({ blueprintId, siteId }: SiteProgress
|
||||
<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"
|
||||
className="w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
aria-label="Continue site builder workflow"
|
||||
>
|
||||
Continue Site Builder Workflow →
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error banner if data loaded but has errors */}
|
||||
{error && progress && (
|
||||
<div className="mt-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircleIcon className="w-4 h-4 text-yellow-600 dark:text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-xs text-yellow-800 dark:text-yellow-300">
|
||||
Some data may be outdated. <button onClick={handleRetry} className="underline font-medium">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -86,11 +86,23 @@ export default function AnalysisPreview() {
|
||||
|
||||
{loading || analyzing ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
<p className="mt-2 text-gray-600 dark:text-gray-400">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mb-3"></div>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{loading ? 'Loading content...' : 'Analyzing content...'}
|
||||
</p>
|
||||
</div>
|
||||
) : !content ? (
|
||||
<div className="text-center py-12">
|
||||
<BoltIcon className="w-12 h-12 text-gray-400 dark:text-gray-500 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-2">Content not found</p>
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500">Unable to load content for analysis</p>
|
||||
</div>
|
||||
) : !scores ? (
|
||||
<div className="text-center py-12">
|
||||
<BoltIcon className="w-12 h-12 text-gray-400 dark:text-gray-500 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-2">Analysis unavailable</p>
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500">Unable to analyze this content</p>
|
||||
</div>
|
||||
) : content && scores ? (
|
||||
<div className="space-y-6">
|
||||
{/* Content Info */}
|
||||
|
||||
@@ -87,6 +87,7 @@ export default function PostEditor() {
|
||||
setValidationResult(result);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load validation:', error);
|
||||
toast.error(`Failed to load validation: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -600,6 +601,7 @@ export default function PostEditor() {
|
||||
variant="primary"
|
||||
onClick={handleValidate}
|
||||
disabled={validating}
|
||||
aria-label="Run content validation"
|
||||
>
|
||||
{validating ? 'Validating...' : 'Run Validation'}
|
||||
</Button>
|
||||
@@ -632,11 +634,11 @@ export default function PostEditor() {
|
||||
</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">
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4" role="region" aria-labelledby="metadata-summary-title">
|
||||
<h4 id="metadata-summary-title" 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 className="grid grid-cols-1 sm: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">
|
||||
@@ -668,7 +670,7 @@ export default function PostEditor() {
|
||||
|
||||
{/* Validation Errors */}
|
||||
{validationResult.validation_errors.length > 0 && (
|
||||
<div>
|
||||
<div role="alert" aria-live="polite">
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Validation Errors
|
||||
</h4>
|
||||
@@ -695,7 +697,7 @@ export default function PostEditor() {
|
||||
|
||||
{/* Publish Errors */}
|
||||
{validationResult.publish_errors && validationResult.publish_errors.length > 0 && (
|
||||
<div>
|
||||
<div role="alert" aria-live="polite">
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Publish Blockers
|
||||
</h4>
|
||||
@@ -721,8 +723,10 @@ export default function PostEditor() {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||
<p>Click "Run Validation" to check your content</p>
|
||||
<div className="text-center py-8">
|
||||
<FileTextIcon className="w-12 h-12 text-gray-400 dark:text-gray-500 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-2">No validation results yet</p>
|
||||
<p className="text-sm text-gray-400 dark:text-gray-500">Click "Run Validation" to check your content</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -733,7 +737,7 @@ export default function PostEditor() {
|
||||
|
||||
{/* Stage 3: Sidebar with Metadata Summary */}
|
||||
{content.id && (
|
||||
<div className="w-80 flex-shrink-0">
|
||||
<div className="w-full lg:w-80 flex-shrink-0 mt-6 lg:mt-0">
|
||||
<Card className="p-4 sticky top-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Content Metadata
|
||||
@@ -741,45 +745,53 @@ export default function PostEditor() {
|
||||
|
||||
<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>
|
||||
<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())
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500 italic">Not set</span>
|
||||
)}
|
||||
</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>
|
||||
<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_name}
|
||||
{content.cluster_role && (
|
||||
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
({content.cluster_role})
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500 italic">Not assigned</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>
|
||||
<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 ? (
|
||||
content.taxonomy_name
|
||||
) : (
|
||||
<span className="text-gray-400 dark:text-gray-500 italic">Not assigned</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation Status */}
|
||||
{validationResult && (
|
||||
@@ -803,21 +815,27 @@ export default function PostEditor() {
|
||||
Quick Actions
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{content.cluster_id && (
|
||||
{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"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline w-full text-left transition-colors focus:outline-none focus:ring-1 focus:ring-blue-500 rounded px-1"
|
||||
aria-label="View cluster details"
|
||||
>
|
||||
View Cluster →
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 italic">No cluster assigned</span>
|
||||
)}
|
||||
{content.taxonomy_id && (
|
||||
{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"
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline w-full text-left transition-colors focus:outline-none focus:ring-1 focus:ring-blue-500 rounded px-1"
|
||||
aria-label="View taxonomy details"
|
||||
>
|
||||
View Taxonomy →
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 italic">No taxonomy assigned</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user