MediaWiki:Common.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* AI-assisted Ingredient draft import. Runs only on Form:Ingredient. */
(function ($, mw) {
'use strict';
$(function () {
var panel = document.getElementById('rp-ai-ingredient-import');
if (!panel) return;
var fields = {
form: 'Ingredient[form]',
role: 'Ingredient[role]',
storage: 'Ingredient[storage]',
season: 'Ingredient[season]',
kcal100: 'Ingredient[kcal100]',
g_tsp: 'Ingredient[g_tsp]',
g_tbsp: 'Ingredient[g_tbsp]',
g_cup: 'Ingredient[g_cup]',
g_each: 'Ingredient[g_each]',
allergens: 'Ingredient[allergens]',
pantry: 'Ingredient[pantry]',
price: 'Ingredient[price]',
price_per: 'Ingredient[price_per]',
summary: 'Ingredient[summary]'
};
var requiredStrings = ['name', 'form', 'role', 'storage', 'season', 'summary'];
var optionalNumbers = ['g_tsp', 'g_tbsp', 'g_cup', 'g_each', 'price'];
var allKeys = requiredStrings.concat(['kcal100'], optionalNumbers, ['allergens', 'pantry', 'price_per']);
// FormEdit is a Special page, so wgPageName is the Special-page route.
// Page Forms exposes the actual selected edit target separately.
var expectedTitle = String(mw.config.get('wgPageFormsTargetName') || '').replace(/_/g, ' ');
if (!expectedTitle) return;
// The wikitext fallback is useful without JavaScript and to rendered-page
// checks. Replace it only after the enhanced controls can be created.
panel.textContent = '';
function addTextElement(tag, text) {
var element = document.createElement(tag);
element.textContent = text;
panel.appendChild(element);
return element;
}
function addProviderLink(provider, url) {
var link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener';
link.setAttribute('data-rp-ai-provider', provider);
link.textContent = provider;
return link;
}
var title = addTextElement('strong', 'Create a draft with AI (optional)');
title.className = 'rp-ai-heading';
addTextElement('p', 'AI output is a draft. Verify nutrition, allergens, weights, and price against a reliable source before saving.');
var actions = document.createElement('p');
var copyButton = document.createElement('button');
copyButton.type = 'button';
copyButton.id = 'rp-ai-copy-prompt';
copyButton.textContent = 'Copy prompt';
actions.appendChild(copyButton);
actions.appendChild(document.createTextNode(' '));
[
['ChatGPT', 'https://chatgpt.com/'],
['Claude', 'https://claude.ai/new'],
['Gemini', 'https://gemini.google.com/app'],
['DeepSeek', 'https://chat.deepseek.com/']
].forEach(function (provider, index) {
if (index) actions.appendChild(document.createTextNode(' · '));
actions.appendChild(addProviderLink(provider[0], provider[1]));
});
panel.appendChild(actions);
var importLabel = document.createElement('label');
importLabel.htmlFor = 'rp-ai-ingredient-json';
importLabel.textContent = 'Paste the returned JSON';
var importArea = document.createElement('textarea');
importArea.id = 'rp-ai-ingredient-json';
importArea.rows = 12;
importArea.cols = 70;
importArea.spellcheck = false;
importArea.placeholder = '{"name":"This page title", ...}';
var importControl = document.createElement('p');
importControl.appendChild(importLabel);
importControl.appendChild(document.createElement('br'));
importControl.appendChild(importArea);
panel.appendChild(importControl);
var importButton = document.createElement('button');
importButton.type = 'button';
importButton.id = 'rp-ai-import-json';
importButton.textContent = 'Import draft into this form';
var importAction = document.createElement('p');
importAction.appendChild(importButton);
panel.appendChild(importAction);
var status = document.createElement('div');
status.id = 'rp-ai-ingredient-status';
status.className = 'rp-ai-status';
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
panel.appendChild(status);
var input = importArea;
function setStatus(message, isError) {
status.textContent = message;
status.className = isError ? 'rp-ai-status rp-ai-status-error' : 'rp-ai-status rp-ai-status-success';
}
function promptFor(title) {
return 'Create a draft Recipedius ingredient record for "' + title + '". ' +
'Return only one JSON object with exactly these keys: ' +
'name, form, role, storage, season, kcal100, g_tsp, g_tbsp, g_cup, g_each, allergens, pantry, price, price_per, summary. ' +
'Use this title exactly as name. form, role, storage, season, and summary must be strings. ' +
'kcal100 must be a non-negative number. g_tsp, g_tbsp, g_cup, g_each, and price must be non-negative numbers or null. ' +
'price_per must be a string or null. allergens must be an array of strings. pantry must be true or false. ' +
'Use null when a unit weight or typical price is unknown; do not invent values. ' +
'Nutrition, allergens, weights, and price are a draft for the contributor to verify against a reliable source before saving.';
}
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
var temporary = document.createElement('textarea');
temporary.value = text;
temporary.setAttribute('readonly', '');
temporary.style.position = 'fixed';
temporary.style.opacity = '0';
document.body.appendChild(temporary);
temporary.select();
var copied = document.execCommand('copy');
document.body.removeChild(temporary);
return copied ? Promise.resolve() : Promise.reject(new Error('Clipboard unavailable'));
}
function copyPrompt() {
return copyText(promptFor(expectedTitle)).then(function () {
setStatus('Prompt copied. Review the AI draft before importing it.', false);
}).catch(function () {
setStatus('Your browser did not allow copying. Select and copy the prompt shown below.', true);
});
}
function hasOwn(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
}
function validString(value) {
return typeof value === 'string' && value.trim() !== '';
}
function validNumber(value) {
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}
function validate(record) {
var i;
if (!record || Array.isArray(record) || typeof record !== 'object') return 'The draft must be one JSON object.';
for (i = 0; i < Object.keys(record).length; i++) {
if (allKeys.indexOf(Object.keys(record)[i]) === -1) return 'Unknown property: ' + Object.keys(record)[i] + '.';
}
for (i = 0; i < allKeys.length; i++) {
if (!hasOwn(record, allKeys[i])) return 'Missing property: ' + allKeys[i] + '.';
}
for (i = 0; i < requiredStrings.length; i++) {
if (!validString(record[requiredStrings[i]])) return requiredStrings[i] + ' must be a non-empty string.';
}
if (record.name !== expectedTitle) return 'The JSON name must exactly match this page title: ' + expectedTitle + '.';
if (!validNumber(record.kcal100)) return 'kcal100 must be a non-negative number.';
for (i = 0; i < optionalNumbers.length; i++) {
if (record[optionalNumbers[i]] !== null && !validNumber(record[optionalNumbers[i]])) {
return optionalNumbers[i] + ' must be a non-negative number or null.';
}
}
if (!Array.isArray(record.allergens) || !record.allergens.every(validString)) return 'allergens must be an array of non-empty strings.';
if (typeof record.pantry !== 'boolean') return 'pantry must be true or false.';
if (record.price_per !== null && !validString(record.price_per)) return 'price_per must be a non-empty string or null.';
return null;
}
function setField(key, value) {
var control = document.querySelector('[name="' + fields[key] + '"]');
if (!control) throw new Error('The Ingredient form is missing the ' + key + ' field.');
control.value = value === null ? '' : String(value);
}
copyButton.addEventListener('click', copyPrompt);
Array.prototype.forEach.call(panel.querySelectorAll('[data-rp-ai-provider]'), function (link) {
link.addEventListener('click', copyPrompt);
});
importButton.addEventListener('click', function () {
var record;
try {
record = JSON.parse(input.value);
} catch (error) {
setStatus('Invalid JSON: ' + error.message, true);
return;
}
var problem = validate(record);
if (problem) {
setStatus(problem, true);
return;
}
try {
Object.keys(fields).forEach(function (key) {
var value = record[key];
if (key === 'allergens') value = value.join(', ');
if (key === 'pantry') value = value ? 'yes' : 'no';
setField(key, value);
});
} catch (error) {
setStatus(error.message, true);
return;
}
setStatus('Draft imported. Review every field, then use the normal Save button to create ' + expectedTitle + '.', false);
});
});
}(jQuery, mediaWiki));