import { useEffect, useState, useCallback, useRef } from 'react'; import { getPost, savePost as savePostApi, deletePost as deletePostApi, type SavePostPayload } from '../services/posts'; import { ghostPreview as ghostPreviewApi, ghostPublish as ghostPublishApi, type GhostPostStatus } from '../services/ghost'; import type { Metadata } from '../components/MetadataPanel'; export function usePostEditor(initialPostId?: string | null) { const [postId, setPostId] = useState(null); const [draft, setDraft] = useState(''); const [meta, setMeta] = useState({ title: '', tagsText: '', featureImage: '' }); const [postClips, setPostClips] = useState>([]); const [postStatus, setPostStatus] = useState<'inbox' | 'editing' | 'ready_for_publish' | 'published' | 'archived'>('editing'); const [promptText, setPromptText] = useState(''); const [toast, setToast] = useState<{ open: boolean; message: string; severity: 'success' | 'error' } | null>(null); const [activeStep, setActiveStep] = useState(0); const [previewHtml, setPreviewHtml] = useState(''); const [previewLoading, setPreviewLoading] = useState(false); const [previewError, setPreviewError] = useState(null); const [genImageKeys, setGenImageKeys] = useState([]); const [referenceImageKeys, setReferenceImageKeys] = useState([]); const [generatedDraft, setGeneratedDraft] = useState(''); const [imagePlaceholders, setImagePlaceholders] = useState([]); const [generationSources, setGenerationSources] = useState>([]); const autoSaveTimeoutRef = useRef | null>(null); // Streaming state (persisted across navigation) const [isGenerating, setIsGenerating] = useState(false); const [streamingContent, setStreamingContent] = useState(''); const [tokenCount, setTokenCount] = useState(0); const [generationError, setGenerationError] = useState(''); const [selectedAuthor, setSelectedAuthor] = useState(undefined); useEffect(() => { const savedId = initialPostId || localStorage.getItem('voxblog_post_id'); const savedLocal = localStorage.getItem('voxblog_draft'); if (savedLocal) setDraft(savedLocal); if (savedId) { (async () => { try { const data = await getPost(savedId); setDraft(data.contentHtml || ''); setPostId(data.id || savedId); localStorage.setItem('voxblog_post_id', data.id || savedId); if (data.contentHtml) localStorage.setItem('voxblog_draft', data.contentHtml); if (Array.isArray(data.audioClips)) setPostClips(data.audioClips); setMeta(m => ({ ...m, title: data.title || '', tagsText: data.tagsText || '', featureImage: data.featureImage || '' })); if (data.status) setPostStatus(data.status); setPromptText(data.prompt || ''); if (Array.isArray(data.selectedImageKeys)) setGenImageKeys(data.selectedImageKeys); if (Array.isArray(data.referenceImageKeys)) setReferenceImageKeys(data.referenceImageKeys); if (data.generatedDraft) setGeneratedDraft(data.generatedDraft); if (Array.isArray(data.imagePlaceholders)) setImagePlaceholders(data.imagePlaceholders); if (Array.isArray(data.generationSources)) setGenerationSources(data.generationSources); } catch {} })(); } }, [initialPostId]); const savePost = useCallback(async (overrides?: Partial, silent = false) => { localStorage.setItem('voxblog_draft', draft); const payload: SavePostPayload = { id: postId ?? undefined, title: meta.title || undefined, contentHtml: draft, tags: meta.tagsText ? meta.tagsText.split(',').map(t => t.trim()).filter(Boolean) : undefined, featureImage: meta.featureImage || undefined, status: postStatus, prompt: promptText || undefined, selectedImageKeys: genImageKeys.length > 0 ? genImageKeys : undefined, referenceImageKeys: referenceImageKeys.length > 0 ? referenceImageKeys : undefined, generatedDraft: generatedDraft || undefined, imagePlaceholders: imagePlaceholders.length > 0 ? imagePlaceholders : undefined, generationSources: generationSources.length > 0 ? generationSources : undefined, ...(overrides || {}), }; const data = await savePostApi(payload); if (data?.id) { setPostId(data.id); localStorage.setItem('voxblog_post_id', data.id); } if (!silent) { setToast({ open: true, message: 'Post saved', severity: 'success' }); } return data; }, [postId, draft, meta, postStatus, promptText, genImageKeys, referenceImageKeys, generatedDraft, imagePlaceholders, generationSources]); const deletePost = async () => { if (!postId) return; await deletePostApi(postId); localStorage.removeItem('voxblog_post_id'); }; const publishToGhost = async (status: GhostPostStatus) => { const tags = meta.tagsText.split(',').map(t => t.trim()).filter(Boolean); await ghostPublishApi({ title: meta.title || 'Untitled', html: draft, feature_image: meta.featureImage || null, tags, status, authors: selectedAuthor ? [selectedAuthor] : undefined, }); // Update post status after successful Ghost publish const newStatus = status === 'published' ? 'published' : 'ready_for_publish'; setPostStatus(newStatus); // Save updated status to backend await savePost({ status: newStatus }); setToast({ open: true, message: status === 'published' ? 'Published to Ghost' : 'Draft saved to Ghost', severity: 'success' }); }; const refreshPreview = async () => { setPreviewLoading(true); setPreviewError(null); try { const data = await ghostPreviewApi({ html: draft, feature_image: meta.featureImage || undefined }); setPreviewHtml(data.html || ''); } catch (e: any) { setPreviewError(e?.message || 'Failed to generate preview'); } finally { setPreviewLoading(false); } }; useEffect(() => { if (activeStep === 5) refreshPreview(); }, [activeStep]); // Auto-save with debouncing (silent save) const triggerAutoSave = useCallback((delay = 500) => { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current); } autoSaveTimeoutRef.current = setTimeout(() => { savePost({}, true).catch(err => { console.error('Auto-save failed:', err); }); }, delay); }, [savePost]); // Immediate auto-save (no debounce, silent) const triggerImmediateAutoSave = useCallback(async () => { try { await savePost({}, true); } catch (err) { console.error('Immediate auto-save failed:', err); } }, [savePost]); const toggleGenImage = useCallback((key: string) => { setGenImageKeys(prev => { const newKeys = prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]; // Trigger immediate auto-save after state update setTimeout(() => triggerImmediateAutoSave(), 0); return newKeys; }); }, [triggerImmediateAutoSave]); const toggleReferenceImage = useCallback((key: string) => { setReferenceImageKeys(prev => { const newKeys = prev.includes(key) ? prev.filter(k => k !== key) : [...prev, key]; // Trigger immediate auto-save after state update setTimeout(() => triggerImmediateAutoSave(), 0); return newKeys; }); }, [triggerImmediateAutoSave]); // Cleanup timeout on unmount useEffect(() => { return () => { if (autoSaveTimeoutRef.current) { clearTimeout(autoSaveTimeoutRef.current); } }; }, []); return { // state postId, draft, meta, postClips, postStatus, promptText, toast, activeStep, previewHtml, previewLoading, previewError, genImageKeys, referenceImageKeys, generatedDraft, imagePlaceholders, generationSources, isGenerating, streamingContent, tokenCount, generationError, selectedAuthor, // setters setDraft, setMeta, setPostClips, setPostStatus, setPromptText, setToast, setActiveStep, setGeneratedDraft, setImagePlaceholders, setGenerationSources, setIsGenerating, setStreamingContent, setTokenCount, setGenerationError, setSelectedAuthor, // actions savePost, deletePost, publishToGhost, refreshPreview, toggleGenImage, toggleReferenceImage, triggerAutoSave, triggerImmediateAutoSave, } as const; }