refactor: Cleanup git state - commit all staged changes
Major refactoring cleanup: - Add new controller architecture (class-controller-*.php) - Add new settings-v2 UI (views/settings-v2/) - Add new CSS architecture (agentic-sidebar.css, tokens) - Add esbuild build pipeline (scripts/build.js, package.json) - Add composer dependencies (vendor/) - Add frontend src directory (assets/js/src/index.jsx) - Add documentation files - Remove old/obsolete files (class-settings.php, old CSS) This commits all pending changes from previous refactoring efforts.
This commit is contained in:
62
assets/js/dist/sidebar.js
vendored
Normal file
62
assets/js/dist/sidebar.js
vendored
Normal file
File diff suppressed because one or more lines are too long
599
assets/js/settings-v2-stitch.js
Normal file
599
assets/js/settings-v2-stitch.js
Normal file
@@ -0,0 +1,599 @@
|
||||
(function ($) {
|
||||
"use strict";
|
||||
|
||||
const qs = (selector, root = document) => root.querySelector(selector);
|
||||
const qsa = (selector, root = document) =>
|
||||
Array.from(root.querySelectorAll(selector));
|
||||
const config = window.wpawSettingsV2 || {};
|
||||
|
||||
function showToast(message, type = "success") {
|
||||
const toast = qs("#wpaw2-toast");
|
||||
const body = qs("#wpaw2-toast-message");
|
||||
if (!toast || !body) return;
|
||||
body.textContent = message;
|
||||
toast.className = "toast show toast-" + type;
|
||||
clearTimeout(toast._timer);
|
||||
toast._timer = setTimeout(() => {
|
||||
toast.classList.remove("show");
|
||||
}, 2600);
|
||||
}
|
||||
|
||||
function bindToastHover() {
|
||||
const toast = qs("#wpaw2-toast");
|
||||
if (!toast) return;
|
||||
toast.addEventListener("mouseenter", () => {
|
||||
clearTimeout(toast._timer);
|
||||
});
|
||||
toast.addEventListener("mouseleave", () => {
|
||||
toast._timer = setTimeout(() => {
|
||||
toast.classList.remove("show");
|
||||
}, 1200);
|
||||
});
|
||||
}
|
||||
|
||||
function activateTab(target) {
|
||||
qsa("[data-aw2-tab]").forEach((button) => {
|
||||
const active = button.dataset.aw2Tab === target;
|
||||
button.setAttribute("aria-selected", active ? "true" : "false");
|
||||
});
|
||||
qsa(".tab-panel").forEach((panel) => {
|
||||
panel.classList.toggle("active", panel.id === target);
|
||||
});
|
||||
const crumb = qs("#wpaw2-crumb-tab");
|
||||
const activeButton = qs(`[data-aw2-tab="${target}"]`);
|
||||
if (crumb && activeButton)
|
||||
crumb.textContent =
|
||||
activeButton.dataset.label || activeButton.textContent.trim();
|
||||
}
|
||||
|
||||
function bindTabs() {
|
||||
qsa("[data-aw2-tab]").forEach((button) => {
|
||||
button.addEventListener("click", () =>
|
||||
activateTab(button.dataset.aw2Tab),
|
||||
);
|
||||
});
|
||||
|
||||
qsa("[data-aw2-subtab-target]").forEach((button) => {
|
||||
button.addEventListener("click", (e) => {
|
||||
const target = e.currentTarget.dataset.aw2SubtabTarget;
|
||||
const nav = e.currentTarget.closest(".subtab-nav");
|
||||
const panelContainer = nav.parentElement;
|
||||
|
||||
nav.querySelectorAll("[data-aw2-subtab-target]").forEach((btn) => {
|
||||
btn.setAttribute(
|
||||
"aria-selected",
|
||||
btn === e.currentTarget ? "true" : "false",
|
||||
);
|
||||
});
|
||||
|
||||
panelContainer.querySelectorAll(".subtab-panel").forEach((panel) => {
|
||||
if (panel.id === target) {
|
||||
panel.classList.add("active");
|
||||
} else {
|
||||
panel.classList.remove("active");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindPasswordToggles() {
|
||||
qsa("[data-aw2-toggle-password]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const input = qs(button.dataset.aw2TogglePassword);
|
||||
if (!input) return;
|
||||
const isPassword = input.type === "password";
|
||||
input.type = isPassword ? "text" : "password";
|
||||
button.textContent = isPassword ? "Hide" : "Show";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindCopyButtons() {
|
||||
qsa("[data-aw2-copy]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const source = qs(button.dataset.aw2Copy);
|
||||
if (!source) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(source.textContent.trim());
|
||||
showToast("Copied command");
|
||||
} catch (error) {
|
||||
showToast("Copy failed", "error");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getModelsForType(type) {
|
||||
const bucket =
|
||||
config.models?.[type] ||
|
||||
config.models?.execution ||
|
||||
config.models?.planning ||
|
||||
{};
|
||||
const all = Array.isArray(bucket.all) ? bucket.all : [];
|
||||
const recommended = Array.isArray(bucket.recommended)
|
||||
? bucket.recommended
|
||||
: [];
|
||||
const merged = [...all];
|
||||
recommended.forEach((model) => {
|
||||
if (!merged.find((item) => item.id === model.id)) merged.push(model);
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
function modelTypeFromSelect(select) {
|
||||
const match = select.name.match(/\[(.+?)_model\]/);
|
||||
return match ? match[1] : "writing";
|
||||
}
|
||||
|
||||
function formatModelData(models) {
|
||||
return models.map((model) => ({
|
||||
id: model.id,
|
||||
text: model.name || model.id,
|
||||
pricing: model.pricing || {},
|
||||
is_free: Boolean(model.is_free),
|
||||
is_custom: Boolean(model.is_custom),
|
||||
}));
|
||||
}
|
||||
|
||||
function formatModelOption(model) {
|
||||
if (!model.id) return model.text;
|
||||
const $row = $('<div class="wpaw2-model-option"></div>');
|
||||
const $name = $('<span class="wpaw2-model-name"></span>').text(model.text);
|
||||
$row.append($name);
|
||||
|
||||
if (model.is_custom) {
|
||||
$row.append('<span class="wpaw2-model-badge">Custom</span>');
|
||||
} else if (model.is_free) {
|
||||
$row.append('<span class="wpaw2-model-badge free">Free</span>');
|
||||
} else {
|
||||
const prompt = parseFloat(model.pricing?.prompt) || 0;
|
||||
const image = parseFloat(model.pricing?.image) || 0;
|
||||
const price = image > 0 ? image : prompt;
|
||||
if (price > 0)
|
||||
$row.append(
|
||||
$('<span class="wpaw2-model-price"></span>').text(
|
||||
`$${(price * 1000000).toFixed(2)}/1M`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
function initSelect2() {
|
||||
if (!$.fn.select2) return;
|
||||
|
||||
const $languageSelect = $("#preferred_languages");
|
||||
if ($languageSelect.length) {
|
||||
if ($languageSelect.data("select2")) {
|
||||
$languageSelect.select2("destroy");
|
||||
}
|
||||
$languageSelect.select2({
|
||||
width: "100%",
|
||||
placeholder: "Select preferred languages...",
|
||||
dropdownCssClass: "wpaw2-select2-dropdown",
|
||||
});
|
||||
}
|
||||
|
||||
qsa(".wpaw2-model-select").forEach((select) => {
|
||||
const type = modelTypeFromSelect(select);
|
||||
const models = getModelsForType(type);
|
||||
const currentValue = select.value || config.currentModels?.[type] || "";
|
||||
const $select = $(select);
|
||||
|
||||
if ($select.data("select2")) {
|
||||
$select.select2("destroy");
|
||||
}
|
||||
|
||||
$select.empty().select2({
|
||||
width: "100%",
|
||||
data: formatModelData(models),
|
||||
placeholder: config.i18n?.searchPlaceholder || "Search models...",
|
||||
allowClear: true,
|
||||
dropdownCssClass: "wpaw2-select2-dropdown",
|
||||
templateResult: formatModelOption,
|
||||
templateSelection: (model) => model.text || model.id,
|
||||
language: {
|
||||
noResults: () => config.i18n?.noResults || "No models found",
|
||||
},
|
||||
});
|
||||
|
||||
if (currentValue) {
|
||||
const model = models.find((item) => item.id === currentValue);
|
||||
const option = new Option(
|
||||
model?.name || currentValue,
|
||||
currentValue,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
$select.append(option).trigger("change");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setSelectValue(select, value) {
|
||||
if (!select || !value) return;
|
||||
let option = Array.from(select.options).find(
|
||||
(item) => item.value === value,
|
||||
);
|
||||
if (!option) {
|
||||
option = new Option(value, value, true, true);
|
||||
select.add(option);
|
||||
}
|
||||
select.value = value;
|
||||
if ($.fn.select2 && $(select).data("select2")) $(select).trigger("change");
|
||||
else select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
|
||||
function bindPresetCards() {
|
||||
const presets = config.presets || {};
|
||||
qsa("[data-aw2-preset]").forEach((card) => {
|
||||
card.addEventListener("click", () => {
|
||||
const preset = presets[card.dataset.aw2Preset];
|
||||
if (!preset) return;
|
||||
Object.entries(preset).forEach(([key, value]) => {
|
||||
setSelectValue(
|
||||
qs(`[name="wp_agentic_writer_settings[${key}_model]"]`),
|
||||
value,
|
||||
);
|
||||
});
|
||||
qsa("[data-aw2-preset]").forEach((item) =>
|
||||
item.classList.remove("active"),
|
||||
);
|
||||
card.classList.add("active");
|
||||
updateEstimate();
|
||||
showToast("Preset applied");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateEstimate() {
|
||||
const writing =
|
||||
qs('[name="wp_agentic_writer_settings[writing_model]"]')?.value || "";
|
||||
const image =
|
||||
qs('[name="wp_agentic_writer_settings[image_model]"]')?.value || "";
|
||||
let estimate = 0.14;
|
||||
const text = `${writing} ${image}`.toLowerCase();
|
||||
if (text.includes("mistral") || text.includes("flash")) estimate = 0.06;
|
||||
if (
|
||||
text.includes("gpt-4.1") ||
|
||||
text.includes("opus") ||
|
||||
text.includes("premium")
|
||||
)
|
||||
estimate = 0.31;
|
||||
const output = qs("#wpaw2-cost-estimate");
|
||||
if (output) output.textContent = `~$${estimate.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function bindEstimateInputs() {
|
||||
qsa(".wpaw2-model-select").forEach((select) =>
|
||||
select.addEventListener("change", updateEstimate),
|
||||
);
|
||||
updateEstimate();
|
||||
}
|
||||
|
||||
function bindTogglePanels() {
|
||||
qsa("[data-aw2-toggle-panel]").forEach((input) => {
|
||||
const panel = qs(input.dataset.aw2TogglePanel);
|
||||
if (!panel) return;
|
||||
const sync = () => {
|
||||
panel.hidden = !input.checked;
|
||||
};
|
||||
input.addEventListener("change", sync);
|
||||
sync();
|
||||
});
|
||||
}
|
||||
|
||||
function bindCustomModels() {
|
||||
const list = qs("#wpaw2-custom-models");
|
||||
const add = qs("#wpaw2-add-custom-model");
|
||||
if (!list || !add) return;
|
||||
add.addEventListener("click", () => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "custom-row";
|
||||
row.innerHTML =
|
||||
'<input class="field-control" type="text" data-field="id" placeholder="provider/model-id"><input class="field-control" type="text" data-field="name" placeholder="Display name"><select class="field-control" data-field="type"><option value="text">Text</option><option value="image">Image</option></select><button type="button" class="btn btn-danger btn-small" data-remove>Remove</button>';
|
||||
list.appendChild(row);
|
||||
});
|
||||
list.addEventListener("click", (event) => {
|
||||
if (event.target.matches("[data-remove]"))
|
||||
event.target.closest(".custom-row")?.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function ajaxPost(action, data) {
|
||||
return $.ajax({
|
||||
url: config.ajaxUrl,
|
||||
type: "POST",
|
||||
data: {
|
||||
action,
|
||||
nonce: config.nonce,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function bindAjaxButton(selector, action, getData, loadingText, onSuccess) {
|
||||
const button = qs(selector);
|
||||
if (!button) return;
|
||||
const original = button.textContent;
|
||||
button.addEventListener("click", () => {
|
||||
button.disabled = true;
|
||||
button.textContent = loadingText || "Testing...";
|
||||
ajaxPost(action, getData ? getData() : {})
|
||||
.done((response) => {
|
||||
if (response?.success && typeof onSuccess === "function") {
|
||||
onSuccess(response);
|
||||
}
|
||||
let message =
|
||||
response?.data?.message ||
|
||||
(response?.success ? "Connection successful" : "Connection failed");
|
||||
if (response?.data?.models_count) {
|
||||
message += ` (${response.data.models_count} models)`;
|
||||
}
|
||||
showToast(message, response?.success ? "success" : "error");
|
||||
})
|
||||
.fail((xhr) => {
|
||||
showToast(
|
||||
xhr.responseJSON?.data?.message || "Request failed",
|
||||
"error",
|
||||
);
|
||||
})
|
||||
.always(() => {
|
||||
button.disabled = false;
|
||||
button.textContent = original;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadCostLog() {
|
||||
const tbody = qs("#wpaw-cost-log-tbody");
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="3">Loading cost data...</td></tr>';
|
||||
ajaxPost("wpaw_get_cost_log_data", { page: 1, per_page: 25 })
|
||||
.done((response) => {
|
||||
if (!response?.success) {
|
||||
tbody.innerHTML =
|
||||
'<tr><td colspan="3">Unable to load cost data.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const records = response.data?.records || [];
|
||||
if (!records.length) {
|
||||
tbody.innerHTML =
|
||||
'<tr><td colspan="3">Cost data will appear after OpenRouter generations.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = records.map(renderCostLogRecord).join("");
|
||||
bindCostLogToggles(tbody);
|
||||
})
|
||||
.fail(() => {
|
||||
tbody.innerHTML =
|
||||
'<tr><td colspan="3">Unable to load cost data.</td></tr>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderCostLogRecord(record) {
|
||||
const postId = String(record.post_id ?? "0");
|
||||
const rowId = `wpaw-cost-child-${postId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
||||
const title = escapeHtml(record.post_title || "System/Other");
|
||||
const titleHtml = record.post_link
|
||||
? `<a href="${escapeAttr(record.post_link)}">${title}</a>`
|
||||
: title;
|
||||
const totalCost = escapeHtml(record.total_cost || "0.0000");
|
||||
const callCount = Number(record.call_count || 0);
|
||||
const actionRows = buildActionSummaries(record.details || []);
|
||||
const hasActions = actionRows.length > 0;
|
||||
const actionSummary = hasActions
|
||||
? `${actionRows.length} actions / ${callCount} calls`
|
||||
: `${callCount} calls`;
|
||||
const toggle = hasActions
|
||||
? `<button type="button" class="cost-row-toggle" aria-expanded="false" aria-controls="${escapeAttr(rowId)}" data-cost-child="#${escapeAttr(rowId)}">▸</button>`
|
||||
: '<span class="cost-row-toggle-placeholder"></span>';
|
||||
|
||||
return `
|
||||
<tr class="cost-parent-row">
|
||||
<td>${toggle}<span class="cost-post-title">${titleHtml}</span></td>
|
||||
<td>${escapeHtml(actionSummary)}</td>
|
||||
<td>$${totalCost}</td>
|
||||
</tr>
|
||||
<tr class="cost-child-row" id="${escapeAttr(rowId)}" hidden>
|
||||
<td colspan="3">
|
||||
${renderActionSummaryTable(actionRows)}
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function buildActionSummaries(details) {
|
||||
const grouped = new Map();
|
||||
details.forEach((detail) => {
|
||||
const action = detail.action || "unknown";
|
||||
const current = grouped.get(action) || {
|
||||
action,
|
||||
calls: 0,
|
||||
cost: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
models: new Set(),
|
||||
};
|
||||
current.calls += 1;
|
||||
current.cost += Number.parseFloat(detail.cost || 0) || 0;
|
||||
current.inputTokens += Number(detail.input_tokens || 0);
|
||||
current.outputTokens += Number(detail.output_tokens || 0);
|
||||
if (detail.model) current.models.add(detail.model);
|
||||
grouped.set(action, current);
|
||||
});
|
||||
|
||||
return Array.from(grouped.values()).sort((a, b) => b.cost - a.cost);
|
||||
}
|
||||
|
||||
function renderActionSummaryTable(actionRows) {
|
||||
if (!actionRows.length) {
|
||||
return '<div class="cost-child-empty">No action details available for this post.</div>';
|
||||
}
|
||||
|
||||
const rows = actionRows
|
||||
.map((item) => {
|
||||
const modelList = Array.from(item.models);
|
||||
const models = modelList.length
|
||||
? modelList
|
||||
.map((m) => `<span class="model-pill">${escapeHtml(m)}</span>`)
|
||||
.join("")
|
||||
: "—";
|
||||
const hasTokens = item.inputTokens > 0 || item.outputTokens > 0;
|
||||
const inLabel = hasTokens ? escapeHtml(String(item.inputTokens)) : "—";
|
||||
const outLabel = hasTokens
|
||||
? escapeHtml(String(item.outputTokens))
|
||||
: "—";
|
||||
return `<tr>
|
||||
<td>${escapeHtml(formatActionLabel(item.action))}</td>
|
||||
<td>${escapeHtml(String(item.calls))}</td>
|
||||
<td>${models}</td>
|
||||
<td>${inLabel}</td>
|
||||
<td>${outLabel}</td>
|
||||
<td>$${escapeHtml(item.cost.toFixed(4))}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return `<table class="cost-action-table">
|
||||
<thead><tr><th>Action</th><th>Calls</th><th>Models</th><th>Input</th><th>Output</th><th>Cost</th></tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function bindCostLogToggles(root) {
|
||||
// Accordion mode: expanding one row collapses all others.
|
||||
qsa("[data-cost-child]", root).forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const child = qs(button.dataset.costChild);
|
||||
if (!child) return;
|
||||
const wasExpanded = button.getAttribute("aria-expanded") === "true";
|
||||
|
||||
// Collapse every open row first.
|
||||
qsa("[data-cost-child][aria-expanded='true']", root).forEach((btn) => {
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
btn.textContent = "▸";
|
||||
const sibling = qs(btn.dataset.costChild);
|
||||
if (sibling) sibling.hidden = true;
|
||||
});
|
||||
|
||||
// If the clicked row was not already open, expand it.
|
||||
if (!wasExpanded) {
|
||||
button.setAttribute("aria-expanded", "true");
|
||||
button.textContent = "▾";
|
||||
child.hidden = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatActionLabel(action) {
|
||||
const normalized = String(action || "unknown").toLowerCase();
|
||||
const labels = {
|
||||
image_generation: "Generate Image",
|
||||
generate_image: "Generate Image",
|
||||
block_refinement: "Block Refinement",
|
||||
chat: "Chat",
|
||||
};
|
||||
if (labels[normalized]) return labels[normalized];
|
||||
|
||||
return normalized
|
||||
.replace(/[_-]+/g, " ")
|
||||
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = String(value ?? "");
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return escapeHtml(value).replace(/"/g, """);
|
||||
}
|
||||
|
||||
function bindAjaxCheckers() {
|
||||
bindAjaxButton("#wpaw-test-api-key", "wpaw_test_api_connection", () => ({
|
||||
api_key: qs("#openrouter_api_key")?.value || "",
|
||||
}));
|
||||
bindAjaxButton(
|
||||
"#wpaw-test-local-backend",
|
||||
"wpaw_test_local_backend",
|
||||
() => {
|
||||
// Find the first non-empty per-task model code for the test
|
||||
const modelInputs = qsa(".wpaw-ce-model");
|
||||
let model = "";
|
||||
for (const input of modelInputs) {
|
||||
if (input.value.trim()) {
|
||||
model = input.value.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Also sync the hidden legacy field
|
||||
const legacy = qs("#local_backend_model");
|
||||
if (legacy) legacy.value = model;
|
||||
return {
|
||||
url: qs("#local_backend_url")?.value || "",
|
||||
key: qs("#local_backend_key")?.value || "",
|
||||
model: model,
|
||||
};
|
||||
},
|
||||
);
|
||||
bindAjaxButton("#wpaw-test-memanto", "wpaw_test_memanto", () => ({
|
||||
url: qs("#memanto_url")?.value || "",
|
||||
key: qs("#memanto_moorcheh_key")?.value || "",
|
||||
}));
|
||||
bindAjaxButton(
|
||||
"#wpaw-refresh-models",
|
||||
"wpaw_refresh_models",
|
||||
() => ({
|
||||
api_key: qs("#openrouter_api_key")?.value || "",
|
||||
}),
|
||||
config.i18n?.refreshing || "Refreshing...",
|
||||
(response) => {
|
||||
if (response?.data?.models) {
|
||||
config.models = response.data.models;
|
||||
initSelect2();
|
||||
updateEstimate();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function bindSaveShortcut() {
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
|
||||
event.preventDefault();
|
||||
qs("#wpaw2-settings-form")?.requestSubmit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
bindTabs();
|
||||
bindPasswordToggles();
|
||||
bindCopyButtons();
|
||||
initSelect2();
|
||||
bindPresetCards();
|
||||
bindEstimateInputs();
|
||||
bindTogglePanels();
|
||||
bindCustomModels();
|
||||
bindAjaxCheckers();
|
||||
loadCostLog();
|
||||
bindSaveShortcut();
|
||||
bindToastHover();
|
||||
qs("#wpaw2-toast-close")?.addEventListener("click", () =>
|
||||
qs("#wpaw2-toast")?.classList.remove("show"),
|
||||
);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})(jQuery);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,293 +0,0 @@
|
||||
/**
|
||||
* WP Agentic Writer - Settings Page Scripts
|
||||
*
|
||||
* @package WP_Agentic_Writer
|
||||
*/
|
||||
|
||||
jQuery( document ).ready( function( $ ) {
|
||||
'use strict';
|
||||
|
||||
// ===========================
|
||||
// TAB NAVIGATION
|
||||
// ===========================
|
||||
$( '.wpaw-settings-nav-btn' ).on( 'click', function() {
|
||||
const tab = $( this ).data( 'tab' );
|
||||
|
||||
// Update nav buttons
|
||||
$( '.wpaw-settings-nav-btn' ).removeClass( 'active' );
|
||||
$( this ).addClass( 'active' );
|
||||
|
||||
// Update tab content
|
||||
$( '.wpaw-tab-content' ).removeClass( 'active' );
|
||||
$( `.wpaw-tab-content[data-tab="${tab}"]` ).addClass( 'active' );
|
||||
} );
|
||||
|
||||
// ===========================
|
||||
// SELECT2 INITIALIZATION
|
||||
// ===========================
|
||||
function initSelect2() {
|
||||
if ( ! $.fn.select2 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Select2 for model dropdowns
|
||||
$( '.wpaw-select2-model' ).each( function() {
|
||||
const $select = $( this );
|
||||
|
||||
// Destroy existing Select2 if present
|
||||
if ( $select.hasClass( 'select2-hidden-accessible' ) ) {
|
||||
$select.select2( 'destroy' );
|
||||
}
|
||||
|
||||
$select.select2( {
|
||||
placeholder: 'Search for a model...',
|
||||
allowClear: false,
|
||||
width: '100%',
|
||||
templateResult: formatModelOption,
|
||||
templateSelection: formatModelSelection,
|
||||
} );
|
||||
} );
|
||||
|
||||
// Initialize Select2 for Context Categories (multiple with tags)
|
||||
const $contextCategories = $( '#required_context_categories' );
|
||||
if ( $contextCategories.length && ! $contextCategories.hasClass( 'select2-hidden-accessible' ) ) {
|
||||
$contextCategories.select2( {
|
||||
placeholder: 'Select categories...',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
tags: false,
|
||||
closeOnSelect: false,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
function formatModelOption( model ) {
|
||||
if ( ! model.id ) {
|
||||
return model.text;
|
||||
}
|
||||
const isFree = model.text && model.text.includes( '(Free)' );
|
||||
const $option = $( '<span>' + model.text + '</span>' );
|
||||
if ( isFree ) {
|
||||
$option.css( 'color', '#28a745' );
|
||||
}
|
||||
return $option;
|
||||
}
|
||||
|
||||
function formatModelSelection( model ) {
|
||||
return model.text || model.id;
|
||||
}
|
||||
|
||||
// Store model pricing data from API (populated by updateModelDropdowns)
|
||||
let modelPricing = {};
|
||||
|
||||
// Estimate cost per article using dynamic pricing from API (6 models)
|
||||
// Estimates: chat 200 tokens, clarity 300, planning 500, writing 3K, refinement 1K, 1 image
|
||||
function estimateArticleCost() {
|
||||
const chatModel = $( '#chat_model' ).val();
|
||||
const clarityModel = $( '#clarity_model' ).val();
|
||||
const planningModel = $( '#planning_model' ).val();
|
||||
const writingModel = $( '#writing_model' ).val();
|
||||
const refinementModel = $( '#refinement_model' ).val();
|
||||
const imageModel = $( '#image_model' ).val();
|
||||
|
||||
let totalCost = 0;
|
||||
|
||||
// Helper to calculate cost for a model
|
||||
const calcCost = ( modelId, inputTokens, outputTokens ) => {
|
||||
const pricing = modelPricing[modelId];
|
||||
if ( pricing ) {
|
||||
return ( pricing.prompt * inputTokens ) + ( pricing.completion * outputTokens );
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// Chat cost (minimal - 100 in, 100 out)
|
||||
totalCost += calcCost( chatModel, 100, 100 );
|
||||
|
||||
// Clarity cost (300 in, 300 out)
|
||||
totalCost += calcCost( clarityModel, 300, 300 );
|
||||
|
||||
// Planning cost (500 in, 500 out)
|
||||
totalCost += calcCost( planningModel, 500, 500 );
|
||||
|
||||
// Writing cost (1.5K in, 1.5K out - main article generation)
|
||||
totalCost += calcCost( writingModel, 1500, 1500 );
|
||||
|
||||
// Refinement cost (500 in, 500 out)
|
||||
totalCost += calcCost( refinementModel, 500, 500 );
|
||||
|
||||
// Image cost (1 image)
|
||||
const imagePricing = modelPricing[imageModel];
|
||||
if ( imagePricing && imagePricing.image > 0 ) {
|
||||
totalCost += imagePricing.image;
|
||||
}
|
||||
|
||||
return totalCost;
|
||||
}
|
||||
|
||||
// Update cost estimation display
|
||||
function updateCostEstimate() {
|
||||
const cost = estimateArticleCost();
|
||||
const $display = $( '#wpaw-cost-estimate' );
|
||||
|
||||
if ( $display.length ) {
|
||||
if ( cost > 0 ) {
|
||||
$display.text( '~$' + cost.toFixed(4) + ' per article' );
|
||||
} else {
|
||||
$display.text( 'Free or pricing unavailable' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for model changes (6 models)
|
||||
$( '#chat_model, #clarity_model, #planning_model, #writing_model, #refinement_model, #image_model' ).on( 'change', function() {
|
||||
updateCostEstimate();
|
||||
} );
|
||||
|
||||
// Initialize on page load
|
||||
updateCostEstimate();
|
||||
initSelect2();
|
||||
|
||||
// Refresh models button.
|
||||
$( '#wpaw-refresh-models' ).on( 'click', function() {
|
||||
const $button = $( this );
|
||||
const $spinner = $( '#wpaw-models-spinner' );
|
||||
|
||||
$button.prop( 'disabled', true );
|
||||
$spinner.show();
|
||||
|
||||
$.ajax( {
|
||||
url: wpawSettings.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'wpaw_refresh_models',
|
||||
nonce: wpawSettings.nonce,
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
// Update model dropdowns.
|
||||
updateModelDropdowns( response.data.models );
|
||||
|
||||
// Show success message.
|
||||
$( '#wpaw-models-message' )
|
||||
.removeClass( 'notice-error' )
|
||||
.addClass( 'notice-success' )
|
||||
.text( response.data.message )
|
||||
.show();
|
||||
} else {
|
||||
showError( response.data.message || 'Unknown error' );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
showError( 'Failed to refresh models. Please try again.' );
|
||||
},
|
||||
complete: function() {
|
||||
$button.prop( 'disabled', false );
|
||||
$spinner.hide();
|
||||
|
||||
// Auto-hide message after 3 seconds.
|
||||
setTimeout( function() {
|
||||
$( '#wpaw-models-message' ).fadeOut();
|
||||
}, 3000 );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
|
||||
// Initialize model dropdowns on page load.
|
||||
if ( wpawSettings.models ) {
|
||||
updateModelDropdowns( wpawSettings.models );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update model dropdowns with fetched models.
|
||||
* Shows all models in a flat list (no optgroups).
|
||||
*/
|
||||
function updateModelDropdowns( models ) {
|
||||
/**
|
||||
* Populate a select dropdown with models and store pricing.
|
||||
* @param {jQuery} $select - The select element
|
||||
* @param {Array} allModels - Array of all models
|
||||
* @param {string} currentValue - Currently saved value
|
||||
*/
|
||||
const populateSelect = ( $select, allModels, currentValue ) => {
|
||||
$select.empty();
|
||||
|
||||
// Add all models to dropdown and store pricing
|
||||
if ( allModels && allModels.length ) {
|
||||
allModels.forEach( function( model ) {
|
||||
// Clean model name - remove existing (free) suffix to avoid duplication
|
||||
let cleanName = model.name.replace( /\s*\(free\)\s*/gi, '' ).trim();
|
||||
$select.append( $( '<option>', {
|
||||
value: model.id,
|
||||
text: cleanName + ( model.is_free ? ' (Free)' : '' )
|
||||
} ) );
|
||||
|
||||
// Store pricing data for cost calculation
|
||||
if ( model.pricing ) {
|
||||
modelPricing[ model.id ] = model.pricing;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// Ensure saved value exists in dropdown (legacy/custom models)
|
||||
if ( currentValue && $select.find( `option[value="${currentValue}"]` ).length === 0 ) {
|
||||
$select.prepend( $( '<option>', {
|
||||
value: currentValue,
|
||||
text: currentValue + ' (Saved)'
|
||||
} ) );
|
||||
}
|
||||
|
||||
// Set the current value
|
||||
if ( currentValue ) {
|
||||
$select.val( currentValue );
|
||||
}
|
||||
};
|
||||
|
||||
// Update chat model dropdown
|
||||
const $chatSelect = $( '#chat_model' );
|
||||
const currentChat = wpawSettings.currentModels?.chat || $chatSelect.val();
|
||||
populateSelect( $chatSelect, models.chat?.all || [], currentChat );
|
||||
|
||||
// Update clarity model dropdown
|
||||
const $claritySelect = $( '#clarity_model' );
|
||||
const currentClarity = wpawSettings.currentModels?.clarity || $claritySelect.val();
|
||||
populateSelect( $claritySelect, models.planning?.all || [], currentClarity );
|
||||
|
||||
// Update planning model dropdown
|
||||
const $planningSelect = $( '#planning_model' );
|
||||
const currentPlanning = wpawSettings.currentModels?.planning || $planningSelect.val();
|
||||
populateSelect( $planningSelect, models.planning?.all || [], currentPlanning );
|
||||
|
||||
// Update writing model dropdown
|
||||
const $writingSelect = $( '#writing_model' );
|
||||
const currentWriting = wpawSettings.currentModels?.writing || wpawSettings.currentModels?.execution || $writingSelect.val();
|
||||
populateSelect( $writingSelect, models.execution?.all || [], currentWriting );
|
||||
|
||||
// Update refinement model dropdown
|
||||
const $refinementSelect = $( '#refinement_model' );
|
||||
const currentRefinement = wpawSettings.currentModels?.refinement || $refinementSelect.val();
|
||||
populateSelect( $refinementSelect, models.execution?.all || [], currentRefinement );
|
||||
|
||||
// Update image model dropdown
|
||||
const $imageSelect = $( '#image_model' );
|
||||
const currentImage = wpawSettings.currentModels?.image || $imageSelect.val();
|
||||
populateSelect( $imageSelect, models.image?.all || [], currentImage );
|
||||
|
||||
// Reinitialize Select2 after updating dropdowns
|
||||
initSelect2();
|
||||
|
||||
// Update cost estimate
|
||||
updateCostEstimate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message.
|
||||
*/
|
||||
function showError( message ) {
|
||||
$( '#wpaw-models-message' )
|
||||
.removeClass( 'notice-success' )
|
||||
.addClass( 'notice-error' )
|
||||
.text( message )
|
||||
.show();
|
||||
}
|
||||
} );
|
||||
1737
assets/js/sidebar.js
1737
assets/js/sidebar.js
File diff suppressed because it is too large
Load Diff
11793
assets/js/src/index.jsx
Normal file
11793
assets/js/src/index.jsx
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user