Add Google and Groq AI providers, enhance provider manager, and implement conversation and logging services
- Introduced `Groq_AI_Provider_Google` and `Groq_AI_Provider_Groq` classes for handling AI interactions with Google and Groq respectively. - Enhanced `Groq_AI_Provider_Manager` to register and manage multiple AI providers. - Implemented `Groq_AI_Conversation_Manager` for managing conversation IDs and context hashes. - Added `Groq_AI_Generation_Logger` for logging AI generation events and managing log tables. - Developed `Groq_AI_Prompt_Builder` for constructing prompts and processing AI responses. - Established `Groq_AI_Settings_Manager` for managing plugin settings, including context fields and module configurations.
This commit is contained in:
381
assets/js/admin.js
Normal file
381
assets/js/admin.js
Normal file
@@ -0,0 +1,381 @@
|
||||
(function ($) {
|
||||
const modal = document.getElementById('groq-ai-modal');
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openButtons = document.querySelectorAll('.groq-ai-open-modal');
|
||||
const closeButton = modal.querySelector('.groq-ai-modal__close');
|
||||
const form = document.getElementById('groq-ai-form');
|
||||
const promptField = document.getElementById('groq-ai-prompt');
|
||||
const statusField = modal.querySelector('.groq-ai-modal__status');
|
||||
const resultWrapper = modal.querySelector('.groq-ai-modal__result');
|
||||
const resultField = document.getElementById('groq-ai-output');
|
||||
const jsonCopyButton = modal.querySelector('.groq-ai-copy-json');
|
||||
const contextToggles = modal.querySelectorAll('.groq-ai-context-toggle');
|
||||
const resultFields = {};
|
||||
modal.querySelectorAll('.groq-ai-result-field').forEach((field) => {
|
||||
const key = field.getAttribute('data-field');
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
resultFields[key] = {
|
||||
key,
|
||||
container: field,
|
||||
textarea: field.querySelector('textarea'),
|
||||
target: field.getAttribute('data-target-input') || '',
|
||||
label: field.getAttribute('data-label') || key,
|
||||
rankMathAction: field.getAttribute('data-rankmath-action') || '',
|
||||
status: field.querySelector('.groq-ai-apply-status') || null,
|
||||
statusTimer: null,
|
||||
};
|
||||
});
|
||||
|
||||
const advancedToggle = modal.querySelector('.groq-ai-advanced-toggle');
|
||||
const advancedPanel = document.getElementById('groq-ai-advanced-panel');
|
||||
|
||||
function setAdvancedState(isOpen) {
|
||||
if (!advancedToggle || !advancedPanel) {
|
||||
return;
|
||||
}
|
||||
advancedToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
advancedToggle.classList.toggle('is-open', isOpen);
|
||||
advancedPanel.hidden = !isOpen;
|
||||
}
|
||||
|
||||
if (advancedToggle && advancedPanel) {
|
||||
advancedToggle.addEventListener('click', () => {
|
||||
const expanded = advancedToggle.getAttribute('aria-expanded') === 'true';
|
||||
setAdvancedState(!expanded);
|
||||
});
|
||||
setAdvancedState(false);
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
modal.classList.add('is-open');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
if (promptField && !promptField.value && GroqAIGenerator.defaultPrompt) {
|
||||
promptField.value = GroqAIGenerator.defaultPrompt;
|
||||
}
|
||||
resetContextToggles();
|
||||
setTimeout(() => promptField.focus(), 50);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
modal.classList.remove('is-open');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
statusField.textContent = '';
|
||||
}
|
||||
|
||||
openButtons.forEach((button) => {
|
||||
button.addEventListener('click', openModal);
|
||||
});
|
||||
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
modal.addEventListener('click', (event) => {
|
||||
if (event.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (event) => {
|
||||
if (event.key === 'Escape' && modal.classList.contains('is-open')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
function setStatus(message, type = '') {
|
||||
statusField.textContent = message;
|
||||
statusField.setAttribute('data-status', type);
|
||||
}
|
||||
|
||||
const loadingText = window.wp && wp.i18n ? wp.i18n.__('AI is bezig met schrijven...', 'groq-ai-product-text') : 'AI is bezig met schrijven...';
|
||||
const retryText = window.wp && wp.i18n ? wp.i18n.__('Probeer het opnieuw of pas je prompt/context aan.', 'groq-ai-product-text') : 'Probeer het opnieuw of pas je prompt/context aan.';
|
||||
|
||||
function toggleLoading(isLoading) {
|
||||
modal.classList.toggle('is-loading', isLoading);
|
||||
if (isLoading) {
|
||||
setStatus(loadingText, 'loading');
|
||||
}
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const prompt = promptField.value.trim();
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.append('action', 'groq_ai_generate_text');
|
||||
payload.append('nonce', GroqAIGenerator.nonce);
|
||||
payload.append('prompt', prompt);
|
||||
payload.append('post_id', GroqAIGenerator.postId || 0);
|
||||
payload.append('context_fields', JSON.stringify(collectContextSelection()));
|
||||
|
||||
toggleLoading(true);
|
||||
resultWrapper.hidden = true;
|
||||
if (jsonCopyButton) {
|
||||
jsonCopyButton.disabled = true;
|
||||
}
|
||||
resetFieldStatuses();
|
||||
|
||||
fetch(GroqAIGenerator.ajaxUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
body: payload.toString(),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
if (!json.success) {
|
||||
const errorMessage = json.data && json.data.message ? json.data.message : 'Onbekende fout';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const fields = json.data.fields || {};
|
||||
Object.keys(resultFields).forEach((key) => {
|
||||
const entry = resultFields[key];
|
||||
if (entry && entry.textarea) {
|
||||
entry.textarea.value = fields[key] || '';
|
||||
}
|
||||
});
|
||||
resultField.textContent = (json.data.raw || '').trim();
|
||||
resultWrapper.hidden = false;
|
||||
if (jsonCopyButton) {
|
||||
jsonCopyButton.disabled = false;
|
||||
}
|
||||
setStatus('Structuur gegenereerd. Kopieer of vul velden in.', 'success');
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error && error.message ? error.message : 'Er ging iets mis bij het genereren.';
|
||||
setStatus(loadingText, 'error');
|
||||
const fullMessage = `${loadingText} ${message}. ${retryText}`;
|
||||
statusField.textContent = fullMessage;
|
||||
})
|
||||
.finally(() => {
|
||||
toggleLoading(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyToClipboard(text) {
|
||||
if (!text) {
|
||||
return Promise.reject();
|
||||
}
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const temp = document.createElement('textarea');
|
||||
temp.value = text;
|
||||
document.body.appendChild(temp);
|
||||
temp.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
} finally {
|
||||
document.body.removeChild(temp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyRankMathField(action, value) {
|
||||
if (!action || !window.wp || !window.wp.data || typeof window.wp.data.dispatch !== 'function') {
|
||||
return false;
|
||||
}
|
||||
const dispatcher = window.wp.data.dispatch('rank-math');
|
||||
if (!dispatcher || typeof dispatcher[action] !== 'function') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
dispatcher[action](value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (window.console && console.warn) {
|
||||
console.warn('[GroqAI] Rank Math veld kon niet worden bijgewerkt', error);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function setFieldStatus(fieldKey, state) {
|
||||
const entry = resultFields[fieldKey];
|
||||
if (!entry || !entry.status) {
|
||||
return;
|
||||
}
|
||||
if (entry.statusTimer) {
|
||||
clearTimeout(entry.statusTimer);
|
||||
entry.statusTimer = null;
|
||||
}
|
||||
entry.status.textContent = '';
|
||||
entry.status.classList.remove('is-success', 'is-error');
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (state === 'success') {
|
||||
entry.status.textContent = '✓';
|
||||
entry.status.classList.add('is-success');
|
||||
} else if (state === 'error') {
|
||||
entry.status.textContent = '!';
|
||||
entry.status.classList.add('is-error');
|
||||
}
|
||||
entry.statusTimer = setTimeout(() => {
|
||||
setFieldStatus(fieldKey, null);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function resetFieldStatuses() {
|
||||
Object.keys(resultFields).forEach((key) => setFieldStatus(key, null));
|
||||
}
|
||||
|
||||
function shouldUseTinyMCE(selector) {
|
||||
return selector === '#content' || selector === '#excerpt';
|
||||
}
|
||||
|
||||
function applyTinyMCEContent(selector, value) {
|
||||
if (!shouldUseTinyMCE(selector) || !window.tinymce) {
|
||||
return false;
|
||||
}
|
||||
const editorId = selector.startsWith('#') ? selector.substring(1) : selector;
|
||||
const editor = window.tinymce.get(editorId);
|
||||
if (!editor) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
editor.setContent(value);
|
||||
editor.save();
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (window.console && console.warn) {
|
||||
console.warn('[GroqAI] TinyMCE update mislukt voor', selector, error);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function applyToTarget(fieldKey) {
|
||||
const entry = resultFields[fieldKey];
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
const value = entry.textarea ? entry.textarea.value : '';
|
||||
let applied = false;
|
||||
const fallbackSelectors = getFallbackSelectors(fieldKey);
|
||||
|
||||
const allSelectors = [];
|
||||
if (entry.target) {
|
||||
allSelectors.push(entry.target);
|
||||
}
|
||||
Array.prototype.push.apply(allSelectors, fallbackSelectors);
|
||||
|
||||
for (let i = 0; i < allSelectors.length && !applied; i += 1) {
|
||||
const selector = allSelectors[i];
|
||||
|
||||
if (shouldUseTinyMCE(selector)) {
|
||||
applied = applyTinyMCEContent(selector, value);
|
||||
if (applied) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const target = document.querySelector(selector);
|
||||
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) {
|
||||
target.value = value;
|
||||
target.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
target.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
applied = true;
|
||||
}
|
||||
|
||||
if (!applied && shouldUseTinyMCE(selector)) {
|
||||
applied = applyTinyMCEContent(selector, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!applied && entry.rankMathAction) {
|
||||
applied = applyRankMathField(entry.rankMathAction, value);
|
||||
}
|
||||
|
||||
if (applied) {
|
||||
setStatus(entry.label + ' ingevuld.', 'success');
|
||||
setFieldStatus(fieldKey, 'success');
|
||||
} else {
|
||||
setStatus('Kon het veld niet automatisch invullen.', 'error');
|
||||
setFieldStatus(fieldKey, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function getFallbackSelectors(fieldKey) {
|
||||
switch (fieldKey) {
|
||||
case 'meta_title':
|
||||
return ['input[name="rank_math_title"]'];
|
||||
case 'meta_description':
|
||||
return ['textarea[name="rank_math_description"]'];
|
||||
case 'focus_keywords':
|
||||
return ['input[name="rank_math_focus_keyword"]'];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
modal.addEventListener('click', (event) => {
|
||||
if (event.target.classList.contains('groq-ai-copy-field')) {
|
||||
const field = event.target.getAttribute('data-field');
|
||||
const entry = resultFields[field];
|
||||
if (!entry || !entry.textarea) {
|
||||
return;
|
||||
}
|
||||
copyToClipboard(entry.textarea.value)
|
||||
.then(() => {
|
||||
setStatus(entry.label + ' gekopieerd naar het klembord.', 'success');
|
||||
})
|
||||
.catch(() => {
|
||||
setStatus('Kopiëren mislukt.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
if (event.target.classList.contains('groq-ai-apply-field')) {
|
||||
const field = event.target.getAttribute('data-field');
|
||||
applyToTarget(field);
|
||||
}
|
||||
});
|
||||
|
||||
if (jsonCopyButton) {
|
||||
jsonCopyButton.addEventListener('click', () => {
|
||||
const text = resultField ? resultField.textContent.trim() : '';
|
||||
copyToClipboard(text)
|
||||
.then(() => {
|
||||
setStatus('JSON gekopieerd naar het klembord.', 'success');
|
||||
})
|
||||
.catch(() => {
|
||||
setStatus('Kopiëren mislukt.', 'error');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetContextToggles() {
|
||||
const defaults = GroqAIGenerator.contextDefaults || {};
|
||||
contextToggles.forEach((toggle) => {
|
||||
const key = toggle.getAttribute('data-field');
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const state = Object.prototype.hasOwnProperty.call(defaults, key) ? !!defaults[key] : true;
|
||||
toggle.checked = state;
|
||||
});
|
||||
}
|
||||
|
||||
function collectContextSelection() {
|
||||
const selected = [];
|
||||
contextToggles.forEach((toggle) => {
|
||||
if (toggle.checked) {
|
||||
selected.push(toggle.getAttribute('data-field'));
|
||||
}
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
})(jQuery);
|
||||
194
assets/js/settings.js
Normal file
194
assets/js/settings.js
Normal file
@@ -0,0 +1,194 @@
|
||||
(function () {
|
||||
function onReady(callback) {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
onReady(function () {
|
||||
const data = window.GroqAISettingsData || {};
|
||||
const optionKey = data.optionKey || '';
|
||||
if (!optionKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providerSelect = document.querySelector('select[name="' + optionKey + '[provider]"]');
|
||||
const modelSelect = document.getElementById('groq-ai-model-select');
|
||||
const refreshButton = document.getElementById('groq-ai-refresh-models');
|
||||
const refreshStatus = document.getElementById('groq-ai-refresh-models-status');
|
||||
let currentModelValue = (modelSelect && modelSelect.dataset.currentModel) || data.currentModel || '';
|
||||
|
||||
function toggleProviderRows() {
|
||||
if (!data.providerRows || !providerSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(data.providerRows).forEach(function (provider) {
|
||||
const rowId = data.providerRows[provider];
|
||||
let row = rowId ? document.getElementById(rowId) : null;
|
||||
if (!row) {
|
||||
const field = document.querySelector('[data-provider-row="' + provider + '"]');
|
||||
if (field) {
|
||||
row = field.closest('tr') || field;
|
||||
}
|
||||
}
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const target = row.tagName && row.tagName.toLowerCase() === 'tr' ? row : row.closest('tr') || row;
|
||||
target.style.display = provider === providerSelect.value ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function buildModelOptions() {
|
||||
if (!modelSelect || !data.providers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = providerSelect ? providerSelect.value : data.currentProvider;
|
||||
const providerData = data.providers[provider];
|
||||
if (!providerData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const models = Array.isArray(providerData.models) ? providerData.models : [];
|
||||
const frag = document.createDocumentFragment();
|
||||
const placeholder = document.createElement('option');
|
||||
placeholder.value = '';
|
||||
const defaultLabel = providerData.default_label || (data.placeholders && data.placeholders.selectModel) || 'Selecteer een model via "Live modellen ophalen"';
|
||||
placeholder.textContent = defaultLabel;
|
||||
frag.appendChild(placeholder);
|
||||
|
||||
let hasCurrent = false;
|
||||
models.forEach(function (model) {
|
||||
const option = document.createElement('option');
|
||||
option.value = model;
|
||||
option.textContent = model;
|
||||
if (model === currentModelValue) {
|
||||
hasCurrent = true;
|
||||
}
|
||||
frag.appendChild(option);
|
||||
});
|
||||
|
||||
if (currentModelValue && !hasCurrent) {
|
||||
const extraOption = document.createElement('option');
|
||||
extraOption.value = currentModelValue;
|
||||
extraOption.textContent = currentModelValue;
|
||||
frag.appendChild(extraOption);
|
||||
}
|
||||
|
||||
modelSelect.innerHTML = '';
|
||||
modelSelect.appendChild(frag);
|
||||
modelSelect.value = currentModelValue || '';
|
||||
modelSelect.dataset.currentModel = currentModelValue || '';
|
||||
}
|
||||
|
||||
function setRefreshStatus(message, type) {
|
||||
if (!refreshStatus) {
|
||||
return;
|
||||
}
|
||||
refreshStatus.textContent = message || '';
|
||||
refreshStatus.dataset.status = type || '';
|
||||
}
|
||||
|
||||
function updateRefreshButtonVisibility() {
|
||||
if (!refreshButton) {
|
||||
return;
|
||||
}
|
||||
const provider = providerSelect ? providerSelect.value : data.currentProvider;
|
||||
const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null;
|
||||
const supports = providerData && providerData.supports_live;
|
||||
refreshButton.style.display = supports ? '' : 'none';
|
||||
if (!supports) {
|
||||
setRefreshStatus('', '');
|
||||
}
|
||||
}
|
||||
|
||||
function handleModelChange() {
|
||||
if (!modelSelect) {
|
||||
return;
|
||||
}
|
||||
currentModelValue = modelSelect.value;
|
||||
modelSelect.dataset.currentModel = currentModelValue;
|
||||
}
|
||||
|
||||
function handleProviderChange() {
|
||||
currentModelValue = '';
|
||||
if (modelSelect) {
|
||||
modelSelect.dataset.currentModel = '';
|
||||
}
|
||||
buildModelOptions();
|
||||
handleModelChange();
|
||||
toggleProviderRows();
|
||||
updateRefreshButtonVisibility();
|
||||
}
|
||||
|
||||
function handleRefreshModels() {
|
||||
if (!refreshButton || !data.ajaxUrl) {
|
||||
return;
|
||||
}
|
||||
const provider = providerSelect ? providerSelect.value : data.currentProvider;
|
||||
const providerData = data.providers && data.providers[provider] ? data.providers[provider] : null;
|
||||
if (!providerData || !providerData.supports_live) {
|
||||
setRefreshStatus('Deze aanbieder ondersteunt dit niet.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const keyField = document.querySelector('[data-provider-row="' + provider + '"] input');
|
||||
const apiKey = keyField ? keyField.value.trim() : '';
|
||||
if (!apiKey) {
|
||||
setRefreshStatus('Vul eerst de API-sleutel in.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
refreshButton.disabled = true;
|
||||
setRefreshStatus('Modellen worden opgehaald…', 'loading');
|
||||
|
||||
const payload = new URLSearchParams();
|
||||
payload.append('action', 'groq_ai_refresh_models');
|
||||
payload.append('nonce', data.refreshNonce || '');
|
||||
payload.append('provider', provider);
|
||||
payload.append('apiKey', apiKey);
|
||||
|
||||
fetch(data.ajaxUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
body: payload.toString(),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
if (!json.success || !json.data || !Array.isArray(json.data.models)) {
|
||||
throw new Error((json.data && json.data.message) || 'Onbekende fout');
|
||||
}
|
||||
data.providers[provider].models = json.data.models;
|
||||
buildModelOptions();
|
||||
setRefreshStatus('Modellen bijgewerkt.', 'success');
|
||||
})
|
||||
.catch((error) => {
|
||||
setRefreshStatus(error.message || 'Ophalen mislukt.', 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
refreshButton.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (modelSelect) {
|
||||
modelSelect.addEventListener('change', handleModelChange);
|
||||
}
|
||||
if (providerSelect) {
|
||||
providerSelect.addEventListener('change', handleProviderChange);
|
||||
}
|
||||
if (refreshButton) {
|
||||
refreshButton.addEventListener('click', handleRefreshModels);
|
||||
}
|
||||
|
||||
buildModelOptions();
|
||||
handleModelChange();
|
||||
toggleProviderRows();
|
||||
updateRefreshButtonVisibility();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user