story writer backend migration complete, Blog writer SEO and story writer backend migration complete, Blog writer SEO and story writer frontend migration complete
This commit is contained in:
@@ -2,6 +2,11 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { BlogOutlineSection, BlogResearchResponse, BlogSEOMetadataResponse, BlogSEOAnalyzeResponse, SourceMappingStats, GroundingInsights, OptimizationResults, ResearchCoverage } from '../services/blogWriterApi';
|
||||
import { researchCache } from '../services/researchCache';
|
||||
|
||||
const MINOR_TITLE_WORDS = new Set([
|
||||
'a', 'an', 'and', 'or', 'but', 'the', 'for', 'nor', 'on', 'at', 'to', 'from', 'by',
|
||||
'of', 'in', 'with', 'as', 'vs', 'vs.', 'into', 'over', 'under'
|
||||
]);
|
||||
|
||||
export const useBlogWriterState = () => {
|
||||
// Core state
|
||||
const [research, setResearch] = useState<BlogResearchResponse | null>(null);
|
||||
@@ -36,6 +41,57 @@ export const useBlogWriterState = () => {
|
||||
// Section images state - persists images generated in outline phase to content phase
|
||||
const [sectionImages, setSectionImages] = useState<Record<string, string>>({});
|
||||
|
||||
const formatContentAngleToTitle = useCallback((angle: string): string => {
|
||||
if (!angle || typeof angle !== 'string') {
|
||||
return '';
|
||||
}
|
||||
const cleaned = angle.replace(/\s+/g, ' ').trim();
|
||||
if (!cleaned) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const words = cleaned.split(' ');
|
||||
const formattedWords = words.map((word, index) => {
|
||||
const lower = word.toLowerCase();
|
||||
if (index !== 0 && MINOR_TITLE_WORDS.has(lower)) {
|
||||
return lower;
|
||||
}
|
||||
if (!lower) {
|
||||
return '';
|
||||
}
|
||||
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
||||
}).filter(Boolean);
|
||||
|
||||
let formatted = formattedWords.join(' ');
|
||||
if (formatted.length > 120) {
|
||||
formatted = formatted.slice(0, 117).trimEnd() + '...';
|
||||
}
|
||||
return formatted;
|
||||
}, []);
|
||||
|
||||
const dedupeTitles = useCallback((titles: string[]): string[] => {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
|
||||
titles.forEach((title) => {
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
const normalized = title.replace(/\s+/g, ' ').trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
const key = normalized.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
return;
|
||||
}
|
||||
seen.add(key);
|
||||
result.push(normalized);
|
||||
});
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
// Cache recovery - restore most recent research on page load
|
||||
useEffect(() => {
|
||||
const cachedEntries = researchCache.getAllCachedEntries();
|
||||
@@ -71,14 +127,46 @@ export const useBlogWriterState = () => {
|
||||
// Handle research completion
|
||||
const handleResearchComplete = useCallback((researchData: BlogResearchResponse) => {
|
||||
setResearch(researchData);
|
||||
}, []);
|
||||
const formattedAngles = dedupeTitles(
|
||||
(researchData?.suggested_angles || []).map(formatContentAngleToTitle)
|
||||
);
|
||||
setResearchTitles(formattedAngles);
|
||||
|
||||
// Prefill title from research if no title is currently selected
|
||||
if (!selectedTitle && formattedAngles.length > 0) {
|
||||
const firstTitle = formattedAngles[0];
|
||||
setSelectedTitle(firstTitle);
|
||||
localStorage.setItem('blog_selected_title', firstTitle);
|
||||
}
|
||||
}, [dedupeTitles, formatContentAngleToTitle, selectedTitle]);
|
||||
|
||||
// Handle outline completion with enhanced metadata
|
||||
const handleOutlineComplete = useCallback((result: any) => {
|
||||
if (result?.outline) {
|
||||
setOutline(result.outline);
|
||||
setTitleOptions(result.title_options || []);
|
||||
|
||||
|
||||
const aiTitleOptions: string[] = result.title_options || [];
|
||||
const formattedAngles = dedupeTitles(
|
||||
(research?.suggested_angles || []).map(formatContentAngleToTitle)
|
||||
);
|
||||
const combinedTitleOptions = dedupeTitles([
|
||||
...formattedAngles,
|
||||
...aiTitleOptions
|
||||
]);
|
||||
|
||||
setTitleOptions(combinedTitleOptions);
|
||||
setResearchTitles(formattedAngles);
|
||||
|
||||
const aiTitlesList = dedupeTitles(
|
||||
aiTitleOptions.filter((title: string) => !formattedAngles.some(angle => angle.toLowerCase() === (title || '').toLowerCase().trim()))
|
||||
);
|
||||
setAiGeneratedTitles(aiTitlesList);
|
||||
|
||||
const nextSelectedTitle = aiTitlesList[0] || formattedAngles[0] || combinedTitleOptions[0] || '';
|
||||
if (nextSelectedTitle) {
|
||||
setSelectedTitle(nextSelectedTitle);
|
||||
}
|
||||
|
||||
// Store enhanced metadata
|
||||
if (result.source_mapping_stats) {
|
||||
setSourceMappingStats(result.source_mapping_stats);
|
||||
@@ -92,35 +180,13 @@ export const useBlogWriterState = () => {
|
||||
if (result.research_coverage) {
|
||||
setResearchCoverage(result.research_coverage);
|
||||
}
|
||||
|
||||
// Separate research titles from AI-generated titles
|
||||
if (result.title_options && research) {
|
||||
const researchAngles = research.suggested_angles || [];
|
||||
const researchTitlesList = result.title_options.filter((title: string) =>
|
||||
researchAngles.some((angle: string) => title.toLowerCase().includes(angle.toLowerCase().substring(0, 20)))
|
||||
);
|
||||
const aiTitlesList = result.title_options.filter((title: string) =>
|
||||
!researchTitlesList.includes(title)
|
||||
);
|
||||
|
||||
setResearchTitles(researchTitlesList);
|
||||
setAiGeneratedTitles(aiTitlesList);
|
||||
|
||||
// Auto-select first AI-generated title if available, otherwise first research title
|
||||
if (aiTitlesList.length > 0) {
|
||||
setSelectedTitle(aiTitlesList[0]);
|
||||
} else if (researchTitlesList.length > 0) {
|
||||
setSelectedTitle(researchTitlesList[0]);
|
||||
} else if (result.title_options.length > 0) {
|
||||
setSelectedTitle(result.title_options[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Save to localStorage for persistence (using shared cache utility)
|
||||
try {
|
||||
const { blogWriterCache } = require('../services/blogWriterCache');
|
||||
blogWriterCache.cacheOutline(result.outline, result.title_options);
|
||||
localStorage.setItem('blog_selected_title', result.title_options?.[0] || '');
|
||||
blogWriterCache.cacheOutline(result.outline, combinedTitleOptions);
|
||||
localStorage.setItem('blog_title_options', JSON.stringify(combinedTitleOptions));
|
||||
localStorage.setItem('blog_selected_title', nextSelectedTitle || '');
|
||||
console.log('Saved outline data to localStorage');
|
||||
} catch (error) {
|
||||
console.error('Error saving outline data:', error);
|
||||
@@ -129,7 +195,7 @@ export const useBlogWriterState = () => {
|
||||
setOutlineTaskId(null);
|
||||
// Reset outline confirmation when new outline is generated
|
||||
setOutlineConfirmed(false);
|
||||
}, [research]);
|
||||
}, [research, dedupeTitles, formatContentAngleToTitle]);
|
||||
|
||||
// Handle outline error
|
||||
const handleOutlineError = useCallback((error: any) => {
|
||||
|
||||
Reference in New Issue
Block a user