<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://recipedius.com/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3ACommon.js</id>
	<title>MediaWiki:Common.js - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://recipedius.com/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3ACommon.js"/>
	<link rel="alternate" type="text/html" href="https://recipedius.com/index.php?title=MediaWiki:Common.js&amp;action=history"/>
	<updated>2026-11-02T10:57:27Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.11</generator>
	<entry>
		<id>https://recipedius.com/index.php?title=MediaWiki:Common.js&amp;diff=137&amp;oldid=prev</id>
		<title>Recipedius deploy: 0.014 T014: deploy AI-assisted Recipe import (e0496d8)</title>
		<link rel="alternate" type="text/html" href="https://recipedius.com/index.php?title=MediaWiki:Common.js&amp;diff=137&amp;oldid=prev"/>
		<updated>2026-09-25T12:55:48Z</updated>

		<summary type="html">&lt;p&gt;0.014 T014: deploy AI-assisted Recipe import (e0496d8)&lt;/p&gt;
&lt;a href=&quot;https://recipedius.com/index.php?title=MediaWiki:Common.js&amp;amp;diff=137&amp;amp;oldid=132&quot;&gt;Show changes&lt;/a&gt;</summary>
		<author><name>Recipedius deploy</name></author>
	</entry>
	<entry>
		<id>https://recipedius.com/index.php?title=MediaWiki:Common.js&amp;diff=132&amp;oldid=prev</id>
		<title>Recipedius deploy: 0.013 T014: deploy AI ingredient draft import (ac09cdd)</title>
		<link rel="alternate" type="text/html" href="https://recipedius.com/index.php?title=MediaWiki:Common.js&amp;diff=132&amp;oldid=prev"/>
		<updated>2026-09-24T16:02:33Z</updated>

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