feat: consolidate docs, backend/session infra, and settings updates

This commit is contained in:
Dwindi Ramadhana
2026-05-28 00:58:20 +07:00
parent 2424acf726
commit 44e06eed88
102 changed files with 35423 additions and 11181 deletions

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -530,3 +530,43 @@
animation: none;
}
}
/* GEO Score Indicator */
.wpaw-geo-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 16px;
background: var(--wpaw-bg-secondary);
border-top: 1px solid var(--wpaw-border);
font-size: var(--wpaw-text-sm);
}
.wpaw-geo-score {
font-weight: 600;
}
.wpaw-geo-score.excellent {
color: var(--wpaw-success);
}
.wpaw-geo-score.good {
color: var(--wpaw-info);
}
.wpaw-geo-score.fair {
color: var(--wpaw-warning);
}
.wpaw-geo-score.poor {
color: var(--wpaw-error);
}
.wpaw-geo-eligible {
background: var(--wpaw-success);
color: #000;
font-size: 10px;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
}

View File

@@ -0,0 +1,366 @@
/**
* Agentic Vibe - Workflow Pipeline Component
* 5-step visualization for AI writing workflow
*
* @package WP_Agentic_Writer
* @since 0.2.0
*/
/* ============================================
Workflow Container
============================================ */
.wpaw-workflow-progress {
background: var(--wpaw-bg-secondary);
padding: var(--wpaw-space-lg);
border-radius: var(--wpaw-radius-md);
margin-bottom: var(--wpaw-space-lg);
border: 1px solid var(--wpaw-border);
}
.wpaw-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--wpaw-space-lg);
}
.wpaw-progress-title {
font-size: var(--wpaw-text-sm);
font-weight: 600;
color: var(--wpaw-text-primary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.wpaw-progress-status {
font-size: var(--wpaw-text-xs);
color: var(--wpaw-text-tertiary);
font-family: var(--wpaw-font-mono);
}
/* ============================================
Progress Steps Container
============================================ */
.wpaw-progress-steps {
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
/* ============================================
Individual Step
============================================ */
.wpaw-step {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--wpaw-space-sm);
flex: 0 0 auto;
z-index: 1;
}
.wpaw-step-circle {
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: var(--wpaw-text-sm);
border: 2px solid var(--wpaw-border);
background: var(--wpaw-bg-primary);
color: var(--wpaw-text-tertiary);
transition: all var(--wpaw-transition-normal);
position: relative;
}
/* Step Icons */
.wpaw-step-icon {
font-size: var(--wpaw-text-lg);
line-height: 1;
}
/* Completed State */
.wpaw-step.completed .wpaw-step-circle {
background: var(--wpaw-success);
border-color: var(--wpaw-success);
color: white;
box-shadow: 0 0 0 4px rgba(40, 167, 69, 0.2);
}
/* Active State */
.wpaw-step.active .wpaw-step-circle {
background: var(--wpaw-primary);
border-color: var(--wpaw-primary);
color: white;
box-shadow: 0 0 0 8px rgba(23, 162, 184, 0.15);
animation: wpaw-step-pulse 2s ease-in-out infinite;
}
@keyframes wpaw-step-pulse {
0%, 100% {
box-shadow: 0 0 0 8px rgba(23, 162, 184, 0.15);
}
50% {
box-shadow: 0 0 0 12px rgba(23, 162, 184, 0.1);
}
}
/* Pending State */
.wpaw-step.pending .wpaw-step-circle {
opacity: 0.5;
}
.wpaw-step.pending .wpaw-step-label {
color: var(--wpaw-text-tertiary);
}
/* Error State */
.wpaw-step.error .wpaw-step-circle {
background: var(--wpaw-error);
border-color: var(--wpaw-error);
color: white;
}
/* ============================================
Step Label
============================================ */
.wpaw-step-label {
font-size: var(--wpaw-text-xs);
font-weight: 500;
text-align: center;
color: var(--wpaw-text-secondary);
width: 70px;
transition: color var(--wpaw-transition-fast);
}
.wpaw-step.active .wpaw-step-label {
color: var(--wpaw-primary);
font-weight: 600;
}
.wpaw-step.completed .wpaw-step-label {
color: var(--wpaw-success);
}
/* ============================================
Step Connector (Line between steps)
============================================ */
.wpaw-step-connector {
flex: 1;
height: 3px;
background: var(--wpaw-border);
margin: 0 var(--wpaw-space-sm);
position: relative;
top: -28px;
min-width: 40px;
border-radius: 2px;
transition: background var(--wpaw-transition-normal);
}
/* Completed Connector */
.wpaw-step-connector.completed {
background: var(--wpaw-success);
}
/* Active Connector - Animated */
.wpaw-step-connector.active {
background: linear-gradient(
90deg,
var(--wpaw-primary) 0%,
var(--wpaw-primary) 50%,
var(--wpaw-border) 50%,
var(--wpaw-border) 100%
);
background-size: 200% 100%;
animation: wpaw-slide-progress 1.5s linear infinite;
}
@keyframes wpaw-slide-progress {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
/* ============================================
Step Status Message
============================================ */
.wpaw-step-message {
margin-top: var(--wpaw-space-md);
padding: var(--wpaw-space-sm) var(--wpaw-space-md);
background: var(--wpaw-bg-tertiary);
border-radius: var(--wpaw-radius-sm);
font-size: var(--wpaw-text-sm);
color: var(--wpaw-text-secondary);
text-align: center;
font-family: var(--wpaw-font-mono);
border-left: 3px solid var(--wpaw-primary);
}
.wpaw-step-message.success {
border-left-color: var(--wpaw-success);
color: var(--wpaw-success);
}
.wpaw-step-message.error {
border-left-color: var(--wpaw-error);
color: var(--wpaw-error);
}
/* ============================================
Compact Version (for header)
============================================ */
.wpaw-workflow-compact {
padding: var(--wpaw-space-md);
}
.wpaw-workflow-compact .wpaw-step-circle {
width: 32px;
height: 32px;
font-size: var(--wpaw-text-xs);
}
.wpaw-workflow-compact .wpaw-step-icon {
font-size: var(--wpaw-text-sm);
}
.wpaw-workflow-compact .wpaw-step-label {
font-size: 10px;
width: 50px;
}
.wpaw-workflow-compact .wpaw-step-connector {
top: -20px;
height: 2px;
min-width: 20px;
}
/* ============================================
Responsive Design
============================================ */
@media (max-width: 768px) {
.wpaw-progress-steps {
flex-wrap: wrap;
gap: var(--wpaw-space-md);
justify-content: center;
}
.wpaw-step-connector {
display: none;
}
.wpaw-step {
flex: 0 0 20%;
}
.wpaw-step-label {
width: 100%;
max-width: 80px;
}
}
@media (max-width: 480px) {
.wpaw-workflow-progress {
padding: var(--wpaw-space-md);
}
.wpaw-step-circle {
width: 36px;
height: 36px;
}
.wpaw-step-label {
font-size: 10px;
}
.wpaw-progress-title {
font-size: var(--wpaw-text-xs);
}
}
/* ============================================
Animation for Active Step
============================================ */
.wpaw-step.active .wpaw-step-circle::after {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border-radius: 50%;
border: 2px solid var(--wpaw-primary);
border-top-color: transparent;
border-right-color: transparent;
animation: wpaw-spin 1s linear infinite;
}
@keyframes wpaw-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* ============================================
Tooltip for Steps
============================================ */
.wpaw-step[data-tooltip] {
cursor: pointer;
}
.wpaw-step[data-tooltip]:hover .wpaw-step-circle {
transform: scale(1.1);
}
/* ============================================
Mini Progress Bar (alternative)
============================================ */
.wpaw-mini-progress {
display: flex;
align-items: center;
gap: var(--wpaw-space-xs);
font-size: var(--wpaw-text-xs);
color: var(--wpaw-text-tertiary);
}
.wpaw-mini-progress-bar {
flex: 1;
height: 4px;
background: var(--wpaw-bg-tertiary);
border-radius: 2px;
overflow: hidden;
}
.wpaw-mini-progress-fill {
height: 100%;
background: var(--wpaw-primary);
transition: width var(--wpaw-transition-normal);
}
.wpaw-mini-progress-fill.success {
background: var(--wpaw-success);
}
.wpaw-mini-progress-text {
font-family: var(--wpaw-font-mono);
white-space: nowrap;
}

View File

@@ -98,6 +98,28 @@ ul.select2-results__options {
color: #ffffff !important;
}
/* ============================================
Workflow Pipeline Override for Dark Theme
============================================ */
.wpaw-settings-v2-wrap .wpaw-workflow-progress {
background: var(--wpaw-bg-secondary);
border: 1px solid var(--wpaw-border);
}
.wpaw-settings-v2-wrap .wpaw-step-connector {
background: var(--wpaw-border);
}
.wpaw-settings-v2-wrap .wpaw-step-message {
background: var(--wpaw-bg-tertiary);
}
/* Compact mode adjustments */
.wpaw-settings-v2-wrap .wpaw-workflow-compact {
padding: var(--wpaw-space-md);
}
.wpaw-settings-v2-wrap .select2-container--bootstrap-5 .select2-results__option--selected {
background-color: #37373d !important;
color: #ffffff !important;

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,15 @@
(function ($) {
'use strict';
// Debug logging utility
const isDebug = typeof wpAgenticWriter !== 'undefined' && wpAgenticWriter.debug;
const wpawLog = {
log: (...args) => { if (isDebug) console.log('[WPAW]', ...args); },
error: (...args) => console.error('[WPAW]', ...args),
info: (...args) => { if (isDebug) console.info('[WPAW]', ...args); },
warn: (...args) => { if (isDebug) console.warn('[WPAW]', ...args); },
};
// Global state
const state = {
models: {},
@@ -20,71 +29,48 @@
}
};
// Preset configurations
const presets = {
budget: {
chat: 'google/gemini-2.5-flash',
clarity: 'google/gemini-2.5-flash',
planning: 'google/gemini-2.5-flash',
writing: 'mistralai/mistral-small-creative',
refinement: 'google/gemini-2.5-flash',
image: 'openai/gpt-4o'
},
balanced: {
chat: 'google/gemini-2.5-flash',
clarity: 'google/gemini-2.5-flash',
planning: 'google/gemini-2.5-flash',
writing: 'anthropic/claude-3.5-sonnet',
refinement: 'anthropic/claude-3.5-sonnet',
image: 'openai/gpt-4o'
},
premium: {
chat: 'google/gemini-3-flash-preview',
clarity: 'anthropic/claude-sonnet-4',
planning: 'google/gemini-3-flash-preview',
writing: 'openai/gpt-4.1',
refinement: 'openai/gpt-4.1',
image: 'openai/gpt-4o'
}
};
// Preset configurations (sourced from PHP for single-source-of-truth).
// These presets represent intentional product decisions for different budget tiers.
// Model IDs may differ from registry defaults to balance cost/quality per tier.
const presets = wpawSettingsV2?.presets || {};
// Debug function to check models
window.wpawDebugModels = function () {
console.log('=== WPAW Models Debug ===');
console.log('Total model categories:', Object.keys(state.models).length);
wpawLog.log('=== WPAW Models Debug ===');
wpawLog.log('Total model categories:', Object.keys(state.models).length);
Object.keys(state.models).forEach(category => {
const models = state.models[category]?.all || [];
console.log(`\n${category.toUpperCase()}: ${models.length} models`);
wpawLog.log(`\n${category.toUpperCase()}: ${models.length} models`);
// Check for specific models
const checkIds = ['deepseek/deepseek-chat-v3-0324', 'anthropic/claude-3.5-sonnet'];
checkIds.forEach(id => {
const found = models.find(m => m.id === id);
if (found) {
console.log(` ✓ FOUND: ${id} => ${found.name}`);
wpawLog.log(` ✓ FOUND: ${id} => ${found.name}`);
} else {
console.log(` ✗ NOT FOUND: ${id}`);
wpawLog.log(` ✗ NOT FOUND: ${id}`);
}
});
// Show models with raw is_free and pricing data
if (category === 'image') {
console.log(` ALL image models (raw data from PHP):`);
wpawLog.log(` ALL image models (raw data from PHP):`);
models.forEach(m => {
console.log(` - ${m.id} | is_free=${m.is_free} | pricing=`, m.pricing);
wpawLog.log(` - ${m.id} | is_free=${m.is_free} | pricing=`, m.pricing);
});
} else {
// Show first 10 models with is_free status
console.log(` First 10 models (raw data from PHP):`);
wpawLog.log(` First 10 models (raw data from PHP):`);
models.slice(0, 10).forEach(m => {
console.log(` - ${m.id} | is_free=${m.is_free} | pricing=`, m.pricing);
wpawLog.log(` - ${m.id} | is_free=${m.is_free} | pricing=`, m.pricing);
});
}
});
// AJAX debug call
console.log('\n=== Fetching from server ===');
wpawLog.log('\n=== Fetching from server ===');
$.ajax({
url: wpawSettingsV2.ajaxUrl,
type: 'POST',
@@ -94,17 +80,17 @@
},
success: function (response) {
if (response.success) {
console.log('Server response:', response.data);
console.log('Total models from API:', response.data.total_models);
console.log('Found models:', response.data.found_models);
console.log('Missing models:', response.data.missing_models);
console.log('Sample models:', response.data.sample_models);
wpawLog.log('Server response:', response.data);
wpawLog.log('Total models from API:', response.data.total_models);
wpawLog.log('Found models:', response.data.found_models);
wpawLog.log('Missing models:', response.data.missing_models);
wpawLog.log('Sample models:', response.data.sample_models);
} else {
console.error('Error:', response.data.message);
wpawLog.error('Error:', response.data.message);
}
},
error: function (xhr, status, error) {
console.error('AJAX error:', error);
wpawLog.error('AJAX error:', error);
}
});
};
@@ -127,7 +113,7 @@
});
// Log debug info
console.log('WPAW Settings V2 loaded. Run wpawDebugModels() to debug model issues.');
wpawLog.log('WPAW Settings V2 loaded. Run wpawDebugModels() to debug model issues.');
});
/**
@@ -170,7 +156,7 @@
const newOption = new Option(modelData.name || currentValue, currentValue, true, true);
$select.append(newOption).trigger('change');
} else {
console.warn('Model not found in list:', currentValue);
wpawLog.warn('Model not found in list:', currentValue);
}
}
@@ -387,17 +373,17 @@
* Initialize cost log functionality
*/
function initCostLog() {
console.log('Initializing cost log...');
wpawLog.log('Initializing cost log...');
// Load on tab show
$('#cost-log-tab').on('shown.bs.tab', function () {
console.log('Cost log tab shown, loading data...');
wpawLog.log('Cost log tab shown, loading data...');
loadCostLogData();
});
// Auto-load if cost-log tab is active on page load
if ($('#cost-log-tab').hasClass('active')) {
console.log('Cost log tab is active on load, loading data...');
wpawLog.log('Cost log tab is active on load, loading data...');
loadCostLogData();
}
@@ -449,12 +435,12 @@
* Load cost log data via AJAX
*/
function loadCostLogData() {
console.log('loadCostLogData called');
console.log('wpawSettingsV2:', wpawSettingsV2);
console.log('State:', state);
wpawLog.log('loadCostLogData called');
wpawLog.log('wpawSettingsV2:', wpawSettingsV2);
wpawLog.log('State:', state);
const $tbody = $('#wpaw-cost-log-tbody');
console.log('Table tbody found:', $tbody.length);
wpawLog.log('Table tbody found:', $tbody.length);
$tbody.html(`
<tr>
@@ -478,14 +464,14 @@
filter_date_to: state.filters.dateTo
};
console.log('AJAX request data:', ajaxData);
wpawLog.log('AJAX request data:', ajaxData);
$.ajax({
url: wpawSettingsV2.ajaxUrl,
type: 'POST',
data: ajaxData,
success: function (response) {
console.log('Cost log response:', response);
wpawLog.log('Cost log response:', response);
if (response.success) {
renderCostLogTable(response.data);
updateCostLogStats(response.data.stats);
@@ -493,14 +479,14 @@
renderPagination(response.data);
} else {
const errorMsg = response.data?.message || 'Error loading data';
console.error('Cost log error:', errorMsg);
wpawLog.error('Cost log error:', errorMsg);
$tbody.html('<tr><td colspan="7" class="text-center py-4 text-danger">' + escapeHtml(errorMsg) + '</td></tr>');
}
},
error: function (xhr, status, error) {
console.error('Cost log AJAX error:', status, error);
console.error('XHR:', xhr);
console.error('Response text:', xhr.responseText);
wpawLog.error('Cost log AJAX error:', status, error);
wpawLog.error('XHR:', xhr);
wpawLog.error('Response text:', xhr.responseText);
$tbody.html('<tr><td colspan="7" class="text-center py-4 text-danger">Failed to load cost log. Check browser console for details.</td></tr>');
}
});
@@ -1022,4 +1008,133 @@
initSelect2();
}
/**
* Workflow Pipeline Status Display
* Updates the 5-step workflow visualization based on backend status
*/
function initWorkflowDisplay() {
// Status mapping from backend to step index
// Backend statuses: starting, planning, plan_complete, writing, writing_section, refinement, checking, complete
const statusToStep = {
'starting': 1, // Context
'planning': 2, // Planning
'plan_complete': 2, // Planning (done)
'writing': 3, // Writing
'writing_section': 3, // Writing
'refinement': 4, // Refinement
'refining': 4, // Refinement
'checking': 4, // Refinement
'complete': 5, // Done
'done': 5, // Done
};
// Status messages mapping
const statusMessages = {
'starting': 'Loading context and analyzing post...',
'planning': 'Creating article outline...',
'plan_complete': 'Outline ready, starting to write...',
'writing': 'Generating article content...',
'writing_section': 'Writing section content...',
'refinement': 'Polishing and optimizing content...',
'refining': 'Applying refinements...',
'checking': 'Checking quality and consistency...',
'complete': 'Article finished successfully!',
'done': 'All done!',
};
/**
* Update workflow display based on status
* @param {string} status - Backend status string
* @param {string} message - Optional custom message
*/
window.updateWorkflowStatus = function(status, message) {
const stepIndex = statusToStep[status] || 0;
const $workflow = $('#wpaw-workflow-display');
if (!$workflow.length) return;
const $steps = $workflow.find('.wpaw-step');
const $connectors = $workflow.find('.wpaw-step-connector');
const $statusText = $('#wpaw-workflow-status');
const $messageEl = $('#wpaw-workflow-message');
// Reset all steps
$steps.removeClass('active completed pending error');
$connectors.removeClass('active completed');
// Update steps based on current status
$steps.each(function(index) {
const $step = $(this);
const stepNum = index + 1;
if (stepNum < stepIndex) {
// Completed steps
$step.addClass('completed');
if ($connectors[index]) {
$($connectors[index]).addClass('completed');
}
} else if (stepNum === stepIndex) {
// Active step
$step.addClass('active');
if ($connectors[index]) {
$($connectors[index]).addClass('active');
}
} else {
// Pending steps
$step.addClass('pending');
}
});
// Update status text
const statusText = stepIndex > 0 ? `Step ${stepIndex} of 5` : 'Idle';
$statusText.text(statusText);
// Show message if provided
if (message || statusMessages[status]) {
const displayMessage = message || statusMessages[status];
$messageEl.text(displayMessage).show();
// Add appropriate class
$messageEl.removeClass('success error');
if (status === 'complete' || status === 'done') {
$messageEl.addClass('success');
} else if (status === 'error') {
$messageEl.addClass('error');
}
} else {
$messageEl.hide();
}
};
// Demo function for testing - cycles through all steps
window.demoWorkflow = function() {
const statuses = ['starting', 'planning', 'plan_complete', 'writing', 'refinement', 'complete'];
let index = 0;
const interval = setInterval(() => {
updateWorkflowStatus(statuses[index]);
index++;
if (index >= statuses.length) {
clearInterval(interval);
setTimeout(() => {
// Reset to idle
$('#wpaw-workflow-status').text('Idle');
$('#wpaw-workflow-message').hide();
$('.wpaw-step').removeClass('active completed').addClass('pending');
$('.wpaw-step-connector').removeClass('active completed');
}, 2000);
}
}, 1000);
};
// Initialize with idle state
updateWorkflowStatus('idle');
}
// Initialize workflow display on page load
$(document).ready(function() {
initWorkflowDisplay();
});
})(jQuery);

215
assets/js/sidebar-utils.js Normal file
View File

@@ -0,0 +1,215 @@
/**
* WP Agentic Writer - Utility Functions
*
* Pure utility functions with no React dependencies
* These are shared utilities that can be used by any script
*
* @package WP_Agentic_Writer
*/
// Escape HTML to prevent XSS
const escapeHtml = (value) => {
if (value === null || value === undefined) return '';
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
};
// Normalize message content (convert objects/arrays to string)
const normalizeMessageContent = (content) => {
if (typeof content === 'string' || typeof content === 'number') {
return String(content);
}
return JSON.stringify(content);
};
// Truncate text with ellipsis
const truncateText = (text, maxLength = 40) => {
if (!text || text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength) + '...';
};
// Convert markdown to HTML (full renderer)
const markdownToHtml = (markdown, markdownit, DOMPurify) => {
const raw = normalizeMessageContent(markdown);
if (!raw) {
return '';
}
let rendered = '';
if (markdownit && DOMPurify) {
const renderer = markdownit({
html: false,
linkify: true,
typographer: true,
});
if (window.markdownitTaskLists) {
renderer.use(window.markdownitTaskLists, { enabled: true, label: true, labelAfter: true });
}
rendered = renderer.render(raw);
if (DOMPurify.sanitize) {
rendered = DOMPurify.sanitize(rendered, {
ADD_TAGS: ['input'],
ADD_ATTR: ['type', 'checked', 'disabled'],
});
}
}
return rendered;
};
// Extract code blocks from HTML
const extractCodeBlocks = (html) => {
const codeBlocks = [];
const preRegex = /<pre[^>]*><code(?:\s+class="language-([^"]*)")?>([\s\S]*?)<\/code><\/pre>/g;
let match;
while ((match = preRegex.exec(html)) !== null) {
const lang = match[1] || '';
const code = match[2]
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
codeBlocks.push({ lang, code });
}
return codeBlocks;
};
// Debounce function
const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// Parse outline plan from AI response
const parseOutlinePlan = (content) => {
const sections = [];
const lines = content.split('\n');
let currentSection = null;
let currentSubsection = null;
lines.forEach((line) => {
const trimmed = line.trim();
if (!trimmed) return;
// H2 section (## Title)
const h2Match = trimmed.match(/^##\s+(.+)$/);
if (h2Match) {
if (currentSection) {
sections.push(currentSection);
}
currentSection = {
id: 'section-' + (sections.length + 1),
title: h2Match[1].trim(),
subsections: [],
};
currentSubsection = null;
return;
}
// H3 subsection (### Title)
const h3Match = trimmed.match(/^###\s+(.+)$/);
if (h3Match && currentSection) {
currentSubsection = {
id: 'subsection-' + (currentSection.subsections.length + 1),
title: h3Match[1].trim(),
content: '',
};
currentSection.subsections.push(currentSubsection);
return;
}
// Content line
if (currentSection) {
if (currentSubsection) {
currentSubsection.content += (currentSubsection.content ? '\n' : '') + trimmed;
} else {
if (!currentSection.content) {
currentSection.content = trimmed;
} else {
currentSection.content += '\n' + trimmed;
}
}
}
});
if (currentSection) {
sections.push(currentSection);
}
return sections;
};
// Parse FAQ schema from AI response
const parseFaqSchema = (content) => {
const faqs = [];
const faqBlocks = content.split(/\n\s*#{1,2}\s*Q[^\n]*\n/);
faqBlocks.slice(1).forEach((block) => {
const lines = block.trim().split('\n');
if (lines.length >= 2) {
const question = lines[0].replace(/^[#*]+\s*/, '').trim();
const answer = lines.slice(1).join('\n').trim();
if (question && answer) {
faqs.push({ question, answer });
}
}
});
return faqs;
};
// Extract block preview from content
const extractBlockPreview = (block) => {
if (!block) return '';
const content = block.innerHTML || block.content || '';
const text = content.replace(/<[^>]+>/g, '').trim();
return truncateText(text, 100);
};
// To text value helper
const toTextValue = (value) => {
if (typeof value === 'string') return value;
if (typeof value === 'number') return String(value);
if (Array.isArray(value)) return value.map(toTextValue).join(', ');
if (typeof value === 'object' && value !== null) {
return JSON.stringify(value);
}
return '';
};
// Export for use in other modules
if (typeof window !== 'undefined') {
window.WPAWUtils = {
escapeHtml,
normalizeMessageContent,
truncateText,
markdownToHtml,
extractCodeBlocks,
debounce,
parseOutlinePlan,
parseFaqSchema,
extractBlockPreview,
toTextValue,
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff