Allowing AI to generate suggestions for the blog writer
This commit is contained in:
293
frontend/src/components/BlogWriter/WYSIWYG/BlogEditor.tsx
Normal file
293
frontend/src/components/BlogWriter/WYSIWYG/BlogEditor.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { createTheme, ThemeProvider, Paper, IconButton, TextField, Tooltip, CircularProgress, Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Box, Divider } from '@mui/material';
|
||||
import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { BlogOutlineSection, BlogResearchResponse } from '../../../services/blogWriterApi';
|
||||
import BlogSection from './BlogSection';
|
||||
|
||||
// Helper to create a consistent theme
|
||||
const theme = createTheme({
|
||||
typography: {
|
||||
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
|
||||
},
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#4f46e5',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface BlogEditorProps {
|
||||
outline: BlogOutlineSection[];
|
||||
research: BlogResearchResponse | null;
|
||||
initialTitle?: string;
|
||||
titleOptions?: string[];
|
||||
researchTitles?: string[];
|
||||
aiGeneratedTitles?: string[];
|
||||
sections?: Record<string, string>;
|
||||
onContentUpdate?: (sections: any[]) => void;
|
||||
onSave?: (content: any) => void;
|
||||
}
|
||||
|
||||
const BlogEditor: React.FC<BlogEditorProps> = ({
|
||||
outline,
|
||||
research,
|
||||
initialTitle,
|
||||
titleOptions = [],
|
||||
researchTitles = [],
|
||||
aiGeneratedTitles = [],
|
||||
sections: parentSections,
|
||||
onContentUpdate,
|
||||
onSave
|
||||
}) => {
|
||||
const [blogTitle, setBlogTitle] = useState(initialTitle || 'Your Amazing Blog Title');
|
||||
const [sections, setSections] = useState<any[]>([]);
|
||||
const [isTitleLoading, setIsTitleLoading] = useState(false);
|
||||
const [expandedSections, setExpandedSections] = useState<Set<any>>(new Set());
|
||||
const [showTitleModal, setShowTitleModal] = useState(false);
|
||||
|
||||
// Initialize sections from outline or use parent sections
|
||||
useEffect(() => {
|
||||
if (outline && outline.length > 0) {
|
||||
const initialSections = outline.map((section, index) => ({
|
||||
id: section.id || index + 1,
|
||||
title: section.heading,
|
||||
content: parentSections?.[section.id] || section.key_points?.join(' ') || '',
|
||||
wordCount: section.target_words || 0,
|
||||
sources: section.references?.length || 0,
|
||||
outlineData: {
|
||||
subheadings: section.subheadings || [],
|
||||
keyPoints: section.key_points || [],
|
||||
keywords: section.keywords || [],
|
||||
references: section.references || [],
|
||||
targetWords: section.target_words || 0
|
||||
}
|
||||
}));
|
||||
setSections(initialSections);
|
||||
}
|
||||
}, [outline, parentSections]);
|
||||
|
||||
// Initialize title from parent when provided
|
||||
useEffect(() => {
|
||||
if (initialTitle && initialTitle.trim().length > 0) {
|
||||
setBlogTitle(initialTitle);
|
||||
}
|
||||
}, [initialTitle]);
|
||||
|
||||
const handleSuggestTitle = useCallback(() => {
|
||||
console.log('Available titles:', { researchTitles, aiGeneratedTitles, titleOptions });
|
||||
setShowTitleModal(true);
|
||||
}, [researchTitles, aiGeneratedTitles, titleOptions]);
|
||||
|
||||
const handleTitleSelect = useCallback((selectedTitle: string) => {
|
||||
setBlogTitle(selectedTitle);
|
||||
setShowTitleModal(false);
|
||||
}, []);
|
||||
|
||||
const toggleSectionExpansion = useCallback((sectionId: any) => {
|
||||
setExpandedSections(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(sectionId)) {
|
||||
newSet.delete(sectionId);
|
||||
} else {
|
||||
newSet.add(sectionId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// Main Render - Exactly like your example
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<div className="bg-gray-50 min-h-screen font-sans">
|
||||
<main className="w-full px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="w-full max-w-4xl mx-auto">
|
||||
<Paper elevation={0} className="bg-white p-8 md:p-12 rounded-xl border border-gray-200/80 w-full">
|
||||
<div className="mb-8 pb-6 border-b">
|
||||
<div className="flex items-start gap-2 group">
|
||||
<h1
|
||||
className="flex-1 text-2xl md:text-4xl font-bold font-serif text-gray-900 leading-tight cursor-pointer hover:bg-gray-50 p-2 rounded-md transition-colors duration-200"
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
whiteSpace: 'normal',
|
||||
lineHeight: '1.3'
|
||||
}}
|
||||
onClick={() => {
|
||||
const newTitle = prompt('Edit blog title:', blogTitle);
|
||||
if (newTitle !== null) {
|
||||
setBlogTitle(newTitle);
|
||||
}
|
||||
}}
|
||||
title="Click to edit title"
|
||||
>
|
||||
{blogTitle}
|
||||
</h1>
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300 mt-1">
|
||||
<Tooltip title="✨ ALwrity it">
|
||||
<IconButton onClick={handleSuggestTitle} disabled={isTitleLoading} size="small">
|
||||
{isTitleLoading ? <CircularProgress size={20} /> : <AutoAwesomeIcon className="text-purple-500" fontSize="small"/>}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-gray-500 text-sm">
|
||||
This is where your blog's subtitle or a brief one-line description will appear. It's editable too!
|
||||
</p>
|
||||
<Divider sx={{ mt: 3, opacity: 0.3 }} />
|
||||
</div>
|
||||
<div>
|
||||
{sections.map((section) => (
|
||||
<BlogSection
|
||||
key={section.id}
|
||||
{...section}
|
||||
onContentUpdate={onContentUpdate}
|
||||
expandedSections={expandedSections}
|
||||
toggleSectionExpansion={toggleSectionExpansion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Title Selection Modal */}
|
||||
<Dialog
|
||||
open={showTitleModal}
|
||||
onClose={() => setShowTitleModal(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold' }}>
|
||||
Choose Your Blog Title
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{/* Research Titles */}
|
||||
{researchTitles.length > 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', mb: 2, color: 'primary.main' }}>
|
||||
📊 Research-Based Titles
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{researchTitles.map((title, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={() => handleTitleSelect(title)}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
textTransform: 'none',
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.light',
|
||||
color: 'white',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* AI Generated Titles */}
|
||||
{aiGeneratedTitles.length > 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', mb: 2, color: 'secondary.main' }}>
|
||||
🤖 AI Generated Titles
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{aiGeneratedTitles.map((title, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={() => handleTitleSelect(title)}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
textTransform: 'none',
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: 'secondary.light',
|
||||
color: 'white',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Title Options */}
|
||||
{titleOptions.length > 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', mb: 2, color: 'success.main' }}>
|
||||
✨ Additional Options
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{titleOptions.map((title, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
onClick={() => handleTitleSelect(title)}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
textTransform: 'none',
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
'&:hover': {
|
||||
backgroundColor: 'success.light',
|
||||
color: 'white',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{researchTitles.length === 0 && aiGeneratedTitles.length === 0 && titleOptions.length === 0 && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 4 }}>
|
||||
No title options available. Please generate an outline first.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Debug info */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Box sx={{ mt: 2, p: 2, bgcolor: 'grey.100', borderRadius: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Debug: Research titles: {researchTitles.length}, AI titles: {aiGeneratedTitles.length}, Options: {titleOptions.length}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowTitleModal(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogEditor;
|
||||
Reference in New Issue
Block a user