diff --git a/TASKLIST_AUDIT_FIXES.md b/TASKLIST_AUDIT_FIXES.md new file mode 100644 index 0000000..801328a --- /dev/null +++ b/TASKLIST_AUDIT_FIXES.md @@ -0,0 +1,23 @@ +# Audit Fix Tasklist + +## Phase 1: UI Theme Consistency & Polish +- [x] 1.1 Make chat messages use dark theme consistently (remove white bg) +- [x] 1.2 Restyle plan cards (remove dashed wireframe look, add fills/icons/status colors) +- [x] 1.3 Fix timeline entry typography (remove monospace, use humanist font) +- [x] 1.4 Structure error messages (icon + title + collapsible detail + action) +- [x] 1.5 Polish input area cohesion (unify focus bar + mode + textarea) + +## Phase 2: UX Flow Improvements +- [x] 2.1 Add contextual placeholder text per agent mode in textarea +- [x] 2.2 Add visual mode indicator badge in chat area +- [x] 2.3 Simplify welcome screen (reduce session list noise) +- [x] 2.4 Add slash command discovery hint in empty input +- [x] 2.5 Add confirmation before writing over existing content +- [x] 2.6 Add streaming timeout heartbeat (30s no-data reassurance) + +## Phase 3: Error Handling Hardening +- [x] 3.1 Add DB table health check on sidebar init +- [x] 3.2 Improve "no API key" error with settings link +- [x] 3.3 Show in-chat warning when provider fallback triggers +- [x] 3.4 Auto-fallback to registry fallback model on unavailability +- [x] 3.5 Ensure isLoading always resets on all error paths diff --git a/assets/css/sidebar.css b/assets/css/sidebar.css index 1378b81..8b1172b 100644 --- a/assets/css/sidebar.css +++ b/assets/css/sidebar.css @@ -144,10 +144,10 @@ display: flex; flex-direction: column; overflow: hidden; - background: #ffffff; + background: #1a1d23; border-radius: 0; min-height: 0; - border: 1px solid #dcdcde; + border: 1px solid #2d3139; } .wpaw-messages-inner { @@ -163,24 +163,24 @@ } .wpaw-messages-inner::-webkit-scrollbar-track { - background: #f0f0f1; + background: #1a1d23; border-radius: 0; } .wpaw-messages-inner::-webkit-scrollbar-thumb { - background: #c3c4c7; - border-radius: 0; + background: #3d4450; + border-radius: 3px; } .wpaw-messages-inner::-webkit-scrollbar-thumb:hover { - background: #8c8f94; + background: #525b6b; } .wpaw-input-area { - background: #f6f7f7; + background: #1e2128; padding: 12px; border-radius: 0; - border: 1px solid #dcdcde; + border: 1px solid #2d3139; border-top: none; margin-top: auto; } @@ -194,7 +194,7 @@ .wpaw-input-label { font-size: 12px; - color: #5f6b7a; + color: #8b95a5; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; @@ -202,24 +202,24 @@ .wpaw-mode-select { padding: 6px 8px; - border-radius: 2px; - border: 1px solid #8c8f94; - background: #fff; + border-radius: 6px; + border: 1px solid #3d4450; + background: #252830; font-size: 13px; font-weight: 400; - color: #1d2227; + color: #c8cdd5; cursor: pointer; transition: border-color 0.1s ease; } .wpaw-mode-select:hover { - border-color: #2271b1; + border-color: #5b8def; } .wpaw-mode-select:focus { outline: none; - border-color: #2271b1; - box-shadow: 0 0 0 1px #2271b1; + border-color: #5b8def; + box-shadow: 0 0 0 1px #5b8def; } #agentMode { @@ -239,9 +239,10 @@ .wpaw-message { margin-bottom: 12px; padding: 10px 12px; - border-radius: 0; - background: #fff; - border: 1px solid #dcdcde; + border-radius: 8px; + background: #252830; + border: 1px solid #2d3139; + color: #e0e4ea; animation: messageSlide 0.2s ease; } @@ -258,32 +259,71 @@ } .wpaw-message-user { - background: #fff; - border-left: 3px solid #2271b1; + background: #2a3040; + border-left: none; margin-left: 0; max-width: 80%; margin-left: auto; - border-radius: 8px; - border: 1px solid #4c4c4c; -} - -.dark-theme .wpaw-message-user { - background: #252830; + border-radius: 12px 12px 4px 12px; + border: 1px solid #3b4560; + color: #e8ecf2; } .wpaw-message-error { - background: rgb(214, 54, 56, 0.05); - /* border-left: 3px solid #d63638; */ - /* border-color: #8a1e1e; */ - border: unset; - color: #d63638; + background: rgba(220, 38, 38, 0.08); + border: 1px solid rgba(220, 38, 38, 0.25); + border-left: 3px solid #ef4444; + border-radius: 8px; + color: #fca5a5; + padding: 12px 14px; +} + +.wpaw-message-error .wpaw-error-title { + font-weight: 600; + font-size: 13px; + color: #fca5a5; + margin-bottom: 4px; + display: flex; + align-items: center; + gap: 6px; +} + +.wpaw-message-error .wpaw-error-detail { + font-size: 12px; + color: #d4a0a0; + line-height: 1.5; + margin-top: 6px; +} + +.wpaw-message-error details { + margin-top: 8px; +} + +.wpaw-message-error details summary { + font-size: 11px; + color: #e87171; + cursor: pointer; + user-select: none; +} + +.wpaw-message-error details[open] summary { + margin-bottom: 6px; } .wpaw-message-error button.is-secondary { - background: #8a1e1e; - color: white; - border: unset !important; - box-shadow: unset !important; + background: rgba(220, 38, 38, 0.15); + color: #fca5a5; + border: 1px solid rgba(220, 38, 38, 0.4) !important; + box-shadow: none !important; + border-radius: 6px; + margin-top: 8px; + font-size: 12px; + padding: 6px 14px; + transition: background 0.15s; +} + +.wpaw-message-error button.is-secondary:hover { + background: rgba(220, 38, 38, 0.25); } /* Research message styling */ @@ -384,39 +424,48 @@ .wpaw-plan-card, .wpaw-edit-plan { - border: 2px dashed #4c4c4c; - padding: 12px; - border-radius: 0; + background: #1e2530; + border: 1px solid #2d3a4a; + padding: 14px; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } .wpaw-plan-card:hover { - border-color: #8c8f94; + border-color: #3d5070; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .wpaw-plan-title { font-weight: 600; font-size: 14px; margin-bottom: 8px; + color: #e8ecf2; } -.wpaw-plan-config-summary { - margin-bottom: 12px; - padding: 8px 10px; - background: #1a1a1a; - border: 1px solid #3c3c3c; - border-radius: 4px; - font-size: 12px; - line-height: 1.6; +.wpaw-plan-section-status { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 2px 8px; + border-radius: 10px; + margin-top: 2px; + font-weight: 600; } -.wpaw-config-summary-item { - color: #b0b0b0; - font-family: ui-monospace, monospace; - margin-bottom: 4px; +.wpaw-plan-section.pending .wpaw-plan-section-status { + color: #94a3b8; + background: rgba(148, 163, 184, 0.1); } -.wpaw-config-summary-item:last-child { - margin-bottom: 0; +.wpaw-plan-section.done .wpaw-plan-section-status { + color: #4ade80; + background: rgba(74, 222, 128, 0.1); +} + +.wpaw-plan-section.in_progress .wpaw-plan-section-status { + color: #60a5fa; + background: rgba(96, 165, 250, 0.1); } .wpaw-plan-sections, @@ -449,25 +498,6 @@ input.wpaw-plan-section-check:checked::before { flex: 1; } -.wpaw-plan-section-title { - font-weight: 600; - margin-bottom: 4px; -} - -.wpaw-plan-section-desc { - color: #6c6c6c; - font-size: 13px; - line-height: 1.5; -} - -.wpaw-plan-section-status { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.05em; - color: #94a3b8; - margin-top: 2px; -} - .wpaw-clear-context { margin-left: auto; background: #f6f7f7; @@ -584,20 +614,22 @@ input.wpaw-plan-section-check:checked::before { } .wpaw-ai-response pre { - background: #f1f5f9; - padding: 10px; + background: #1a1d23; + padding: 12px; border-radius: 8px; overflow-x: auto; - font-family: "Courier New", monospace; + font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 12px; + border: 1px solid #2d3139; + color: #c8cdd5; } .wpaw-ai-response code { - background: #e2e8f0; - color: #1f2937; - padding: 2px 4px; + background: #2d3139; + color: #a5d6ff; + padding: 2px 5px; border-radius: 4px; - font-family: "Courier New", monospace; + font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 12px; } @@ -614,14 +646,6 @@ input.wpaw-plan-section-check:checked::before { pointer-events: all; } -.wpaw-plan-section.done .wpaw-plan-section-status { - color: #15803d; -} - -.wpaw-plan-section.in_progress .wpaw-plan-section-status { - color: #2563eb; -} - /* Outline Version Tracking & Inline Editing */ .wpaw-plan-header { display: flex; @@ -857,12 +881,12 @@ input.wpaw-plan-section-check:checked::before { .wpaw-response { margin: 0 0 12px 0; - border-left: 2px solid #e0e6ed; - color: #1f2937; + border-left: 2px solid #3d4450; + color: #dce0e8; } .dark-theme .wpaw-response { - color: #cecece; + color: #dce0e8; } .wpaw-ai-response .wpaw-response { @@ -943,70 +967,6 @@ input.wpaw-plan-section-check:checked::before { } } -.wpaw-response-content { - line-height: 1.6; - word-wrap: break-word; - white-space: normal; -} - -.wpaw-response-content>* { - padding: 1rem; -} - -.wpaw-response-content p { - margin: 0 0 8px; -} - -.wpaw-response-content p:last-child { - margin-bottom: 0; -} - -.wpaw-response-content h1, -.wpaw-response-content h2, -.wpaw-response-content h3, -.wpaw-response-content h4, -.wpaw-response-content h5, -.wpaw-response-content h6 { - margin: 12px 0 6px; - line-height: 1.4; -} - -.wpaw-response-content ul, -.wpaw-response-content ol { - margin: 6px 0 10px 18px; - padding: 0; -} - -.wpaw-response-content ul li { - list-style: square; -} - -.wpaw-response-content li { - margin: 4px 0; -} - -.wpaw-response-content li p { - margin: 4px 0 6px; -} - -.dark-theme .wpaw-response-content *:is(h1, h2, h3, h4, h5, h6) { - color: #cecece; - font-weight: bold; -} - -.wpaw-response-content table { - border: 1px solid; - border-collapse: collapse; - margin-bottom: 10px; -} - -.wpaw-response-content table th, -.wpaw-response-content table td { - border: 1px solid; - padding: 5px; - text-align: left; -} - /* Timeline Progress */ .wpaw-timeline-entry { display: flex; @@ -1133,19 +1093,19 @@ input.wpaw-plan-section-check:checked::before { .wpaw-timeline-content { flex: 1; - font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; - color: #334155; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + color: #c8cdd5; } .wpaw-timeline-message { - font-size: 12px; - color: #334155; + font-size: 12.5px; + color: #c8cdd5; margin-bottom: 5px; line-height: 1.5; } .dark-theme .wpaw-timeline-message { - color: white; + color: #e0e4ea; } .wpaw-timeline-complete { @@ -4589,3 +4549,237 @@ input.wpaw-plan-section-check:checked::before { .wpaw-provider-info:has(.wpaw-fallback) { color: #f59e0b; } + + +/* =========================== + AUDIT FIXES: Mode Indicator Badge + =========================== */ +.wpaw-mode-badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 8px; +} + +.wpaw-mode-badge.mode-chat { + background: rgba(96, 165, 250, 0.12); + color: #60a5fa; + border: 1px solid rgba(96, 165, 250, 0.25); +} + +.wpaw-mode-badge.mode-planning { + background: rgba(251, 191, 36, 0.12); + color: #fbbf24; + border: 1px solid rgba(251, 191, 36, 0.25); +} + +.wpaw-mode-badge.mode-writing { + background: rgba(74, 222, 128, 0.12); + color: #4ade80; + border: 1px solid rgba(74, 222, 128, 0.25); +} + +/* =========================== + AUDIT FIXES: Streaming Heartbeat + =========================== */ +.wpaw-heartbeat-notice { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin: 8px 0; + background: rgba(251, 191, 36, 0.08); + border: 1px solid rgba(251, 191, 36, 0.2); + border-radius: 8px; + font-size: 12px; + color: #fbbf24; + animation: fadeIn 0.3s ease; +} + +.wpaw-heartbeat-notice .wpaw-heartbeat-icon { + animation: pulse-ring 2s infinite; +} + +/* =========================== + AUDIT FIXES: Slash Command Hint + =========================== */ +.wpaw-input-hint { + position: absolute; + bottom: 100%; + left: 12px; + right: 12px; + padding: 6px 10px; + background: #252830; + border: 1px solid #3d4450; + border-bottom: none; + border-radius: 8px 8px 0 0; + font-size: 11px; + color: #6b7a8d; + display: flex; + align-items: center; + gap: 6px; +} + +.wpaw-input-hint kbd { + background: #3d4450; + color: #a0aec0; + padding: 1px 5px; + border-radius: 3px; + font-size: 10px; + font-family: inherit; +} + +/* =========================== + AUDIT FIXES: Provider Fallback Warning + =========================== */ +.wpaw-provider-warning { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 10px 12px; + margin: 8px 0; + background: rgba(251, 146, 60, 0.08); + border: 1px solid rgba(251, 146, 60, 0.2); + border-left: 3px solid #fb923c; + border-radius: 8px; + font-size: 12px; + color: #fdba74; + line-height: 1.5; +} + +.wpaw-provider-warning a { + color: #fb923c; + text-decoration: underline; +} + +/* =========================== + AUDIT FIXES: DB Health Notice + =========================== */ +.wpaw-health-notice { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + margin: 8px 0; + background: rgba(220, 38, 38, 0.06); + border: 1px solid rgba(220, 38, 38, 0.2); + border-radius: 8px; + font-size: 12px; + color: #fca5a5; +} + +.wpaw-health-notice a { + color: #ef4444; + text-decoration: underline; +} + +/* =========================== + AUDIT FIXES: Confirm Modal for Writing + =========================== */ +.wpaw-write-confirm-overlay { + position: absolute; + inset: 0; + z-index: 1200; + background: rgba(10, 16, 27, 0.75); + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + animation: fadeIn 0.2s ease; +} + +.wpaw-write-confirm-modal { + width: 100%; + max-width: 380px; + background: #1e2530; + color: #e5e7eb; + border: 1px solid #334155; + border-radius: 12px; + padding: 20px; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5); +} + +.wpaw-write-confirm-title { + font-size: 15px; + font-weight: 700; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 8px; +} + +.wpaw-write-confirm-body { + font-size: 13px; + line-height: 1.6; + color: #94a3b8; + margin-bottom: 16px; +} + +.wpaw-write-confirm-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +/* =========================== + AUDIT FIXES: Response content dark theme + =========================== */ +.wpaw-response-content { + line-height: 1.6; + word-wrap: break-word; + white-space: normal; + color: #dce0e8; +} + +.wpaw-response-content>* { + padding: 1rem; +} + +.wpaw-response-content p { + margin: 0 0 8px; +} + +.wpaw-response-content p:last-child { + margin-bottom: 0; +} + +/* Plan section title in dark theme */ +.wpaw-plan-section-title { + font-weight: 600; + margin-bottom: 4px; + color: #e0e4ea; +} + +.wpaw-plan-section-desc { + color: #8b95a5; + font-size: 13px; + line-height: 1.5; +} + +/* Config summary dark */ +.wpaw-plan-config-summary { + margin-bottom: 12px; + padding: 10px 12px; + background: #161a20; + border: 1px solid #2d3a4a; + border-radius: 8px; + font-size: 12px; + line-height: 1.6; +} + +.wpaw-config-summary-item { + color: #9aa5b4; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + margin-bottom: 4px; + font-size: 11.5px; +} + +.wpaw-config-summary-item:last-child { + margin-bottom: 0; +} diff --git a/assets/js/sidebar.js b/assets/js/sidebar.js index 275b946..686b9c3 100644 --- a/assets/js/sidebar.js +++ b/assets/js/sidebar.js @@ -37,21 +37,28 @@ const cleanMessage = String(rawMessage || fallback).replace(/^API error:\s*/i, '').trim(); const lowerMessage = cleanMessage.toLowerCase(); + // Returns structured object { title, detail, actionUrl, actionLabel } + const structured = (title, detail, actionUrl, actionLabel) => { + return { title, detail, actionUrl: actionUrl || '', actionLabel: actionLabel || '' }; + }; + if ( lowerMessage.includes('no allowed providers are available') || (lowerMessage.includes('allowed providers') && lowerMessage.includes('selected model')) ) { const routedProvider = settings?.openrouter_provider_slug && settings.openrouter_provider_slug !== 'auto' - ? ` Current pinned provider: ${settings.openrouter_provider_slug}.` + ? ` Pinned: ${settings.openrouter_provider_slug}.` : ''; - - return 'The selected model is not available from the current OpenRouter provider routing settings.' - + routedProvider - + ' Open Settings -> WP Agentic Writer -> Models -> OpenRouter Provider Routing, then either choose a provider that supports this model, turn off "Only use this provider", enable fallback providers, or select a model from the pinned BYOK provider.'; + return structured( + 'Model unavailable from current provider', + `The pinned provider routing doesn't support this model.${routedProvider} Change provider routing or select a compatible model.`, + settings?.settings_url || '', + 'Open Settings' + ); } if (cleanMessage.includes('429') || lowerMessage.includes('rate limit')) { - return 'Rate limit exceeded. Please wait a moment and try again.'; + return structured('Rate limit exceeded', 'The AI provider is throttling requests. Wait a moment and try again.'); } if ( @@ -59,18 +66,37 @@ || lowerMessage.includes('operation timed out') || lowerMessage.includes('timed out after') ) { - return 'The selected model/provider started the planning request but did not finish before the 2-minute timeout. Try a faster planning model, reduce the outline/article length, or switch OpenRouter Provider Routing back to Auto/fallback-capable routing before retrying.'; + return structured( + 'Request timed out', + 'The model took too long to respond. Try a faster model, reduce content length, or check your provider routing.', + settings?.settings_url || '', + 'Open Settings' + ); } if (cleanMessage.startsWith('HTTP 401') || lowerMessage.includes('unauthorized')) { - return 'The AI provider rejected the API key. Please check the OpenRouter/API key settings and try again.'; + return structured( + 'API key rejected', + 'The provider rejected your API key. Check your key in settings.', + settings?.settings_url || '', + 'Open Settings' + ); } if (cleanMessage.startsWith('HTTP 402') || lowerMessage.includes('insufficient credits')) { - return 'The AI provider says the account has insufficient credits or quota. Please check your provider billing/BYOK setup.'; + return structured('Insufficient credits', 'Your provider account has no remaining credits or quota.'); } - return `Error: ${cleanMessage || fallback}`; + if (lowerMessage.includes('api key is not configured') || lowerMessage.includes('no_api_key')) { + return structured( + 'API key not configured', + 'Add your OpenRouter API key in plugin settings to start using AI features.', + settings?.settings_url || '', + 'Configure API Key' + ); + } + + return structured(cleanMessage || fallback, ''); }; // Tab state @@ -1857,10 +1883,29 @@ let streamBuffer = ''; let fullContent = ''; let streamError = null; + let lastDataTime = Date.now(); + let heartbeatShown = false; + // Heartbeat: show reassurance if no data for 30s + const heartbeatInterval = setInterval(() => { + if (Date.now() - lastDataTime > 30000 && !heartbeatShown) { + heartbeatShown = true; + setMessages(prev => [...prev, { + role: 'system', + type: 'timeline', + status: 'active', + message: '⏳ Still waiting for response — the model is processing...', + timestamp: new Date() + }]); + } + }, 10000); + + try { while (true) { const { done, value } = await reader.read(); if (done) break; + lastDataTime = Date.now(); + heartbeatShown = false; streamBuffer += decoder.decode(value, { stream: true }); const lines = streamBuffer.split('\n'); @@ -1901,6 +1946,15 @@ addFocusKeywordSuggestions(suggestions); } } + } else if (data.type === 'provider' && data.fallback_used) { + // Show in-chat provider fallback warning + setMessages(prev => [...prev, { + role: 'system', + type: 'timeline', + status: 'active', + message: `⚠️ ${data.selectedProvider || 'Selected provider'} unavailable — using ${data.provider || 'fallback'}`, + timestamp: new Date() + }]); } } catch (e) { wpawLog.error('Failed to parse retry streaming data:', line, e); @@ -1911,6 +1965,9 @@ throw streamError; } } + } finally { + clearInterval(heartbeatInterval); + } } catch (error) { const errorMsg = formatAiErrorMessage(error, 'Failed to chat'); setMessages(prev => [...prev, { @@ -2549,6 +2606,24 @@ } const plan = currentPlanRef.current; + + // Confirmation: warn if editor already has content blocks + const existingBlocks = select('core/block-editor').getBlocks(); + const hasExistingContent = existingBlocks.some(b => + b.name !== 'core/paragraph' || (b.attributes?.content && b.attributes.content.trim().length > 0) + ); + if (hasExistingContent && !options.skipConfirm) { + const pendingSections = Array.isArray(plan?.sections) + ? plan.sections.filter((section) => section.status !== 'done').length + : 0; + const confirmed = window.confirm( + `This will write ${pendingSections} sections into the editor. Existing content will be preserved below the new content.\n\nContinue?` + ); + if (!confirmed) { + return; + } + } + setAgentMode('writing'); const pendingCount = Array.isArray(plan?.sections) ? plan.sections.filter((section) => section.status !== 'done').length @@ -5583,23 +5658,32 @@ // Render Welcome Screen (chatty, friendly) const renderWelcomeScreen = () => { + const recentSession = availableSessions.length > 0 ? availableSessions[0] : null; + return wp.element.createElement('div', { className: 'wpaw-welcome-screen' }, wp.element.createElement('div', { className: 'wpaw-welcome-content' }, wp.element.createElement('span', { className: 'wpaw-welcome-icon', dangerouslySetInnerHTML: { __html: '' } }), - 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('h2', { className: 'wpaw-welcome-title' }, 'Agentic Writer'), + wp.element.createElement('p', { className: 'wpaw-welcome-subtitle' }, "What are we writing today?"), + // Show single "Continue last conversation" button if available + recentSession && wp.element.createElement('button', { + className: 'wpaw-welcome-pill', + style: { width: '100%', marginBottom: '12px' }, + disabled: isSessionActionLoading, + onClick: () => openSessionById(recentSession.session_id || '') + }, `↩ Continue: ${getSessionDisplayTitle(recentSession, 0)}`), + // Show older sessions in collapsible + availableSessions.length > 1 && wp.element.createElement('details', { + style: { marginBottom: '12px', width: '100%' } }, - wp.element.createElement('div', { - style: { fontSize: '12px', opacity: 0.8, marginBottom: '8px' } - }, 'Continue a previous conversation'), - wp.element.createElement('div', { className: 'wpaw-session-list' }, - ...availableSessions.map((session, idx) => + wp.element.createElement('summary', { + style: { fontSize: '12px', color: '#8b95a5', cursor: 'pointer', marginBottom: '8px' } + }, `${availableSessions.length - 1} more session${availableSessions.length > 2 ? 's' : ''}`), + wp.element.createElement('div', { className: 'wpaw-session-list' }, + ...availableSessions.slice(1).map((session, idx) => wp.element.createElement('div', { key: session.session_id || idx, className: 'wpaw-welcome-pill', @@ -5624,14 +5708,9 @@ textAlign: 'left', cursor: isSessionActionLoading ? 'wait' : 'pointer' }, - onClick: () => { - openSessionById(session.session_id || ''); - } + onClick: () => openSessionById(session.session_id || '') }, - wp.element.createElement('div', null, getSessionDisplayTitle(session, idx)), - // wp.element.createElement('div', { style: { opacity: 0.55, fontSize: '10px', marginTop: '2px' } }, - // getSessionDebugMeta(session) - // ), + wp.element.createElement('div', null, getSessionDisplayTitle(session, idx + 1)), wp.element.createElement('div', { style: { opacity: 0.7, fontSize: '11px' } }, `${Number(session?.message_count ?? (Array.isArray(session?.messages) ? session.messages.length : 0))} msgs` ) @@ -5652,19 +5731,13 @@ }, '×') ) ) - ), - 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', className: 'wpaw-welcome-input', - placeholder: 'Your focus keyword (optional)', + placeholder: 'Focus keyword (optional)', value: welcomeKeywordInput, onChange: (e) => setWelcomeKeywordInput(e.target.value), onKeyDown: (e) => { @@ -5682,14 +5755,14 @@ wp.element.createElement('button', { className: 'wpaw-welcome-pill' + (welcomeStartMode === 'planning' ? ' active' : ''), onClick: () => setWelcomeStartMode('planning') - }, '📝 Make an Outline') + }, '📝 Create Outline') ), // Start button wp.element.createElement(Button, { isPrimary: true, onClick: handleWelcomeStart, className: 'wpaw-welcome-start-btn' - }, 'Start') + }, 'Start Writing') ) ); }; @@ -6615,15 +6688,30 @@ retryLastGeneration(); }; + // Support structured error objects { title, detail, actionUrl, actionLabel } + const errContent = message.content; + const isStructured = errContent && typeof errContent === 'object' && errContent.title; + return wp.element.createElement('div', { key: `error-${index}`, className: 'wpaw-ai-item wpaw-message wpaw-message-error', }, - wp.element.createElement('div', { className: 'wpaw-message-content' }, renderMessageContent(message.content, true)), + isStructured + ? wp.element.createElement('div', null, + wp.element.createElement('div', { className: 'wpaw-error-title' }, '⚠ ', errContent.title), + errContent.detail && wp.element.createElement('div', { className: 'wpaw-error-detail' }, errContent.detail), + errContent.actionUrl && wp.element.createElement('a', { + href: errContent.actionUrl, + target: '_blank', + rel: 'noopener', + style: { display: 'inline-block', marginTop: '8px', fontSize: '12px', color: '#fca5a5', textDecoration: 'underline' } + }, errContent.actionLabel || 'Open Settings') + ) + : wp.element.createElement('div', { className: 'wpaw-message-content' }, renderMessageContent(errContent, true)), message.canRetry && wp.element.createElement(Button, { isSecondary: true, onClick: handleRetry, - }, 'Retry') + }, '↻ Retry') ); } @@ -7024,6 +7112,19 @@ isRefinementLocked && wp.element.createElement('div', { className: 'wpaw-refinement-lock-banner' }, `Refining in progress — editing is temporarily locked. You can still scroll and review changes live (${refiningBlockIds.length} target block(s)).` ), + // Health Check Warnings + wpAgenticWriter.health && !wpAgenticWriter.health.ok && wpAgenticWriter.health.issues.map((issue, idx) => + wp.element.createElement('div', { key: `health-${idx}`, className: 'wpaw-health-notice' }, + '⚠️ ', + issue.message, + issue.actionUrl && wp.element.createElement('a', { + href: issue.actionUrl, + target: '_blank', + rel: 'noopener', + style: { marginLeft: '8px' } + }, issue.actionLabel || 'Fix') + ) + ), // Welcome Screen (first time) showWelcome && !isEditorLocked && renderWelcomeScreen(), // Writing Mode Empty State @@ -7037,8 +7138,20 @@ ), // Context Indicator (moved above textarea) - hide when showing empty state or welcome !showWelcome && !shouldShowWritingEmptyState() && renderContextIndicator(), + // Mode Badge + !showWelcome && !shouldShowWritingEmptyState() && wp.element.createElement('div', { + className: `wpaw-mode-badge mode-${agentMode}` + }, + agentMode === 'chat' ? '💬' : agentMode === 'planning' ? '📝' : '✍️', + agentMode === 'chat' ? 'Chat Mode' : agentMode === 'planning' ? 'Planning Mode' : 'Writing Mode' + ), // Command Input Area - hide when showing empty state or welcome !showWelcome && !shouldShowWritingEmptyState() && wp.element.createElement('div', { className: 'wpaw-command-area', style: { position: 'relative' } }, + // Slash command hint when input is empty + !input && !isLoading && wp.element.createElement('div', { className: 'wpaw-input-hint' }, + 'Type ', wp.element.createElement('kbd', null, '/'), ' for commands or ', + wp.element.createElement('kbd', null, '@'), ' to mention a block' + ), // Removed Toolbar from Top wp.element.createElement('div', { className: 'wpaw-command-input-wrapper' + (isTextareaExpanded ? ' expanded' : '') @@ -7051,8 +7164,10 @@ onKeyDown: handleKeyDown, rows: isTextareaExpanded ? 20 : 3, placeholder: agentMode === 'planning' - ? 'Describe what you want to write about...' - : 'Ask me anything about your content...' + ? 'Describe your article topic...' + : agentMode === 'writing' + ? 'Refine content — use @block to target specific sections...' + : 'Ask anything about your content, or type / for commands...' }) ), showMentionAutocomplete && mentionOptions.length > 0 && wp.element.createElement('div', { diff --git a/includes/class-gutenberg-sidebar.php b/includes/class-gutenberg-sidebar.php index 2abfa90..ee3ddf9 100644 --- a/includes/class-gutenberg-sidebar.php +++ b/includes/class-gutenberg-sidebar.php @@ -243,6 +243,9 @@ class WP_Agentic_Writer_Gutenberg_Sidebar { // Get settings for JS. $settings = $this->get_settings_for_js(); + // Health check: verify DB table and API key exist + $health = $this->run_health_check(); + // Localize script with data. $data = array( 'apiUrl' => rest_url( 'wp-agentic-writer/v1' ), @@ -252,11 +255,48 @@ class WP_Agentic_Writer_Gutenberg_Sidebar { 'version' => WP_AGENTIC_WRITER_VERSION, 'debug' => defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG, 'pluginUrl' => plugin_dir_url( dirname( __FILE__ ) ), + 'health' => $health, ); wp_localize_script( 'wp-agentic-writer-sidebar', 'wpAgenticWriter', $data ); } + /** + * Run health check for sidebar initialization. + * + * @since 0.2.4 + * @return array Health status. + */ + private function run_health_check() { + global $wpdb; + $table_name = $wpdb->prefix . 'wpaw_conversations'; + $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name; + + $settings = get_option( 'wp_agentic_writer_settings', array() ); + $has_api_key = ! empty( $settings['openrouter_api_key'] ); + + $issues = array(); + if ( ! $table_exists ) { + $issues[] = array( + 'type' => 'db_table_missing', + 'message' => 'Conversation table not found. Please deactivate and reactivate the plugin.', + ); + } + if ( ! $has_api_key ) { + $issues[] = array( + 'type' => 'no_api_key', + 'message' => 'API key not configured. Add your OpenRouter key in settings.', + 'actionUrl' => admin_url( 'options-general.php?page=wp-agentic-writer-settings' ), + 'actionLabel' => 'Open Settings', + ); + } + + return array( + 'ok' => empty( $issues ), + 'issues' => $issues, + ); + } + /** * Get settings for JavaScript. * diff --git a/includes/class-openrouter-provider.php b/includes/class-openrouter-provider.php index f1682aa..2d69e4d 100644 --- a/includes/class-openrouter-provider.php +++ b/includes/class-openrouter-provider.php @@ -784,7 +784,21 @@ class WP_Agentic_Writer_OpenRouter_Provider implements WP_Agentic_Writer_AI_Prov // Validate model availability before making API call $model_validation = $this->validate_model_availability( $model ); if ( is_wp_error( $model_validation ) ) { - return $model_validation; + // Auto-fallback: try registry fallback model instead of hard-failing + $fallback_model = WPAW_Model_Registry::get_fallback_model( $type ); + if ( $fallback_model && $fallback_model !== $model ) { + $fallback_validation = $this->validate_model_availability( $fallback_model ); + if ( true === $fallback_validation ) { + $model = $fallback_model; + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( "WPAW: Model unavailable, auto-fallback to: {$fallback_model}" ); + } + } else { + return $model_validation; + } + } else { + return $model_validation; + } } // Build request body.