Added image generation to blog writer
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
* - Integration with backend metadata generation
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
CircularProgress,
|
||||
Alert,
|
||||
IconButton,
|
||||
Chip
|
||||
Chip,
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
@@ -42,6 +43,7 @@ import { CoreMetadataTab } from './SEO/MetadataDisplay/CoreMetadataTab';
|
||||
import { SocialMediaTab } from './SEO/MetadataDisplay/SocialMediaTab';
|
||||
import { StructuredDataTab } from './SEO/MetadataDisplay/StructuredDataTab';
|
||||
import { PreviewCard } from './SEO/MetadataDisplay/PreviewCard';
|
||||
import { subscribeImage } from '../../utils/imageBus';
|
||||
|
||||
interface SEOMetadataModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -49,6 +51,8 @@ interface SEOMetadataModalProps {
|
||||
blogContent: string;
|
||||
blogTitle: string;
|
||||
researchData: any;
|
||||
outline?: any[]; // Add outline structure
|
||||
seoAnalysis?: any; // Add SEO analysis results
|
||||
onMetadataGenerated: (metadata: any) => void;
|
||||
}
|
||||
|
||||
@@ -71,20 +75,55 @@ interface SEOMetadataResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Cache helper functions (similar to SEOAnalysisModal)
|
||||
async function hashContent(text: string): Promise<string> {
|
||||
try {
|
||||
const enc = new TextEncoder().encode(text);
|
||||
const digest = await crypto.subtle.digest('SHA-256', enc);
|
||||
const bytes = Array.from(new Uint8Array(digest));
|
||||
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
} catch {
|
||||
// Fallback hash
|
||||
let h = 0;
|
||||
for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) | 0;
|
||||
return String(h);
|
||||
}
|
||||
}
|
||||
|
||||
function getMetadataCacheKey(contentHash: string, title?: string): string {
|
||||
return `seo_metadata_cache:${contentHash}:${title || ''}`;
|
||||
}
|
||||
|
||||
export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
blogContent,
|
||||
blogTitle,
|
||||
researchData,
|
||||
outline,
|
||||
seoAnalysis,
|
||||
onMetadataGenerated
|
||||
}) => {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [metadataResult, setMetadataResult] = useState<SEOMetadataResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tabValue, setTabValue] = useState('core');
|
||||
const [tabValue, setTabValue] = useState('preview'); // Start with preview tab first
|
||||
const [previewTabValue, setPreviewTabValue] = useState('google'); // Sub-tab for preview platforms
|
||||
const [copiedItems, setCopiedItems] = useState<Set<string>>(new Set());
|
||||
const [editableMetadata, setEditableMetadata] = useState<SEOMetadataResult | null>(null);
|
||||
const [contentHash, setContentHash] = useState<string>('');
|
||||
// Subscribe to image generation bus to auto-fill OG/Twitter image fields
|
||||
useEffect(() => {
|
||||
const unsub = subscribeImage(({ base64 }: { base64: string }) => {
|
||||
setEditableMetadata(prev => {
|
||||
const next = { ...(prev || metadataResult || {}) } as any;
|
||||
next.open_graph = { ...(next.open_graph || {}), image: `data:image/png;base64,${base64}` };
|
||||
next.twitter_card = { ...(next.twitter_card || {}), image: `data:image/png;base64,${base64}` };
|
||||
return next;
|
||||
});
|
||||
});
|
||||
return unsub;
|
||||
}, [metadataResult]);
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
@@ -96,19 +135,67 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
});
|
||||
}, [isOpen, blogContent, blogTitle, researchData]);
|
||||
|
||||
const generateMetadata = async () => {
|
||||
// Reset state when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
// Reset state when modal closes (but keep result for next time)
|
||||
setError(null);
|
||||
setIsGenerating(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Auto-generate metadata when modal opens (only once)
|
||||
const hasAutoGeneratedRef = React.useRef(false);
|
||||
useEffect(() => {
|
||||
if (isOpen && blogContent && !hasAutoGeneratedRef.current) {
|
||||
hasAutoGeneratedRef.current = true;
|
||||
generateMetadata(false); // Auto-generate from cache or API
|
||||
}
|
||||
if (!isOpen) {
|
||||
hasAutoGeneratedRef.current = false; // Reset when modal closes
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]); // Only trigger when modal opens
|
||||
|
||||
const generateMetadata = useCallback(async (forceRefresh = false) => {
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
setError(null);
|
||||
setMetadataResult(null);
|
||||
if (forceRefresh) {
|
||||
setMetadataResult(null);
|
||||
}
|
||||
|
||||
console.log('🚀 Starting SEO metadata generation...');
|
||||
console.log('🚀 Starting SEO metadata generation...', { forceRefresh });
|
||||
|
||||
// Calculate content hash for caching
|
||||
const hash = await hashContent(`${blogTitle || ''}\n${blogContent}`);
|
||||
setContentHash(hash);
|
||||
const cacheKey = getMetadataCacheKey(hash, blogTitle);
|
||||
|
||||
// Check cache first (unless force refresh)
|
||||
if (!forceRefresh && typeof window !== 'undefined') {
|
||||
const cached = window.localStorage.getItem(cacheKey);
|
||||
if (cached) {
|
||||
try {
|
||||
const parsed = JSON.parse(cached) as SEOMetadataResult;
|
||||
console.log('✅ Using cached SEO metadata');
|
||||
setMetadataResult(parsed);
|
||||
setEditableMetadata(parsed);
|
||||
setIsGenerating(false);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse cached metadata:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make API call to generate metadata
|
||||
const response = await apiClient.post('/api/blog/seo/metadata', {
|
||||
content: blogContent,
|
||||
title: blogTitle,
|
||||
research_data: researchData
|
||||
research_data: researchData,
|
||||
outline: outline || null,
|
||||
seo_analysis: seoAnalysis || null
|
||||
});
|
||||
|
||||
const result = response.data;
|
||||
@@ -118,6 +205,16 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
throw new Error(result.error || 'Metadata generation failed');
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.localStorage.setItem(cacheKey, JSON.stringify(result));
|
||||
console.log('💾 SEO metadata cached');
|
||||
} catch (e) {
|
||||
console.warn('Failed to cache metadata:', e);
|
||||
}
|
||||
}
|
||||
|
||||
setMetadataResult(result);
|
||||
setEditableMetadata(result);
|
||||
console.log('📊 Metadata result set:', result);
|
||||
@@ -128,7 +225,7 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
}, [blogContent, blogTitle, researchData, outline, seoAnalysis]);
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||
setTabValue(newValue);
|
||||
@@ -159,6 +256,23 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Apply Metadata button click
|
||||
*
|
||||
* This saves the generated/edited metadata to the parent component's state.
|
||||
* The metadata is then used when publishing to platforms:
|
||||
* - WordPress: Requires SEO metadata for proper post creation with SEO fields
|
||||
* - Wix: Currently doesn't require metadata, but could be added in future
|
||||
*
|
||||
* The metadata includes:
|
||||
* - SEO title, meta description, URL slug
|
||||
* - Blog tags, categories, focus keyword
|
||||
* - Open Graph tags (Facebook/LinkedIn)
|
||||
* - Twitter Card tags
|
||||
* - JSON-LD structured data (Schema.org Article)
|
||||
*
|
||||
* All of these will be passed to the platform's API when publishing.
|
||||
*/
|
||||
const handleApplyMetadata = () => {
|
||||
if (editableMetadata) {
|
||||
onMetadataGenerated(editableMetadata);
|
||||
@@ -222,32 +336,26 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<IconButton onClick={onClose} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{metadataResult && (
|
||||
<Tooltip title="Regenerate SEO metadata">
|
||||
<IconButton
|
||||
onClick={() => generateMetadata(true)}
|
||||
size="small"
|
||||
disabled={isGenerating}
|
||||
color="primary"
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton onClick={onClose} size="small">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
{!metadataResult && !isGenerating && (
|
||||
<Box sx={{ p: 4, textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||
Generate Comprehensive SEO Metadata
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 3, color: 'text.secondary' }}>
|
||||
Create optimized titles, descriptions, Open Graph tags, Twitter cards, and structured data for your blog post.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={generateMetadata}
|
||||
startIcon={<RefreshIcon />}
|
||||
sx={{ px: 4 }}
|
||||
>
|
||||
Generate SEO Metadata
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isGenerating && (
|
||||
<Box sx={{ p: 4, textAlign: 'center' }}>
|
||||
<CircularProgress size={60} sx={{ mb: 2 }} />
|
||||
@@ -267,7 +375,7 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
</Alert>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={generateMetadata}
|
||||
onClick={() => generateMetadata(true)}
|
||||
startIcon={<RefreshIcon />}
|
||||
>
|
||||
Try Again
|
||||
@@ -286,7 +394,7 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
scrollButtons="auto"
|
||||
sx={{ minHeight: 48 }}
|
||||
>
|
||||
{['core', 'social', 'structured', 'preview'].map((tab) => (
|
||||
{['preview', 'core', 'social', 'structured'].map((tab) => (
|
||||
<Tab
|
||||
key={tab}
|
||||
value={tab}
|
||||
@@ -332,6 +440,8 @@ export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||||
<PreviewCard
|
||||
metadata={editableMetadata || metadataResult}
|
||||
blogTitle={blogTitle}
|
||||
previewTabValue={previewTabValue}
|
||||
onPreviewTabChange={setPreviewTabValue}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user