Subscription dashboard improvements, AI text generation limit, and other fixes.

This commit is contained in:
ajaysi
2025-11-01 18:01:14 +05:30
parent cdb41aec1b
commit de4328175d
64 changed files with 5809 additions and 444 deletions

View File

@@ -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}

View File

@@ -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' }}>

View File

@@ -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;

View File

@@ -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 }));
}
}}
/>

View File

@@ -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;

View File

@@ -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'
});
}
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -119,25 +119,44 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
const fetchDetailedStats = async () => {
try {
const response = await apiClient.get('/api/content-planning/monitoring/api-stats');
const result = response.data;
if (result.status === 'success') {
setDetailedStats(result.data);
if (result.data?.cache_performance) {
setCachePerf(result.data.cache_performance);
const result = response?.data;
// Validate response structure
if (!result || result.status !== 'success' || !result.data) {
console.warn('Invalid response structure from api-stats endpoint:', result);
setChartData([]);
return;
}
const data = result.data;
setDetailedStats(data);
if (data?.cache_performance) {
setCachePerf(data.cache_performance);
}
// Generate chart data
const chartData = result.data.top_endpoints.slice(0, 5).map((endpoint: any, index: number) => ({
name: endpoint.endpoint.split(' ')[1].split('/').pop() || 'API',
requests: endpoint.count,
avgTime: endpoint.avg_time,
errors: endpoint.errors,
hitRate: endpoint.cache_hit_rate
// Generate chart data - safely handle missing top_endpoints
if (data?.top_endpoints && Array.isArray(data.top_endpoints) && data.top_endpoints.length > 0) {
try {
const chartData = data.top_endpoints.slice(0, 5).map((endpoint: any) => ({
name: endpoint?.endpoint?.split(' ')[1]?.split('/').pop() || 'API',
requests: endpoint?.count || 0,
avgTime: endpoint?.avg_time || 0,
errors: endpoint?.errors || 0,
hitRate: endpoint?.cache_hit_rate || 0
}));
setChartData(chartData);
} catch (mapError) {
console.error('Error mapping chart data:', mapError);
setChartData([]);
}
} else {
// If top_endpoints is missing or not an array, set empty chart data
setChartData([]);
}
} catch (err) {
console.error('Error fetching detailed stats:', err);
setChartData([]);
}
};
@@ -353,7 +372,7 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
)}
{/* Recent Errors Section */}
{detailedStats?.recent_errors && detailedStats.recent_errors.length > 0 && (
{detailedStats?.recent_errors && Array.isArray(detailedStats.recent_errors) && detailedStats.recent_errors.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@@ -395,6 +414,8 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
>
Close
</Button>
<Tooltip title={loading ? "Refreshing data..." : "Refresh monitoring data"}>
<span>
<Button
onClick={fetchDetailedStats}
variant="contained"
@@ -403,6 +424,8 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
>
Refresh Data
</Button>
</span>
</Tooltip>
</DialogActions>
</Dialog>
</>

View File

@@ -56,18 +56,10 @@ export interface PromptSuggestion {
}
export async function fetchPromptSuggestions(payload: any): Promise<PromptSuggestion[]> {
const res = await fetch('/api/images/suggest-prompts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(payload)
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || 'Failed to fetch prompt suggestions');
}
const data = await res.json();
return data.suggestions || [];
// Use apiClient directly (same pattern as SEO analysis in SEOAnalysisModal.tsx)
// The apiClient interceptor will handle auth token injection automatically
const response = await apiClient.post('/api/images/suggest-prompts', payload);
return response.data.suggestions || [];
}

View File

@@ -28,6 +28,7 @@ import {
Modal,
Fade,
Backdrop,
Snackbar,
} from '@mui/material';
import {
Check as CheckIcon,
@@ -35,6 +36,7 @@ import {
Star as StarIcon,
WorkspacePremium as PremiumIcon,
Info as InfoIcon,
Warning,
Psychology,
Search,
FactCheck,
@@ -83,6 +85,7 @@ const PricingPage: React.FC = () => {
const [subscribing, setSubscribing] = useState(false);
const [paymentModalOpen, setPaymentModalOpen] = useState(false);
const [showSignInPrompt, setShowSignInPrompt] = useState(false);
const [successSnackbar, setSuccessSnackbar] = useState({ open: false, message: '', countdown: 3 });
const [knowMoreModal, setKnowMoreModal] = useState<{ open: boolean; title: string; content: React.ReactNode }>({
open: false,
title: '',
@@ -172,27 +175,70 @@ const PricingPage: React.FC = () => {
setSubscribing(true);
const userId = localStorage.getItem('user_id') || 'anonymous';
await apiClient.post(`/api/subscription/subscribe/${userId}`, {
const response = await apiClient.post(`/api/subscription/subscribe/${userId}`, {
plan_id: selectedPlan,
billing_cycle: yearlyBilling ? 'yearly' : 'monthly'
});
// Refresh subscription status
console.log('Subscription renewed successfully:', response.data);
// Refresh subscription status immediately
window.dispatchEvent(new CustomEvent('subscription-updated'));
// Also trigger user authenticated event to refresh subscription context
window.dispatchEvent(new CustomEvent('user-authenticated'));
setPaymentModalOpen(false);
// After subscription, check if onboarding is complete
// If not complete, redirect to onboarding; otherwise to dashboard
const onboardingComplete = localStorage.getItem('onboarding_complete') === 'true';
if (onboardingComplete) {
navigate('/dashboard');
} else {
navigate('/onboarding');
}
// Get plan name for success message
const planName = plans.find(p => p.id === selectedPlan)?.name || 'subscription';
// Show success message with countdown
setSuccessSnackbar({
open: true,
message: `🎉 ${planName} plan activated! Your usage limits have been reset. Returning to your work in 3 seconds...`,
countdown: 3
});
// Countdown timer
let countdown = 3;
const countdownInterval = setInterval(() => {
countdown -= 1;
if (countdown > 0) {
setSuccessSnackbar(prev => ({
...prev,
message: `🎉 ${planName} plan activated! Your usage limits have been reset. Returning to your work in ${countdown} second${countdown !== 1 ? 's' : ''}...`,
countdown
}));
} else {
clearInterval(countdownInterval);
}
}, 1000);
// Auto-redirect after 3 seconds
setTimeout(() => {
clearInterval(countdownInterval);
// After subscription, check if onboarding is complete
// If not complete, redirect to onboarding; otherwise to dashboard
const onboardingComplete = localStorage.getItem('onboarding_complete') === 'true';
if (onboardingComplete) {
// Try to go back to where the user was (e.g., blog writer)
// If no history, go to dashboard
const referrer = sessionStorage.getItem('subscription_referrer');
if (referrer && referrer !== '/pricing') {
navigate(referrer);
} else {
navigate('/dashboard');
}
} else {
navigate('/onboarding');
}
}, 3000);
} catch (err) {
console.error('Error subscribing:', err);
setError('Failed to process subscription');
setSuccessSnackbar({ open: false, message: '', countdown: 0 });
} finally {
setSubscribing(false);
}
@@ -900,32 +946,71 @@ const PricingPage: React.FC = () => {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
width: 450,
bgcolor: 'background.paper',
border: '2px solid #000',
boxShadow: 24,
p: 4,
borderRadius: 2,
}}>
<Typography variant="h6" component="h2" gutterBottom>
<Typography variant="h6" component="h2" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Warning sx={{ color: 'warning.main' }} />
Alpha Testing Subscription
</Typography>
<Typography variant="body1" sx={{ mb: 3 }}>
Thank you for participating in our alpha testing! For the Basic plan, we're crediting $29 to your account.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
In production, this would integrate with Stripe/Paddle for real payment processing.
{/* Alpha Testing Notice */}
<Alert severity="warning" sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
⚠️ Alpha Testing Mode - No Payment Required
</Typography>
<Typography variant="caption" sx={{ display: 'block' }}>
Payment integration is coming soon. For now, subscriptions are activated without charge.
</Typography>
</Alert>
<Typography variant="body1" sx={{ mb: 2 }}>
Thank you for participating in our alpha testing! We're crediting the Basic plan ($29 value) to your account.
</Typography>
{/* TODO: Payment Integration Notice */}
<Box sx={{
p: 2,
mb: 3,
bgcolor: 'info.lighter',
borderRadius: 1,
border: '1px solid',
borderColor: 'info.light'
}}>
<Typography variant="body2" color="info.dark">
<strong>Coming in Production:</strong>
</Typography>
<Typography variant="caption" color="info.dark" sx={{ display: 'block', mt: 0.5 }}>
Secure Stripe/PayPal payment processing<br />
Automatic renewal management<br />
Payment verification & receipts<br />
Upgrade/downgrade options
</Typography>
</Box>
{/* Note: Current behavior allows renewal without payment verification */}
{/* This is intentional for alpha testing but will be secured in production */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
<Button onClick={() => setPaymentModalOpen(false)}>
<Button onClick={() => setPaymentModalOpen(false)} variant="outlined">
Cancel
</Button>
<Button
variant="contained"
onClick={handlePaymentConfirm}
disabled={subscribing}
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'&:hover': {
background: 'linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%)',
}
}}
>
{subscribing ? <CircularProgress size={20} /> : 'Confirm Subscription'}
{subscribing ? <CircularProgress size={20} sx={{ color: 'white' }} /> : 'Confirm Subscription'}
</Button>
</Box>
</Box>
@@ -981,6 +1066,37 @@ const PricingPage: React.FC = () => {
</Button>
</DialogActions>
</Dialog>
{/* Success Snackbar */}
<Snackbar
open={successSnackbar.open}
autoHideDuration={3000}
onClose={() => setSuccessSnackbar({ open: false, message: '', countdown: 0 })}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
sx={{
top: { xs: 16, sm: 24 },
'& .MuiSnackbarContent-root': {
minWidth: { xs: '90vw', sm: '500px' }
}
}}
>
<Alert
severity="success"
variant="filled"
onClose={() => setSuccessSnackbar({ open: false, message: '', countdown: 0 })}
sx={{
width: '100%',
fontSize: '1rem',
alignItems: 'center',
boxShadow: '0 8px 24px rgba(76, 175, 80, 0.4)',
'& .MuiAlert-icon': {
fontSize: '2rem'
}
}}
>
{successSnackbar.message}
</Alert>
</Snackbar>
</Container>
);
};

View File

@@ -39,6 +39,25 @@ const SubscriptionExpiredModal: React.FC<SubscriptionExpiredModalProps> = ({
subscriptionData,
errorData
}) => {
// Debug logging to verify modal state
React.useEffect(() => {
if (open) {
console.log('SubscriptionExpiredModal: Modal opened', {
open,
errorData,
hasUsageInfo: !!errorData?.usage_info
});
}
}, [open, errorData]);
const handleDialogClose = (_event: object, reason?: string) => {
if (reason === 'backdropClick') {
console.log('SubscriptionExpiredModal: Ignoring backdrop click close');
return;
}
onClose();
};
const handleRenewClick = () => {
onRenewSubscription();
onClose();
@@ -47,16 +66,21 @@ const SubscriptionExpiredModal: React.FC<SubscriptionExpiredModalProps> = ({
return (
<Dialog
open={open}
onClose={onClose}
onClose={handleDialogClose}
maxWidth="sm"
fullWidth
disableEscapeKeyDown
PaperProps={{
sx: {
borderRadius: 3,
background: 'linear-gradient(135deg, #fff 0%, #f8fafc 100%)',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
zIndex: 9999, // Ensure modal appears above everything
}
}}
sx={{
zIndex: 9999, // Ensure modal backdrop appears above everything
}}
>
<DialogTitle sx={{ textAlign: 'center', pb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 2 }}>
@@ -93,56 +117,156 @@ const SubscriptionExpiredModal: React.FC<SubscriptionExpiredModalProps> = ({
borderRadius: 2
}}
>
<Typography variant="body1" sx={{ mb: 2, color: 'text.secondary' }}>
{/* Main error message */}
<Typography variant="body1" sx={{ mb: 2, color: 'text.secondary', lineHeight: 1.6 }}>
{errorData?.message || (errorData?.usage_info
? 'You\'ve reached your monthly usage limit for this plan. Upgrade your plan to get higher limits.'
: 'To continue using Alwrity and access all features, you need to renew your subscription.'
)}
</Typography>
{/* Detailed usage information */}
{errorData?.usage_info && (
<Box sx={{ mb: 2, p: 2, background: 'rgba(255,255,255,0.7)', borderRadius: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 600, mb: 1, color: 'text.primary' }}>
<Box sx={{ mb: 2, p: 2.5, background: 'rgba(255,255,255,0.9)', borderRadius: 2, border: '1px solid #e2e8f0' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 2, color: 'text.primary', display: 'flex', alignItems: 'center', gap: 1 }}>
<Warning sx={{ fontSize: 18, color: 'warning.main' }} />
Usage Information:
</Typography>
{errorData.usage_info.call_usage_percentage && (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
You've used {errorData.usage_info.call_usage_percentage.toFixed(1)}% of your monthly limit
</Typography>
{/* Provider and operation type */}
<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
{errorData.provider && (
<Box sx={{
flex: '1 1 auto',
px: 2,
py: 1.5,
background: 'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)',
borderRadius: 1.5,
border: '1px solid #a5b4fc'
}}>
<Typography variant="caption" sx={{ color: '#4338ca', fontWeight: 600, display: 'block', mb: 0.5 }}>
Provider:
</Typography>
<Typography variant="body2" sx={{ color: '#312e81', fontWeight: 700 }}>
{errorData.provider}
</Typography>
</Box>
)}
{errorData.usage_info.operation_type && (
<Box sx={{
flex: '1 1 auto',
px: 2,
py: 1.5,
background: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
borderRadius: 1.5,
border: '1px solid #fbbf24'
}}>
<Typography variant="caption" sx={{ color: '#92400e', fontWeight: 600, display: 'block', mb: 0.5 }}>
Operation:
</Typography>
<Typography variant="body2" sx={{ color: '#78350f', fontWeight: 700, textTransform: 'capitalize' }}>
{errorData.usage_info.operation_type.replace(/_/g, ' ')}
</Typography>
</Box>
)}
</Box>
{/* Token usage details (if available) */}
{(errorData.usage_info.current_tokens !== undefined || errorData.usage_info.current_calls !== undefined) && (
<Box sx={{
p: 2,
background: 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)',
borderRadius: 1.5,
border: '1px solid #f87171',
mb: 2
}}>
{errorData.usage_info.current_tokens !== undefined && (
<>
<Typography variant="body2" sx={{ color: '#7f1d1d', fontWeight: 600, mb: 1 }}>
Token Usage:
</Typography>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1, mb: 0.5 }}>
<Typography variant="h6" sx={{ color: '#991b1b', fontWeight: 700 }}>
{errorData.usage_info.current_tokens?.toLocaleString() || 0}
</Typography>
<Typography variant="body2" sx={{ color: '#7f1d1d' }}>
/ {errorData.usage_info.limit?.toLocaleString() || 0}
</Typography>
<Typography variant="caption" sx={{ color: '#7f1d1d', ml: 'auto' }}>
({((errorData.usage_info.current_tokens / errorData.usage_info.limit) * 100).toFixed(1)}% used)
</Typography>
</Box>
{errorData.usage_info.requested_tokens && (
<Typography variant="caption" sx={{ color: '#7f1d1d', display: 'block', mt: 1 }}>
Requested: {errorData.usage_info.requested_tokens.toLocaleString()} tokens
{errorData.usage_info.current_tokens + errorData.usage_info.requested_tokens > errorData.usage_info.limit && (
<span style={{ fontWeight: 700, marginLeft: 4 }}>
(Would exceed by: {((errorData.usage_info.current_tokens + errorData.usage_info.requested_tokens) - errorData.usage_info.limit).toLocaleString()} tokens)
</span>
)}
</Typography>
)}
</>
)}
{errorData.usage_info.current_calls !== undefined && (
<>
<Typography variant="body2" sx={{ color: '#7f1d1d', fontWeight: 600, mb: 1, mt: errorData.usage_info.current_tokens !== undefined ? 2 : 0 }}>
API Call Usage:
</Typography>
<Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1 }}>
<Typography variant="h6" sx={{ color: '#991b1b', fontWeight: 700 }}>
{errorData.usage_info.current_calls?.toLocaleString() || 0}
</Typography>
<Typography variant="body2" sx={{ color: '#7f1d1d' }}>
/ {errorData.usage_info.call_limit?.toLocaleString() || 0}
</Typography>
<Typography variant="caption" sx={{ color: '#7f1d1d', ml: 'auto' }}>
({((errorData.usage_info.current_calls / errorData.usage_info.call_limit) * 100).toFixed(1)}% used)
</Typography>
</Box>
</>
)}
</Box>
)}
{errorData.provider && (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
Provider: {errorData.provider}
</Typography>
{/* Error type badge */}
{errorData.usage_info.error_type && (
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Box sx={{
px: 2,
py: 0.5,
background: '#dc2626',
borderRadius: 1,
display: 'inline-block'
}}>
<Typography variant="caption" sx={{ color: 'white', fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5 }}>
{errorData.usage_info.error_type.replace(/_/g, ' ')}
</Typography>
</Box>
</Box>
)}
</Box>
)}
{/* Current plan information */}
{subscriptionData && (
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, flexWrap: 'wrap' }}>
{subscriptionData.plan && (
<Box sx={{
px: 2,
py: 1,
background: 'rgba(255,255,255,0.7)',
borderRadius: 1,
border: '1px solid #e2e8f0'
px: 3,
py: 1.5,
background: 'rgba(255,255,255,0.9)',
borderRadius: 1.5,
border: '2px solid #e2e8f0'
}}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
Current Plan: {subscriptionData.plan}
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600, display: 'block', mb: 0.5 }}>
Current Plan:
</Typography>
</Box>
)}
{subscriptionData.tier && subscriptionData.tier !== subscriptionData.plan && (
<Box sx={{
px: 2,
py: 1,
background: 'rgba(255,255,255,0.7)',
borderRadius: 1,
border: '1px solid #e2e8f0'
}}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
Tier: {subscriptionData.tier}
<Typography variant="body2" sx={{ color: 'text.primary', fontWeight: 700, textTransform: 'capitalize' }}>
{subscriptionData.plan}
</Typography>
</Box>
)}

View File

@@ -105,12 +105,13 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
/* Enhanced Start Button with Phase 1 Improvements */
<Box sx={{ position: 'relative', display: 'inline-flex' }}>
<Tooltip title={tooltipMessage} arrow placement="bottom">
<Button
variant="contained"
size={isFirstVisit ? "medium" : "small"}
startIcon={<PlayArrow />}
onClick={workflowControls.onStartWorkflow}
disabled={workflowControls.isLoading}
<span>
<Button
variant="contained"
size={isFirstVisit ? "medium" : "small"}
startIcon={<PlayArrow />}
onClick={workflowControls.onStartWorkflow}
disabled={workflowControls.isLoading}
sx={{
position: 'relative',
overflow: 'hidden',
@@ -180,8 +181,9 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
},
}}
>
{isFirstVisit ? '🚀 Start Journey' : 'Start'}
</Button>
{isFirstVisit ? '🚀 Start Journey' : 'Start'}
</Button>
</span>
</Tooltip>
<Box
sx={{