fix: WYSIWYG editor, content generation, and writing assistant bug fixes
- Fix text selection menu not showing: wire contentRef via inputRef on multiline TextField - Fix blog title not truncating: add min-w-0 for flex item overflow - Fix outline generation 500: escape curly braces in f-string prompt template - Fix content generation 'NoneType not callable': replace SessionLocal() with get_session_for_user(), add db param to MediumBlogGenerator, fix signature mismatch in database_task_manager - Fix writing assistant suggest 500: add auth + user_id to API endpoint and service, replace sync requests with httpx.AsyncClient - Fix hallucination detector 404: explicitly include router in main.py and app.py - Fix missing error_data in task failure responses - Hide CopilotKit web inspector button - Remove hardcoded fallback suggestions from SmartTypingAssist - Fix stale closure refs in SmartTypingAssist handleTypingChange - Add two-column editor layout, stats bar, section hover menu - Various subscription, billing, and research module improvements
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { BlogOutlineSection, BlogResearchResponse, BlogSEOMetadataResponse, BlogSEOAnalyzeResponse, SourceMappingStats, GroundingInsights, OptimizationResults, ResearchCoverage } from '../services/blogWriterApi';
|
||||
import { researchCache } from '../services/researchCache';
|
||||
import { blogWriterCache } from '../services/blogWriterCache';
|
||||
|
||||
const MINOR_TITLE_WORDS = new Set([
|
||||
'a', 'an', 'and', 'or', 'but', 'the', 'for', 'nor', 'on', 'at', 'to', 'from', 'by',
|
||||
@@ -94,36 +95,70 @@ export const useBlogWriterState = () => {
|
||||
|
||||
// Cache recovery - restore most recent research on page load
|
||||
useEffect(() => {
|
||||
const cachedEntries = researchCache.getAllCachedEntries();
|
||||
if (cachedEntries.length > 0) {
|
||||
// Get the most recent cached research
|
||||
const mostRecent = cachedEntries[0];
|
||||
console.log('Restoring cached research from page load:', mostRecent.keywords);
|
||||
setResearch(mostRecent.result);
|
||||
|
||||
// Also try to restore outline if it exists in localStorage
|
||||
try {
|
||||
const savedOutline = localStorage.getItem('blog_outline');
|
||||
const savedTitleOptions = localStorage.getItem('blog_title_options');
|
||||
const savedSelectedTitle = localStorage.getItem('blog_selected_title');
|
||||
const restoreState = async () => {
|
||||
const cachedEntries = researchCache.getAllCachedEntries();
|
||||
if (cachedEntries.length > 0) {
|
||||
// Get the most recent cached research
|
||||
const mostRecent = cachedEntries[0];
|
||||
console.log('Restoring cached research from page load:', mostRecent.keywords);
|
||||
setResearch(mostRecent.result);
|
||||
|
||||
if (savedOutline) {
|
||||
setOutline(JSON.parse(savedOutline));
|
||||
// Also try to restore outline if it exists in localStorage
|
||||
try {
|
||||
const savedOutline = localStorage.getItem('blog_outline');
|
||||
const savedTitleOptions = localStorage.getItem('blog_title_options');
|
||||
const savedSelectedTitle = localStorage.getItem('blog_selected_title');
|
||||
|
||||
if (savedOutline) {
|
||||
const parsedOutline = JSON.parse(savedOutline);
|
||||
setOutline(parsedOutline);
|
||||
|
||||
// Restore content sections from cache when outline is available
|
||||
const outlineIds = parsedOutline.map((s: any) => String(s.id));
|
||||
const cachedContent = blogWriterCache.getCachedContent(outlineIds);
|
||||
if (cachedContent && Object.keys(cachedContent).length > 0) {
|
||||
setSections(cachedContent);
|
||||
console.log('Restored content sections from cache', { sections: Object.keys(cachedContent).length });
|
||||
}
|
||||
}
|
||||
if (savedTitleOptions) {
|
||||
setTitleOptions(JSON.parse(savedTitleOptions));
|
||||
}
|
||||
if (savedSelectedTitle) {
|
||||
setSelectedTitle(savedSelectedTitle);
|
||||
}
|
||||
|
||||
// Restore contentConfirmed from localStorage
|
||||
const savedContentConfirmed = localStorage.getItem('blog_content_confirmed');
|
||||
if (savedContentConfirmed === 'true') {
|
||||
setContentConfirmed(true);
|
||||
}
|
||||
|
||||
console.log('Restored outline, content, and title data from localStorage');
|
||||
} catch (error) {
|
||||
console.error('Error restoring outline data:', error);
|
||||
}
|
||||
if (savedTitleOptions) {
|
||||
setTitleOptions(JSON.parse(savedTitleOptions));
|
||||
}
|
||||
if (savedSelectedTitle) {
|
||||
setSelectedTitle(savedSelectedTitle);
|
||||
}
|
||||
|
||||
console.log('Restored outline and title data from localStorage');
|
||||
} catch (error) {
|
||||
console.error('Error restoring outline data:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
restoreState();
|
||||
}, []);
|
||||
|
||||
// Persist contentConfirmed to localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem('blog_content_confirmed', String(contentConfirmed));
|
||||
} catch {}
|
||||
}, [contentConfirmed]);
|
||||
|
||||
// Persist sections to blogWriterCache whenever they change
|
||||
useEffect(() => {
|
||||
const outlineIds = outline.map(s => String(s.id));
|
||||
if (outlineIds.length > 0 && Object.keys(sections).length > 0) {
|
||||
blogWriterCache.cacheContent(sections, outlineIds);
|
||||
}
|
||||
}, [sections, outline]);
|
||||
|
||||
// Handle research completion
|
||||
const handleResearchComplete = useCallback((researchData: BlogResearchResponse) => {
|
||||
setResearch(researchData);
|
||||
|
||||
@@ -130,6 +130,13 @@ export const usePhaseNavigation = (
|
||||
if (currentPhase === '') {
|
||||
return; // Don't validate empty phase - it's intentional for landing page
|
||||
}
|
||||
|
||||
// If user manually selected this phase, respect their choice even if data
|
||||
// hasn't been restored yet (e.g., on page load before cache restoration).
|
||||
// The data restoration effects will populate the necessary state shortly.
|
||||
if (userSelectedPhase) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = phases.find(p => p.id === currentPhase);
|
||||
if (!current) {
|
||||
@@ -146,7 +153,7 @@ export const usePhaseNavigation = (
|
||||
setCurrentPhase(fallback.id);
|
||||
}
|
||||
}
|
||||
}, [phases, currentPhase, research]);
|
||||
}, [phases, currentPhase, research, userSelectedPhase]);
|
||||
|
||||
// Auto-update current phase based on completion status (only if user hasn't manually selected a phase)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const usePriority2Alerts = (
|
||||
|
||||
const generateAlerts = useCallback((data: DashboardData): Priority2Alert[] => {
|
||||
const generatedAlerts: Priority2Alert[] = [];
|
||||
const currentUsage = data.current_usage;
|
||||
const currentUsage = data.total_usage;
|
||||
const limits = data.limits;
|
||||
const projections = data.projections;
|
||||
|
||||
|
||||
120
frontend/src/hooks/useResearchSubmit.ts
Normal file
120
frontend/src/hooks/useResearchSubmit.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { blogWriterApi, BlogResearchRequest, BlogResearchResponse } from '../services/blogWriterApi';
|
||||
import { useBlogWriterResearchPolling } from './usePolling';
|
||||
import { researchCache } from '../services/researchCache';
|
||||
|
||||
export interface UseResearchSubmitOptions {
|
||||
onResearchComplete?: (research: BlogResearchResponse) => void;
|
||||
navigateToPhase?: (phase: string) => void;
|
||||
}
|
||||
|
||||
export interface UseResearchSubmitReturn {
|
||||
startResearch: (keywords: string, blogLength?: string, industry?: string, audience?: string) => Promise<BlogResearchResponse | null>;
|
||||
isSubmitting: boolean;
|
||||
showProgressModal: boolean;
|
||||
setShowProgressModal: (show: boolean) => void;
|
||||
currentMessage: string;
|
||||
currentStatus: string;
|
||||
progressMessages: Array<{ timestamp: string; message: string }>;
|
||||
error: string | null;
|
||||
result: BlogResearchResponse | null;
|
||||
isPolling: boolean;
|
||||
}
|
||||
|
||||
export const useResearchSubmit = ({
|
||||
onResearchComplete,
|
||||
navigateToPhase,
|
||||
}: UseResearchSubmitOptions): UseResearchSubmitReturn => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showProgressModal, setShowProgressModal] = useState(false);
|
||||
const [currentMessage, setCurrentMessage] = useState('');
|
||||
const keywordListRef = useRef<string[]>([]);
|
||||
|
||||
const polling = useBlogWriterResearchPolling({
|
||||
onProgress: (message) => {
|
||||
setCurrentMessage(message);
|
||||
},
|
||||
onComplete: (result) => {
|
||||
if (result) {
|
||||
researchCache.cacheResult(
|
||||
keywordListRef.current,
|
||||
result.industry || 'General',
|
||||
result.target_audience || 'General',
|
||||
result
|
||||
);
|
||||
}
|
||||
onResearchComplete?.(result);
|
||||
setCurrentMessage('');
|
||||
setShowProgressModal(false);
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
setCurrentMessage('');
|
||||
setShowProgressModal(false);
|
||||
setIsSubmitting(false);
|
||||
},
|
||||
});
|
||||
|
||||
const startResearch = useCallback(async (
|
||||
keywords: string,
|
||||
blogLength: string = '1000',
|
||||
industry: string = 'General',
|
||||
audience: string = 'General',
|
||||
): Promise<BlogResearchResponse | null> => {
|
||||
const trimmed = keywords.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const keywordList = trimmed.includes(',')
|
||||
? trimmed.split(',').map(k => k.trim()).filter(Boolean)
|
||||
: [trimmed];
|
||||
|
||||
keywordListRef.current = keywordList;
|
||||
|
||||
const cachedResult = researchCache.getCachedResult(keywordList, industry, audience);
|
||||
if (cachedResult) {
|
||||
onResearchComplete?.(cachedResult);
|
||||
setIsSubmitting(false);
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
navigateToPhase?.('research');
|
||||
|
||||
setShowProgressModal(true);
|
||||
setCurrentMessage('Starting research...');
|
||||
|
||||
const payload: BlogResearchRequest = {
|
||||
keywords: keywordList,
|
||||
industry,
|
||||
target_audience: audience,
|
||||
word_count_target: parseInt(blogLength),
|
||||
};
|
||||
|
||||
const { task_id } = await blogWriterApi.startResearch(payload);
|
||||
polling.startPolling(task_id);
|
||||
return null;
|
||||
} catch (error) {
|
||||
setCurrentMessage('');
|
||||
setShowProgressModal(false);
|
||||
setIsSubmitting(false);
|
||||
throw error;
|
||||
}
|
||||
}, [onResearchComplete, navigateToPhase, polling]);
|
||||
|
||||
return {
|
||||
startResearch,
|
||||
isSubmitting,
|
||||
showProgressModal,
|
||||
setShowProgressModal,
|
||||
currentMessage,
|
||||
currentStatus: polling.currentStatus,
|
||||
progressMessages: polling.progressMessages,
|
||||
error: polling.error,
|
||||
result: polling.result,
|
||||
isPolling: polling.isPolling,
|
||||
};
|
||||
};
|
||||
|
||||
export default useResearchSubmit;
|
||||
@@ -3,7 +3,7 @@ import { useAuth } from '@clerk/clerk-react';
|
||||
import { showToastNotification } from '../utils/toastNotifications';
|
||||
import { getTasksNeedingIntervention, TaskNeedingIntervention } from '../api/schedulerDashboard';
|
||||
import { isBackendCooldownActive, logBackendCooldownSkipOnce } from '../api/client';
|
||||
import { isPodcastOnlyDemoMode } from '../utils/demoMode';
|
||||
import { isFeatureOnlyMode } from '../utils/demoMode';
|
||||
|
||||
/**
|
||||
* Hook to poll for tasks needing intervention and show toast notifications
|
||||
@@ -20,8 +20,8 @@ export function useSchedulerTaskAlerts(options: {
|
||||
const shownTaskIdsRef = useRef<Set<number>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
// Skip scheduler alerts in podcast-only mode (endpoint not available)
|
||||
if (isPodcastOnlyDemoMode()) {
|
||||
// Skip scheduler alerts in feature-limited mode (endpoint not available)
|
||||
if (isFeatureOnlyMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user