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:
ajaysi
2026-05-14 09:11:30 +05:30
parent 7385100017
commit 928c2f20aa
113 changed files with 4344 additions and 10064 deletions

View File

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

View File

@@ -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(() => {

View File

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

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

View File

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