- Simulate progress step advancement at 1.5s intervals during API calls so users see incremental progress instead of all-at-once bursts - PersonaChip skips API calls entirely in feature-only mode (no console spam) - getUserPersonas/getPlatformPersona return null on 404 instead of throwing - PersonaChip shows neutral gray state when no persona data exists - Back button now clears draft to return to LinkedIn writer home screen - Article title extracted from markdown content (fixes KeyError) - InitialRouteHandler: demo mode subscribes getDefaultLandingRoute() - Header: back button shown when draft exists, navigates to home screen
729 lines
32 KiB
TypeScript
729 lines
32 KiB
TypeScript
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
import {
|
|
loadHistory,
|
|
clearHistory,
|
|
getHistoryLength,
|
|
getPreferences,
|
|
savePreferences,
|
|
getCurrentContext,
|
|
saveCurrentContext,
|
|
summarizeHistory,
|
|
type ChatMsg,
|
|
type LinkedInPreferences
|
|
} from '../utils/storageUtils';
|
|
import { getContextAwareSuggestions, mapPostType, mapTone, mapIndustry, mapSearchEngine, readPrefs } from '../utils/linkedInWriterUtils';
|
|
import { linkedInWriterApi, GroundingLevel } from '../../../services/linkedInWriterApi';
|
|
|
|
export function useLinkedInWriter() {
|
|
// Core state
|
|
const [draft, setDraft] = useState('');
|
|
const [context, setContext] = useState('');
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
const [isPreviewing, setIsPreviewing] = useState(false);
|
|
const [livePreviewHtml, setLivePreviewHtml] = useState('');
|
|
const [pendingEdit, setPendingEdit] = useState<{ src: string; target: string } | null>(null);
|
|
const [loadingMessage, setLoadingMessage] = useState('');
|
|
const [currentAction, setCurrentAction] = useState<string | null>(null);
|
|
|
|
// Grounding data state
|
|
const [researchSources, setResearchSources] = useState<any[]>([]);
|
|
const [citations, setCitations] = useState<any[]>([]);
|
|
const [qualityMetrics, setQualityMetrics] = useState<any>(null);
|
|
const [groundingEnabled, setGroundingEnabled] = useState(false);
|
|
const [searchQueries, setSearchQueries] = useState<string[]>([]);
|
|
|
|
// Progress state (lightweight custom system)
|
|
type ProgressStatus = 'pending' | 'active' | 'completed' | 'error';
|
|
type ProgressStep = {
|
|
id: string;
|
|
label: string;
|
|
status: ProgressStatus;
|
|
message?: string;
|
|
details?: any;
|
|
timestamp?: string;
|
|
};
|
|
const [progressSteps, setProgressSteps] = useState<ProgressStep[]>([]);
|
|
const [progressActive, setProgressActive] = useState<boolean>(false);
|
|
|
|
// Chat history state
|
|
const [historyVersion, setHistoryVersion] = useState<number>(0);
|
|
const [chatHistory, setChatHistory] = useState<ChatMsg[]>([]);
|
|
const [userPreferences, setUserPreferences] = useState<LinkedInPreferences>(getPreferences());
|
|
|
|
// UI state
|
|
const currentSuggestions = useMemo(() => getContextAwareSuggestions(
|
|
userPreferences,
|
|
draft,
|
|
chatHistory.slice(-5),
|
|
userPreferences.last_used_actions || []
|
|
), [userPreferences, draft, chatHistory]);
|
|
const [showContextPanel, setShowContextPanel] = useState(false);
|
|
const [showPreferencesModal, setShowPreferencesModal] = useState(false);
|
|
const [showContextModal, setShowContextModal] = useState(false);
|
|
const [showPreview, setShowPreview] = useState(false);
|
|
const [justGeneratedContent, setJustGeneratedContent] = useState(false);
|
|
|
|
// Track action usage and update preferences
|
|
const trackActionUsage = useCallback((actionName: string) => {
|
|
const currentPrefs = getPreferences();
|
|
const updatedActions = [...(currentPrefs.last_used_actions || []), actionName].slice(-5);
|
|
savePreferences({ last_used_actions: updatedActions });
|
|
setUserPreferences(prev => ({ ...prev, last_used_actions: updatedActions }));
|
|
|
|
// Mark content as just generated for content creation actions
|
|
if (['generateLinkedInPost', 'generateLinkedInArticle', 'generateLinkedInCarousel', 'generateLinkedInVideoScript'].includes(actionName)) {
|
|
setJustGeneratedContent(true);
|
|
// Reset the flag after 30 seconds
|
|
setTimeout(() => setJustGeneratedContent(false), 30000);
|
|
}
|
|
}, []);
|
|
|
|
// ── Direct generation methods (UI-driven, no CopilotKit dependency) ──────────
|
|
const generatePost = useCallback(async (params?: any) => {
|
|
const prefs = readPrefs();
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
|
|
detail: { action: 'generateLinkedInPost', message: 'Generating LinkedIn post...' }
|
|
}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
|
|
steps: [
|
|
{ id: 'personalize', label: 'Personalizing topic & context' },
|
|
{ id: 'prepare_queries', label: 'Preparing research queries' },
|
|
{ id: 'research', label: 'Conducting research & analysis' },
|
|
{ id: 'grounding', label: 'Applying AI grounding' },
|
|
{ id: 'content_generation', label: 'Generating content' },
|
|
{ id: 'citations', label: 'Extracting citations' },
|
|
{ id: 'quality_analysis', label: 'Quality assessment' },
|
|
{ id: 'finalize', label: 'Finalizing & optimizing' }
|
|
]
|
|
}}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
|
}));
|
|
|
|
// Simulate progress advancement during API call
|
|
const progressStepIds = ['prepare_queries', 'research', 'grounding', 'content_generation', 'citations', 'quality_analysis'];
|
|
let stepIndex = 0;
|
|
const progressInterval = setInterval(() => {
|
|
if (stepIndex < progressStepIds.length) {
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
|
}));
|
|
stepIndex++;
|
|
}
|
|
}, 1500);
|
|
|
|
try {
|
|
const res = await linkedInWriterApi.generatePost({
|
|
topic: params?.topic || prefs.topic || 'AI transformation in business',
|
|
industry: mapIndustry(params?.industry || prefs.industry),
|
|
post_type: mapPostType(params?.post_type || prefs.post_type),
|
|
tone: mapTone(params?.tone || prefs.tone),
|
|
target_audience: params?.target_audience || prefs.target_audience || 'Business leaders and professionals',
|
|
key_points: params?.key_points || prefs.key_points || [],
|
|
include_hashtags: params?.include_hashtags ?? (prefs.include_hashtags ?? true),
|
|
include_call_to_action: params?.include_call_to_action ?? (prefs.include_call_to_action ?? true),
|
|
research_enabled: params?.research_enabled ?? (prefs.research_enabled ?? true),
|
|
search_engine: mapSearchEngine(params?.search_engine || prefs.search_engine),
|
|
max_length: params?.max_length || prefs.max_length || 2000,
|
|
grounding_level: 'enhanced' as GroundingLevel,
|
|
include_citations: true
|
|
});
|
|
clearInterval(progressInterval);
|
|
if (res.success && res.data) {
|
|
// Catch up remaining steps
|
|
while (stepIndex < progressStepIds.length) {
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
|
}));
|
|
stepIndex++;
|
|
}
|
|
const content = res.data.content;
|
|
const hashtags = res.data.hashtags?.map((h: any) => h.hashtag).join(' ') || '';
|
|
const cta = res.data.call_to_action || '';
|
|
let fullContent = content;
|
|
if (hashtags) fullContent += `\n\n${hashtags}`;
|
|
if (cta) fullContent += `\n\n${cta}`;
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:updateGroundingData', { detail: {
|
|
researchSources: res.research_sources || [],
|
|
citations: res.data?.citations || [],
|
|
qualityMetrics: res.data?.quality_metrics || null,
|
|
groundingEnabled: res.data?.grounding_enabled || false,
|
|
searchQueries: res.data?.search_queries || []
|
|
}}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: fullContent }));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'finalize', status: 'completed', message: 'Content finalized' } }));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
trackActionUsage('generateLinkedInPost');
|
|
return { success: true, data: res.data };
|
|
}
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
|
return { success: false, error: res.error || 'Generation failed' };
|
|
} catch (error: any) {
|
|
clearInterval(progressInterval);
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
|
return { success: false, error: error.message || 'Generation failed' };
|
|
}
|
|
}, []);
|
|
|
|
const generateArticle = useCallback(async (params?: any) => {
|
|
const prefs = readPrefs();
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
|
|
detail: { action: 'generateLinkedInArticle', message: 'Generating LinkedIn article...' }
|
|
}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
|
|
steps: [
|
|
{ id: 'personalize', label: 'Personalizing topic & context' },
|
|
{ id: 'prepare_queries', label: 'Preparing research queries' },
|
|
{ id: 'research', label: 'Conducting research & analysis' },
|
|
{ id: 'grounding', label: 'Applying AI grounding' },
|
|
{ id: 'content_generation', label: 'Generating article content' },
|
|
{ id: 'citations', label: 'Extracting citations' },
|
|
{ id: 'quality_analysis', label: 'Quality assessment' },
|
|
{ id: 'finalize', label: 'Finalizing & optimizing' }
|
|
]
|
|
}}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
|
}));
|
|
|
|
const progressStepIds = ['prepare_queries', 'research', 'grounding', 'content_generation', 'citations', 'quality_analysis'];
|
|
let stepIndex = 0;
|
|
const progressInterval = setInterval(() => {
|
|
if (stepIndex < progressStepIds.length) {
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
|
}));
|
|
stepIndex++;
|
|
}
|
|
}, 1500);
|
|
|
|
try {
|
|
const res = await linkedInWriterApi.generateArticle({
|
|
topic: params?.topic || prefs.topic || 'Digital transformation strategies',
|
|
industry: mapIndustry(params?.industry || prefs.industry),
|
|
tone: mapTone(params?.tone || prefs.tone),
|
|
target_audience: params?.target_audience || prefs.target_audience || 'Industry professionals and executives',
|
|
key_sections: params?.key_sections || prefs.key_sections || [],
|
|
include_images: params?.include_images ?? (prefs.include_images ?? true),
|
|
seo_optimization: params?.seo_optimization ?? (prefs.seo_optimization ?? true),
|
|
research_enabled: params?.research_enabled ?? (prefs.research_enabled ?? true),
|
|
search_engine: mapSearchEngine(params?.search_engine || prefs.search_engine),
|
|
word_count: params?.word_count || prefs.word_count || 1500,
|
|
grounding_level: 'enhanced' as GroundingLevel,
|
|
include_citations: true
|
|
});
|
|
clearInterval(progressInterval);
|
|
if (res.success && res.data) {
|
|
while (stepIndex < progressStepIds.length) {
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
|
}));
|
|
stepIndex++;
|
|
}
|
|
const content = `# ${res.data.title}\n\n${res.data.content}`;
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:updateGroundingData', { detail: {
|
|
researchSources: res.research_sources || [],
|
|
citations: res.data?.citations || [],
|
|
qualityMetrics: res.data?.quality_metrics || null,
|
|
groundingEnabled: res.data?.grounding_enabled || false,
|
|
searchQueries: res.data?.search_queries || []
|
|
}}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'finalize', status: 'completed', message: 'Article finalized' } }));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
trackActionUsage('generateLinkedInArticle');
|
|
return { success: true, data: res.data };
|
|
}
|
|
clearInterval(progressInterval);
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
|
return { success: false, error: res.error || 'Generation failed' };
|
|
} catch (error: any) {
|
|
clearInterval(progressInterval);
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
|
return { success: false, error: error.message || 'Generation failed' };
|
|
}
|
|
}, []);
|
|
|
|
const generateCarousel = useCallback(async (params?: any) => {
|
|
const prefs = readPrefs();
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
|
|
detail: { action: 'generateLinkedInCarousel', message: 'Generating LinkedIn carousel...' }
|
|
}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
|
|
steps: [
|
|
{ id: 'personalize', label: 'Personalizing topic & context' },
|
|
{ id: 'prepare_queries', label: 'Preparing research queries' },
|
|
{ id: 'research', label: 'Conducting research & analysis' },
|
|
{ id: 'grounding', label: 'Applying AI grounding' },
|
|
{ id: 'content_generation', label: 'Generating carousel slides' },
|
|
{ id: 'citations', label: 'Extracting citations' },
|
|
{ id: 'quality_analysis', label: 'Quality assessment' },
|
|
{ id: 'finalize', label: 'Finalizing & optimizing' }
|
|
]
|
|
}}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
|
}));
|
|
|
|
const progressStepIds = ['prepare_queries', 'research', 'grounding', 'content_generation', 'citations', 'quality_analysis'];
|
|
let stepIndex = 0;
|
|
const progressInterval = setInterval(() => {
|
|
if (stepIndex < progressStepIds.length) {
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
|
}));
|
|
stepIndex++;
|
|
}
|
|
}, 1500);
|
|
|
|
try {
|
|
const res = await linkedInWriterApi.generateCarousel({
|
|
topic: params?.topic || prefs.topic || 'Professional development tips',
|
|
industry: mapIndustry(params?.industry || prefs.industry),
|
|
number_of_slides: params?.number_of_slides || prefs.number_of_slides || 8,
|
|
tone: mapTone(params?.tone || prefs.tone),
|
|
target_audience: params?.target_audience || prefs.target_audience || 'Professionals seeking growth',
|
|
key_takeaways: params?.key_takeaways || prefs.key_takeaways || [],
|
|
include_cover_slide: params?.include_cover_slide ?? (prefs.include_cover_slide ?? true),
|
|
include_cta_slide: params?.include_cta_slide ?? (prefs.include_cta_slide ?? true),
|
|
visual_style: params?.visual_style || prefs.visual_style || 'modern'
|
|
});
|
|
clearInterval(progressInterval);
|
|
if (res.success && res.data) {
|
|
while (stepIndex < progressStepIds.length) {
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
|
}));
|
|
stepIndex++;
|
|
}
|
|
let content = `# ${res.data.title}\n\n`;
|
|
res.data.slides.forEach((slide: any, index: number) => {
|
|
content += `## Slide ${index + 1}: ${slide.title}\n\n${slide.content}\n\n`;
|
|
});
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'finalize', status: 'completed', message: 'Carousel finalized' } }));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
trackActionUsage('generateLinkedInCarousel');
|
|
return { success: true, data: res.data };
|
|
}
|
|
clearInterval(progressInterval);
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
|
return { success: false, error: res.error || 'Generation failed' };
|
|
} catch (error: any) {
|
|
clearInterval(progressInterval);
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
|
return { success: false, error: error.message || 'Generation failed' };
|
|
}
|
|
}, []);
|
|
|
|
const generateVideoScript = useCallback(async (params?: any) => {
|
|
const prefs = readPrefs();
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingStart', {
|
|
detail: { action: 'generateLinkedInVideoScript', message: 'Generating LinkedIn video script...' }
|
|
}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressInit', { detail: {
|
|
steps: [
|
|
{ id: 'personalize', label: 'Personalizing topic & context' },
|
|
{ id: 'prepare_queries', label: 'Preparing research queries' },
|
|
{ id: 'research', label: 'Conducting research & analysis' },
|
|
{ id: 'grounding', label: 'Applying AI grounding' },
|
|
{ id: 'content_generation', label: 'Generating video script' },
|
|
{ id: 'citations', label: 'Extracting citations' },
|
|
{ id: 'quality_analysis', label: 'Quality assessment' },
|
|
{ id: 'finalize', label: 'Finalizing & optimizing' }
|
|
]
|
|
}}));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: 'personalize', status: 'active', message: 'Analyzing topic, industry context, and target audience...' }
|
|
}));
|
|
|
|
const progressStepIds = ['prepare_queries', 'research', 'grounding', 'content_generation', 'citations', 'quality_analysis'];
|
|
let stepIndex = 0;
|
|
const progressInterval = setInterval(() => {
|
|
if (stepIndex < progressStepIds.length) {
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
|
}));
|
|
stepIndex++;
|
|
}
|
|
}, 1500);
|
|
|
|
try {
|
|
const res = await linkedInWriterApi.generateVideoScript({
|
|
topic: params?.topic || prefs.topic || 'Professional networking tips',
|
|
industry: mapIndustry(params?.industry || prefs.industry),
|
|
video_length: params?.video_length || prefs.video_length || 60,
|
|
tone: mapTone(params?.tone || prefs.tone),
|
|
target_audience: params?.target_audience || prefs.target_audience || 'Professional networkers',
|
|
key_messages: params?.key_messages || prefs.key_messages || [],
|
|
include_hook: params?.include_hook ?? (prefs.include_hook ?? true),
|
|
include_captions: params?.include_captions ?? (prefs.include_captions ?? true)
|
|
});
|
|
clearInterval(progressInterval);
|
|
if (res.success && res.data) {
|
|
while (stepIndex < progressStepIds.length) {
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', {
|
|
detail: { id: progressStepIds[stepIndex], status: 'completed' }
|
|
}));
|
|
stepIndex++;
|
|
}
|
|
let content = `# Video Script: ${params?.topic || 'Professional Content'}\n\n`;
|
|
content += `## Hook\n${res.data.hook}\n\n`;
|
|
content += `## Main Content\n`;
|
|
res.data.main_content.forEach((scene: any, index: number) => {
|
|
content += `### Scene ${index + 1} (${scene.duration || '30s'})\n${scene.content}\n\n`;
|
|
});
|
|
content += `## Conclusion\n${res.data.conclusion}\n\n`;
|
|
content += `## Video Description\n${res.data.video_description}\n\n`;
|
|
if (res.data.captions) {
|
|
content += `## Captions\n${res.data.captions.join('\n')}\n\n`;
|
|
}
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:updateDraft', { detail: content }));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressStep', { detail: { id: 'finalize', status: 'completed', message: 'Video script finalized' } }));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressComplete'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
trackActionUsage('generateLinkedInVideoScript');
|
|
return { success: true, data: res.data };
|
|
}
|
|
clearInterval(progressInterval);
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: res.error } }));
|
|
return { success: false, error: res.error || 'Generation failed' };
|
|
} catch (error: any) {
|
|
clearInterval(progressInterval);
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:loadingEnd'));
|
|
window.dispatchEvent(new CustomEvent('linkedinwriter:progressError', { detail: { id: 'finalize', details: error.message } }));
|
|
return { success: false, error: error.message || 'Generation failed' };
|
|
}
|
|
}, []);
|
|
|
|
// Initialize chat history and preferences from localStorage
|
|
useEffect(() => {
|
|
const loadInitialData = () => {
|
|
try {
|
|
const history = loadHistory();
|
|
const prefs = getPreferences();
|
|
const savedContext = getCurrentContext();
|
|
|
|
setChatHistory(history);
|
|
setUserPreferences(prefs);
|
|
if (savedContext && !context) {
|
|
setContext(savedContext);
|
|
}
|
|
|
|
console.log('[LinkedIn Writer] Initialized with:', {
|
|
historyCount: history.length,
|
|
preferences: prefs,
|
|
hasContext: !!savedContext
|
|
});
|
|
} catch (error) {
|
|
console.warn('[LinkedIn Writer] Failed to initialize from localStorage:', error);
|
|
}
|
|
};
|
|
|
|
loadInitialData();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// Listen for lightweight progress events
|
|
useEffect(() => {
|
|
const handleProgressInit = (event: CustomEvent) => {
|
|
const steps: Array<{ id: string; label: string; message?: string }> = event.detail?.steps || [];
|
|
const initialized: ProgressStep[] = steps.map((s, index) => ({
|
|
id: s.id,
|
|
label: s.label,
|
|
message: s.message,
|
|
status: index === 0 ? 'active' : 'pending',
|
|
timestamp: new Date().toISOString()
|
|
}));
|
|
setProgressSteps(initialized);
|
|
setProgressActive(true);
|
|
};
|
|
|
|
const handleProgressStep = (event: CustomEvent) => {
|
|
const { id, status, details, message } = event.detail || {};
|
|
if (!id) return;
|
|
setProgressSteps(prev => {
|
|
const updated = prev.map(step => step.id === id ? {
|
|
...step,
|
|
status: (status || 'completed') as ProgressStatus,
|
|
details,
|
|
message,
|
|
timestamp: new Date().toISOString()
|
|
} : step);
|
|
// Mark next pending as active if current completed
|
|
if ((status || 'completed') === 'completed') {
|
|
const nextIdx = updated.findIndex(s => s.status === 'pending');
|
|
if (nextIdx !== -1) {
|
|
updated[nextIdx] = {
|
|
...updated[nextIdx],
|
|
status: 'active',
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
}
|
|
return updated;
|
|
});
|
|
};
|
|
|
|
const handleProgressComplete = () => {
|
|
console.log('[LinkedIn Writer] Progress completed - hiding progress tracker');
|
|
setProgressSteps(prev => prev.map(s => s.status === 'completed' ? s : { ...s, status: 'completed', timestamp: new Date().toISOString() }));
|
|
setProgressActive(false);
|
|
// Keep progress visible for a moment to show completion, then hide
|
|
setTimeout(() => {
|
|
console.log('[LinkedIn Writer] Hiding progress steps after delay');
|
|
setProgressSteps([]);
|
|
}, 1500);
|
|
};
|
|
|
|
const handleProgressError = (event: CustomEvent) => {
|
|
const { id, details } = event.detail || {};
|
|
setProgressSteps(prev => prev.map(s => (id ? (s.id === id) : (s.status === 'active')) ? { ...s, status: 'error', details, timestamp: new Date().toISOString() } : s));
|
|
setProgressActive(false);
|
|
};
|
|
|
|
window.addEventListener('linkedinwriter:progressInit', handleProgressInit as EventListener);
|
|
window.addEventListener('linkedinwriter:progressStep', handleProgressStep as EventListener);
|
|
window.addEventListener('linkedinwriter:progressComplete', handleProgressComplete as EventListener);
|
|
window.addEventListener('linkedinwriter:progressError', handleProgressError as EventListener);
|
|
|
|
return () => {
|
|
window.removeEventListener('linkedinwriter:progressInit', handleProgressInit as EventListener);
|
|
window.removeEventListener('linkedinwriter:progressStep', handleProgressStep as EventListener);
|
|
window.removeEventListener('linkedinwriter:progressComplete', handleProgressComplete as EventListener);
|
|
window.removeEventListener('linkedinwriter:progressError', handleProgressError as EventListener);
|
|
};
|
|
}, []);
|
|
|
|
// Listen for grounding data updates from CopilotKit actions
|
|
useEffect(() => {
|
|
const handleGroundingDataUpdate = (event: CustomEvent) => {
|
|
console.log('[LinkedIn Writer] Received grounding data event:', event.detail);
|
|
|
|
const { researchSources, citations, qualityMetrics, groundingEnabled, searchQueries } = event.detail;
|
|
|
|
console.log('[LinkedIn Writer] Extracted data:', {
|
|
researchSources: researchSources?.length || 0,
|
|
citations: citations?.length || 0,
|
|
qualityMetrics: !!qualityMetrics,
|
|
groundingEnabled,
|
|
searchQueries: searchQueries?.length || 0
|
|
});
|
|
|
|
setResearchSources(researchSources || []);
|
|
setCitations(citations || []);
|
|
setQualityMetrics(qualityMetrics || null);
|
|
setGroundingEnabled(groundingEnabled || false);
|
|
setSearchQueries(searchQueries || []);
|
|
|
|
console.log('[LinkedIn Writer] Grounding data updated:', {
|
|
sourcesCount: researchSources?.length || 0,
|
|
citationsCount: citations?.length || 0,
|
|
hasQualityMetrics: !!qualityMetrics,
|
|
groundingEnabled
|
|
});
|
|
};
|
|
|
|
window.addEventListener('linkedinwriter:updateGroundingData', handleGroundingDataUpdate as EventListener);
|
|
|
|
return () => {
|
|
window.removeEventListener('linkedinwriter:updateGroundingData', handleGroundingDataUpdate as EventListener);
|
|
};
|
|
}, []);
|
|
|
|
// Save context changes to localStorage
|
|
useEffect(() => {
|
|
if (context) {
|
|
saveCurrentContext(context);
|
|
}
|
|
}, [context]);
|
|
|
|
// Handle draft updates from CopilotKit actions
|
|
useEffect(() => {
|
|
const handleUpdateDraft = (event: CustomEvent) => {
|
|
console.log('[LinkedIn Writer] Draft updated:', event.detail?.substring(0, 100) + '...');
|
|
console.log('[LinkedIn Writer] Draft length:', event.detail?.length);
|
|
console.log('[LinkedIn Writer] Setting draft and clearing loading state...');
|
|
setDraft(event.detail);
|
|
setIsGenerating(false);
|
|
setLoadingMessage('');
|
|
setCurrentAction(null);
|
|
// Auto-show preview when new content is generated
|
|
setShowPreview(true);
|
|
// Progress is finalized by the progressStep/progressComplete events dispatched after this
|
|
console.log('[LinkedIn Writer] Draft update complete');
|
|
};
|
|
|
|
const handleAppendDraft = (event: CustomEvent) => {
|
|
setDraft(prev => prev + event.detail);
|
|
};
|
|
|
|
const handleAssistantMessage = (event: CustomEvent) => {
|
|
console.log('LinkedIn Assistant:', event.detail);
|
|
};
|
|
|
|
const handleLoadingStart = (event: CustomEvent) => {
|
|
const { action, message } = event.detail;
|
|
console.log('[LinkedIn Writer] Loading started:', { action, message });
|
|
setCurrentAction(action);
|
|
setLoadingMessage(message);
|
|
setIsGenerating(true);
|
|
};
|
|
|
|
const handleLoadingEnd = (event: CustomEvent) => {
|
|
console.log('[LinkedIn Writer] Loading ended - clearing all loading states');
|
|
setIsGenerating(false);
|
|
setLoadingMessage('');
|
|
setCurrentAction(null);
|
|
console.log('[LinkedIn Writer] Loading state cleared');
|
|
};
|
|
|
|
const handleApplyEdit = (event: CustomEvent) => {
|
|
const target: string = typeof event.detail === 'string' ? event.detail : (event.detail?.target ?? '');
|
|
const src = draft || '';
|
|
if (!target) return;
|
|
setPendingEdit({ src, target });
|
|
setIsPreviewing(true);
|
|
|
|
// Use diff highlighting for professional content changes
|
|
try {
|
|
const { diffMarkup } = require('../utils/contentFormatters');
|
|
setLivePreviewHtml(diffMarkup(src, target));
|
|
} catch (error) {
|
|
// Fallback to simple text if diffMarkup fails to load
|
|
console.warn('Failed to load diffMarkup, using fallback:', error);
|
|
setLivePreviewHtml(target);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('linkedinwriter:updateDraft', handleUpdateDraft as EventListener);
|
|
window.addEventListener('linkedinwriter:appendDraft', handleAppendDraft as EventListener);
|
|
window.addEventListener('linkedinwriter:assistantMessage', handleAssistantMessage as EventListener);
|
|
window.addEventListener('linkedinwriter:applyEdit', handleApplyEdit as EventListener);
|
|
window.addEventListener('linkedinwriter:loadingStart', handleLoadingStart as EventListener);
|
|
window.addEventListener('linkedinwriter:loadingEnd', handleLoadingEnd as EventListener);
|
|
|
|
return () => {
|
|
window.removeEventListener('linkedinwriter:updateDraft', handleUpdateDraft as EventListener);
|
|
window.removeEventListener('linkedinwriter:appendDraft', handleAppendDraft as EventListener);
|
|
window.removeEventListener('linkedinwriter:assistantMessage', handleAssistantMessage as EventListener);
|
|
window.removeEventListener('linkedinwriter:applyEdit', handleApplyEdit as EventListener);
|
|
window.removeEventListener('linkedinwriter:loadingStart', handleLoadingStart as EventListener);
|
|
window.removeEventListener('linkedinwriter:loadingEnd', handleLoadingEnd as EventListener);
|
|
};
|
|
}, [draft]);
|
|
|
|
// Event handlers
|
|
const handleDraftChange = useCallback((value: string) => {
|
|
setDraft(value);
|
|
}, []);
|
|
|
|
const handleContextChange = useCallback((value: string) => {
|
|
setContext(value);
|
|
}, []);
|
|
|
|
const handleClear = useCallback(() => {
|
|
setDraft('');
|
|
setContext('');
|
|
}, []);
|
|
|
|
const handleCopy = useCallback(async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(draft);
|
|
} catch (err) {
|
|
console.error('Failed to copy text: ', err);
|
|
}
|
|
}, [draft]);
|
|
|
|
const handleClearHistory = useCallback(() => {
|
|
clearHistory();
|
|
setHistoryVersion(v => v + 1);
|
|
setChatHistory([]);
|
|
console.log('[LinkedIn Writer] Chat memory cleared by user');
|
|
}, []);
|
|
|
|
return {
|
|
// State
|
|
draft,
|
|
context,
|
|
isGenerating,
|
|
isPreviewing,
|
|
livePreviewHtml,
|
|
pendingEdit,
|
|
loadingMessage,
|
|
currentAction,
|
|
historyVersion,
|
|
chatHistory,
|
|
userPreferences,
|
|
currentSuggestions,
|
|
showContextPanel,
|
|
showPreferencesModal,
|
|
showContextModal,
|
|
showPreview,
|
|
justGeneratedContent,
|
|
|
|
// Setters
|
|
setDraft,
|
|
setContext,
|
|
setIsGenerating,
|
|
setIsPreviewing,
|
|
setLivePreviewHtml,
|
|
setPendingEdit,
|
|
setLoadingMessage,
|
|
setCurrentAction,
|
|
setHistoryVersion,
|
|
setChatHistory,
|
|
setUserPreferences,
|
|
setShowContextPanel,
|
|
setShowPreferencesModal,
|
|
setShowContextModal,
|
|
setShowPreview,
|
|
setJustGeneratedContent: setJustGeneratedContent,
|
|
|
|
// Handlers
|
|
handleDraftChange,
|
|
handleContextChange,
|
|
handleClear,
|
|
handleCopy,
|
|
handleClearHistory,
|
|
|
|
// Utilities
|
|
trackActionUsage,
|
|
getHistoryLength,
|
|
savePreferences,
|
|
summarizeHistory,
|
|
|
|
// Direct generation methods
|
|
generatePost,
|
|
generateArticle,
|
|
generateCarousel,
|
|
generateVideoScript,
|
|
|
|
// Grounding data
|
|
researchSources,
|
|
citations,
|
|
qualityMetrics,
|
|
groundingEnabled,
|
|
searchQueries,
|
|
setResearchSources,
|
|
setCitations,
|
|
setQualityMetrics,
|
|
setGroundingEnabled,
|
|
setSearchQueries,
|
|
|
|
// Progress (exposed to UI)
|
|
progressSteps,
|
|
progressActive
|
|
};
|
|
}
|