Subscription dashboard improvements, AI text generation limit, and other fixes.
This commit is contained in:
@@ -66,6 +66,7 @@ export const BlogWriter: React.FC = () => {
|
||||
contentConfirmed,
|
||||
flowAnalysisCompleted,
|
||||
flowAnalysisResults,
|
||||
sectionImages,
|
||||
setOutline,
|
||||
setTitleOptions,
|
||||
setSelectedTitle,
|
||||
@@ -78,6 +79,7 @@ export const BlogWriter: React.FC = () => {
|
||||
setContentConfirmed,
|
||||
setFlowAnalysisCompleted,
|
||||
setFlowAnalysisResults,
|
||||
setSectionImages,
|
||||
handleResearchComplete,
|
||||
handleOutlineComplete,
|
||||
handleOutlineError,
|
||||
@@ -670,6 +672,8 @@ export const BlogWriter: React.FC = () => {
|
||||
flowAnalysisResults={flowAnalysisResults}
|
||||
outlineGenRef={outlineGenRef}
|
||||
blogWriterApi={blogWriterApi}
|
||||
sectionImages={sectionImages}
|
||||
setSectionImages={setSectionImages}
|
||||
contentConfirmed={contentConfirmed}
|
||||
seoAnalysis={seoAnalysis}
|
||||
seoMetadata={seoMetadata}
|
||||
|
||||
@@ -31,6 +31,8 @@ interface PhaseContentProps {
|
||||
seoMetadata: any;
|
||||
onTitleSelect: any;
|
||||
onCustomTitle: any;
|
||||
sectionImages?: Record<string, string>;
|
||||
setSectionImages?: (images: Record<string, string> | ((prev: Record<string, string>) => Record<string, string>)) => void;
|
||||
}
|
||||
|
||||
export const PhaseContent: React.FC<PhaseContentProps> = ({
|
||||
@@ -58,7 +60,9 @@ export const PhaseContent: React.FC<PhaseContentProps> = ({
|
||||
seoAnalysis,
|
||||
seoMetadata,
|
||||
onTitleSelect,
|
||||
onCustomTitle
|
||||
onCustomTitle,
|
||||
sectionImages,
|
||||
setSectionImages
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
||||
@@ -100,6 +104,8 @@ export const PhaseContent: React.FC<PhaseContentProps> = ({
|
||||
optimizationResults={optimizationResults}
|
||||
researchCoverage={researchCoverage}
|
||||
onRefine={(op: any, id: any, payload: any) => blogWriterApi.refineOutline({ outline, operation: op, section_id: id, payload }).then((res: any) => setOutline(res.outline))}
|
||||
sectionImages={sectionImages}
|
||||
setSectionImages={setSectionImages}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -126,6 +132,7 @@ export const PhaseContent: React.FC<PhaseContentProps> = ({
|
||||
onSave={handleContentSave}
|
||||
continuityRefresh={continuityRefresh || undefined}
|
||||
flowAnalysisResults={flowAnalysisResults}
|
||||
sectionImages={sectionImages}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
@@ -151,6 +158,7 @@ export const PhaseContent: React.FC<PhaseContentProps> = ({
|
||||
onSave={handleContentSave}
|
||||
continuityRefresh={continuityRefresh || undefined}
|
||||
flowAnalysisResults={flowAnalysisResults}
|
||||
sectionImages={sectionImages}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import { usePlatformConnections } from '../../../components/OnboardingWizard/common/usePlatformConnections';
|
||||
|
||||
interface WixConnectModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConnectionSuccess?: () => void;
|
||||
}
|
||||
|
||||
export const WixConnectModal: React.FC<WixConnectModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConnectionSuccess
|
||||
}) => {
|
||||
const { handleConnect, isLoading } = usePlatformConnections();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
|
||||
// Handle OAuth success via postMessage (same pattern as onboarding)
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handler = (event: MessageEvent) => {
|
||||
const trusted = [window.location.origin, 'https://littery-sonny-unscrutinisingly.ngrok-free.dev'];
|
||||
if (!trusted.includes(event.origin)) return;
|
||||
if (!event.data || typeof event.data !== 'object') return;
|
||||
|
||||
if (event.data.type === 'WIX_OAUTH_SUCCESS') {
|
||||
console.log('Wix OAuth success in modal');
|
||||
setIsConnecting(false);
|
||||
setError(null);
|
||||
// Close modal and notify parent
|
||||
if (onConnectionSuccess) {
|
||||
onConnectionSuccess();
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
|
||||
if (event.data.type === 'WIX_OAUTH_ERROR') {
|
||||
console.error('Wix OAuth error in modal:', event.data.error);
|
||||
setIsConnecting(false);
|
||||
setError(event.data.error || 'Wix connection failed. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
return () => window.removeEventListener('message', handler);
|
||||
}, [isOpen, onClose, onConnectionSuccess]);
|
||||
|
||||
// Also check for URL param (fallback for same-tab redirect)
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get('wix_connected') === 'true') {
|
||||
console.log('Wix connected via URL param in modal');
|
||||
setIsConnecting(false);
|
||||
setError(null);
|
||||
if (onConnectionSuccess) {
|
||||
onConnectionSuccess();
|
||||
}
|
||||
onClose();
|
||||
// Clean URL
|
||||
const clean = window.location.pathname + window.location.hash;
|
||||
window.history.replaceState({}, document.title, clean || '/');
|
||||
}
|
||||
}, [isOpen, onClose, onConnectionSuccess]);
|
||||
|
||||
const handleConnectClick = async () => {
|
||||
try {
|
||||
setIsConnecting(true);
|
||||
setError(null);
|
||||
await handleConnect('wix');
|
||||
// OAuth will redirect, so we don't need to do anything else here
|
||||
// The postMessage handler or URL param handler will close the modal
|
||||
} catch (err: any) {
|
||||
console.error('Error connecting to Wix:', err);
|
||||
setIsConnecting(false);
|
||||
setError(err?.message || 'Failed to start Wix connection. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: 2,
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.15)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ pb: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b' }}>
|
||||
Connect Your Wix Account
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Box sx={{ py: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary" paragraph>
|
||||
Connect your Wix account to publish blog posts directly to your website.
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{isConnecting && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 2 }}>
|
||||
<CircularProgress size={20} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Opening Wix authorization page...
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 2, p: 2, bgcolor: '#f8fafc', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
<strong>What happens next:</strong>
|
||||
</Typography>
|
||||
<Typography variant="caption" component="div" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||
<ol style={{ margin: '8px 0 0 20px', padding: 0 }}>
|
||||
<li>You'll be redirected to Wix to authorize ALwrity</li>
|
||||
<li>Grant permissions for blog creation and publishing</li>
|
||||
<li>You'll be redirected back to ALwrity</li>
|
||||
<li>Your blog post will be published automatically</li>
|
||||
</ol>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={onClose} disabled={isConnecting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleConnectClick}
|
||||
disabled={isConnecting || isLoading}
|
||||
startIcon={isConnecting ? <CircularProgress size={16} /> : undefined}
|
||||
>
|
||||
{isConnecting ? 'Connecting...' : 'Connect to Wix'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default WixConnectModal;
|
||||
|
||||
@@ -12,6 +12,8 @@ interface Props {
|
||||
groundingInsights?: GroundingInsights | null;
|
||||
optimizationResults?: OptimizationResults | null;
|
||||
researchCoverage?: ResearchCoverage | null;
|
||||
sectionImages?: Record<string, string>;
|
||||
setSectionImages?: (images: Record<string, string> | ((prev: Record<string, string>) => Record<string, string>)) => void;
|
||||
}
|
||||
|
||||
const EnhancedOutlineEditor: React.FC<Props> = ({
|
||||
@@ -21,14 +23,15 @@ const EnhancedOutlineEditor: React.FC<Props> = ({
|
||||
sourceMappingStats,
|
||||
groundingInsights,
|
||||
optimizationResults,
|
||||
researchCoverage
|
||||
researchCoverage,
|
||||
sectionImages = {},
|
||||
setSectionImages
|
||||
}) => {
|
||||
const [editingSection, setEditingSection] = useState<string | null>(null);
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
||||
const [hoveredSection, setHoveredSection] = useState<string | null>(null);
|
||||
const [showAddSection, setShowAddSection] = useState(false);
|
||||
const [imageModalState, setImageModalState] = useState<{ open: boolean; sectionId?: string }>(() => ({ open: false }));
|
||||
const [sectionImages, setSectionImages] = useState<Record<string, string>>({});
|
||||
const [newSectionData, setNewSectionData] = useState({
|
||||
heading: '',
|
||||
subheadings: '',
|
||||
@@ -117,8 +120,8 @@ const EnhancedOutlineEditor: React.FC<Props> = ({
|
||||
};
|
||||
})()}
|
||||
onImageGenerated={(imageBase64, sectionId) => {
|
||||
if (sectionId) {
|
||||
setSectionImages(prev => ({ ...prev, [sectionId]: imageBase64 }));
|
||||
if (sectionId && setSectionImages) {
|
||||
setSectionImages((prev: Record<string, string>) => ({ ...prev, [sectionId]: imageBase64 }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useCopilotAction } from '@copilotkit/react-core';
|
||||
import { blogWriterApi, BlogSEOMetadataResponse } from '../../services/blogWriterApi';
|
||||
import { BlogSEOMetadataResponse } from '../../services/blogWriterApi';
|
||||
import { apiClient } from '../../api/client';
|
||||
import { wordpressAPI, WordPressSite, WordPressPublishRequest } from '../../api/wordpress';
|
||||
import { validateAndRefreshWixTokens } from '../../utils/wixTokenUtils';
|
||||
import WixConnectModal from './BlogWriterUtils/WixConnectModal';
|
||||
|
||||
interface PublisherProps {
|
||||
buildFullMarkdown: () => string;
|
||||
@@ -26,10 +29,15 @@ export const Publisher: React.FC<PublisherProps> = ({
|
||||
}) => {
|
||||
const [wixConnectionStatus, setWixConnectionStatus] = useState<WixConnectionStatus | null>(null);
|
||||
const [checkingWixStatus, setCheckingWixStatus] = useState(false);
|
||||
const [wordpressSites, setWordpressSites] = useState<WordPressSite[]>([]);
|
||||
const [checkingWordPressStatus, setCheckingWordPressStatus] = useState(false);
|
||||
const [showWixConnectModal, setShowWixConnectModal] = useState(false);
|
||||
const [pendingWixPublish, setPendingWixPublish] = useState<(() => Promise<any>) | null>(null);
|
||||
|
||||
// Check Wix connection status on component mount
|
||||
// Check platform connection statuses on component mount
|
||||
useEffect(() => {
|
||||
checkWixConnectionStatus();
|
||||
checkWordPressConnectionStatus();
|
||||
}, []);
|
||||
|
||||
const checkWixConnectionStatus = async () => {
|
||||
@@ -48,6 +56,137 @@ export const Publisher: React.FC<PublisherProps> = ({
|
||||
setCheckingWixStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkWordPressConnectionStatus = async () => {
|
||||
setCheckingWordPressStatus(true);
|
||||
try {
|
||||
const status = await wordpressAPI.getStatus();
|
||||
setWordpressSites(status.sites || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to check WordPress connection status:', error);
|
||||
setWordpressSites([]);
|
||||
} finally {
|
||||
setCheckingWordPressStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to publish to Wix
|
||||
const publishToWix = async (md: string, metadata: BlogSEOMetadataResponse | null, accessToken?: string): Promise<any> => {
|
||||
// Get access token if not provided
|
||||
if (!accessToken) {
|
||||
const tokenResult = await validateAndRefreshWixTokens();
|
||||
if (!tokenResult.accessToken) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Wix tokens not available. Please connect your Wix account.',
|
||||
action_required: 'connect_wix'
|
||||
};
|
||||
}
|
||||
accessToken = tokenResult.accessToken;
|
||||
}
|
||||
|
||||
// Extract title from SEO metadata or markdown
|
||||
const title = metadata?.seo_title || (() => {
|
||||
const titleMatch = md.match(/^#\s+(.+)$/m);
|
||||
return titleMatch ? titleMatch[1] : 'Blog Post from ALwrity';
|
||||
})();
|
||||
|
||||
// Extract cover image URL, skip if base64 (Wix needs HTTP URL)
|
||||
let coverImageUrl: string | undefined = undefined;
|
||||
if (metadata?.open_graph?.image) {
|
||||
const imageUrl = metadata.open_graph.image;
|
||||
// Skip base64 images - Wix import_image needs HTTP/HTTPS URL
|
||||
if (typeof imageUrl === 'string' && (imageUrl.startsWith('http://') || imageUrl.startsWith('https://'))) {
|
||||
coverImageUrl = imageUrl;
|
||||
} else {
|
||||
console.warn('Skipping cover image - Wix requires HTTP/HTTPS URL, received:', imageUrl?.substring(0, 50));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Publish using same endpoint as WixTestPage
|
||||
// Note: Wix requires category/tag IDs (UUIDs), not names
|
||||
// For now, skip categories/tags until we implement ID lookup/creation
|
||||
const response = await apiClient.post('/api/wix/test/publish/real', {
|
||||
title: title,
|
||||
content: md, // Use markdown, backend converts it
|
||||
cover_image_url: coverImageUrl,
|
||||
// TODO: Lookup/create category IDs from metadata?.blog_categories
|
||||
// TODO: Lookup/create tag IDs from metadata?.blog_tags
|
||||
category_ids: undefined,
|
||||
tag_ids: undefined,
|
||||
publish: true,
|
||||
access_token: accessToken,
|
||||
member_id: undefined // Let backend derive from token
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
return {
|
||||
success: true,
|
||||
url: response.data.url,
|
||||
post_id: response.data.post_id,
|
||||
message: 'Blog post published successfully to Wix!'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: response.data.error || 'Failed to publish to Wix'
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
// If auth error, token may be invalid - try refreshing or reconnect
|
||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||
// Try to refresh one more time
|
||||
const tokenResult = await validateAndRefreshWixTokens();
|
||||
if (tokenResult.needsReconnect) {
|
||||
const publishFunction = async () => {
|
||||
return await publishToWix(md, metadata);
|
||||
};
|
||||
setPendingWixPublish(() => publishFunction);
|
||||
setShowWixConnectModal(true);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Wix tokens expired. Please reconnect your Wix account.',
|
||||
action_required: 'reconnect_wix'
|
||||
};
|
||||
}
|
||||
// If refresh worked, retry once
|
||||
if (tokenResult.accessToken) {
|
||||
return await publishToWix(md, metadata, tokenResult.accessToken);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to publish to Wix: ${error.response?.data?.detail || error.message}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Wix connection success - retry publish
|
||||
const handleWixConnectionSuccess = async () => {
|
||||
if (pendingWixPublish) {
|
||||
const publishFn = pendingWixPublish;
|
||||
setPendingWixPublish(null);
|
||||
// Small delay to ensure tokens are saved in sessionStorage
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// Retry the publish - this will be executed and return result
|
||||
// Note: The result won't show in CopilotKit UI since we're outside the action handler
|
||||
// But the publish will succeed and user will see their blog on Wix
|
||||
const result = await publishFn();
|
||||
console.log('Wix publish after connection:', result);
|
||||
// Optionally show a success notification
|
||||
if (result.success) {
|
||||
// Publish succeeded - user's blog is now on Wix
|
||||
console.log('Blog published to Wix successfully after connection');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error retrying publish after connection:', error);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
// Enhanced publish action with Wix support
|
||||
useCopilotActionTyped({
|
||||
name: 'publishToPlatform',
|
||||
@@ -61,58 +200,101 @@ export const Publisher: React.FC<PublisherProps> = ({
|
||||
const html = convertMarkdownToHTML(md);
|
||||
|
||||
if (platform === 'wix') {
|
||||
// Check Wix connection status first
|
||||
if (!wixConnectionStatus?.connected) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Wix account not connected. Please connect your Wix account first using the Wix Test Page.',
|
||||
// Proactively validate and refresh tokens
|
||||
const tokenResult = await validateAndRefreshWixTokens();
|
||||
|
||||
if (tokenResult.needsReconnect || !tokenResult.accessToken) {
|
||||
// Store the publish function to retry after connection
|
||||
const publishFunction = async () => {
|
||||
return await publishToWix(md, seoMetadata);
|
||||
};
|
||||
setPendingWixPublish(() => publishFunction);
|
||||
setShowWixConnectModal(true);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Wix account not connected. Please connect your Wix account to publish.',
|
||||
action_required: 'connect_wix'
|
||||
};
|
||||
}
|
||||
|
||||
if (!wixConnectionStatus?.has_permissions) {
|
||||
|
||||
// We have a valid access token, proceed with publishing
|
||||
return await publishToWix(md, seoMetadata, tokenResult.accessToken);
|
||||
} else if (platform === 'wordpress') {
|
||||
// WordPress publishing
|
||||
if (!seoMetadata) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Insufficient Wix permissions. Please reconnect your Wix account.',
|
||||
action_required: 'reconnect_wix'
|
||||
message: 'Generate SEO metadata first. Use the "Next: Generate SEO Metadata" suggestion to create metadata before publishing.'
|
||||
};
|
||||
}
|
||||
|
||||
// Extract title from markdown (first heading or use default)
|
||||
const titleMatch = md.match(/^#\s+(.+)$/m);
|
||||
const title = titleMatch ? titleMatch[1] : 'Blog Post from ALwrity';
|
||||
|
||||
|
||||
// Check if user has connected WordPress sites
|
||||
if (wordpressSites.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No WordPress sites connected. Please connect a WordPress site first. Go to Settings > Integrations to add your WordPress site.',
|
||||
action_required: 'connect_wordpress'
|
||||
};
|
||||
}
|
||||
|
||||
// Find first active site, or use first site if none are active
|
||||
const activeSite = wordpressSites.find(site => site.is_active) || wordpressSites[0];
|
||||
if (!activeSite) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No active WordPress sites found. Please activate a WordPress site connection.',
|
||||
action_required: 'activate_wordpress'
|
||||
};
|
||||
}
|
||||
|
||||
// Extract title from SEO metadata or markdown
|
||||
const title = seoMetadata.seo_title || (() => {
|
||||
const titleMatch = md.match(/^#\s+(.+)$/m);
|
||||
return titleMatch ? titleMatch[1] : 'Blog Post from ALwrity';
|
||||
})();
|
||||
|
||||
// Extract excerpt from SEO metadata
|
||||
const excerpt = seoMetadata.meta_description || '';
|
||||
|
||||
// Build WordPress publish request
|
||||
const publishRequest: WordPressPublishRequest = {
|
||||
site_id: activeSite.id,
|
||||
title: title,
|
||||
content: html,
|
||||
excerpt: excerpt,
|
||||
status: 'publish',
|
||||
meta_description: seoMetadata.meta_description || excerpt,
|
||||
tags: seoMetadata.blog_tags || [],
|
||||
categories: seoMetadata.blog_categories || []
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/wix/publish', {
|
||||
title: title,
|
||||
content: md,
|
||||
publish: true
|
||||
});
|
||||
const result = await wordpressAPI.publishContent(publishRequest);
|
||||
|
||||
if (response.data.success) {
|
||||
return {
|
||||
success: true,
|
||||
url: response.data.url,
|
||||
post_id: response.data.post_id,
|
||||
message: 'Blog post published successfully to Wix!'
|
||||
if (result.success) {
|
||||
return {
|
||||
success: true,
|
||||
url: result.post_url || `${activeSite.site_url}/?p=${result.post_id}`,
|
||||
post_id: result.post_id,
|
||||
message: `Blog post published successfully to WordPress site "${activeSite.site_name}"!`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: response.data.error || 'Failed to publish to Wix'
|
||||
return {
|
||||
success: false,
|
||||
message: result.error || 'Failed to publish to WordPress'
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to publish to Wix: ${error.response?.data?.detail || error.message}`
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to publish to WordPress: ${error.response?.data?.detail || error.message || 'Unknown error'}`
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// WordPress or other platforms
|
||||
if (!seoMetadata) return { success: false, message: 'Generate SEO metadata first' };
|
||||
const res = await blogWriterApi.publish({ platform, html, metadata: seoMetadata, schedule_time });
|
||||
return { success: true, url: res.url };
|
||||
return {
|
||||
success: false,
|
||||
message: `Unsupported platform: ${platform}. Supported platforms are 'wix' and 'wordpress'.`
|
||||
};
|
||||
}
|
||||
},
|
||||
render: ({ status, result }: any) => {
|
||||
@@ -153,6 +335,13 @@ export const Publisher: React.FC<PublisherProps> = ({
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{(result?.action_required === 'connect_wordpress' || result?.action_required === 'activate_wordpress') && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<a href="/settings/integrations" target="_blank" rel="noopener noreferrer">
|
||||
Manage WordPress Connections
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -161,7 +350,18 @@ export const Publisher: React.FC<PublisherProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
return null; // This component only provides the copilot action
|
||||
return (
|
||||
<>
|
||||
<WixConnectModal
|
||||
isOpen={showWixConnectModal}
|
||||
onClose={() => {
|
||||
setShowWixConnectModal(false);
|
||||
setPendingWixPublish(null);
|
||||
}}
|
||||
onConnectionSuccess={handleWixConnectionSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Publisher;
|
||||
|
||||
@@ -145,11 +145,7 @@ export const useSuggestions = ({
|
||||
priority: 'high'
|
||||
});
|
||||
items.push({
|
||||
title: 'Content Analysis',
|
||||
message: 'Analyze the flow and quality of my blog content to get improvement suggestions'
|
||||
});
|
||||
items.push({
|
||||
title: 'Content Analysis',
|
||||
title: '📊 Content Analysis',
|
||||
message: 'Analyze the flow and quality of my blog content to get improvement suggestions'
|
||||
});
|
||||
} else if (seoAnalysis && !seoRecommendationsApplied) {
|
||||
@@ -160,7 +156,7 @@ export const useSuggestions = ({
|
||||
priority: 'high'
|
||||
});
|
||||
items.push({
|
||||
title: 'Content Analysis',
|
||||
title: '📊 Content Analysis',
|
||||
message: 'Run analyzeContentQuality to review narrative flow and get final improvement suggestions before publishing.'
|
||||
});
|
||||
items.push({
|
||||
@@ -175,33 +171,21 @@ export const useSuggestions = ({
|
||||
message: 'SEO recommendations are applied. Execute generateSEOMetadata immediately so we can prepare titles, descriptions, and schema without further prompts.',
|
||||
priority: 'high'
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
title: 'Next: Publish',
|
||||
message: 'The blog is SEO-optimized. Use publishToPlatform with your preferred destination (wix|wordpress) right away—no additional confirmation needed.',
|
||||
priority: 'high'
|
||||
title: '📊 Content Analysis',
|
||||
message: 'Run analyzeContentQuality to validate flow, consistency, and progression before publishing.'
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
title: 'Content Analysis',
|
||||
message: 'Run analyzeContentQuality to validate flow, consistency, and progression before publishing.'
|
||||
});
|
||||
items.push({
|
||||
title: 'Publish',
|
||||
message: seoMetadata
|
||||
? 'Publish my blog to your preferred platform using publishToPlatform.'
|
||||
: 'Generate SEO metadata first, then publish your blog.'
|
||||
});
|
||||
|
||||
if (seoMetadata) {
|
||||
} else {
|
||||
// SEO metadata is ready - show publishing options
|
||||
items.push({
|
||||
title: '🚀 Publish to Wix',
|
||||
message: 'Publish my blog to Wix using publishToPlatform with platform "wix".'
|
||||
message: 'Publish my blog to Wix using publishToPlatform with platform "wix".',
|
||||
priority: 'high'
|
||||
});
|
||||
items.push({
|
||||
title: '🌐 Publish to WordPress',
|
||||
message: 'Publish my blog to WordPress using publishToPlatform with platform "wordpress".'
|
||||
message: 'Publish my blog to WordPress using publishToPlatform with platform "wordpress".',
|
||||
priority: 'high'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ interface BlogEditorProps {
|
||||
onSave?: (content: any) => void;
|
||||
continuityRefresh?: number;
|
||||
flowAnalysisResults?: any;
|
||||
sectionImages?: Record<string, string>;
|
||||
}
|
||||
|
||||
const BlogEditor: React.FC<BlogEditorProps> = ({
|
||||
@@ -43,7 +44,8 @@ const BlogEditor: React.FC<BlogEditorProps> = ({
|
||||
onContentUpdate,
|
||||
onSave,
|
||||
continuityRefresh,
|
||||
flowAnalysisResults
|
||||
flowAnalysisResults,
|
||||
sectionImages = {}
|
||||
}) => {
|
||||
const [blogTitle, setBlogTitle] = useState(initialTitle || 'Your Amazing Blog Title');
|
||||
const [sections, setSections] = useState<any[]>([]);
|
||||
@@ -143,17 +145,25 @@ const BlogEditor: React.FC<BlogEditorProps> = ({
|
||||
<Divider sx={{ mt: 3, opacity: 0.3 }} />
|
||||
</div>
|
||||
<div>
|
||||
{sections.map((section) => (
|
||||
<BlogSection
|
||||
key={section.id}
|
||||
{...section}
|
||||
onContentUpdate={onContentUpdate}
|
||||
expandedSections={expandedSections}
|
||||
toggleSectionExpansion={toggleSectionExpansion}
|
||||
refreshToken={continuityRefresh}
|
||||
flowAnalysisResults={flowAnalysisResults}
|
||||
/>
|
||||
))}
|
||||
{sections.map((section, index) => {
|
||||
// Robust image mapping: prefer outline index id (order is consistent across phases)
|
||||
const imageIdByIndex = outline[index]?.id;
|
||||
const outlineSection = outline.find(s => (s.id === section.id) || (s.heading === section.title));
|
||||
const imageId = imageIdByIndex || outlineSection?.id || section.id;
|
||||
const sectionImage = sectionImages?.[imageId] || null;
|
||||
return (
|
||||
<BlogSection
|
||||
key={section.id}
|
||||
{...section}
|
||||
onContentUpdate={onContentUpdate}
|
||||
expandedSections={expandedSections}
|
||||
toggleSectionExpansion={toggleSectionExpansion}
|
||||
refreshToken={continuityRefresh}
|
||||
flowAnalysisResults={flowAnalysisResults}
|
||||
sectionImage={sectionImage}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@ interface BlogSectionProps {
|
||||
toggleSectionExpansion: (sectionId: any) => void;
|
||||
refreshToken?: number;
|
||||
flowAnalysisResults?: any;
|
||||
sectionImage?: string;
|
||||
}
|
||||
|
||||
const BlogSection: React.FC<BlogSectionProps> = ({
|
||||
@@ -53,7 +54,8 @@ const BlogSection: React.FC<BlogSectionProps> = ({
|
||||
expandedSections,
|
||||
toggleSectionExpansion,
|
||||
refreshToken,
|
||||
flowAnalysisResults
|
||||
flowAnalysisResults,
|
||||
sectionImage
|
||||
}) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [sectionTitle, setSectionTitle] = useState(title);
|
||||
@@ -181,6 +183,31 @@ const BlogSection: React.FC<BlogSectionProps> = ({
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Section Image Display */}
|
||||
{sectionImage && (
|
||||
<div style={{ marginBottom: '16px', marginTop: '8px' }}>
|
||||
<div style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
maxWidth: '100%',
|
||||
backgroundColor: '#fff'
|
||||
}}>
|
||||
<img
|
||||
src={`data:image/png;base64,${sectionImage}`}
|
||||
alt={`Cover image for ${sectionTitle}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'block',
|
||||
maxHeight: '400px',
|
||||
objectFit: 'contain'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="relative"
|
||||
|
||||
Reference in New Issue
Block a user