diff --git a/assets/js/block-image-generate.js b/assets/js/block-image-generate.js index 9f7c4eb..aa04c1b 100644 --- a/assets/js/block-image-generate.js +++ b/assets/js/block-image-generate.js @@ -60,16 +60,13 @@ }; const agentImageId = getAgentImageId(); - if (!agentImageId) { - return wp.element.createElement(BlockEdit, props); - } const openImageModal = () => { // Dispatch custom event to open image generation modal window.dispatchEvent( new CustomEvent('wpaw:open-image-modal', { detail: { - agentImageId: agentImageId, + agentImageId: agentImageId || null, blockId: clientId, }, }) diff --git a/assets/js/image-modal.js b/assets/js/image-modal.js index 0e7049a..74a92ca 100644 --- a/assets/js/image-modal.js +++ b/assets/js/image-modal.js @@ -8,7 +8,7 @@ (function() { const { Modal, Button, Spinner, TextControl, TextareaControl } = wp.components; - const { useState, useEffect, render } = wp.element; + const { useState, useEffect, render, createRoot } = wp.element; window.wpAgenticWriter = window.wpAgenticWriter || {}; @@ -16,17 +16,74 @@ * Image Review Modal * Shows after article generation with image recommendations */ - window.wpAgenticWriter.ImageReviewModal = function({ postId, initialImageId, onClose, onComplete }) { + window.wpAgenticWriter.ImageReviewModal = function({ postId, initialImageId, initialBlockId, onClose, onComplete }) { const [step, setStep] = useState('loading'); const [images, setImages] = useState([]); + const [generatedImages, setGeneratedImages] = useState([]); const [selectedImages, setSelectedImages] = useState([]); const [variantCounts, setVariantCounts] = useState({}); const [isGenerating, setIsGenerating] = useState(false); + const [committingVariantId, setCommittingVariantId] = useState(null); + const [extraVariantCounts, setExtraVariantCounts] = useState({}); + const [generatingMoreFor, setGeneratingMoreFor] = useState(null); const [error, setError] = useState(null); useEffect(() => { loadImageRecommendations(); }, []); + + const scopedImages = (() => { + if (initialImageId) { + const direct = images.filter((img) => String(img.agent_image_id || '').trim() === String(initialImageId).trim()); + if (direct.length > 0) { + return direct; + } + } + + if (!initialBlockId) { + return images; + } + + const allBlocks = wp.data.select('core/block-editor').getBlocks() || []; + const flatImageBlocks = []; + const walk = (blocks) => { + blocks.forEach((block) => { + if (block?.name === 'core/image') { + flatImageBlocks.push(block); + } + if (Array.isArray(block?.innerBlocks) && block.innerBlocks.length > 0) { + walk(block.innerBlocks); + } + }); + }; + walk(allBlocks); + + const slotIndex = flatImageBlocks.findIndex((block) => block?.clientId === initialBlockId); + if (slotIndex < 0) { + return images; + } + + const placement = `slot_${slotIndex + 1}`; + const placementScoped = images.filter((img) => String(img.placement || '').toLowerCase() === placement); + return placementScoped.length > 0 ? placementScoped : images; + })(); + + useEffect(() => { + if ((!initialImageId && !initialBlockId) || scopedImages.length === 0) { + return; + } + const scopedIds = scopedImages.map((img) => img.agent_image_id).filter(Boolean); + if (scopedIds.length === 0) { + return; + } + const normalize = (items) => [...items].sort().join('|'); + setSelectedImages((prev) => { + if (normalize(prev) === normalize(scopedIds)) { + return prev; + } + return scopedIds; + }); + }, [initialImageId, initialBlockId, scopedImages.length]); const loadImageRecommendations = async () => { try { @@ -40,7 +97,7 @@ ); if (!response.ok) { - throw new Error('Failed to load image recommendations'); + throw new Error(await extractApiErrorMessage(response, 'Failed to load image recommendations')); } const data = await response.json(); @@ -52,6 +109,13 @@ initialCounts[img.agent_image_id] = 2; }); setVariantCounts(initialCounts); + + if (initialImageId) { + const preferred = imgs.filter((img) => String(img.agent_image_id || '').trim() === String(initialImageId).trim()); + if (preferred.length > 0) { + setSelectedImages(preferred.map((img) => img.agent_image_id)); + } + } setStep('review'); } catch (err) { @@ -103,6 +167,14 @@ return total.toFixed(3); }; + + const calculateTotalVariants = () => { + let total = 0; + selectedImages.forEach((imageId) => { + total += variantCounts[imageId] || 2; + }); + return total; + }; const handleGenerateSelected = async () => { if (selectedImages.length === 0) { @@ -114,8 +186,12 @@ setStep('generating'); try { + const generated = []; for (const imageId of selectedImages) { const image = images.find(img => img.agent_image_id === imageId); + if (!image) { + throw new Error(`Image recommendation was not found for ${imageId}`); + } const response = await fetch( `${wpAgenticWriter.apiUrl}/generate-image`, @@ -130,24 +206,31 @@ agent_image_id: imageId, prompt: image.prompt_edited || image.prompt_initial, alt: image.alt_text_edited || image.alt_text_initial, - variant_count: variantCounts[imageId] || 1, + variant_count: variantCounts[imageId] || 2, }), } ); if (!response.ok) { - throw new Error(`Failed to generate image: ${imageId}`); + const detailedError = await extractApiErrorMessage( + response, + `Failed to generate image: ${imageId}` + ); + throw new Error(detailedError); } const result = await response.json(); + const imageWithVariants = { ...image, variants: result.variants || [] }; + generated.push(imageWithVariants); setImages(prev => prev.map(img => img.agent_image_id === imageId - ? { ...img, variants: result.variants } + ? imageWithVariants : img )); } + setGeneratedImages(generated); setStep('selecting'); } catch (err) { setError(err.message); @@ -158,9 +241,11 @@ }; const handleSelectVariant = async (imageId, variantId) => { - const image = images.find(img => img.agent_image_id === imageId); + const image = generatedImages.find(img => img.agent_image_id === imageId) + || images.find(img => img.agent_image_id === imageId); try { + setCommittingVariantId(variantId); const response = await fetch( `${wpAgenticWriter.apiUrl}/commit-image`, { @@ -179,41 +264,175 @@ ); if (!response.ok) { - throw new Error('Failed to commit image'); + throw new Error(await extractApiErrorMessage(response, 'Failed to commit image')); } const result = await response.json(); - updateGutenbergBlock(imageId, result); + const didUpdateBlock = updateGutenbergBlock(imageId, result); setImages(prev => prev.map(img => img.agent_image_id === imageId ? { ...img, status: 'committed', attachment_id: result.attachment_id } : img )); + setGeneratedImages(prev => prev.map(img => + img.agent_image_id === imageId + ? { ...img, status: 'committed', attachment_id: result.attachment_id } + : img + )); + if (!didUpdateBlock) { + throw new Error('Image committed to Media Library, but the matching image block could not be found.'); + } + onComplete(); } catch (err) { alert('Failed to commit image: ' + err.message); + setCommittingVariantId(null); } }; + + const handleGenerateMore = async (imageId) => { + const image = generatedImages.find(img => img.agent_image_id === imageId) + || images.find(img => img.agent_image_id === imageId); + if (!image) { + alert('Image recommendation not found.'); + return; + } + + const moreCount = extraVariantCounts[imageId] || 1; + setGeneratingMoreFor(imageId); + try { + const response = await fetch( + `${wpAgenticWriter.apiUrl}/generate-image`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': wpAgenticWriter.nonce, + }, + body: JSON.stringify({ + post_id: postId, + agent_image_id: imageId, + prompt: image.prompt_edited || image.prompt_initial, + alt: image.alt_text_edited || image.alt_text_initial, + variant_count: moreCount, + }), + } + ); + + if (!response.ok) { + throw new Error(await extractApiErrorMessage(response, `Failed to generate more variants for ${imageId}`)); + } + + const result = await response.json(); + const newVariants = Array.isArray(result?.variants) ? result.variants : []; + if (newVariants.length === 0) { + throw new Error('No additional variants returned.'); + } + + setGeneratedImages(prev => prev.map(img => + img.agent_image_id === imageId + ? { ...img, variants: [...(img.variants || []), ...newVariants] } + : img + )); + setImages(prev => prev.map(img => + img.agent_image_id === imageId + ? { ...img, variants: [...(img.variants || []), ...newVariants] } + : img + )); + } catch (err) { + alert('Failed to generate more variants: ' + err.message); + } finally { + setGeneratingMoreFor(null); + } + }; + + const extractApiErrorMessage = async (response, fallback) => { + try { + const data = await response.clone().json(); + const message = data?.message || data?.error || ''; + const details = data?.details || data?.reason || data?.provider_error || ''; + const trace = data?.data?.trace || data?.trace || null; + const traceText = formatTraceForDisplay(trace); + const combined = [message, details, traceText].filter(Boolean).join('\n\n').trim(); + if (combined) { + return combined; + } + } catch (jsonError) { + // Continue to text fallback. + } + + try { + const text = (await response.text()).trim(); + if (text) { + return text; + } + } catch (textError) { + // Ignore read error. + } + + return fallback; + }; + + const formatTraceForDisplay = (trace) => { + if (!trace || typeof trace !== 'object') { + return ''; + } + + const openRouterMessage = trace.openrouter_response?.error?.message + || trace.openrouter_response?.message + || ''; + const lines = [ + 'Trace:', + `selected_model: ${trace.selected_model || ''}`, + `settings_image_model: ${trace.settings_image_model || ''}`, + `image_task_provider: ${trace.image_task_provider || ''}`, + `custom_model_match: ${trace.custom_model_match ? 'yes' : 'no'}`, + `custom_model_ids: ${Array.isArray(trace.custom_model_ids) ? trace.custom_model_ids.join(', ') : ''}`, + `model_cache_loaded: ${trace.model_cache_loaded ? 'yes' : 'no'}`, + `model_cache_has_model: ${trace.model_cache_has_model ? 'yes' : 'no'}`, + `refreshed_has_model: ${trace.refreshed_has_model === null ? 'n/a' : (trace.refreshed_has_model ? 'yes' : 'no')}`, + `endpoint: ${trace.endpoint || ''}`, + `request_model: ${trace.request_model || ''}`, + `request_size: ${trace.request_size || ''}`, + `request_quality: ${trace.request_quality || ''}`, + `request_n: ${trace.request_n || ''}`, + `openrouter_http: ${trace.openrouter_http || ''}`, + ]; + + if (openRouterMessage) { + lines.push(`openrouter_message: ${openRouterMessage}`); + } + + return lines.filter((line) => !line.endsWith(': ')).join('\n'); + }; const updateGutenbergBlock = (agentImageId, attachmentData) => { - const blocks = wp.data.select('core/block-editor').getBlocks(); + const editor = wp.data.select('core/block-editor'); + const dispatcher = wp.data.dispatch('core/block-editor'); + const blocks = editor.getBlocks(); + const updateBlock = (clientId) => { + if (!clientId) { + return false; + } + dispatcher.updateBlockAttributes( + clientId, + { + id: attachmentData.attachment_id, + url: attachmentData.attachment_url, + alt: attachmentData.alt, + caption: '', + 'data-agent-image-id': agentImageId, + } + ); + return true; + }; const findAndUpdateBlock = (blocks) => { for (const block of blocks) { if (block.name === 'core/image' && block.attributes['data-agent-image-id'] === agentImageId) { - - wp.data.dispatch('core/block-editor').updateBlockAttributes( - block.clientId, - { - id: attachmentData.attachment_id, - url: attachmentData.attachment_url, - alt: attachmentData.alt, - 'data-agent-image-id': undefined, - } - ); - return true; + return updateBlock(block.clientId); } if (block.innerBlocks && block.innerBlocks.length > 0) { @@ -225,7 +444,21 @@ return false; }; - findAndUpdateBlock(blocks); + if (findAndUpdateBlock(blocks)) { + return true; + } + + const initialBlock = initialBlockId ? editor.getBlock(initialBlockId) : null; + if (initialBlock?.name === 'core/image') { + return updateBlock(initialBlockId); + } + + const selectedBlock = editor.getSelectedBlock(); + if (selectedBlock?.name === 'core/image') { + return updateBlock(selectedBlock.clientId); + } + + return false; }; if (step === 'loading') { @@ -241,7 +474,7 @@ if (step === 'review') { return wp.element.createElement(Modal, { - title: `Image Recommendations (${images.length})`, + title: `Image Recommendations (${scopedImages.length})`, onRequestClose: onClose, style: { maxWidth: '800px' }, }, @@ -251,7 +484,7 @@ style: { marginBottom: '20px' } }, error), - images.length === 0 && wp.element.createElement('div', { + scopedImages.length === 0 && wp.element.createElement('div', { style: { padding: '40px 20px', textAlign: 'center', @@ -270,7 +503,7 @@ }, 'Continue Without Images') ), - images.map(image => + scopedImages.map(image => wp.element.createElement('div', { key: image.agent_image_id, className: 'wpaw-image-card', @@ -311,6 +544,7 @@ padding: '5px', borderRadius: '3px', border: '1px solid #ddd', + minWidth: '150px' } }, wp.element.createElement('option', { value: '1' }, '1 variant'), @@ -356,7 +590,7 @@ variant: 'primary', onClick: handleGenerateSelected, disabled: selectedImages.length === 0 || isGenerating, - }, `Generate ${selectedImages.length} Image(s) (~$${calculateTotalCost()})`) + }, `Generate ${calculateTotalVariants()} Image(s) (~$${calculateTotalCost()})`) ) ) ); @@ -380,20 +614,56 @@ } if (step === 'selecting') { + const selectableImages = generatedImages.filter(img => img.variants && img.variants.length > 0); return wp.element.createElement(Modal, { title: 'Select Image Variants', onRequestClose: onClose, style: { maxWidth: '900px' }, }, wp.element.createElement('div', { className: 'wpaw-variant-selection' }, - images - .filter(img => img.variants && img.variants.length > 0) + selectableImages.length === 0 && wp.element.createElement('div', { + style: { padding: '24px', color: '#666' } + }, 'No generated variants were returned. Please try generating again.'), + selectableImages .map(image => wp.element.createElement('div', { key: image.agent_image_id, style: { marginBottom: '30px' }, }, wp.element.createElement('h3', null, image.section_title), + wp.element.createElement('div', { + style: { + display: 'flex', + alignItems: 'center', + gap: '8px', + marginBottom: '10px', + } + }, + wp.element.createElement('label', { style: { fontSize: '12px', color: '#555' } }, 'Generate More'), + wp.element.createElement('select', { + value: extraVariantCounts[image.agent_image_id] || 1, + onChange: (e) => setExtraVariantCounts(prev => ({ + ...prev, + [image.agent_image_id]: parseInt(e.target.value, 10) + })), + disabled: generatingMoreFor !== null || committingVariantId !== null, + style: { + padding: '4px 6px', + borderRadius: '4px', + border: '1px solid #ddd', + minWidth: '60px' + } + }, + wp.element.createElement('option', { value: '1' }, '+1'), + wp.element.createElement('option', { value: '2' }, '+2'), + wp.element.createElement('option', { value: '3' }, '+3') + ), + wp.element.createElement(Button, { + variant: 'secondary', + onClick: () => handleGenerateMore(image.agent_image_id), + disabled: generatingMoreFor !== null || committingVariantId !== null, + }, generatingMoreFor === image.agent_image_id ? 'Generating...' : 'Generate More') + ), wp.element.createElement('div', { style: { @@ -419,14 +689,15 @@ wp.element.createElement('div', { style: { padding: '10px' } }, wp.element.createElement('p', { style: { fontSize: '12px', margin: '0 0 10px' } }, - `Cost: $${variant.cost.toFixed(3)} β’ ${variant.generation_time}s` + `Cost: $${variant.cost.toFixed(3)} β’ ${Math.round(Number(variant.generation_time) || 0)}s` ), wp.element.createElement(Button, { variant: 'primary', onClick: () => handleSelectVariant(image.agent_image_id, variant.id), + disabled: committingVariantId !== null, style: { width: '100%' }, - }, 'Select') + }, committingVariantId === variant.id ? 'Selecting...' : 'Select') ) ) ) @@ -450,6 +721,28 @@ // Initialize modal container and event listeners let modalContainer = null; let currentModalInstance = null; + let modalRoot = null; + + const mountModal = (element) => { + if (typeof createRoot === 'function') { + if (!modalRoot) { + modalRoot = createRoot(modalContainer); + } + modalRoot.render(element); + return; + } + render(element, modalContainer); + }; + + const unmountModal = () => { + if (modalRoot && typeof modalRoot.unmount === 'function') { + modalRoot.unmount(); + modalRoot = null; + } else if (modalContainer) { + render(null, modalContainer); + } + currentModalInstance = null; + }; /** * Open image modal for review after article generation @@ -463,24 +756,16 @@ document.body.appendChild(modalContainer); } - currentModalInstance = render( - wp.element.createElement(window.wpAgenticWriter.ImageReviewModal, { + currentModalInstance = wp.element.createElement(window.wpAgenticWriter.ImageReviewModal, { postId: postId, onClose: () => { - if (modalContainer) { - render(null, modalContainer); - currentModalInstance = null; - } + unmountModal(); }, onComplete: () => { - if (modalContainer) { - render(null, modalContainer); - currentModalInstance = null; - } + unmountModal(); }, - }), - modalContainer - ); + }); + mountModal(currentModalInstance); }); /** @@ -496,24 +781,17 @@ document.body.appendChild(modalContainer); } - currentModalInstance = render( - wp.element.createElement(window.wpAgenticWriter.ImageReviewModal, { + currentModalInstance = wp.element.createElement(window.wpAgenticWriter.ImageReviewModal, { postId: postId, initialImageId: agentImageId, + initialBlockId: blockId, onClose: () => { - if (modalContainer) { - render(null, modalContainer); - currentModalInstance = null; - } + unmountModal(); }, onComplete: () => { - if (modalContainer) { - render(null, modalContainer); - currentModalInstance = null; - } + unmountModal(); }, - }), - modalContainer - ); + }); + mountModal(currentModalInstance); }); })(); diff --git a/assets/js/sidebar.js b/assets/js/sidebar.js index 49c330d..3627d47 100644 --- a/assets/js/sidebar.js +++ b/assets/js/sidebar.js @@ -6,11 +6,26 @@ (function (wp) { const { registerPlugin } = wp.plugins; + const { PluginSidebarMoreMenuItem } = wp.editPost; const { PluginSidebar } = wp.editPost; const { Panel, TextareaControl, TextControl, CheckboxControl, Button } = wp.components; const { dispatch, select } = wp.data; const { RawHTML } = wp.element; + // Debug logger - only logs when SCRIPT_DEBUG is enabled + const isDebug = typeof wpAgenticWriter !== 'undefined' && wpAgenticWriter.debug; + const wpawLog = { + log: (...args) => { if (isDebug) console.log('[WPAW]', ...args); }, + error: (...args) => console.error('[WPAW]', ...args), // Always log errors + info: (...args) => { if (isDebug) console.info('[WPAW]', ...args); }, + warn: (...args) => { if (isDebug) console.warn('[WPAW]', ...args); }, + }; + const pluginIcon = wp.element.createElement('img', { + src: wpAgenticWriter.pluginUrl + '/assets/img/icon.svg', + alt: 'WP Agentic Writer', + style: { width: '20px', height: '20px' } + }); + // Sidebar Component. const AgenticWriterSidebar = ({ postId }) => { // Get settings from wpAgenticWriter global. @@ -23,6 +38,9 @@ const [messages, setMessages] = React.useState([]); const [input, setInput] = React.useState(''); const [isLoading, setIsLoading] = React.useState(false); + const [currentSessionId, setCurrentSessionId] = React.useState(''); + const [availableSessions, setAvailableSessions] = React.useState([]); + const [isSessionActionLoading, setIsSessionActionLoading] = React.useState(false); const [agentMode, setAgentMode] = React.useState(() => { try { return window.localStorage.getItem('wpawAgentMode') || 'chat'; @@ -59,6 +77,31 @@ // Cost state const [cost, setCost] = React.useState({ session: 0, today: 0, monthlyUsed: 0 }); const [monthlyBudget, setMonthlyBudget] = React.useState(settings.monthly_budget || 600); + + // Provider info state for transparency display + const [providerInfo, setProviderInfo] = React.useState(null); + + // Helper to extract and apply provider metadata from any AI response + const applyProviderMetadata = (data) => { + if (!data) return; + if (data.session_id) { + setCurrentSessionId(data.session_id); + } + + // Support both nested provider_metadata and top-level provider fields + const meta = data.provider_metadata || data; + const provider = meta.provider || meta.selected_provider || meta.provider; + + if (provider) { + setProviderInfo({ + provider: provider, + model: meta.model, + fallbackUsed: meta.fallback_used || meta.fallbackUsed, + warnings: meta.warnings || [] + }); + } + }; + const [isEditorLocked, setIsEditorLocked] = React.useState(false); // SEO audit state @@ -356,7 +399,7 @@ timestamp: new Date(), }]); } catch (error) { - console.error('Failed to undo AI operation:', error); + wpawLog.error('Failed to undo AI operation:', error); setMessages((prev) => [...prev, { role: 'system', type: 'error', @@ -525,7 +568,7 @@ } setSeoAudit(data); } catch (error) { - console.error('SEO Audit error:', error); + wpawLog.error('SEO Audit error:', error); setMessages((prev) => [...prev, { role: 'assistant', content: `SEO Audit error: ${error.message}`, @@ -551,6 +594,7 @@ }, body: JSON.stringify({ postId: postId, + sessionId: currentSessionId, focusKeyword: postConfig.seo_focus_keyword, chatHistory: messages.filter(m => m.role !== 'system'), }), @@ -562,6 +606,7 @@ } const data = await response.json(); + applyProviderMetadata(data); if (data.meta_description) { updatePostConfig('seo_meta_description', data.meta_description); setMessages((prev) => [...prev, { @@ -573,7 +618,7 @@ throw new Error('No meta description returned from API'); } } catch (error) { - console.error('Error generating meta description:', error); + wpawLog.error('Error generating meta description:', error); setMessages((prev) => [...prev, { role: 'system', content: `β Failed to generate meta description: ${error.message}`, @@ -628,6 +673,19 @@ } }, [messages, isLoading]); + React.useEffect(() => { + const handleBeforeUnload = (event) => { + if (!isLoading) { + return; + } + event.preventDefault(); + event.returnValue = ''; + return ''; + }; + window.addEventListener('beforeunload', handleBeforeUnload); + return () => window.removeEventListener('beforeunload', handleBeforeUnload); + }, [isLoading]); + React.useEffect(() => { loadSectionBlocks(); }, [postId]); @@ -636,27 +694,153 @@ if (!postId) { return; } + try { + const savedSession = window.localStorage.getItem(`wpawSessionId_${postId}`); + if (savedSession) { + setCurrentSessionId(savedSession); + } + } catch (error) { + // Ignore storage read errors. + } + }, [postId]); + + React.useEffect(() => { + if (!postId || !currentSessionId) { + return; + } + try { + window.localStorage.setItem(`wpawSessionId_${postId}`, currentSessionId); + } catch (error) { + // Ignore storage write errors. + } + }, [postId, currentSessionId]); + + React.useEffect(() => { const loadChatHistory = async () => { try { - const response = await fetch(`${wpAgenticWriter.apiUrl}/chat-history/${postId}`, { - method: 'GET', - headers: { - 'X-WP-Nonce': wpAgenticWriter.nonce, - }, - }); - if (!response.ok) { - return; + const headers = { + 'X-WP-Nonce': wpAgenticWriter.nonce, + }; + let historyMessages = []; + let resolvedSessionId = ''; + + // Primary source: merged sessions list (post sessions + unassigned sessions). + const sessions = await loadPostSessions(); + if (sessions.length > 0) { + if (sessions.length > 0) { + let selected = sessions[0]; + const preferred = currentSessionId || (() => { + try { + return window.localStorage.getItem(`wpawSessionId_${postId}`) || ''; + } catch (error) { + return ''; + } + })(); + if (preferred) { + const match = sessions.find((s) => s?.session_id === preferred); + if (match) { + selected = match; + } + } + resolvedSessionId = selected?.session_id || ''; + if (Array.isArray(selected?.messages) && selected.messages.length > 0) { + historyMessages = selected.messages; + } + } } - const data = await response.json(); - if (data && Array.isArray(data.messages) && data.messages.length > 0) { - setMessages((prev) => (prev.length > 0 ? prev : data.messages)); + + // Canonical single-session endpoint fallback. + if (postId && !resolvedSessionId) { + const primary = await fetch(`${wpAgenticWriter.apiUrl}/conversation/${postId}`, { + method: 'GET', + headers, + }); + if (primary.ok) { + const data = await primary.json(); + if (data?.session_id) { + resolvedSessionId = data.session_id; + } + if (data && Array.isArray(data.messages) && data.messages.length > 0) { + historyMessages = data.messages; + } + } + } + + // Legacy endpoint fallback. + if (postId && historyMessages.length === 0) { + const legacy = await fetch(`${wpAgenticWriter.apiUrl}/chat-history/${postId}`, { + method: 'GET', + headers, + }); + if (legacy.ok) { + const legacyData = await legacy.json(); + if (legacyData && Array.isArray(legacyData.messages) && legacyData.messages.length > 0) { + historyMessages = legacyData.messages; + } + } + } + + if (historyMessages.length > 0) { + setMessages((prev) => (prev.length > 0 ? prev : historyMessages)); + } + if (resolvedSessionId) { + setCurrentSessionId(resolvedSessionId); } } catch (error) { // Ignore history load failures. } }; loadChatHistory(); - }, [postId]); + }, [postId, currentSessionId]); + + const loadPostSessions = async () => { + const headers = { + 'X-WP-Nonce': wpAgenticWriter.nonce, + }; + let postSessions = []; + let unassignedSessions = []; + + if (postId) { + const postRes = await fetch(`${wpAgenticWriter.apiUrl}/conversations/post/${postId}`, { + method: 'GET', + headers, + }); + if (postRes.ok) { + const postData = await postRes.json(); + postSessions = Array.isArray(postData?.sessions) ? postData.sessions : []; + } + } else { + // New post flow: include unassigned/auto-draft sessions for recovery. + const activeRes = await fetch(`${wpAgenticWriter.apiUrl}/conversations?status=active&limit=50`, { + method: 'GET', + headers, + }); + if (activeRes.ok) { + const activeData = await activeRes.json(); + const allActive = Array.isArray(activeData?.sessions) ? activeData.sessions : []; + unassignedSessions = allActive.filter((s) => { + const pid = Number(s?.post_id || 0); + const postStatus = String(s?.post_status || '').toLowerCase(); + return pid === 0 || postStatus === 'auto-draft'; + }); + } + } + + const merged = [...postSessions, ...unassignedSessions]; + const deduped = []; + const seen = new Set(); + merged.forEach((session) => { + const sid = session?.session_id || ''; + if (!sid || seen.has(sid)) { + return; + } + seen.add(sid); + deduped.push(session); + }); + + setAvailableSessions(deduped); + return deduped; + }; const resolveStreamTarget = (content) => { if (progressRegex.test(content)) { @@ -894,7 +1078,7 @@ const decoder = new TextDecoder(); const timeout = setTimeout(() => { if (isLoading) { - console.error('Generation timeout - no response received'); + wpawLog.error('Generation timeout - no response received'); setMessages(prev => [...prev, { role: 'system', type: 'error', @@ -991,7 +1175,10 @@ const content = item.innerHTML?.match(/
(.*?)<\/p>/)?.[1] || ''; newBlock = wp.blocks.createBlock('core/quote', { value: content }); @@ -1005,6 +1192,7 @@ insertBlocks(newBlock); } } else if (data.type === 'complete') { + applyProviderMetadata(data); clearTimeout(timeout); setCost({ ...cost, session: cost.session + data.totalCost }); @@ -1031,7 +1219,7 @@ }]); } } catch (parseError) { - console.error('Failed to parse streaming data:', line, parseError); + wpawLog.error('Failed to parse streaming data:', line, parseError); } } } @@ -1039,7 +1227,7 @@ clearTimeout(timeout); } catch (error) { - console.error('Article generation error:', error); + wpawLog.error('Article generation error:', error); setMessages(prev => [...prev, { role: 'system', type: 'error', @@ -1110,6 +1298,7 @@ body: JSON.stringify({ messages: [...chatHistory, { role: 'user', content: userMessage }], postId: postId, + sessionId: currentSessionId, type: 'chat', stream: true, postConfig: postConfig, @@ -1148,6 +1337,9 @@ return [...prev, { role: 'assistant', content: fullContent, isStreaming: true }]; }); } else if (data.type === 'complete') { + // Apply provider metadata from completion. + applyProviderMetadata(data); + setMessages(prev => { const lastMsg = prev[prev.length - 1]; if (lastMsg && lastMsg.role === 'assistant') { @@ -1155,7 +1347,7 @@ } return prev; }); - // Extract ALL focus keyword suggestions from completed response + // Extract ALL focus keyword suggestions from completed response. if (fullContent) { const suggestions = extractFocusKeywordSuggestions(fullContent); if (suggestions.length > 0) { @@ -1209,7 +1401,10 @@ if (blockType === 'core/list') { const items = content.split('\n').map((line) => line.trim()).filter(Boolean); const listItems = items.map((item) => wp.blocks.createBlock('core/list-item', { content: item })); - return wp.blocks.createBlock('core/list', { ordered: action.ordered || false }, listItems); + return wp.blocks.createBlock('core/list', { + ordered: action.ordered || false, + ...(action.start ? { start: parseInt(action.start, 10) } : {}) + }, listItems); } if (blockType === 'core/code') { @@ -1358,6 +1553,7 @@ }, body: JSON.stringify({ postId: postId, + sessionId: currentSessionId, sectionId: sectionId, blockIds: blockIds, }), @@ -1557,7 +1753,8 @@ body: JSON.stringify({ chatHistory: chatMessages, postId: postId, - }), + sessionId: currentSessionId, + }), }); if (!response.ok) { @@ -1565,9 +1762,10 @@ } const data = await response.json(); + applyProviderMetadata(data); if (data.tokens_saved > 0) { - console.log(`π‘ Context optimized: ~${data.tokens_saved} tokens saved (~$${(data.tokens_saved * 0.0000002).toFixed(4)})`); + wpawLog.log(`Context optimized: ~${data.tokens_saved} tokens saved (~$${(data.tokens_saved * 0.0000002).toFixed(4)})`); } return { @@ -1577,7 +1775,7 @@ tokensSaved: data.tokens_saved || 0, }; } catch (error) { - console.error('Summarization error:', error); + wpawLog.error('Summarization error:', error); return { summary: '', useFullHistory: true, cost: 0 }; } }; @@ -1600,7 +1798,8 @@ hasPlan: Boolean(currentPlanRef.current), currentMode: agentMode, postId: postId, - }), + sessionId: currentSessionId, + }), }); if (!response.ok) { @@ -1608,12 +1807,13 @@ } const data = await response.json(); + applyProviderMetadata(data); return { intent: data.intent || 'continue_chat', cost: data.cost || 0, }; } catch (error) { - console.error('Intent detection error:', error); + wpawLog.error('Intent detection error:', error); return { intent: 'continue_chat', cost: 0 }; } }; @@ -1665,7 +1865,7 @@ content: 'β Context cleared. Starting fresh conversation.' }]); } catch (error) { - console.error('Reset error:', error); + wpawLog.error('Reset error:', error); setMessages(prev => [...prev, { role: 'system', type: 'error', @@ -1712,6 +1912,7 @@ }, body: JSON.stringify({ postId: postId, + sessionId: currentSessionId, title: plan.title, sections: plan.sections, }), @@ -1731,10 +1932,11 @@ updatePostConfig('seo_secondary_keywords', data.secondary_keywords.join(', ')); } - // Track cost + // Track cost and apply provider metadata if (data.cost) { setCost({ ...cost, session: cost.session + data.cost }); } + applyProviderMetadata(data); // Add assistant message about keyword suggestions setMessages(prev => [...prev, { @@ -1742,7 +1944,7 @@ content: `π― **SEO Keywords Suggested:**\n\n**Focus Keyword:** ${data.focus_keyword}\n\n**Secondary Keywords:** ${data.secondary_keywords.join(', ')}\n\n${data.reasoning || ''}\n\nYou can review and edit these in the Config panel before writing.` }]); } catch (error) { - console.error('Keyword suggestion error:', error); + wpawLog.error('Keyword suggestion error:', error); // Silently fail - don't interrupt the workflow } }; @@ -1791,7 +1993,8 @@ const { retry = false } = options; lastExecuteRequestRef.current = { postId: postId, - stream: true, + sessionId: currentSessionId, + stream: true, postConfig: postConfig, detectedLanguage: detectedLanguage, chatHistory: messages.filter(m => m.role !== 'system'), @@ -1957,6 +2160,7 @@ if (data.totalCost) { setCost({ ...cost, session: cost.session + data.totalCost }); } + applyProviderMetadata(data); setMessages(prev => { const newMessages = [...prev]; const lastTimelineIndex = findLastActiveTimelineIndex(newMessages); @@ -1977,7 +2181,7 @@ throw new Error(data.message || 'Failed to execute outline'); } } catch (parseError) { - console.error('Failed to parse streaming data:', line, parseError); + wpawLog.error('Failed to parse streaming data:', line, parseError); } } } @@ -2006,20 +2210,39 @@ return; } - const confirmMessage = 'Clear chat context? This removes the current sidebar conversation and resets the agentβs chat memory (including stored chat history) for this post. It wonβt change your article content or outline.'; + const confirmMessage = 'Start a new conversation for this post? Your current conversation history will be kept and you can return to it later.'; if (!window.confirm(confirmMessage)) { return; } try { - await fetch(wpAgenticWriter.apiUrl + '/clear-context', { + setIsSessionActionLoading(true); + if (currentSessionId) { + await fetch(`${wpAgenticWriter.apiUrl}/conversations/${currentSessionId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-WP-Nonce': wpAgenticWriter.nonce, + }, + body: JSON.stringify({ status: 'completed' }), + }); + } + const response = await fetch(wpAgenticWriter.apiUrl + '/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpAgenticWriter.nonce, }, - body: JSON.stringify({ postId }), + body: JSON.stringify({ post_id: postId }), }); + if (!response.ok) { + throw new Error('Failed to create a new conversation'); + } + const data = await response.json(); + if (data?.session_id) { + setCurrentSessionId(data.session_id); + } + await loadPostSessions(); setMessages([]); setInClarification(false); setQuestions([]); @@ -2032,8 +2255,10 @@ setMessages(prev => [...prev, { role: 'system', type: 'error', - content: 'Error: Failed to clear chat context.', + content: 'Error: Failed to start a new conversation.', }]); + } finally { + setIsSessionActionLoading(false); } }; const createBlocksFromSerialized = (block) => { @@ -2144,7 +2369,8 @@ body: JSON.stringify({ blocks: blocksToReformat, postId: postId, - }), + sessionId: currentSessionId, + }), }); if (!response.ok) { @@ -2153,6 +2379,7 @@ } const data = await response.json(); + applyProviderMetadata(data); const results = data.results || []; const { replaceBlocks } = dispatch('core/block-editor'); const currentTitle = select('core/editor').getEditedPostAttribute('title') || ''; @@ -2225,6 +2452,7 @@ instruction: instruction, plan: existingPlan, postId: postId, + sessionId: currentSessionId, postConfig: postConfig, }), }); @@ -2242,6 +2470,7 @@ if (data.cost) { setCost({ ...cost, session: cost.session + data.cost }); } + applyProviderMetadata(data); setMessages(prev => { const newMessages = [...prev]; @@ -2395,7 +2624,7 @@ const currentHeading = (block.attributes?.content || '').trim().toLowerCase(); if (currentHeading === lastHeadingContent) { - console.log('WP Agentic Writer: Removed duplicate heading:', block.attributes.content); + wpawLog.log('WP Agentic Writer: Removed duplicate heading:', block.attributes.content); continue; } @@ -2700,6 +2929,7 @@ blocksToRefine: blocksToRefineData, // Send actual block objects allBlocks: normalizedAllBlocks, postId: postId, + sessionId: currentSessionId, stream: true, diffPlan: useDiffPlan, postConfig: postConfig, @@ -2791,6 +3021,9 @@ refinedCount++; } else if (data.type === 'complete') { + // Apply provider metadata from completion + applyProviderMetadata(data); + // Update timeline setMessages(prev => { const newMessages = [...prev]; @@ -2821,7 +3054,7 @@ }); } } catch (e) { - console.error('Failed to parse streaming data:', line, e); + wpawLog.error('Failed to parse streaming data:', line, e); } } if (refinementFailed) { @@ -3235,7 +3468,8 @@ body: JSON.stringify({ messages: [...chatHistory, { role: 'user', content: userMessage }], postId: postId, - type: 'chat', + sessionId: currentSessionId, + type: 'chat', stream: true, postConfig: postConfig, }), @@ -3309,6 +3543,7 @@ if (data.totalCost) { setCost({ ...cost, session: cost.session + data.totalCost }); } + applyProviderMetadata(data); // Extract ALL focus keyword suggestions from AI response setMessages(prev => { const lastAssistantMsg = prev.filter(m => m.role === 'assistant').pop(); @@ -3322,7 +3557,7 @@ }); } } catch (parseError) { - console.error('Failed to parse streaming data:', line, parseError); + wpawLog.error('Failed to parse streaming data:', line, parseError); } } } @@ -3347,7 +3582,7 @@ }); } } catch (intentError) { - console.error('Intent detection failed:', intentError); + wpawLog.error('Intent detection failed:', intentError); } } catch (error) { const errorMsg = error.message || 'Failed to chat'; @@ -3431,7 +3666,8 @@ topic: userMessage, answers: [], postId: postId, - mode: 'generation', + sessionId: currentSessionId, + mode: 'generation', postConfig: postConfig, chatHistory: messages.filter(m => m.role === 'user' || m.role === 'assistant').slice(-10), }), @@ -3472,7 +3708,7 @@ } // If clarity check fails, proceed with generation anyway } catch (clarityError) { - console.warn('Clarity check failed, proceeding with generation:', clarityError); + wpawLog.warn('Clarity check failed, proceeding with generation:', clarityError); // Continue to article generation } @@ -3503,7 +3739,8 @@ topic: userMessage, context: '', postId: postId, - answers: [], + sessionId: currentSessionId, + answers: [], autoExecute: agentMode !== 'planning', stream: true, articleLength: postConfig.article_length, @@ -3533,7 +3770,7 @@ // Add timeout to detect hanging responses const timeout = setTimeout(() => { if (isLoading) { - console.error('Generation timeout - no response received'); + wpawLog.error('Generation timeout - no response received'); setMessages(prev => [...prev, { role: 'system', type: 'error', @@ -3638,7 +3875,10 @@ const content = item.innerHTML?.match(/
(.*?)<\/p>/)?.[1] || ''; newBlock = wp.blocks.createBlock('core/quote', { value: content }); @@ -3650,6 +3890,7 @@ insertBlocks(newBlock); } } else if (data.type === 'complete') { + applyProviderMetadata(data); clearTimeout(timeout); setCost({ ...cost, session: cost.session + data.totalCost }); @@ -3678,7 +3919,7 @@ setIsLoading(false); } } catch (parseError) { - console.error('Failed to parse streaming data:', line, parseError); + wpawLog.error('Failed to parse streaming data:', line, parseError); } } } @@ -3687,7 +3928,7 @@ clearTimeout(timeout); } catch (error) { clearTimeout(timeout); - console.error('Article generation error:', error); + wpawLog.error('Article generation error:', error); setMessages(prev => [...prev, { role: 'system', type: 'error', @@ -3755,6 +3996,7 @@ topic: userMessage, context: '', postId: postId, + sessionId: currentSessionId, answers: [], autoExecute: agentMode !== 'planning', stream: true, @@ -3868,7 +4110,10 @@ const content = item.innerHTML?.match(/
(.*?)<\/p>/)?.[1] || ''; newBlock = wp.blocks.createBlock('core/quote', { value: content }); @@ -3883,6 +4128,7 @@ insertBlocks(newBlock); } } else if (data.type === 'complete') { + applyProviderMetadata(data); setCost({ ...cost, session: cost.session + data.totalCost }); // Update timeline to complete @@ -3913,7 +4159,8 @@ new CustomEvent('wpaw:open-image-review-modal', { detail: { postId: postId, - imageCount: imagePlaceholders.length + sessionId: currentSessionId, + imageCount: imagePlaceholders.length } }) ); @@ -3924,7 +4171,7 @@ throw new Error(data.message); } } catch (parseError) { - console.error('Failed to parse streaming data:', line, parseError); + wpawLog.error('Failed to parse streaming data:', line, parseError); } } } @@ -3981,7 +4228,7 @@ updatePostConfig('seo_secondary_keywords', configData.secondary_keywords); } } catch (e) { - console.error('Failed to parse config answers:', e); + wpawLog.error('Failed to parse config answers:', e); } } @@ -4023,6 +4270,7 @@ topic: topic, context: '', postId: postId, + sessionId: currentSessionId, clarificationAnswers: answers, autoExecute: agentMode !== 'planning', stream: true, @@ -4054,7 +4302,7 @@ // Add timeout to detect hanging responses const timeout = setTimeout(() => { if (isLoading) { - console.error('Generation timeout - no response received'); + wpawLog.error('Generation timeout - no response received'); setMessages(prev => [...prev, { role: 'system', type: 'error', @@ -4161,7 +4409,10 @@ const content = item.innerHTML?.match(/
(.*?)<\/p>/)?.[1] || '';
newBlock = wp.blocks.createBlock('core/quote', { value: content });
@@ -4173,6 +4424,7 @@
insertBlocks(newBlock);
}
} else if (data.type === 'complete') {
+ applyProviderMetadata(data);
clearTimeout(timeout);
setCost({ ...cost, session: cost.session + data.totalCost });
@@ -4202,7 +4454,7 @@
setIsLoading(false);
}
} catch (parseError) {
- console.error('Failed to parse streaming data:', line, parseError);
+ wpawLog.error('Failed to parse streaming data:', line, parseError);
}
}
}
@@ -4211,7 +4463,7 @@
}
} catch (error) {
clearTimeout(timeout);
- console.error('Article generation error:', error);
+ wpawLog.error('Article generation error:', error);
setMessages(prev => [...prev, {
role: 'system',
type: 'error',
@@ -4484,6 +4736,96 @@
);
};
+ const startNewConversation = async () => {
+ if (isLoading || isSessionActionLoading) {
+ return;
+ }
+ try {
+ setIsSessionActionLoading(true);
+ const response = await fetch(wpAgenticWriter.apiUrl + '/conversations', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-WP-Nonce': wpAgenticWriter.nonce,
+ },
+ body: JSON.stringify({ post_id: postId || 0 }),
+ });
+ if (!response.ok) {
+ throw new Error('Failed to create a new conversation');
+ }
+ const data = await response.json();
+ if (data?.session_id) {
+ setCurrentSessionId(data.session_id);
+ }
+ await loadPostSessions();
+ setMessages([]);
+ setShowWelcome(false);
+ } catch (error) {
+ setMessages(prev => [...prev, {
+ role: 'system',
+ type: 'error',
+ content: 'Error: Failed to start a new conversation.',
+ }]);
+ } finally {
+ setIsSessionActionLoading(false);
+ }
+ };
+
+ const deleteConversationSession = async (sessionId) => {
+ if (!sessionId || isSessionActionLoading) {
+ return;
+ }
+ if (!window.confirm('Delete this session permanently?')) {
+ return;
+ }
+ try {
+ setIsSessionActionLoading(true);
+ const response = await fetch(`${wpAgenticWriter.apiUrl}/conversations/${sessionId}`, {
+ method: 'DELETE',
+ headers: {
+ 'X-WP-Nonce': wpAgenticWriter.nonce,
+ },
+ });
+ if (!response.ok) {
+ throw new Error('Failed to delete session');
+ }
+ const sessions = await loadPostSessions();
+ if (currentSessionId === sessionId) {
+ const replacement = sessions[0]?.session_id || '';
+ setCurrentSessionId(replacement);
+ setMessages(Array.isArray(sessions[0]?.messages) ? sessions[0].messages : []);
+ }
+ } catch (error) {
+ setMessages(prev => [...prev, {
+ role: 'system',
+ type: 'error',
+ content: 'Error: Failed to delete session.',
+ }]);
+ } finally {
+ setIsSessionActionLoading(false);
+ }
+ };
+
+ const getSessionDisplayTitle = (session, index) => {
+ if (session?.title && session.title.trim()) {
+ return session.title.trim();
+ }
+ const firstUser = Array.isArray(session?.messages)
+ ? session.messages.find((m) => m?.role === 'user' && typeof m?.content === 'string' && m.content.trim())
+ : null;
+ if (firstUser?.content) {
+ return firstUser.content.trim().slice(0, 56);
+ }
+ const updatedRaw = session?.updated_at || session?.last_activity || '';
+ if (updatedRaw) {
+ const d = new Date(updatedRaw);
+ if (!Number.isNaN(d.getTime())) {
+ return `Session ${index + 1} - ${d.toLocaleDateString()}`;
+ }
+ }
+ return `Session ${index + 1}`;
+ };
+
// Render Welcome Screen (chatty, friendly)
const renderWelcomeScreen = () => {
return wp.element.createElement('div', { className: 'wpaw-welcome-screen' },
@@ -4494,6 +4836,70 @@
}),
wp.element.createElement('h2', { className: 'wpaw-welcome-title' }, 'Welcome to Agentic Writer'),
wp.element.createElement('p', { className: 'wpaw-welcome-subtitle' }, "What's your concern today?"),
+ availableSessions.length > 0 && wp.element.createElement('div', {
+ className: 'wpaw-existing-sessions',
+ style: { marginBottom: '16px' }
+ },
+ wp.element.createElement('div', {
+ style: { fontSize: '12px', opacity: 0.8, marginBottom: '8px' }
+ }, 'Continue a previous conversation'),
+ ...availableSessions.slice(0, 5).map((session, idx) =>
+ wp.element.createElement('div', {
+ key: session.session_id || idx,
+ className: 'wpaw-welcome-pill',
+ style: {
+ width: '100%',
+ marginBottom: '6px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: '8px'
+ }
+ },
+ wp.element.createElement('button', {
+ type: 'button',
+ style: {
+ flex: 1,
+ background: 'transparent',
+ border: 'none',
+ color: 'inherit',
+ textAlign: 'left',
+ cursor: 'pointer'
+ },
+ onClick: () => {
+ setCurrentSessionId(session.session_id || '');
+ setMessages(Array.isArray(session.messages) ? session.messages : []);
+ setShowWelcome(false);
+ }
+ },
+ wp.element.createElement('div', null, getSessionDisplayTitle(session, idx)),
+ wp.element.createElement('div', { style: { opacity: 0.7, fontSize: '11px' } },
+ `${Array.isArray(session.messages) ? session.messages.length : 0} msgs`
+ )
+ ),
+ wp.element.createElement('button', {
+ type: 'button',
+ title: 'Delete session',
+ disabled: isSessionActionLoading,
+ style: {
+ background: 'transparent',
+ border: '1px solid rgba(255,255,255,0.25)',
+ color: 'inherit',
+ borderRadius: '6px',
+ padding: '2px 6px',
+ cursor: 'pointer'
+ },
+ onClick: () => deleteConversationSession(session.session_id)
+ }, 'Γ')
+ )
+ ),
+ wp.element.createElement('button', {
+ className: 'wpaw-welcome-pill',
+ style: { width: '100%' },
+ disabled: isSessionActionLoading,
+ onClick: startNewConversation
+ }, '+ Start New Conversation')
+ ),
// Focus keyword input
wp.element.createElement('input', {
type: 'text',
@@ -4634,6 +5040,12 @@
// Stats
wp.element.createElement('div', { className: 'wpaw-fk-stats' },
wp.element.createElement('span', null, `π° $${(cost.session || 0).toFixed(4)}`),
+ providerInfo && wp.element.createElement('span', {
+ className: 'wpaw-provider-info',
+ title: providerInfo.warnings.length > 0 ? providerInfo.warnings.join('; ') : 'AI provider used'
+ },
+ providerInfo.fallbackUsed ? ' β οΈ ' + (providerInfo.provider || 'fallback') : ' π‘ ' + (providerInfo.provider || 'AI')
+ ),
wp.element.createElement('span', { className: 'wpaw-fk-divider' }, 'β'),
wp.element.createElement('span', null, `π ~${messages.filter(m => m.role !== 'system').length * 500} tokens`)
)
@@ -4670,7 +5082,13 @@
})
),
wp.element.createElement('span', { className: 'wpaw-fk-cost' },
- `$${(cost.session || 0).toFixed(4)}`
+ `$${(cost.session || 0).toFixed(4)}`,
+ providerInfo && wp.element.createElement('span', {
+ className: 'wpaw-provider-badge',
+ title: providerInfo.warnings.length > 0 ? providerInfo.warnings.join('; ') : 'AI provider'
+ },
+ providerInfo.fallbackUsed ? 'β ' : 'π‘'
+ )
),
wp.element.createElement('button', {
className: 'wpaw-fk-expand',
@@ -4733,7 +5151,7 @@
// Call clarity check - MANDATORY before outline generation
try {
- console.log('[WPAW] Calling clarity check with topic:', topic);
+ wpawLog.log('[WPAW] Calling clarity check with topic:', topic);
const clarityResponse = await fetch(wpAgenticWriter.apiUrl + '/check-clarity', {
method: 'POST',
headers: {
@@ -4744,23 +5162,25 @@
topic: topic || 'article outline',
answers: [],
postId: postId,
- mode: 'generation',
+ sessionId: currentSessionId,
+ mode: 'generation',
postConfig: postConfig,
chatHistory: messages.filter(m => m.role === 'user' || m.role === 'assistant').slice(-10),
}),
});
- console.log('[WPAW] Clarity response status:', clarityResponse.status);
+ wpawLog.log('[WPAW] Clarity response status:', clarityResponse.status);
if (!clarityResponse.ok) {
const errorText = await clarityResponse.text();
- console.error('[WPAW] Clarity check failed:', errorText);
+ wpawLog.error('[WPAW] Clarity check failed:', errorText);
throw new Error('Clarity check failed: ' + errorText);
}
const clarityData = await clarityResponse.json();
+ applyProviderMetadata(clarityData);
const clarityResult = clarityData.result;
- console.log('[WPAW] Clarity result:', clarityResult);
+ wpawLog.log('[WPAW] Clarity result:', clarityResult);
if (clarityResult.detected_language) {
setDetectedLanguage(clarityResult.detected_language);
@@ -4768,7 +5188,7 @@
// MANDATORY: Always show quiz if questions exist
if (clarityResult.questions && clarityResult.questions.length > 0) {
- console.log('[WPAW] Showing quiz with', clarityResult.questions.length, 'questions');
+ wpawLog.log('[WPAW] Showing quiz with', clarityResult.questions.length, 'questions');
setQuestions(clarityResult.questions);
setInClarification(true);
setCurrentQuestionIndex(0);
@@ -4789,10 +5209,10 @@
});
return; // Stop here - quiz must be completed first
} else {
- console.warn('[WPAW] No questions returned from clarity check!');
+ wpawLog.warn('[WPAW] No questions returned from clarity check!');
}
} catch (clarityError) {
- console.error('[WPAW] Clarity check error:', clarityError);
+ wpawLog.error('[WPAW] Clarity check error:', clarityError);
// Show error to user instead of silently proceeding
setMessages(prev => [...prev, {
role: 'system',
@@ -4829,7 +5249,8 @@
topic: topic || 'article outline',
context: '',
postId: postId,
- answers: [],
+ sessionId: currentSessionId,
+ answers: [],
autoExecute: false,
stream: true,
articleLength: postConfig.article_length,
@@ -4892,7 +5313,7 @@
});
}
} catch (parseError) {
- console.error('Failed to parse streaming data:', parseError);
+ wpawLog.error('Failed to parse streaming data:', parseError);
}
}
}
@@ -5812,7 +6233,13 @@
wp.element.createElement('span', { className: 'wpaw-status-dot ' + agentStatus }),
wp.element.createElement('span', { className: 'wpaw-status-label' }, statusLabels[agentStatus])
),
- wp.element.createElement('div', { className: 'wpaw-status-actions' },
+ wp.element.createElement('div', { className: 'wpaw-status-actions' },
+ !showWelcome && wp.element.createElement('button', {
+ className: 'wpaw-status-icon-btn',
+ title: 'Back to Sessions',
+ onClick: () => setShowWelcome(true),
+ disabled: isLoading
+ }, 'Sessions'),
// Undo Button
aiUndoStack.length > 0 && wp.element.createElement('button', {
className: 'wpaw-status-icon-btn wpaw-undo-btn',
@@ -5829,14 +6256,16 @@
className: 'wpaw-status-icon-btn',
dangerouslySetInnerHTML: { __html: '' },
title: 'Configuration',
- onClick: () => setActiveTab('config')
+ onClick: () => setActiveTab('config'),
+ disabled: isLoading
}),
// Cost Icon Button
wp.element.createElement('button', {
className: 'wpaw-status-icon-btn',
dangerouslySetInnerHTML: { __html: '' },
title: 'Cost Tracking',
- onClick: () => setActiveTab('cost')
+ onClick: () => setActiveTab('cost'),
+ disabled: isLoading
})
)
),
@@ -6011,6 +6440,12 @@
),
wp.element.createElement('div', { className: 'wpaw-command-actions-group' },
+ !showWelcome && wp.element.createElement('button', {
+ className: 'wpaw-command-text-btn',
+ type: 'button',
+ onClick: () => setShowWelcome(true),
+ disabled: isLoading,
+ }, 'Sessions'),
// Clear Context (Bottom Middle-ish)
wp.element.createElement('button', {
className: 'wpaw-command-text-btn',
@@ -6077,7 +6512,7 @@
setCostHistory(data.history);
}
} catch (e) {
- console.error('Failed to refresh cost data:', e);
+ wpawLog.error('Failed to refresh cost data:', e);
}
};
@@ -6181,7 +6616,12 @@
};
// Main render.
- return wp.element.createElement(PluginSidebar, {
+ return wp.element.createElement(wp.element.Fragment, null,
+ wp.element.createElement(PluginSidebarMoreMenuItem, {
+ target: 'wp-agentic-writer',
+ icon: pluginIcon,
+ }, 'WP Agentic Writer'),
+ wp.element.createElement(PluginSidebar, {
name: 'wp-agentic-writer',
title: wp.element.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
wp.element.createElement('img', {
@@ -6199,7 +6639,7 @@
activeTab === 'cost' && renderCostTab()
)
)
- );
+ ));
};
// HOC to get post ID.
@@ -6210,16 +6650,9 @@
// Connect sidebar to Redux store.
const ConnectedSidebar = wp.data.withSelect(mapSelectToProps)(AgenticWriterSidebar);
- // Custom icon component using the SVG
- const AgenticWriterIcon = () => wp.element.createElement('img', {
- src: wpAgenticWriter.pluginUrl + '/assets/img/icon.svg',
- alt: 'WP Agentic Writer',
- style: { width: '20px', height: '20px' }
- });
-
// Register plugin.
registerPlugin('wp-agentic-writer', {
- icon: AgenticWriterIcon,
+ icon: pluginIcon,
render: ConnectedSidebar,
});
})(window.wp);
diff --git a/includes/class-admin-columns.php b/includes/class-admin-columns.php
index 8256f9e..5a60970 100644
--- a/includes/class-admin-columns.php
+++ b/includes/class-admin-columns.php
@@ -77,16 +77,10 @@ class WP_Agentic_Writer_Admin_Columns {
return;
}
- global $wpdb;
- $table_name = $wpdb->prefix . 'wpaw_cost_tracking';
-
- // Get total cost for this post
- $total_cost = $wpdb->get_var(
- $wpdb->prepare(
- "SELECT SUM(cost) FROM {$table_name} WHERE post_id = %d",
- $post_id
- )
- );
+ $total_cost = 0.0;
+ if ( class_exists( 'WP_Agentic_Writer_Cost_Tracker' ) ) {
+ $total_cost = WP_Agentic_Writer_Cost_Tracker::get_instance()->get_session_total( $post_id );
+ }
if ( $total_cost && $total_cost > 0 ) {
// Color code based on cost
diff --git a/includes/class-gutenberg-sidebar.php b/includes/class-gutenberg-sidebar.php
index e56490d..25d60a8 100644
--- a/includes/class-gutenberg-sidebar.php
+++ b/includes/class-gutenberg-sidebar.php
@@ -11,6 +11,23 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
+/**
+ * Debug logging helper - only logs if WP_DEBUG is enabled.
+ *
+ * @param string $message Log message.
+ * @param mixed $data Optional data to log.
+ */
+function wpaw_debug_log( $message, $data = null ) {
+ if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
+ $prefix = '[WPAW Debug] ';
+ if ( null === $data ) {
+ error_log( $prefix . $message );
+ } else {
+ error_log( $prefix . $message . ' ' . wp_json_encode( $data ) );
+ }
+ }
+}
+
/**
* Class WP_Agentic_Writer_Gutenberg_Sidebar
*
@@ -69,9 +86,9 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
$dompurify_url = WP_AGENTIC_WRITER_URL . 'assets/js/vendor/purify.min.js';
$markdown_task_lists_url = WP_AGENTIC_WRITER_URL . 'assets/js/vendor/markdown-it-task-lists.min.js';
- // Debug: Log the script URL.
- error_log( 'WP Agentic Writer - Script URL: ' . $script_url );
- error_log( 'WP Agentic Writer - File exists: ' . ( file_exists( WP_AGENTIC_WRITER_DIR . 'assets/js/sidebar.js' ) ? 'YES' : 'NO' ) );
+ // Debug: Log the script URL (only when WP_DEBUG is on).
+ wpaw_debug_log( 'Script URL: ' . $script_url );
+ wpaw_debug_log( 'File exists: ' . ( file_exists( WP_AGENTIC_WRITER_DIR . 'assets/js/sidebar.js' ) ? 'YES' : 'NO' ) );
// Enqueue markdown renderer and sanitizer.
wp_enqueue_script(
@@ -96,6 +113,16 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
true
);
+ // Enqueue utility functions (loaded before main sidebar).
+ $utils_script_path = WP_AGENTIC_WRITER_DIR . 'assets/js/sidebar-utils.js';
+ wp_enqueue_script(
+ 'wp-agentic-writer-sidebar-utils',
+ WP_AGENTIC_WRITER_URL . 'assets/js/sidebar-utils.js',
+ array(),
+ file_exists( $utils_script_path ) ? filemtime( $utils_script_path ) : WP_AGENTIC_WRITER_VERSION,
+ true
+ );
+
// Enqueue sidebar script.
$script_path = WP_AGENTIC_WRITER_DIR . 'assets/js/sidebar.js';
@@ -114,6 +141,7 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
'wp-agentic-writer-markdown-it',
'wp-agentic-writer-dompurify',
'wp-agentic-writer-markdown-task-lists',
+ 'wp-agentic-writer-sidebar-utils',
),
file_exists( $script_path ) ? filemtime( $script_path ) : WP_AGENTIC_WRITER_VERSION,
true
@@ -178,6 +206,26 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
file_exists( $style_path ) ? filemtime( $style_path ) : WP_AGENTIC_WRITER_VERSION
);
+ // Enqueue agentic components styles.
+ $components_style_path = WP_AGENTIC_WRITER_DIR . 'assets/css/agentic-components.css';
+ $components_style_url = WP_AGENTIC_WRITER_URL . 'assets/css/agentic-components.css';
+ wp_enqueue_style(
+ 'wp-agentic-writer-components',
+ $components_style_url,
+ array(),
+ file_exists( $components_style_path ) ? filemtime( $components_style_path ) : WP_AGENTIC_WRITER_VERSION
+ );
+
+ // Enqueue workflow styles.
+ $workflow_style_path = WP_AGENTIC_WRITER_DIR . 'assets/css/agentic-workflow.css';
+ $workflow_style_url = WP_AGENTIC_WRITER_URL . 'assets/css/agentic-workflow.css';
+ wp_enqueue_style(
+ 'wp-agentic-writer-workflow',
+ $workflow_style_url,
+ array(),
+ file_exists( $workflow_style_path ) ? filemtime( $workflow_style_path ) : WP_AGENTIC_WRITER_VERSION
+ );
+
// Enqueue editor styles for image placeholders.
$editor_style_path = WP_AGENTIC_WRITER_DIR . 'assets/css/editor.css';
wp_enqueue_style(
@@ -225,20 +273,20 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
// Don't expose API key to frontend.
unset( $settings['openrouter_api_key'] );
- // Ensure all required keys exist with defaults (6 models per model-preset-brief.md).
+ // Ensure all required keys exist with defaults from model registry.
$defaults = array(
- 'chat_model' => 'google/gemini-2.5-flash',
- 'clarity_model' => 'google/gemini-2.5-flash',
- 'planning_model' => 'google/gemini-2.5-flash',
- 'writing_model' => 'anthropic/claude-3.5-sonnet',
- 'refinement_model' => 'anthropic/claude-3.5-sonnet',
- 'image_model' => 'openai/gpt-4o',
+ 'chat_model' => WPAW_Model_Registry::get_default_model( 'chat' ),
+ 'clarity_model' => WPAW_Model_Registry::get_default_model( 'clarity' ),
+ 'planning_model' => WPAW_Model_Registry::get_default_model( 'planning' ),
+ 'writing_model' => WPAW_Model_Registry::get_default_model( 'writing' ),
+ 'refinement_model' => WPAW_Model_Registry::get_default_model( 'refinement' ),
+ 'image_model' => WPAW_Model_Registry::get_default_model( 'image' ),
'web_search_enabled' => false,
'search_engine' => 'auto',
'search_depth' => 'medium',
'cost_tracking_enabled' => true,
'monthly_budget' => 600,
- 'settings_url' => admin_url( 'options-general.php?page=wp-agentic-writer' ),
+ 'settings_url' => admin_url( 'options-general.php?page=wp-agentic-writer-settings' ),
'preferred_languages' => array( 'auto', 'English', 'Indonesian' ),
'custom_languages' => array(),
);
@@ -295,7 +343,7 @@ class WP_Agentic_Writer_Gutenberg_Sidebar {
'permission_callback' => array( $this, 'check_permissions' ),
)
);
- // Chat history endpoint.
+ // Chat history endpoint (deprecated - for backward compatibility only).
register_rest_route(
'wp-agentic-writer/v1',
'/chat-history/(?P ]*>/i', $post->post_content, $matches );
+
+ if ( $heading_count >= 3 && $paragraph_count >= 5 ) {
+ $geo['checks'][] = array(
+ 'name' => 'Structure',
+ 'status' => 'good',
+ 'message' => "Excellent structure with {$heading_count} headings and {$paragraph_count} paragraphs",
+ 'score' => 20,
+ );
+ $total_score += 20;
+ } elseif ( $heading_count >= 1 ) {
+ $geo['checks'][] = array(
+ 'name' => 'Structure',
+ 'status' => 'ok',
+ 'message' => 'Basic structure present, consider adding more subheadings',
+ 'score' => 12,
+ );
+ $total_score += 12;
+ } else {
+ $geo['checks'][] = array(
+ 'name' => 'Structure',
+ 'status' => 'warning',
+ 'message' => 'Content lacks structure. Add clear H2/H3 headings to break up content.',
+ 'score' => 5,
+ );
+ $total_score += 5;
+ $geo['suggestions'][] = 'Add H2 headings every 200-300 words to organize content into scannable sections';
+ }
+
+ // Check 3: Authority - Does the content demonstrate expertise?
+ $total_checks++;
+ $authority_indicators = array(
+ 'experience', 'years', 'research', 'study', 'according to', 'expert',
+ 'professional', 'certified', 'proven', 'tested', 'verified'
+ );
+ $authority_count = 0;
+ foreach ( $authority_indicators as $indicator ) {
+ $authority_count += substr_count( strtolower( $content ), $indicator );
+ }
+
+ if ( $authority_count >= 3 ) {
+ $geo['checks'][] = array(
+ 'name' => 'Authority',
+ 'status' => 'good',
+ 'message' => 'Content demonstrates strong expertise',
+ 'score' => 20,
+ );
+ $total_score += 20;
+ } elseif ( $authority_count >= 1 ) {
+ $geo['checks'][] = array(
+ 'name' => 'Authority',
+ 'status' => 'ok',
+ 'message' => 'Some authority signals present',
+ 'score' => 12,
+ );
+ $total_score += 12;
+ } else {
+ $geo['checks'][] = array(
+ 'name' => 'Authority',
+ 'status' => 'warning',
+ 'message' => 'Content lacks authority signals. Add experience, research, or expert references.',
+ 'score' => 5,
+ );
+ $total_score += 5;
+ $geo['suggestions'][] = 'Add phrases like "Based on years of experience", "Research shows", or "Experts recommend"';
+ }
+
+ // Check 4: Clarity - Is the content easy to understand?
+ $total_checks++;
+ $word_count = str_word_count( $content );
+ $sentence_count = preg_match_all( '/[.!?]+/', $content );
+ $avg_sentence_length = $sentence_count > 0 ? $word_count / $sentence_count : 0;
+
+ // Count complex words (7+ characters)
+ $words = preg_split( '/\s+/', $content );
+ $complex_words = 0;
+ foreach ( $words as $word ) {
+ $clean_word = preg_replace( '/[^a-zA-Z]/', '', $word );
+ if ( strlen( $clean_word ) >= 7 ) {
+ $complex_words++;
+ }
+ }
+ $flesch_score = $word_count > 0 ? 206.835 - (1.015 * ($word_count / max( 1, $sentence_count ))) - (84.6 * ($complex_words / $word_count)) : 0;
+ $readability = $flesch_score >= 60 ? 'good' : ( $flesch_score >= 40 ? 'ok' : 'complex' );
+
+ if ( $readability === 'good' ) {
+ $geo['checks'][] = array(
+ 'name' => 'Clarity',
+ 'status' => 'good',
+ 'message' => sprintf( 'Excellent readability (Flesch: %.0f)', $flesch_score ),
+ 'score' => 20,
+ );
+ $total_score += 20;
+ } elseif ( $readability === 'ok' ) {
+ $geo['checks'][] = array(
+ 'name' => 'Clarity',
+ 'status' => 'ok',
+ 'message' => sprintf( 'Average readability (Flesch: %.0f)', $flesch_score ),
+ 'score' => 12,
+ );
+ $total_score += 12;
+ } else {
+ $geo['checks'][] = array(
+ 'name' => 'Clarity',
+ 'status' => 'warning',
+ 'message' => sprintf( 'Complex text (Flesch: %.0f). Consider shorter sentences.', $flesch_score ),
+ 'score' => 5,
+ );
+ $total_score += 5;
+ $geo['suggestions'][] = 'Break long sentences into shorter ones. Aim for 15-20 words per sentence average.';
+ }
+
+ // Check 5: Completeness - Does the content cover the topic thoroughly?
+ $total_checks++;
+ $focus_keyword = $post_config['seo_focus_keyword'] ?? '';
+
+ if ( ! empty( $focus_keyword ) ) {
+ $keyword_in_intro = stripos( substr( $content, 0, 200 ), $focus_keyword ) !== false;
+ $keyword_in_conclusion = stripos( substr( $content, -200 ), $focus_keyword ) !== false;
+ $keyword_count = substr_count( strtolower( $content ), strtolower( $focus_keyword ) );
+ $keyword_density = $word_count > 0 ? ($keyword_count / $word_count) * 100 : 0;
+
+ if ( $keyword_in_intro && $keyword_in_conclusion && $keyword_density >= 0.5 ) {
+ $geo['checks'][] = array(
+ 'name' => 'Completeness',
+ 'status' => 'good',
+ 'message' => 'Topic covered comprehensively with keyword in intro and conclusion',
+ 'score' => 20,
+ );
+ $total_score += 20;
+ } elseif ( $keyword_density >= 0.5 ) {
+ $geo['checks'][] = array(
+ 'name' => 'Completeness',
+ 'status' => 'ok',
+ 'message' => 'Topic covered but improve keyword placement',
+ 'score' => 12,
+ );
+ $total_score += 12;
+ } else {
+ $geo['checks'][] = array(
+ 'name' => 'Completeness',
+ 'status' => 'warning',
+ 'message' => 'Topic may not be fully covered. Ensure keyword appears in intro, body, and conclusion.',
+ 'score' => 5,
+ );
+ $total_score += 5;
+ $geo['suggestions'][] = 'Include focus keyword in your introduction and conclusion paragraph';
+ }
+ } else {
+ $geo['checks'][] = array(
+ 'name' => 'Completeness',
+ 'status' => 'ok',
+ 'message' => 'Focus keyword not set - cannot fully assess completeness',
+ 'score' => 10,
+ );
+ $total_score += 10;
+ $geo['suggestions'][] = 'Set a focus keyword to enable full GEO analysis';
+ }
+
+ // Calculate final score
+ $geo['score'] = $total_score;
+
+ // Determine rating
+ if ( $geo['score'] >= 80 ) {
+ $geo['rating'] = 'excellent';
+ } elseif ( $geo['score'] >= 60 ) {
+ $geo['rating'] = 'good';
+ } elseif ( $geo['score'] >= 40 ) {
+ $geo['rating'] = 'fair';
+ } else {
+ $geo['rating'] = 'poor';
+ }
+
+ // Add AI Overview eligibility note
+ $geo['ai_overview_eligible'] = $geo['score'] >= 80;
+
+ return new WP_REST_Response( $geo, 200 );
+ }
+
+ /**
+ * Handle generate title request.
+ *
+ * Uses WordPress 7.0 AI Client when available, falls back to legacy.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_generate_title( $request ) {
+ $params = $request->get_json_params();
+ $content = sanitize_textarea_field( $params['content'] ?? '' );
+
+ if ( empty( $content ) ) {
+ return new WP_Error(
+ 'missing_content',
+ __( 'Content is required for title generation.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ $options = array(
+ 'post_id' => isset( $params['post_id'] ) ? (int) $params['post_id'] : 0,
+ );
+
+ $client = WPAW_WP_AI_Client::get_instance();
+ $result = $client->generate_title( $content, $options );
+
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+
+ return new WP_REST_Response(
+ array(
+ 'title' => $result,
+ 'source' => $client->get_ai_mode(),
+ ),
+ 200
+ );
+ }
+
+ /**
+ * Handle generate excerpt request.
+ *
+ * Uses WordPress 7.0 AI Client when available, falls back to legacy.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_generate_excerpt( $request ) {
+ $params = $request->get_json_params();
+ $content = sanitize_textarea_field( $params['content'] ?? '' );
+
+ if ( empty( $content ) ) {
+ return new WP_Error(
+ 'missing_content',
+ __( 'Content is required for excerpt generation.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ $options = array(
+ 'post_id' => isset( $params['post_id'] ) ? (int) $params['post_id'] : 0,
+ );
+
+ $client = WPAW_WP_AI_Client::get_instance();
+ $result = $client->generate_excerpt( $content, $options );
+
+ if ( is_wp_error( $result ) ) {
+ return $result;
+ }
+
+ return new WP_REST_Response(
+ array(
+ 'excerpt' => $result,
+ 'source' => $client->get_ai_mode(),
+ ),
+ 200
+ );
+ }
+
+ /**
+ * Handle get AI capabilities request.
+ *
+ * Returns the current AI capabilities based on available providers.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response
+ */
+ public function handle_get_ai_capabilities( $request ) {
+ $client = WPAW_WP_AI_Client::get_instance();
+ $capabilities = $client->get_capabilities();
+
+ return new WP_REST_Response( $capabilities, 200 );
+ }
+
+ /**
+ * Handle search request for research.
+ *
+ * Uses Brave Search API for web search results.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_search( $request ) {
+ $params = $request->get_json_params();
+ $query = sanitize_text_field( $params['query'] ?? '' );
+ $count = isset( $params['count'] ) ? absint( $params['count'] ) : 5;
+
+ if ( empty( $query ) ) {
+ return new WP_Error(
+ 'missing_query',
+ __( 'Search query is required.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ $brave = WP_Agentic_Writer_Brave_Search_API::get_instance();
+ $results = $brave->search( $query, $count );
+
+ if ( is_wp_error( $results ) ) {
+ return $results;
+ }
+
+ return new WP_REST_Response(
+ array(
+ 'query' => $query,
+ 'results' => $results,
+ 'count' => count( $results ),
+ ),
+ 200
+ );
+ }
+
+ /**
+ * Handle fetch content request for research.
+ *
+ * Fetches and extracts content from a URL for AI context.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_fetch_content( $request ) {
+ $params = $request->get_json_params();
+ $url = esc_url_raw( $params['url'] ?? '' );
+
+ if ( empty( $url ) ) {
+ return new WP_Error(
+ 'missing_url',
+ __( 'URL is required.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Validate URL format
+ if ( ! wp_http_validate_url( $url ) ) {
+ return new WP_Error(
+ 'invalid_url',
+ __( 'Invalid URL provided.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Fetch the content
+ $response = wp_remote_get(
+ $url,
+ array(
+ 'timeout' => 20,
+ 'user-agent' => 'Mozilla/5.0 (compatible; WP-Agentic-Writer/1.0)',
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ return $response;
+ }
+
+ $http_code = wp_remote_retrieve_response_code( $response );
+ if ( $http_code !== 200 ) {
+ return new WP_Error(
+ 'fetch_failed',
+ sprintf( __( 'Failed to fetch URL (HTTP %d).', 'wp-agentic-writer' ), $http_code ),
+ array( 'status' => $http_code )
+ );
+ }
+
+ $body = wp_remote_retrieve_body( $response );
+
+ // Strip HTML tags and get clean text
+ $content = wp_strip_all_tags( $body );
+
+ // Truncate to prevent token overflow (max ~4000 chars for context)
+ if ( strlen( $content ) > 4000 ) {
+ $content = substr( $content, 0, 4000 ) . '...';
+ }
+
+ return new WP_REST_Response(
+ array(
+ 'url' => $url,
+ 'content' => $content,
+ 'length' => strlen( $content ),
+ ),
+ 200
+ );
+ }
+
+ /**
+ * Handle research summary request.
+ *
+ * Performs multiple searches and generates a research summary.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_research_summary( $request ) {
+ $params = $request->get_json_params();
+ $topic = sanitize_text_field( $params['topic'] ?? '' );
+ $depth = sanitize_text_field( $params['depth'] ?? 'basic' );
+ $include_urls = isset( $params['include_urls'] ) ? (bool) $params['include_urls'] : false;
+
+ if ( empty( $topic ) ) {
+ return new WP_Error(
+ 'missing_topic',
+ __( 'Research topic is required.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Determine search count based on depth
+ $search_counts = array(
+ 'basic' => 3,
+ 'medium' => 5,
+ 'deep' => 8,
+ );
+ $count = $search_counts[ $depth ] ?? 3;
+
+ $brave = WP_Agentic_Writer_Brave_Search_API::get_instance();
+
+ // Perform main search
+ $main_results = $brave->search( $topic, $count );
+
+ if ( is_wp_error( $main_results ) ) {
+ return $main_results;
+ }
+
+ $research_data = array(
+ 'topic' => $topic,
+ 'depth' => $depth,
+ 'search_results' => $main_results,
+ 'formatted_context' => $brave->format_results_for_llm( $main_results, $topic ),
+ );
+
+ // Optionally fetch content from top URLs
+ if ( $include_urls && ! empty( $main_results ) ) {
+ $fetched_content = array();
+ $max_urls = min( 2, count( $main_results ) ); // Limit to 2 URLs
+
+ for ( $i = 0; $i < $max_urls; $i++ ) {
+ $url = $main_results[ $i ]['url'] ?? '';
+ if ( empty( $url ) ) {
+ continue;
+ }
+
+ $fetch_response = wp_remote_get(
+ $url,
+ array(
+ 'timeout' => 15,
+ 'user-agent' => 'Mozilla/5.0 (compatible; WP-Agentic-Writer/1.0)',
+ )
+ );
+
+ if ( ! is_wp_error( $fetch_response ) && 200 === wp_remote_retrieve_response_code( $fetch_response ) ) {
+ $body = wp_remote_retrieve_body( $fetch_response );
+ $content = wp_strip_all_tags( $body );
+
+ if ( strlen( $content ) > 2000 ) {
+ $content = substr( $content, 0, 2000 ) . '...';
+ }
+
+ $fetched_content[] = array(
+ 'title' => $main_results[ $i ]['title'],
+ 'url' => $url,
+ 'excerpt' => $content,
+ );
+ }
+ }
+
+ $research_data['fetched_content'] = $fetched_content;
+ }
+
+ return new WP_REST_Response( $research_data, 200 );
+ }
+
+ /**
+ * Handle get conversations list request.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_get_conversations( $request ) {
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+
+ $status = sanitize_text_field( $request->get_param( 'status' ) ?: 'active' );
+ $limit = (int) $request->get_param( 'limit' ) ?: 20;
+ $post_id = (int) $request->get_param( 'post_id' ) ?: 0;
+
+ // If post_id is specified, check authorization before returning session.
+ if ( $post_id > 0 ) {
+ // Authorization: User must be able to edit this post.
+ if ( ! current_user_can( 'edit_post', $post_id ) ) {
+ return new WP_Error(
+ 'forbidden',
+ __( 'You do not have permission to access this post.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ $sessions = $manager->get_sessions_for_post( $post_id );
+ return new WP_REST_Response(
+ array(
+ 'sessions' => $sessions,
+ 'count' => count( $sessions ),
+ ),
+ 200
+ );
+ }
+
+ if ( $request->get_param( 'uncompleted' ) ) {
+ $sessions = $manager->get_uncompleted_sessions( $limit );
+ } else {
+ $sessions = $manager->get_user_sessions( $status, $limit );
+ }
+
+ return new WP_REST_Response(
+ array(
+ 'sessions' => $sessions,
+ 'count' => count( $sessions ),
+ ),
+ 200
+ );
+ }
+
+ /**
+ * Handle create conversation request.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_create_conversation( $request ) {
+ $params = $request->get_json_params();
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+
+ $post_id = isset( $params['post_id'] ) ? (int) $params['post_id'] : 0;
+ $focus_keyword = isset( $params['focus_keyword'] ) ? sanitize_text_field( $params['focus_keyword'] ) : '';
+ $title = isset( $params['title'] ) ? sanitize_text_field( $params['title'] ) : '';
+
+ // Authorization: If linking to a post, check edit permission.
+ if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) {
+ return new WP_Error(
+ 'forbidden',
+ __( 'You do not have permission to create a session for this post.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ if ( '' === $title && $post_id > 0 ) {
+ $post = get_post( $post_id );
+ $base_title = $post ? sanitize_text_field( $post->post_title ) : '';
+ if ( '' === $base_title ) {
+ $base_title = 'Conversation';
+ }
+ $title = sprintf( '%s - %s', $base_title, current_time( 'Y-m-d H:i' ) );
+ }
+
+ $session_id = $manager->create_session( array(
+ 'post_id' => $post_id,
+ 'focus_keyword' => $focus_keyword,
+ 'title' => $title,
+ ) );
+
+ if ( is_wp_error( $session_id ) ) {
+ return $session_id;
+ }
+
+ $session = $manager->get_session( $session_id );
+
+ return new WP_REST_Response( $session, 201 );
+ }
+
+ /**
+ * Handle get single conversation request.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_get_conversation( $request ) {
+ $session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+
+ // Check authorization
+ if ( ! $manager->current_user_can_access( $session_id ) ) {
+ return new WP_Error(
+ 'forbidden',
+ __( 'You do not have permission to access this conversation.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ $session = $manager->get_session( $session_id );
+
+ if ( ! $session ) {
+ return new WP_Error(
+ 'not_found',
+ __( 'Conversation not found.', 'wp-agentic-writer' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ return new WP_REST_Response( $session, 200 );
+ }
+
+ /**
+ * Handle update conversation request.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_update_conversation( $request ) {
+ $params = $request->get_json_params();
+ $session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+
+ // Check authorization
+ if ( ! $manager->current_user_can_access( $session_id ) ) {
+ return new WP_Error(
+ 'forbidden',
+ __( 'You do not have permission to modify this conversation.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ $session = $manager->get_session( $session_id );
+
+ if ( ! $session ) {
+ return new WP_Error(
+ 'not_found',
+ __( 'Conversation not found.', 'wp-agentic-writer' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ // Update fields
+ if ( isset( $params['title'] ) ) {
+ $manager->update_title( $session_id, $params['title'] );
+ }
+
+ if ( isset( $params['focus_keyword'] ) ) {
+ $manager->update_focus_keyword( $session_id, $params['focus_keyword'] );
+ }
+
+ if ( isset( $params['status'] ) ) {
+ if ( $params['status'] === 'completed' ) {
+ $manager->mark_completed( $session_id );
+ }
+ }
+
+ $updated_session = $manager->get_session( $session_id );
+
+ return new WP_REST_Response( $updated_session, 200 );
+ }
+
+ /**
+ * Handle delete conversation request.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_delete_conversation( $request ) {
+ $session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+
+ // Check authorization
+ if ( ! $manager->current_user_can_access( $session_id ) ) {
+ return new WP_Error(
+ 'forbidden',
+ __( 'You do not have permission to delete this conversation.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ $result = $manager->delete_session( $session_id );
+
+ if ( ! $result ) {
+ return new WP_Error(
+ 'delete_failed',
+ __( 'Failed to delete conversation.', 'wp-agentic-writer' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ return new WP_REST_Response(
+ array( 'deleted' => true ),
+ 200
+ );
+ }
+
+ /**
+ * Handle update conversation messages request.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_update_conversation_messages( $request ) {
+ $params = $request->get_json_params();
+ $session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+
+ // Check authorization
+ if ( ! $manager->current_user_can_access( $session_id ) ) {
+ return new WP_Error(
+ 'forbidden',
+ __( 'You do not have permission to modify this conversation.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ $session = $manager->get_session( $session_id );
+
+ if ( ! $session ) {
+ return new WP_Error(
+ 'not_found',
+ __( 'Conversation not found.', 'wp-agentic-writer' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $messages = isset( $params['messages'] ) ? $params['messages'] : array();
+
+ if ( ! is_array( $messages ) ) {
+ return new WP_Error(
+ 'invalid_messages',
+ __( 'Messages must be an array.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ $manager->update_messages( $session_id, $messages );
+
+ return new WP_REST_Response(
+ array( 'updated' => true, 'message_count' => count( $messages ) ),
+ 200
+ );
+ }
+
+ /**
+ * Handle link conversation to post request.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_link_conversation_to_post( $request ) {
+ $params = $request->get_json_params();
+ $session_id = sanitize_text_field( $request->get_param( 'session_id' ) );
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+
+ // First verify user has access to this session (before linking to post).
+ if ( ! $manager->current_user_can_access( $session_id ) ) {
+ return new WP_Error(
+ 'forbidden',
+ __( 'You do not have access to this conversation.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ $session = $manager->get_session( $session_id );
+
+ if ( ! $session ) {
+ return new WP_Error(
+ 'not_found',
+ __( 'Conversation not found.', 'wp-agentic-writer' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ $post_id = isset( $params['post_id'] ) ? (int) $params['post_id'] : 0;
+
+ if ( $post_id <= 0 ) {
+ return new WP_Error(
+ 'invalid_post',
+ __( 'Valid post ID is required.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Verify post exists and user can edit
+ if ( ! current_user_can( 'edit_post', $post_id ) ) {
+ return new WP_Error(
+ 'permission_denied',
+ __( 'You do not have permission to edit this post.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ $manager->link_to_post( $session_id, $post_id );
+
+ $updated_session = $manager->get_session( $session_id );
+
+ return new WP_REST_Response(
+ array(
+ 'linked' => true,
+ 'post_id' => $post_id,
+ 'session' => $updated_session,
+ ),
+ 200
+ );
+ }
+
+ /**
+ * Handle migrate chat history request.
+ *
+ * Migrates legacy _wpaw_chat_history from post meta to session table.
+ *
+ * @since 0.1.4
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_migrate_chat_history( $request ) {
+ $post_id = (int) $request->get_param( 'post_id' );
+
+ if ( $post_id <= 0 ) {
+ return new WP_Error(
+ 'invalid_post',
+ __( 'Valid post ID is required.', 'wp-agentic-writer' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ // Verify post exists and user can edit
+ if ( ! current_user_can( 'edit_post', $post_id ) ) {
+ return new WP_Error(
+ 'permission_denied',
+ __( 'You do not have permission to edit this post.', 'wp-agentic-writer' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ // Use Context Service for migration
+ $context_service = WP_Agentic_Writer_Context_Service::get_instance();
+ $result = $context_service->migrate_legacy_chat_history( $post_id );
+
+ if ( ! $result ) {
+ return new WP_Error(
+ 'migration_failed',
+ __( 'Failed to migrate chat history.', 'wp-agentic-writer' ),
+ array( 'status' => 500 )
+ );
+ }
+
+ // Return migration status
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+ $sessions = $manager->get_sessions_for_post( $post_id );
+
+ return new WP_REST_Response(
+ array(
+ 'migrated' => true,
+ 'post_id' => $post_id,
+ 'sessions_count' => count( $sessions ),
+ 'message' => 'Legacy chat history has been migrated to session table.',
+ ),
+ 200
+ );
+ }
+
+ /**
+ * Auto-save post and link conversation when writing execution begins.
+ *
+ * @since 0.1.4
+ * @param string $session_id Session ID.
+ * @param int $post_id Current post ID (can be 0).
+ * @return int New post ID if saved, or original if not needed.
+ */
+ public function ensure_conversation_linked_to_post( $session_id, $post_id = 0 ) {
+ $manager = WP_Agentic_Writer_Conversation_Manager::get_instance();
+
+ // Already linked
+ if ( $post_id > 0 ) {
+ return $post_id;
+ }
+
+ // Check if editor has content
+ if ( ! $manager->post_has_content( get_the_ID() ) ) {
+ // No content yet, keep as post_id = 0
+ return 0;
+ }
+
+ // Get current post (auto-save with placeholder title)
+ $current_post_id = get_the_ID();
+ if ( $current_post_id && $current_post_id > 0 ) {
+ // Update post with placeholder title if needed
+ $post = get_post( $current_post_id );
+ if ( $post && empty( $post->post_title ) ) {
+ wp_update_post( array(
+ 'ID' => $current_post_id,
+ 'post_title' => 'Draft - ' . date( 'Y-m-d H:i' ),
+ ) );
+ }
+
+ // Link conversation to post
+ $manager->link_to_post( $session_id, $current_post_id );
+
+ return $current_post_id;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Get user preferences (per-user settings).
+ *
+ * @since 0.2.0
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_get_user_preferences( $request ) {
+ $user_id = get_current_user_id();
+
+ // Return defaults if not logged in
+ if ( $user_id === 0 ) {
+ return new WP_REST_Response(
+ array(
+ 'proactive_suggestions' => true,
+ 'command_palette_enabled' => true,
+ 'outline_panel_enabled' => true,
+ 'auto_save_interval' => 30,
+ 'preferred_model' => '',
+ 'preferred_language' => 'auto',
+ 'theme' => 'dark',
+ ),
+ 200
+ );
+ }
+
+ $preferences = get_user_meta( $user_id, 'wpaw_user_preferences', true );
+
+ // Merge with defaults
+ $defaults = array(
+ 'proactive_suggestions' => true,
+ 'command_palette_enabled' => true,
+ 'outline_panel_enabled' => true,
+ 'auto_save_interval' => 30,
+ 'preferred_model' => '',
+ 'preferred_language' => 'auto',
+ 'theme' => 'dark',
+ );
+
+ $preferences = is_array( $preferences ) ? array_merge( $defaults, $preferences ) : $defaults;
+
+ return new WP_REST_Response( $preferences, 200 );
+ }
+
+ /**
+ * Save user preferences (per-user settings).
+ *
+ * @since 0.2.0
+ * @param WP_REST_Request $request REST request.
+ * @return WP_REST_Response|WP_Error
+ */
+ public function handle_save_user_preferences( $request ) {
+ $user_id = get_current_user_id();
+
+ if ( $user_id === 0 ) {
+ return new WP_Error(
+ 'unauthorized',
+ __( 'You must be logged in to save preferences.', 'wp-agentic-writer' ),
+ array( 'status' => 401 )
+ );
+ }
+
+ $preferences = $request->get_json_params();
+
+ // Validate and sanitize
+ $sanitized = array(
+ 'proactive_suggestions' => ! empty( $preferences['proactive_suggestions'] ),
+ 'command_palette_enabled' => ! empty( $preferences['command_palette_enabled'] ),
+ 'outline_panel_enabled' => ! empty( $preferences['outline_panel_enabled'] ),
+ 'auto_save_interval' => max( 5, min( 300, (int) ( $preferences['auto_save_interval'] ?? 30 ) ) ),
+ 'preferred_model' => sanitize_text_field( $preferences['preferred_model'] ?? '' ),
+ 'preferred_language' => sanitize_text_field( $preferences['preferred_language'] ?? 'auto' ),
+ 'theme' => in_array( $preferences['theme'] ?? 'dark', array( 'dark', 'light' ), true ) ? $preferences['theme'] : 'dark',
+ );
+
+ update_user_meta( $user_id, 'wpaw_user_preferences', $sanitized );
+
+ return new WP_REST_Response( $sanitized, 200 );
+ }
}