Spaces:
Running
Running
Download js/app.js from CGIAR/mcl-builder: direct link, hf CLI and curl.
- Browser
- Download file 38.5 kB
-
https://huggingface.co/spaces/CGIAR/mcl-builder/resolve/main/js/app.js
- Command line
-
hf download hf://spaces/CGIAR/mcl-builder/js/app.js
-
curl -L -o app.js https://huggingface.co/spaces/CGIAR/mcl-builder/resolve/main/js/app.js
38.5 kB
| /* MCL Builder — all state lives in this browser. Nothing is transmitted anywhere. */ | |
| (function () { | |
| ; | |
| var STORE = "mcl-builder-v1"; | |
| var SCHEMA = window.MCL_SCHEMA; | |
| var GT = window.MCL_GENERAL_TERMS; | |
| var DEFS = window.MCL_DEFINITIONS; | |
| var NOTES = window.MCL_NOTES; | |
| var PURPOSES = window.MCL_PURPOSE_OPTIONS; | |
| var FIELDS = {}; | |
| SCHEMA.forEach(function (s) { s.fields.forEach(function (f) { FIELDS[f.k] = f; }); }); | |
| var PURPOSE_FIELDS = ["pTextOutput", "pAvOutput", "pTextDisplay", "pAvDisplay"]; | |
| var RIGHT_OF_PURPOSE = { pTextOutput:"rTextOutput", pAvOutput:"rAvOutput", pTextDisplay:"rTextDisplay", pAvDisplay:"rAvDisplay" }; | |
| var RIGHT_LABEL = { | |
| rTraining:"the Training Rights for internal use only", | |
| rTextOutput:"the Text Output Rights", | |
| rAvOutput:"the Audio-Visual Output Rights", | |
| rTextDisplay:"the Text Display Rights", | |
| rAvDisplay:"the Audio-Visual Display Rights" | |
| }; | |
| var state = {}; | |
| var activeSection = 0; | |
| var renderMode = "preview"; | |
| var highlight = true; | |
| /* ───────────── state ───────────── */ | |
| function defaults() { | |
| var d = {}; | |
| SCHEMA.forEach(function (s) { | |
| s.fields.forEach(function (f) { d[f.k] = f.defaultValue !== undefined ? f.defaultValue : ""; }); | |
| }); | |
| PURPOSE_FIELDS.forEach(function (k) { d[k] = ""; }); | |
| ["rTraining","rTextOutput","rAvOutput","rTextDisplay","rAvDisplay"].forEach(function (k) { d[k] = false; }); | |
| return d; | |
| } | |
| function load() { | |
| state = defaults(); | |
| try { | |
| var raw = localStorage.getItem(STORE); | |
| if (raw) { | |
| var saved = JSON.parse(raw); | |
| Object.keys(saved).forEach(function (k) { if (k in state) state[k] = saved[k]; }); | |
| } | |
| } catch (e) { /* corrupt or unavailable storage — fall back to defaults */ } | |
| } | |
| function save() { | |
| try { localStorage.setItem(STORE, JSON.stringify(state)); } catch (e) { /* private mode */ } | |
| } | |
| /* ───────────── helpers ───────────── */ | |
| function esc(s) { | |
| return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); | |
| } | |
| function nl2br(s) { return esc(s).replace(/\n/g, "<br>"); } | |
| function txt(k) { return (state[k] == null ? "" : String(state[k])).trim(); } | |
| function has(k) { return txt(k).length > 0; } | |
| /* Renders a value, or the template's own bracketed placeholder if empty. */ | |
| function v(k, fallbackPh) { | |
| var raw = txt(k); | |
| if (raw) return nl2br(raw); | |
| var f = FIELDS[k]; | |
| var label = (f && f.ph) || fallbackPh || k.toUpperCase(); | |
| if (renderMode === "print") return '<span class="blank">' + esc("[" + label + "]") + "</span>"; | |
| return '<span class="ph' + (highlight ? "" : " ph-quiet") + '" data-field="' + esc(k) + '" tabindex="0" role="button">' + esc("[" + label + "]") + "</span>"; | |
| } | |
| function grantedRights() { | |
| return ["rTraining","rTextOutput","rAvOutput","rTextDisplay","rAvDisplay"].filter(function (k) { return !!state[k]; }); | |
| } | |
| function purposesUsed() { | |
| var out = []; | |
| PURPOSE_FIELDS.forEach(function (p) { if (state[RIGHT_OF_PURPOSE[p]] && txt(p)) out.push(txt(p)); }); | |
| return out; | |
| } | |
| function anyNonCommercial() { return purposesUsed().indexOf("nc") !== -1; } | |
| function hasOutputRights() { return !!(state.rTextOutput || state.rAvOutput); } | |
| function hasDisplayRights() { return !!(state.rTextDisplay || state.rAvDisplay); } | |
| /* ───────────── completeness ───────────── */ | |
| function visible(f) { | |
| if (!f.showIf) return true; | |
| return f.showIf.in.indexOf(txt(f.showIf.k)) !== -1; | |
| } | |
| function isRequired(f) { | |
| if (f.requiredIf) return f.requiredIf.in.indexOf(txt(f.requiredIf.k)) !== -1; | |
| if (f.requiredIfNonCommercial) return anyNonCommercial(); | |
| return !!f.required; | |
| } | |
| /* Returns {req, done, missing:[{k,label}]} for a section. */ | |
| function sectionStatus(s) { | |
| var req = 0, done = 0, missing = []; | |
| if (s.id === "rights") { | |
| PURPOSE_FIELDS.forEach(function (p) { | |
| var r = RIGHT_OF_PURPOSE[p]; | |
| if (state[r]) { | |
| req++; | |
| if (txt(p)) done++; | |
| else missing.push({ k: p, label: "Purpose for " + RIGHT_LABEL[r].replace(/^the /, "") }); | |
| } | |
| }); | |
| return { req: req, done: done, missing: missing, optional: req === 0 }; | |
| } | |
| s.fields.forEach(function (f) { | |
| if (!visible(f) || !isRequired(f)) return; | |
| req++; | |
| if (has(f.k)) done++; else missing.push({ k: f.k, label: f.label }); | |
| }); | |
| return { req: req, done: done, missing: missing, optional: req === 0 }; | |
| } | |
| function overall() { | |
| var req = 0, done = 0; | |
| SCHEMA.forEach(function (s) { var st = sectionStatus(s); req += st.req; done += st.done; }); | |
| return { req: req, done: done }; | |
| } | |
| /* ───────────── form rendering ───────────── */ | |
| /* CABI's own drafting notes, shown verbatim and attributed, so they read as the | |
| template author's guidance rather than as this app's commentary. */ | |
| function draftingNote(text) { | |
| if (!text) return ""; | |
| var paras = text.split(/\n+/).filter(function (x) { return x.trim(); }); | |
| return '<div class="dnote"><span class="dnote-tag">CABI drafting note</span>' + | |
| paras.map(function (x) { return "<p>" + esc(x) + "</p>"; }).join("") + "</div>"; | |
| } | |
| function fieldControl(f) { | |
| var id = "f_" + f.k; | |
| var val = state[f.k]; | |
| var badge = ""; | |
| if (f.templateDefault) badge = '<span class="badge badge-default">template default — confirm</span>'; | |
| if (f.sourceDefect) badge = '<span class="badge badge-defect">source template defect</span>'; | |
| if (f.departure) badge = '<span class="badge badge-departure">departs from published wording</span>'; | |
| var reqMark = isRequired(f) ? '<span class="req" title="Required">*</span>' : '<span class="opt">optional</span>'; | |
| var help = f.help ? '<p class="help">' + f.help + "</p>" : ""; | |
| var body; | |
| if (f.type === "textarea") { | |
| body = '<textarea id="' + id + '" data-k="' + f.k + '" rows="' + (f.rows || 4) + '" placeholder="' + esc(f.ph || "") + '">' + esc(val || "") + "</textarea>"; | |
| } else if (f.type === "radio") { | |
| body = '<div class="radios">' + f.options.map(function (o) { | |
| return '<label class="radio' + (val === o.v ? " is-on" : "") + '">' + | |
| '<input type="radio" name="' + f.k + '" data-k="' + f.k + '" value="' + o.v + '"' + (val === o.v ? " checked" : "") + ">" + | |
| '<span class="radio-body"><span class="radio-title">' + esc(o.label) + "</span>" + | |
| '<span class="radio-desc">' + esc(o.desc) + "</span></span></label>"; | |
| }).join("") + "</div>"; | |
| } else if (f.type === "static-on") { | |
| return '<div class="field field-locked"><div class="lockrow"><span class="tick">✓</span>' + | |
| '<div><strong>' + f.label + "</strong>" + help + "</div></div></div>"; | |
| } else if (f.type === "check") { | |
| var on = !!val; | |
| var pk = f.purposeKey; | |
| var purposeBlock = ""; | |
| if (pk) { | |
| purposeBlock = '<div class="purpose' + (on ? "" : " is-hidden") + '">' + | |
| '<span class="purpose-label">Granted for:</span>' + | |
| PURPOSES.map(function (p) { | |
| return '<label class="chip' + (state[pk] === p.v ? " is-on" : "") + '">' + | |
| '<input type="radio" name="' + pk + '" data-k="' + pk + '" value="' + p.v + '"' + | |
| (state[pk] === p.v ? " checked" : "") + ">" + esc(p.label) + "</label>"; | |
| }).join("") + | |
| (on && !txt(pk) ? '<span class="purpose-missing">choose one</span>' : "") + "</div>"; | |
| } | |
| return '<div class="field field-check' + (on ? " is-on" : "") + '">' + | |
| '<label class="checkrow"><input type="checkbox" data-k="' + f.k + '"' + (on ? " checked" : "") + ">" + | |
| "<span><strong>" + f.label + "</strong>" + help + "</span></label>" + purposeBlock + | |
| draftingNote(NOTES.fields[f.k]) + "</div>"; | |
| } else { | |
| body = '<input type="text" id="' + id + '" data-k="' + f.k + '" value="' + esc(val || "") + '" placeholder="' + esc(f.ph || "") + '">'; | |
| } | |
| return '<div class="field" data-field-row="' + f.k + '">' + | |
| '<label class="flabel" for="' + id + '">' + f.label + " " + reqMark + badge + "</label>" + | |
| body + help + draftingNote(NOTES.fields[f.k]) + "</div>"; | |
| } | |
| function renderForm() { | |
| var s = SCHEMA[activeSection]; | |
| var html = '<div class="form-head"><h2>' + s.heading + "</h2><p>" + s.intro + "</p></div>"; | |
| if (s.contactNotice) { | |
| html += '<div class="callout callout-note callout-tight"><strong>Personal contact details.</strong> ' + | |
| "These are entered and stored only in your own browser. If you are working from a shared or public " + | |
| "machine, clear your answers when you are done.</div>"; | |
| } | |
| html += draftingNote(NOTES.sections[s.id]); | |
| html += s.fields.filter(visible).map(fieldControl).join(""); | |
| document.getElementById("formHost").innerHTML = html; | |
| document.getElementById("btnPrev").disabled = activeSection === 0; | |
| document.getElementById("btnNext").textContent = activeSection === SCHEMA.length - 1 ? "Review & export →" : "Next →"; | |
| } | |
| /* The nav is built once and then patched in place. Replacing its innerHTML on every | |
| keystroke would destroy the button between mousedown and mouseup whenever a user | |
| typed in a field and then clicked straight onto a nav item, swallowing that click. */ | |
| function buildNav() { | |
| document.getElementById("sectionNav").innerHTML = SCHEMA.map(function (s, i) { | |
| return '<li><button class="navbtn" data-sec="' + i + '">' + | |
| '<span class="dot"></span><span class="navlabel">' + s.nav + "</span>" + | |
| '<span class="navcount"></span></button></li>'; | |
| }).join(""); | |
| } | |
| function renderNav() { | |
| var o = overall(); | |
| document.getElementById("progressFill").style.width = (o.req ? (o.done / o.req * 100) : 0) + "%"; | |
| document.getElementById("progressText").textContent = o.done + " of " + o.req + " required answers"; | |
| SCHEMA.forEach(function (s, i) { | |
| var btn = document.querySelector('.navbtn[data-sec="' + i + '"]'); | |
| if (!btn) return; | |
| var st = sectionStatus(s); | |
| var cls = st.optional ? "opt" : (st.done === st.req ? "done" : (st.done ? "part" : "todo")); | |
| btn.className = "navbtn " + cls + (i === activeSection ? " is-active" : ""); | |
| btn.querySelector(".navcount").textContent = st.optional ? "" : st.done + "/" + st.req; | |
| }); | |
| } | |
| /* ───────────── document rendering ───────────── */ | |
| function row(n, term, body) { | |
| return '<tr><td class="st-n">' + n + '</td><td class="st-t">' + term + '</td><td class="st-b">' + body + "</td></tr>"; | |
| } | |
| function rightsList() { | |
| var items = ['<li>the Technical Rights for internal use only;</li>']; | |
| if (state.rTraining) items.push("<li>the Training Rights for internal use only;</li>"); | |
| [["rTextOutput","pTextOutput"],["rAvOutput","pAvOutput"],["rTextDisplay","pTextDisplay"],["rAvDisplay","pAvDisplay"]] | |
| .forEach(function (pair) { | |
| if (!state[pair[0]]) return; | |
| var p = txt(pair[1]); | |
| var pl = p === "nc" ? "Non-Commercial Use" : p === "c" ? "Commercial Use" : null; | |
| var purpose = pl ? esc(pl) | |
| : (renderMode === "print" ? '<span class="blank">[NON-COMMERCIAL USE / COMMERCIAL USE]</span>' | |
| : '<span class="ph' + (highlight ? "" : " ph-quiet") + '" data-field="' + pair[1] + '" tabindex="0" role="button">[NON-COMMERCIAL USE / COMMERCIAL USE]</span>'); | |
| items.push("<li>" + RIGHT_LABEL[pair[0]] + " for " + purpose + ";</li>"); | |
| }); | |
| var last = items.pop().replace(/;<\/li>$/, ".</li>"); | |
| items.push(last); | |
| return "<ul>" + items.join("") + "</ul>"; | |
| } | |
| function deliverySentence() { | |
| var m = txt("deliveryMethod"); | |
| if (m === "sftp") return "deliver the Licensed Materials by secure file transfer, via encrypted protocols (e.g. SFTP) to the Licensee."; | |
| if (m === "cloud") return "make the Licensed Materials available by providing the Licensee access to its private cloud-based repository."; | |
| if (m === "api") return "make the Licensed Materials available to the Licensee via streaming or querying the Licensor’s API."; | |
| return v("deliveryMethod", "SELECT A DELIVERY METHOD"); | |
| } | |
| function sigLine() { return '<span class="rule"></span>'; } | |
| function buildSpecialTerms() { | |
| var h = ""; | |
| h += '<div class="doc-title-block"><p class="doc-kicker">AI Licence Agreement</p>' + | |
| '<h1 class="doc-h1">Model Content Licence for AI</h1>' + | |
| '<p class="doc-sub">Special Terms and General Terms</p></div>'; | |
| h += '<p class="agno"><strong>AI LICENCE AGREEMENT No.</strong> ' + v("agreementNo") + "</p>"; | |
| h += "<h2>Introduction</h2><ol class='alpha'>" + | |
| "<li>The Licensor owns or has the right to grant rights in certain materials.</li>" + | |
| "<li>The Licensee is developing an artificial intelligence solution.</li>" + | |
| "<li>The Licensor has agreed to licence the Licensee to use certain of its materials on the terms set out in this AI Licence Agreement, which is made up of the Special Terms below and the General Terms attached (together, the <strong>Agreement</strong>).</li>" + | |
| "</ol>"; | |
| h += "<h2>Special Terms</h2>"; | |
| h += '<table class="st"><thead><tr><th class="st-n">#</th><th class="st-t">Term</th><th class="st-b">Key terms</th></tr></thead><tbody>'; | |
| h += row(1, "Parties", "<p>(1) " + v("licensorName") + ", registered in " + v("licensorCountry") + | |
| ", with company number " + v("licensorNumber") + " whose registered office is at " + v("licensorAddress") + | |
| " (<strong>Licensor</strong>); and</p><p>(2) " + v("licenseeName") + ", incorporated and registered in " + | |
| v("licenseeCountry") + " with company number " + v("licenseeNumber") + " whose registered office is at " + | |
| v("licenseeAddress") + " (<strong>Licensee</strong>).</p>"); | |
| h += row(2, "Term", "<p>This Agreement will come into force from date of last signature of these Special Terms, and unless it is terminated earlier in accordance with its terms, will continue for a period of " + | |
| v("termYears") + " years.</p>"); | |
| var mm = txt("materialsMode"); | |
| var matBody; | |
| if (mm === "updating") { | |
| matBody = "<p>Subject to the terms of this Agreement, the Licensor will make available to the Licensee:</p><ul>" + | |
| "<li>the materials specified in Part A of Annex 1 to these Special Terms; and</li>" + | |
| "<li>updated versions of those materials, as and when they become available. The Licensor makes no guarantee as to the frequency, scope, or availability of updates.</li></ul>"; | |
| } else if (mm === "fixed") { | |
| matBody = "<p>Subject to the terms of this Agreement, the Licensor will make available to the Licensee the materials specified in Part A of Annex 1 to these Special Terms. Those materials do not include any update, revisions or new versions.</p>"; | |
| } else { | |
| matBody = "<p>Subject to the terms of this Agreement, the Licensor will make available to the Licensee " + v("materialsMode", "CHOOSE: FIXED SNAPSHOT OR SNAPSHOT PLUS UPDATES") + "</p>"; | |
| } | |
| matBody += "<p>The Licensee does not have the right to use any content included in the Licensed Materials, that is referenced or identified in Part B of Annex 1.</p>"; | |
| h += row(3, "Licensed Materials", matBody); | |
| h += row(4, "Designated AI Solution", | |
| "<p>The Licensee will use the Licensed Materials according to the Agreement, in connection with the following AI solution only:</p><ul>" + | |
| "<li><strong>Name of Designated AI Solution:</strong> " + v("aiName") + "</li>" + | |
| "<li><strong>Description of the Designated AI Solution:</strong> " + v("aiDescription") + "</li>" + | |
| "<li><strong>Model / architecture / version:</strong> " + v("aiModel") + "</li></ul>" + | |
| "<p>No use of the Licensed Materials is permitted in connection with any AI system or solution, other than the Designated AI Solution without the Licensor’s prior written consent (not to be unreasonably withheld or delayed) and subject to further terms and conditions.</p>"); | |
| h += row(5, "Rights to use Licensed Materials and Purpose", | |
| "<p>The Licensor grants the Licensee the rights set out below in accordance with clause 5 of the General Terms:</p>" + rightsList()); | |
| h += row(6, "Additional Limitations on Use", | |
| has("additionalLimitations") | |
| ? "<p>The usage restrictions for the Licensed Materials described in clause 6 of the General Terms apply, together with the following additional limitations: " + nl2br(txt("additionalLimitations")) + "</p>" | |
| : "<p>The usage restrictions for the Licensed Materials described in clause 6 of the General Terms apply.</p>"); | |
| h += row(7, "Delivery Method", | |
| "<p>The Licensor will within " + v("deliveryDays") + " days of execution of these Special Terms " + deliverySentence() + "</p>"); | |
| h += row(8, "Fees", | |
| "<p>The Licensee will pay the Licensor the following fees: " + v("feeCurrency") + " " + v("feeAmount") + | |
| " according to the following payment schedule: " + v("paymentSchedule") + "</p>"); | |
| h += row(9, "Representatives", | |
| "<p>The Parties’ representatives for implementing the Agreement are:</p><ul>" + | |
| "<li>" + v("licensorRepName") + ", " + v("licensorRepEmail") + ", " + v("licensorRepMobile") + " for the Licensor; and</li>" + | |
| "<li>" + v("licenseeRepName") + ", " + v("licenseeRepEmail") + ", " + v("licenseeRepMobile") + " for the Licensee,</li></ul>" + | |
| "<p>or their designated representatives or replacements.</p>"); | |
| h += row(10, "Documents that make up this Agreement", | |
| "<p>The individual documents that make up the Agreement are as follows:</p><ol><li>These Special Terms</li><li>General Terms</li></ol>" + | |
| "<p>If there is a conflict between the provisions of the different documents which form part of the Agreement, these Special Terms will take precedence over the General Terms to the extent of that conflict.</p>"); | |
| var amendments = []; | |
| if (txt("purposeWording") !== "original") { | |
| amendments.push("The definition of <strong>Purpose</strong> in clause 2.1 of the General Terms is amended to provide for a separate Purpose in respect of each of the Output Rights and Display Rights granted under row 5 above, in place of a single Purpose governing the Agreement as a whole."); | |
| } | |
| var typed = txt("amendments"); | |
| if (typed && typed.toUpperCase() !== "N/A") amendments.push(nl2br(typed)); | |
| h += row(11, "Amendments to the General Terms", | |
| amendments.length | |
| ? (amendments.length === 1 ? "<p>" + amendments[0] + "</p>" | |
| : "<ol>" + amendments.map(function (a) { return "<li>" + a + "</li>"; }).join("") + "</ol>") | |
| : "<p>N/A</p>"); | |
| h += "</tbody></table>"; | |
| h += '<div class="sig-block"><p>These Special Terms and the General Terms overleaf, govern the licensing of the Licensed Materials to the Licensee for use in accordance with this Agreement. By signing below, the Parties indicate their acceptance of this Agreement.</p>' + | |
| '<div class="sigs">' + | |
| '<div class="sig"><p class="sig-for">For and on behalf of the Licensor: ' + (has("licensorName") ? "<strong>" + esc(txt("licensorName")) + "</strong>" : "") + "</p>" + | |
| '<p class="sigline">Signature ' + sigLine() + "</p>" + | |
| '<p class="sigline">Name ' + sigLine() + "</p>" + | |
| '<p class="sigline">Title ' + sigLine() + "</p>" + | |
| '<p class="sigline">Date ' + sigLine() + "</p></div>" + | |
| '<div class="sig"><p class="sig-for">For and on behalf of the Licensee: ' + (has("licenseeName") ? "<strong>" + esc(txt("licenseeName")) + "</strong>" : "") + "</p>" + | |
| '<p class="sigline">Signature ' + sigLine() + "</p>" + | |
| '<p class="sigline">Name ' + sigLine() + "</p>" + | |
| '<p class="sigline">Title ' + sigLine() + "</p>" + | |
| '<p class="sigline">Date ' + sigLine() + "</p></div>" + | |
| "</div></div>"; | |
| return h; | |
| } | |
| function buildAnnex() { | |
| var mode = txt("annexBMode"); | |
| var partB = ""; | |
| if (mode === "default" || mode === "both" || !mode) { | |
| partB += "<li>any audio-visual materials, including graphics, illustrations, images or video included in the Licensed Materials, unless a particular item has been expressly included in Part A of this Annex 1" + | |
| (mode === "both" ? "; and" : ".") + "</li>"; | |
| } | |
| if (mode === "custom" || mode === "both") { | |
| partB += "<li>" + v("annexBCustom") + "</li>"; | |
| } | |
| if (!mode) { | |
| partB = "<li>" + v("annexBMode", "CHOOSE THE PART B EXCLUSIONS") + "</li>" + partB; | |
| } | |
| return '<div class="page-break"></div><h2>Annex 1 to Special Terms</h2>' + | |
| "<h3>Part A — Licensed Materials</h3><p>" + v("annexADescription") + "</p>" + | |
| "<h3>Technical Specification for Licensed Materials</h3><p>" + v("annexATechSpec") + "</p>" + | |
| "<h3>Part B</h3><p>To avoid doubt, Licensed Materials do not include the following items:</p><ul>" + partB + "</ul>"; | |
| } | |
| /* Tokens whose text is derived from a choice rather than typed into a field. */ | |
| var COMPUTED = { | |
| purposeTail: function () { | |
| if (txt("purposeWording") === "original") { | |
| return "which will either be Non-Commercial Use or Commercial Use, whichever is specified in the Special Terms."; | |
| } | |
| return "which will be either Non-Commercial Use or Commercial Use in respect of each of the Output Rights and Display Rights granted, as specified in row 5 of the Special Terms. Where different Purposes are specified for different Rights, each such Right is exercisable only for the Purpose specified for it."; | |
| } | |
| }; | |
| function fillTokens(s) { | |
| return s.replace(/\{\{(\w+)\}\}/g, function (_, k) { | |
| return COMPUTED[k] ? esc(COMPUTED[k]()) : v(k); | |
| }); | |
| } | |
| function defsTable() { | |
| return '<table class="defs"><tbody>' + DEFS.map(function (d) { | |
| return "<tr><td class=\"defs-t\"><strong>" + esc(d[0]) + "</strong></td><td class=\"defs-d\">" + | |
| fillTokens(esc(d[1])).replace(/\n/g, "<br>") + "</td></tr>"; | |
| }).join("") + "</tbody></table>"; | |
| } | |
| function renderItems(items, depth) { | |
| return items.map(function (it) { | |
| var body = '<div class="cl cl-' + depth + '">' + | |
| (it.n ? '<span class="cl-n">' + esc(it.n) + "</span>" : '<span class="cl-n cl-n-empty"></span>') + | |
| '<span class="cl-t">' + fillTokens(esc(it.text)) + "</span></div>"; | |
| if (it.definitions) body += defsTable(); | |
| if (it.items) body += renderItems(it.items, depth + 1); | |
| return body; | |
| }).join(""); | |
| } | |
| function buildGeneralTerms() { | |
| return '<div class="page-break"></div><h2>General Terms for AI Content Licence Agreement</h2>' + | |
| GT.map(function (c) { | |
| return '<section class="gt-clause"><h3 class="gt-h"><span class="gt-n">' + esc(c.n) + "</span>" + esc(c.title) + "</h3>" + | |
| renderItems(c.items, 1) + "</section>"; | |
| }).join(""); | |
| } | |
| function buildDocument() { | |
| return buildSpecialTerms() + buildAnnex() + buildGeneralTerms(); | |
| } | |
| var previewTimer = null, pointerDown = false, previewDirty = false; | |
| function renderPreview() { | |
| renderMode = "preview"; | |
| document.getElementById("docPreview").innerHTML = buildDocument(); | |
| previewDirty = false; | |
| } | |
| /* Debounced, and held off entirely while a pointer is down so that a click starting | |
| on a placeholder chip is not orphaned by the preview being rebuilt underneath it. */ | |
| function renderPreviewSoon() { | |
| previewDirty = true; | |
| clearTimeout(previewTimer); | |
| previewTimer = setTimeout(function () { | |
| if (pointerDown) { renderPreviewSoon(); return; } | |
| renderPreview(); | |
| }, 140); | |
| } | |
| function renderFullDoc() { | |
| renderMode = "preview"; | |
| document.getElementById("docFull").innerHTML = buildDocument(); | |
| } | |
| /* ───────────── review tab ───────────── */ | |
| /* AI-Rights label (draft proposal — see Annex A). Derived mechanically from row 5: | |
| P is always granted (Technical Rights), and purpose attaches only to Output and | |
| Display Rights, per CABI's drafting note for row 5. */ | |
| var LABEL_RSL = { | |
| "AI-NONE": { permits: [], prohibits: ["all"] }, | |
| "AI-PREP": { permits: ["ai-index*"], prohibits: ["ai-train", "ai-input", "search"] }, | |
| "AI-TRAIN": { permits: ["ai-index*", "ai-train"], prohibits: ["ai-input", "search"] }, | |
| "AI-QUOTE": { permits: ["ai-index*", "search"], prohibits: ["ai-train", "ai-input"] }, | |
| "AI-GROUND": { permits: ["ai-index*", "ai-input", "search"], prohibits: ["ai-train"] }, | |
| "AI-FULL": { permits: ["ai-all"], prohibits: [] } | |
| }; | |
| var LABEL_PROSE = { | |
| "AI-NONE": "No AI use permitted.", | |
| "AI-PREP": "Prepare and analyse only \u2014 no training, no outputs, nothing surfaced to end users.", | |
| "AI-TRAIN": "Prepare and train \u2014 internal use only; no user-facing outputs or extracts.", | |
| "AI-QUOTE": "Prepare and show attributed verbatim extracts \u2014 no training, no generated outputs.", | |
| "AI-GROUND": "Prepare, generate grounded outputs and quote attributed extracts \u2014 no training on the corpus.", | |
| "AI-FULL": "Prepare, train, generate outputs and quote attributed extracts." | |
| }; | |
| function aiRightsLabel() { | |
| var T = !!state.rTraining, G = hasOutputRights(), Q = hasDisplayRights(); | |
| var profile = "AI-PREP"; // Technical Rights are always granted | |
| if (T && (G || Q)) profile = "AI-FULL"; | |
| else if (T) profile = "AI-TRAIN"; | |
| else if (G) profile = "AI-GROUND"; | |
| else if (Q) profile = "AI-QUOTE"; | |
| var suffix = "", note = ""; | |
| if (G || Q) { | |
| var ps = purposesUsed(); | |
| var uniq = ps.filter(function (v, i) { return ps.indexOf(v) === i; }); | |
| if (uniq.length === 1) suffix = uniq[0] === "nc" ? "-NC" : "-C"; | |
| else if (uniq.length > 1) { suffix = "-C"; note = "Row 5 mixes Commercial and Non-Commercial purposes; the label takes the broader of the two."; } | |
| else note = "No purpose chosen yet for the granted Output or Display Rights."; | |
| } | |
| return { label: profile + suffix, profile: profile, note: note, rsl: LABEL_RSL[profile] }; | |
| } | |
| function renderLabel() { | |
| var host = document.getElementById("labelBox"); | |
| if (!host) return; | |
| var L = aiRightsLabel(); | |
| function toks(a) { return a.length ? "<code>" + a.join("</code> <code>") + "</code>" : "<em>none</em>"; } | |
| host.innerHTML = | |
| '<div class="lbl-badge">' + esc(L.label) + "</div>" + | |
| '<p class="lbl-exp">' + esc(LABEL_PROSE[L.profile]) + "</p>" + | |
| (L.note ? '<p class="lbl-note">' + esc(L.note) + "</p>" : "") + | |
| '<details class="lbl-mach"><summary>Machine-readable form</summary>' + | |
| "<p><strong>RSL 1.0</strong> \u2014 permits " + toks(L.rsl.permits) + | |
| ", prohibits " + toks(L.rsl.prohibits) + ".</p>" + | |
| '<p class="lbl-star">* <code>ai-index</code> stands in for a preparation token. GAIA proposes ' + | |
| "<code>ai-prepare</code>; RSL 1.0 has no equivalent.</p></details>"; | |
| } | |
| function renderChecklist() { | |
| var out = "", total = 0; | |
| SCHEMA.forEach(function (s, i) { | |
| var st = sectionStatus(s); | |
| if (st.optional || st.done === st.req) return; | |
| total += st.missing.length; | |
| out += '<div class="chk-sec"><button class="chk-head" data-sec="' + i + '">' + s.nav + | |
| ' <span class="chk-count">' + st.missing.length + " outstanding</span></button><ul>" + | |
| st.missing.map(function (m) { return "<li>" + esc(m.label) + "</li>"; }).join("") + "</ul></div>"; | |
| }); | |
| document.getElementById("checklist").innerHTML = total === 0 | |
| ? '<div class="chk-ok">Every required answer is filled in. The document below is ready to export.</div>' | |
| : '<div class="chk-warn"><strong>' + total + " item" + (total === 1 ? "" : "s") + " still blank.</strong> " + | |
| "You can export anyway — blanks print as bracketed prompts.</div>" + out; | |
| } | |
| function renderGuidance() { | |
| var g = []; | |
| if (hasOutputRights()) { | |
| g.push(["Output Rights assign the results back to the Licensor", | |
| "Clause 14.2 provides that all Outputs which translate, substantially reproduce, summarise, transform or derive from the Licensed Materials vest in the Licensor as Derived Outputs, and that the Licensee assigns the copyright in them. AI developers frequently do not expect this. Decide whether it is intended."]); | |
| } | |
| if (hasDisplayRights()) { | |
| g.push(["Display Rights trigger attribution", | |
| "Clause 9 requires every Display to name the Work, the Author and the Author’s affiliation, and to identify the Licensor as publisher with a link to the source where feasible. The Licensor must approve the look and placement before any rights are exercised."]); | |
| } | |
| if ((hasOutputRights() || hasDisplayRights()) && purposesUsed().indexOf("c") !== -1) { | |
| g.push(["Public-facing use requires an end user licence agreement", | |
| "Clause 12.1.2 obliges the Licensee to put an EULA in front of end users covering personal-use-only, IP reservation, an ‘as is’ disclaimer and a no-reliance acknowledgement."]); | |
| } | |
| var ps = purposesUsed(); | |
| if (ps.indexOf("nc") !== -1 && ps.indexOf("c") !== -1) { | |
| if (txt("purposeWording") === "original") { | |
| g.push(["Mixed purposes are not supported by the wording you have selected", | |
| "You have chosen CABI’s published definition of Purpose, which reads as a single value governing the whole agreement, but row 5 grants different rights on different commercial terms. Either switch clause 2.1 to the per-right wording in the General Terms section, or align every granted right on one purpose."]); | |
| } else { | |
| g.push(["Mixed purposes are carried by the amended Purpose definition", | |
| "Row 5 grants rights on both Commercial and Non-Commercial terms. The per-right wording of clause 2.1 accommodates that split, and row 11 records the amendment. Flag the departure to your counterparty."]); | |
| } | |
| } | |
| if (anyNonCommercial() && !has("strategiesLink")) { | |
| g.push(["Non-Commercial Use is not yet anchored", | |
| "The definition of Non-Commercial Use turns on alignment with the Licensor’s published mission and strategies. Without the link in the General Terms section, that test has nothing to point at."]); | |
| } | |
| if ((state.rAvOutput || state.rAvDisplay) && txt("annexBMode") === "default") { | |
| g.push(["Audio-visual rights conflict with the Part B carve-out", | |
| "You have granted audio-visual Output or Display Rights, but Part B of Annex 1 currently excludes all graphics, illustrations, images and video from the Licensed Materials. Either name the included items expressly in Part A, or change the Part B option."]); | |
| } | |
| if (hasOutputRights() && !state.rTraining) { | |
| g.push(["Output Rights without Training Rights", | |
| "Output Rights do not include Training Rights (clause 2.1). As drafted the Licensee may generate outputs influenced by the materials but may not train or fine-tune on them — for example a retrieval-augmented set-up rather than a training set. Confirm that is the intent."]); | |
| } | |
| if (txt("deliveryMethod") === "api") { | |
| g.push(["API delivery activates clause 6.1.10", | |
| "Where delivery is by API, the Licensee may not systematically download the materials in a way that allows later offline retrieval, beyond temporary copies."]); | |
| } | |
| if (state.rTraining) { | |
| g.push(["Training survives termination; use does not", | |
| "Clause 23.1.1 does not require the Licensee to delete or withdraw a model already trained on the materials. What ends is further use of the materials and the generation of new Displays."]); | |
| } | |
| if (txt("purposeWording") !== "original") { | |
| g.push(["This document departs from CABI’s published wording in one place", | |
| "The definition of Purpose in clause 2.1 has been amended to allow a separate Purpose for each right granted; every other clause is verbatim. The amendment is disclosed in row 11 of the Special Terms. Revert it in the General Terms section if CABI confirms the single-Purpose reading."]); | |
| } | |
| g.push(["Standing obligations on the Licensee, whatever you grant", | |
| "Monthly analytics reports covering activity, ingestion counts, prompt logs and output volumes (clause 10.2); a 24-hour security breach notification (clause 8.1.2); removal of withdrawn content within 4 days (clause 7.7.1); audit access during the term and for 2 years after (clause 13.2); professional indemnity and cyber insurance (clause 21)."]); | |
| document.getElementById("guidance").innerHTML = g.map(function (x) { | |
| return '<div class="guide"><h4>' + esc(x[0]) + "</h4><p>" + esc(x[1]) + "</p></div>"; | |
| }).join(""); | |
| } | |
| /* ───────────── export ───────────── */ | |
| function doPrint() { | |
| renderMode = "print"; | |
| document.getElementById("printRoot").innerHTML = buildDocument(); | |
| renderMode = "preview"; | |
| document.body.classList.add("printing"); | |
| window.print(); | |
| } | |
| function afterPrint() { document.body.classList.remove("printing"); } | |
| function exportPdf() { | |
| var o = overall(); | |
| var gap = o.req - o.done; | |
| if (gap === 0) { doPrint(); return; } | |
| openModal( | |
| "Export with " + gap + " blank" + (gap === 1 ? "" : "s") + "?", | |
| "<p>" + gap + " required answer" + (gap === 1 ? " is" : "s are") + " still empty. They will print as bracketed prompts such as " + | |
| "<code>[FULL LEGAL NAME OF LICENSOR]</code>, which is usually what you want for a draft circulated for comment — " + | |
| "but not for a document going to signature.</p>", | |
| [{ label: "Go back and complete", cls: "btn-ghost", fn: function () { closeModal(); showTab("build"); } }, | |
| { label: "Export anyway", cls: "btn-primary", fn: function () { closeModal(); doPrint(); } }] | |
| ); | |
| } | |
| /* ───────────── modal ───────────── */ | |
| function openModal(title, body, actions) { | |
| document.getElementById("modalTitle").textContent = title; | |
| document.getElementById("modalBody").innerHTML = body; | |
| var host = document.getElementById("modalActions"); | |
| host.innerHTML = ""; | |
| actions.forEach(function (a) { | |
| var b = document.createElement("button"); | |
| b.className = "btn " + a.cls; | |
| b.textContent = a.label; | |
| b.onclick = a.fn; | |
| host.appendChild(b); | |
| }); | |
| document.getElementById("modal").hidden = false; | |
| } | |
| function closeModal() { document.getElementById("modal").hidden = true; } | |
| /* ───────────── tabs & wiring ───────────── */ | |
| function showTab(name) { | |
| ["about","build","review"].forEach(function (t) { | |
| document.getElementById("tab-" + t).classList.toggle("is-active", t === name); | |
| }); | |
| Array.prototype.forEach.call(document.querySelectorAll(".tab"), function (b) { | |
| b.classList.toggle("is-active", b.dataset.tab === name); | |
| }); | |
| if (name === "review") { renderChecklist(); renderGuidance(); renderLabel(); renderFullDoc(); } | |
| scrollTop(window); | |
| } | |
| /* Scroll helpers: guarded so an environment without smooth-scroll support, or | |
| without these APIs at all, degrades instead of throwing mid-handler. */ | |
| function scrollTop(el) { try { el.scrollTo(0, 0); } catch (e) { if ("scrollTop" in el) el.scrollTop = 0; } } | |
| function scrollIntoView(el, opts) { try { if (el && el.scrollIntoView) el.scrollIntoView(opts); } catch (e) {} } | |
| function refresh() { save(); renderNav(); renderPreviewSoon(); } | |
| function gotoSection(i) { | |
| activeSection = Math.max(0, Math.min(SCHEMA.length - 1, i)); | |
| renderForm(); renderNav(); | |
| document.querySelector(".formpane").scrollTop = 0; | |
| scrollIntoView(document.querySelector('.navbtn[data-sec="' + activeSection + '"]'), { block: "nearest" }); | |
| } | |
| function findSectionOfField(k) { | |
| for (var i = 0; i < SCHEMA.length; i++) { | |
| if (SCHEMA[i].fields.some(function (f) { return f.k === k; })) return i; | |
| if (SCHEMA[i].id === "rights" && PURPOSE_FIELDS.indexOf(k) !== -1) return i; | |
| } | |
| return 0; | |
| } | |
| function focusField(k) { | |
| showTab("build"); | |
| gotoSection(findSectionOfField(k)); | |
| setTimeout(function () { | |
| var el = document.querySelector('[data-k="' + k + '"]'); | |
| var rowEl = document.querySelector('[data-field-row="' + k + '"]') || (el && el.closest(".field")); | |
| if (rowEl) { | |
| rowEl.classList.add("flash"); | |
| setTimeout(function () { rowEl.classList.remove("flash"); }, 1400); | |
| scrollIntoView(rowEl, { block: "center", behavior: "smooth" }); | |
| } | |
| if (el && el.focus) el.focus(); | |
| }, 40); | |
| } | |
| function init() { | |
| load(); | |
| buildNav(); | |
| renderForm(); renderNav(); renderPreview(); | |
| document.addEventListener("pointerdown", function () { pointerDown = true; }); | |
| document.addEventListener("pointerup", function () { | |
| pointerDown = false; | |
| if (previewDirty) renderPreviewSoon(); | |
| }); | |
| document.addEventListener("input", function (e) { | |
| var k = e.target.dataset && e.target.dataset.k; | |
| if (!k) return; | |
| state[k] = e.target.type === "checkbox" ? e.target.checked : e.target.value; | |
| refresh(); | |
| }); | |
| document.addEventListener("change", function (e) { | |
| var k = e.target.dataset && e.target.dataset.k; | |
| if (!k) return; | |
| state[k] = e.target.type === "checkbox" ? e.target.checked : e.target.value; | |
| if (e.target.type === "checkbox" || e.target.type === "radio") renderForm(); | |
| refresh(); | |
| }); | |
| document.addEventListener("click", function (e) { | |
| var t = e.target.closest("[data-tab],[data-sec],[data-goto],[data-field]"); | |
| if (!t) return; | |
| if (t.dataset.tab) { showTab(t.dataset.tab); return; } | |
| if (t.dataset.goto) { showTab(t.dataset.goto); return; } | |
| if (t.dataset.sec !== undefined) { showTab("build"); gotoSection(+t.dataset.sec); return; } | |
| if (t.dataset.field) { focusField(t.dataset.field); } | |
| }); | |
| document.addEventListener("keydown", function (e) { | |
| if (e.key === "Escape") { closeModal(); return; } | |
| if (e.key !== "Enter" && e.key !== " ") return; | |
| var t = e.target.closest && e.target.closest("[data-field]"); | |
| if (t) { e.preventDefault(); focusField(t.dataset.field); } | |
| }); | |
| document.getElementById("btnPrev").onclick = function () { gotoSection(activeSection - 1); }; | |
| document.getElementById("btnNext").onclick = function () { | |
| if (activeSection === SCHEMA.length - 1) showTab("review"); | |
| else gotoSection(activeSection + 1); | |
| }; | |
| document.getElementById("btnExport").onclick = exportPdf; | |
| document.getElementById("chkHighlight").onchange = function (e) { highlight = e.target.checked; renderPreview(); }; | |
| document.getElementById("btnReset").onclick = function () { | |
| openModal("Clear all answers?", "<p>This removes everything you have entered from this browser. It cannot be undone.</p>", [ | |
| { label: "Keep my answers", cls: "btn-ghost", fn: closeModal }, | |
| { label: "Clear everything", cls: "btn-danger", fn: function () { | |
| try { localStorage.removeItem(STORE); } catch (err) {} | |
| state = defaults(); activeSection = 0; closeModal(); renderForm(); save(); renderNav(); renderPreview(); | |
| } } | |
| ]); | |
| }; | |
| document.getElementById("modal").addEventListener("click", function (e) { | |
| if (e.target.id === "modal") closeModal(); | |
| }); | |
| window.addEventListener("afterprint", afterPrint); | |
| } | |
| document.addEventListener("DOMContentLoaded", init); | |
| })(); | |