MediaWiki:Gadget-DiagnosticTreeScoring.js: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
No edit summary |
||
| Line 14: | Line 14: | ||
'use strict'; | 'use strict'; | ||
/* | /* ══════════════════════════════════════════════════════ | ||
ALL SCORING ENGINE FUNCTIONS | |||
T, MUSCLE_IDS, DAG are module-level vars set from schema | |||
══════════════════════════════════════════════════════ */ | |||
// T, MUSCLE_IDS, DAG are set in bootScoringInterface after SCHEMA loads | |||
var T, MUSCLE_IDS, DAG; | |||
const LS_KEY='painwiki_upper_thoracic_counts_v1'; | const LS_KEY='painwiki_upper_thoracic_counts_v1'; | ||
const RF_EMERGENCY=[{"id": "rf-e1", "label": "Aortic dissection", "question": "Sudden tearing or ripping interscapular pain; hypertension or Marfan features; pulse or BP difference between arms?"}, {"id": "rf-e2", "label": "Pulmonary embolism", "question": "Sudden-onset pleuritic chest or back pain (sharp, worse on inhalation); unexplained breathlessness, tachycardia, or hypoxia; recent immobility, surgery, or long-haul travel?"}, {"id": "rf-e3", "label": "Spinal cord compression / myelopathy", "question": "Bilateral arm or leg weakness, gait disturbance, loss of hand dexterity, or bowel/bladder dysfunction alongside neck or upper back pain?"}, {"id": "rf-e4", "label": "Meningism", "question": "Neck and upper back pain with fever, photophobia, or cerebellar signs (ataxia, dysarthria, nystagmus)?"}]; | const RF_EMERGENCY=[{"id": "rf-e1", "label": "Aortic dissection", "question": "Sudden tearing or ripping interscapular pain; hypertension or Marfan features; pulse or BP difference between arms?"}, {"id": "rf-e2", "label": "Pulmonary embolism", "question": "Sudden-onset pleuritic chest or back pain (sharp, worse on inhalation); unexplained breathlessness, tachycardia, or hypoxia; recent immobility, surgery, or long-haul travel?"}, {"id": "rf-e3", "label": "Spinal cord compression / myelopathy", "question": "Bilateral arm or leg weakness, gait disturbance, loss of hand dexterity, or bowel/bladder dysfunction alongside neck or upper back pain?"}, {"id": "rf-e4", "label": "Meningism", "question": "Neck and upper back pain with fever, photophobia, or cerebellar signs (ataxia, dysarthria, nystagmus)?"}]; | ||
| Line 90: | Line 92: | ||
function condMet(q){if(!q.condition)return true;return Object.entries(q.condition).every(([k,v])=>answers[k]===v);} | function condMet(q){if(!q.condition)return true;return Object.entries(q.condition).every(([k,v])=>answers[k]===v);} | ||
function nextQ(){for(let i=queueIdx;i<SCHEMA.questions.length;i++){const q=SCHEMA.questions[i];if(answers[q.id]!==undefined)continue;if(!condMet(q))continue;queueIdx=i;return q;}return null;} | function nextQ(){for(let i=queueIdx;i<SCHEMA.questions.length;i++){const q=SCHEMA.questions[i];if(answers[q.id]!==undefined)continue;if(!condMet(q))continue;queueIdx=i;return q;}return null;} | ||
function buildTreatmentOrder(r){const active=r.filter(m=>m.p>=0.05).map(m=>m.id);if(!active.length)return[];const steps=[],placed=new Set(),eL=DAG.edge_type_labels;active.forEach(mid=>{const out=DAG.edges.filter(e=>e.from===mid&&e.type==='key_satellite'&&active.includes(e.to));if(out.length&&!placed.has(mid)){const m=r.find(x=>x.id===mid);steps.push({label:m.label,page:m.page,edgeType:'key_satellite',edgeLabel:eL | function buildTreatmentOrder(r){const active=r.filter(m=>m.p>=0.05).map(m=>m.id);if(!active.length)return[];const steps=[],placed=new Set(),eL=DAG.edge_type_labels;active.forEach(mid=>{const out=DAG.edges.filter(e=>e.from===mid&&e.type==='key_satellite'&&active.includes(e.to));if(out.length&&!placed.has(mid)){const m=r.find(x=>x.id===mid);steps.push({label:m.label,page:m.page,edgeType:'key_satellite',edgeLabel:eL['key_satellite'],note:out.map(e=>e.label).join('; ')});placed.add(mid);}});active.filter(m=>!placed.has(m)).forEach(mid=>{const m=r.find(x=>x.id===mid);const inE=DAG.edges.find(e=>e.to===mid&&placed.has(e.from));const wE=DAG.edges.find(e=>e.type==='antagonist_risk'&&((e.from===mid&&active.includes(e.to))||(e.to===mid&&active.includes(e.from))));const et=wE?'antagonist_risk':(inE?inE.type:'functional_unit');steps.push({label:m.label,page:m.page,edgeType:et,edgeLabel:eL[et]||'Treat in session',note:wE?wE.label:(inE?inE.label:'')});placed.add(mid);});return steps;} | ||
// ── RENDER ───────────────────────────────────────────────────────── | // ── RENDER ───────────────────────────────────────────────────────── | ||
| Line 168: | Line 170: | ||
}); | }); | ||
document.getElementById('reset-btn').addEventListener('click',()=>{answers={};trail=[];pairwiseDone=new Set();queueIdx=0;earlyDone=false;posteriors=getUpdatedPriors();document.getElementById('trail-panel').classList.remove('visible');document.getElementById('treat-panel').classList.remove('visible');advance();}); | |||
document.getElementById('learning-toggle-btn').addEventListener('click',()=>{const h=document.body.classList.toggle('hide-learning');document.getElementById('learning-toggle-btn').textContent='Learning: '+(h?'OFF':'ON');}); | |||
buildRedFlags(); | buildRedFlags(); | ||
buildBroadDiff(); | buildBroadDiff(); | ||
/* | /* ══════════════════════════════════════════════════════ | ||
BOOT — called after schema is loaded from wiki data page | |||
Injects interface HTML then wires all event listeners | |||
══════════════════════════════════════════════════════ */ | |||
function bootScoringInterface( hostEl, SCHEMA ) { | function bootScoringInterface( hostEl, SCHEMA ) { | ||
// | // 1. Set schema-derived globals FIRST — functions depend on these | ||
T = SCHEMA.thresholds; | |||
MUSCLE_IDS = Object.keys( SCHEMA.muscles ); | |||
DAG = SCHEMA.treatment_dag; | |||
// | // 2. Rebuild posteriors from schema priors + any stored confirmations | ||
posteriors = getUpdatedPriors(); | |||
posteriors = | |||
// 3. Run the interface build functions — DOM elements now exist | |||
buildRedFlags(); | buildRedFlags(); | ||
buildBroadDiff(); | buildBroadDiff(); | ||
advance(); | advance(); | ||
renderLearnPanel(); | renderLearnPanel(); | ||
} | |||
/* ══════════════════════════════════════════════════════ | |||
SCHEMA LOADER — fetches JSON from wiki data page | |||
══════════════════════════════════════════════════════ */ | |||
function bootHost( hostEl ) { | function bootHost( hostEl ) { | ||
var treePage = hostEl.getAttribute( 'data-tree-page' ); | var treePage = hostEl.getAttribute( 'data-tree-page' ); | ||
if ( !treePage ) return; | if ( !treePage ) return; | ||
hostEl.innerHTML = '<div style="padding:1.5em;font-family:DM Mono,monospace;' + | hostEl.innerHTML = | ||
'font-size:0.8em;color:#a8a29e;text-align:center">Loading scoring model…</div>'; | '<div style="padding:1.5em;font-family:DM Mono,monospace;' + | ||
'font-size:0.8em;color:#a8a29e;text-align:center">' + | |||
'Loading scoring model…</div>'; | |||
var api = new mw.Api(); | var api = new mw.Api(); | ||
api.get({ | api.get( { | ||
action: 'query', | action: 'query', | ||
titles: treePage, | titles: treePage, | ||
| Line 240: | Line 218: | ||
rvslots: 'main', | rvslots: 'main', | ||
format: 'json' | format: 'json' | ||
}).done( function( data ) { | } ).done( function ( data ) { | ||
var pages = data.query.pages; | var pages = data.query.pages; | ||
var pageId = Object.keys( pages )[0]; | var pageId = Object.keys( pages )[0]; | ||
if ( pageId === '-1' ) { | if ( pageId === '-1' ) { | ||
hostEl.innerHTML = '<div style="color:#b91c1c;padding:1em;font-family:monospace">' + | hostEl.innerHTML = | ||
'<div style="color:#b91c1c;padding:1em;font-family:monospace">' + | |||
'Scoring model not found: ' + treePage + '</div>'; | 'Scoring model not found: ' + treePage + '</div>'; | ||
return; | return; | ||
| Line 250: | Line 229: | ||
var raw = pages[ pageId ].revisions[0].slots.main['*']; | var raw = pages[ pageId ].revisions[0].slots.main['*']; | ||
var schema; | var schema; | ||
try { schema = JSON.parse( raw ); | try { | ||
catch ( e ) { | schema = JSON.parse( raw ); | ||
hostEl.innerHTML = '<div style="color:#b91c1c;padding:1em;font-family:monospace">' + | } catch ( e ) { | ||
hostEl.innerHTML = | |||
'<div style="color:#b91c1c;padding:1em;font-family:monospace">' + | |||
'Invalid JSON in ' + treePage + ': ' + e.message + '</div>'; | 'Invalid JSON in ' + treePage + ': ' + e.message + '</div>'; | ||
return; | return; | ||
} | } | ||
// Render the interface HTML into the host element | |||
// then boot the scoring engine | |||
renderInterfaceHTML( hostEl ); | |||
bootScoringInterface( hostEl, schema ); | bootScoringInterface( hostEl, schema ); | ||
}).fail( function() { | |||
hostEl.innerHTML = '<div style="color:#b91c1c;padding:1em;font-family:monospace">' + | } ).fail( function () { | ||
'Failed to load | hostEl.innerHTML = | ||
}); | '<div style="color:#b91c1c;padding:1em;font-family:monospace">' + | ||
'Failed to load: ' + treePage + '</div>'; | |||
} ); | |||
} | |||
/* ══════════════════════════════════════════════════════ | |||
INTERFACE HTML RENDERER | |||
Builds the shell, red flag panels, grid, and broad diff | |||
into the host element — without any JS execution | |||
══════════════════════════════════════════════════════ */ | |||
function renderInterfaceHTML( hostEl ) { | |||
hostEl.innerHTML = [ | |||
'<div class="proto-shell">', | |||
// Masthead | |||
'<header class="proto-masthead">', | |||
' <div class="proto-logo">Pain<span>Wiki</span></div>', | |||
' <div class="proto-right">', | |||
' <span class="proto-region">Diagnostic Algorithm · Upper Thoracic Back Pain</span>', | |||
' <button class="learning-toggle" id="learning-toggle-btn">Learning: ON</button>', | |||
' </div>', | |||
'</header>', | |||
// Red flags | |||
'<div class="rf-outer">', | |||
' <div class="rf-panel-wrap" id="rf-emergency-panel">', | |||
' <div class="rf-col-header">⚠ Emergency — stop and act if any are present</div>', | |||
' <div class="rf-items" id="rf-emergency-items"></div>', | |||
' </div>', | |||
' <div class="rf-panel-wrap urgent" id="rf-urgent-panel">', | |||
' <div class="rf-col-header">⚠ Urgent — refer before myofascial assessment</div>', | |||
' <div class="rf-items" id="rf-urgent-items"></div>', | |||
' <div class="rf-affirm">', | |||
' <div class="rf-affirm-note">All emergency and urgent flags screened and negative — or appropriate action taken.</div>', | |||
' <button class="btn-affirm" id="btn-affirm">Proceed to clinical interview →</button>', | |||
' <div class="affirmed-badge" id="affirmed-badge">Red flags cleared</div>', | |||
' </div>', | |||
' </div>', | |||
'</div>', | |||
// Main grid — hidden until red flags affirmed | |||
'<div id="main-grid">', | |||
' <div class="col-left">', | |||
' <div id="question-area"></div>', | |||
' <div class="dt-trail-panel" id="trail-panel">', | |||
' <div class="dt-trail-head">', | |||
' <span class="dt-trail-title">Clinical Interview Record</span>', | |||
' <span class="dt-trail-meta" id="trail-counter"></span>', | |||
' </div>', | |||
' <div id="trail-items"></div>', | |||
' </div>', | |||
' </div>', | |||
' <div class="col-right">', | |||
' <div class="dt-running-panel">', | |||
' <div class="dt-running-head">', | |||
' <span class="dt-running-title">Running Differential</span>', | |||
' <span class="dt-running-meta" id="q-counter">Prior weights</span>', | |||
' </div>', | |||
' <div id="diff-rows"></div>', | |||
' <div class="dt-diff-caption">Diagnostic weights are relative, not absolute probabilities.</div>', | |||
' </div>', | |||
' <div class="dt-treat-panel" id="treat-panel">', | |||
' <div class="dt-treat-head"><span class="dt-treat-title">Suggested Treatment Order</span></div>', | |||
' <div id="treat-items"></div>', | |||
' </div>', | |||
' <div class="learn-panel" id="learn-panel">', | |||
' <div class="learn-head">', | |||
' <span class="learn-title">Confirmed Cases — This Device</span>', | |||
' <span class="learn-meta" id="learn-total"></span>', | |||
' </div>', | |||
' <div class="learn-intro"><strong>Multiple muscles can be confirmed per case.</strong></div>', | |||
' <div id="learn-rows"></div>', | |||
' </div>', | |||
' </div>', | |||
// Broad differential — full width | |||
' <div style="grid-column:1/-1">', | |||
' <div class="dt-broad-panel" id="broad-panel">', | |||
' <div class="dt-broad-header">', | |||
' <div class="dt-broad-header-top">', | |||
' <div class="dt-broad-title">■ Broad Differential Diagnosis</div>', | |||
' <button class="dt-broad-toggle" id="broad-toggle-btn">Hide</button>', | |||
' </div>', | |||
' <div class="dt-epigraph">', | |||
' “If he does not expect the unexpected, he will not discover it —', | |||
' for it is difficult to discover and intractable.”', | |||
' <cite>— Heraclitus, Fr. 18</cite>', | |||
' </div>', | |||
' </div>', | |||
' <div class="dt-broad-grid" id="broad-grid"></div>', | |||
' </div>', | |||
' </div>', | |||
'</div>', // end main-grid | |||
// Footer | |||
'<div class="proto-foot">', | |||
' <div class="proto-disclaimer"><strong>Not validated for clinical use.</strong> Research prototype.</div>', | |||
' <button class="btn-reset" id="reset-btn">↻ Start over</button>', | |||
'</div>', | |||
'</div>' // end proto-shell | |||
].join( ' | |||
' ); | |||
// Wire reset and toggle — these elements now exist | |||
var resetBtn = document.getElementById( 'reset-btn' ); | |||
var learnBtn = document.getElementById( 'learning-toggle-btn' ); | |||
if ( resetBtn ) { | |||
resetBtn.addEventListener( 'click', function () { | |||
answers = {}; trail = []; | |||
pairwiseDone = new Set(); | |||
queueIdx = 0; earlyDone = false; | |||
posteriors = getUpdatedPriors(); | |||
var tp = document.getElementById( 'trail-panel' ); | |||
var tp2 = document.getElementById( 'treat-panel' ); | |||
if ( tp ) tp.classList.remove( 'visible' ); | |||
if ( tp2 ) tp2.classList.remove( 'visible' ); | |||
advance(); | |||
} ); | |||
} | |||
if ( learnBtn ) { | |||
learnBtn.addEventListener( 'click', function () { | |||
var h = document.body.classList.toggle( 'hide-learning' ); | |||
learnBtn.textContent = 'Learning: ' + ( h ? 'OFF' : 'ON' ); | |||
} ); | |||
} | |||
} | } | ||
/* | /* ══════════════════════════════════════════════════════ | ||
MEDIAWIKI ENTRY POINT | |||
══════════════════════════════════════════════════════ */ | |||
function init() { | function init() { | ||
document.querySelectorAll( '.scoring-tree-host' ).forEach( function( el ) { | document.querySelectorAll( '.scoring-tree-host' ).forEach( function ( el ) { | ||
bootHost( el ); | bootHost( el ); | ||
}); | } ); | ||
} | } | ||
Revision as of 17:36, 22 May 2026
/**
* DiagnosticTree-Scoring.js — Upper Thoracic Probabilistic Scoring Model
* Copy entire contents to: MediaWiki:Gadget-DiagnosticTreeScoring.js
*
* Embed on a wiki page with:
* <div class="scoring-tree-host"
* data-tree-page="DiagnosticTree/UpperThoracicBackPain">
* </div>
*
* Requires: mediawiki.api
*/
( function () {
'use strict';
/* ══════════════════════════════════════════════════════
ALL SCORING ENGINE FUNCTIONS
T, MUSCLE_IDS, DAG are module-level vars set from schema
══════════════════════════════════════════════════════ */
// T, MUSCLE_IDS, DAG are set in bootScoringInterface after SCHEMA loads
var T, MUSCLE_IDS, DAG;
const LS_KEY='painwiki_upper_thoracic_counts_v1';
const RF_EMERGENCY=[{"id": "rf-e1", "label": "Aortic dissection", "question": "Sudden tearing or ripping interscapular pain; hypertension or Marfan features; pulse or BP difference between arms?"}, {"id": "rf-e2", "label": "Pulmonary embolism", "question": "Sudden-onset pleuritic chest or back pain (sharp, worse on inhalation); unexplained breathlessness, tachycardia, or hypoxia; recent immobility, surgery, or long-haul travel?"}, {"id": "rf-e3", "label": "Spinal cord compression / myelopathy", "question": "Bilateral arm or leg weakness, gait disturbance, loss of hand dexterity, or bowel/bladder dysfunction alongside neck or upper back pain?"}, {"id": "rf-e4", "label": "Meningism", "question": "Neck and upper back pain with fever, photophobia, or cerebellar signs (ataxia, dysarthria, nystagmus)?"}];
const RF_URGENT=[{"id": "rf-u1", "label": "Vertebral fracture", "question": "History of significant trauma, or patient is osteoporotic (post-menopausal, long-term corticosteroids, age > 70) with sudden-onset upper thoracic pain?"}, {"id": "rf-u2", "label": "Serious spinal pathology (tumour / infection)", "question": "Constant, progressive upper thoracic pain unrelated to posture or movement, worse at night lying down? Unexplained weight loss, fever, or history of cancer?"}, {"id": "rf-u3", "label": "Cervical instability", "question": "History of head or neck trauma combined with bilateral upper limb symptoms, gait disturbance, or upper cervical pain?"}, {"id": "rf-u4", "label": "Cardiac angina (exertional component)", "question": "Upper thoracic or interscapular pain with a clear exertional component, relieved by rest or GTN? Left-sided with arm radiation?"}, {"id": "rf-u5", "label": "Inflammatory arthropathy", "question": "Bilateral posterior neck or thoracic stiffness WORSE in the morning and improving with movement, with peripheral joint swelling or systemic symptoms?"}];
const BROAD_DIFF=[{"condition": "Cervical disc herniation (C5\u2013C7)", "confidence": "uncommon", "mimics": "Upper thoracic and interscapular referred pain via dorsal rami; arm symptoms overlapping with scaleni and trapezius TrP patterns", "distinguishing_feature": "Dermatomal arm pain; reflex change (biceps C5\u2013C6, triceps C7); true myotomal weakness; Spurling's test positive. TrP pain does not produce reflex changes or dermatomal sensory deficit. TrPs commonly develop secondarily to radiculopathy.", "action": "MRI cervical spine if neurological signs present. Treat TrPs concurrently \u2014 they frequently coexist with disc pathology and may be the dominant pain source."}, {"condition": "Thoracic outlet syndrome \u2014 neurological", "confidence": "rare", "mimics": "Arm and hand symptoms with upper thoracic pain; ulnar symptoms overlap with scaleni and pectoralis minor patterns", "distinguishing_feature": "Roos test (EAST test) positive at 3 minutes; nerve conduction studies confirm. Scaleni TrPs frequently coexist with and drive TOS symptoms \u2014 TrP inactivation often resolves or substantially reduces TOS.", "action": "Inactivate scaleni TrPs first before TOS workup. Refer for nerve conduction studies if symptoms persist after TrP treatment."}, {"condition": "Thoracic outlet syndrome \u2014 vascular", "confidence": "rare", "mimics": "Anterior shoulder and arm pain with upper thoracic aching; overlaps with subclavius and scaleni TrP patterns", "distinguishing_feature": "Radial pulse reduction or loss with arm abduction; hand oedema and finger stiffness. Wright manoeuvre positive. Scaleni and subclavius TrPs contribute to vascular compression via taut band tension.", "action": "Check radial pulse in standard and abducted positions. Scaleni and subclavius TrP inactivation is first-line. Refer for vascular assessment if pulse loss persists after TrP treatment."}, {"condition": "Rotator cuff tendinopathy / subacromial impingement", "confidence": "uncommon", "mimics": "Painful arc on shoulder abduction and lateral arm aching overlapping with supraspinatus TrP referral to the upper thoracic region", "distinguishing_feature": "Tenderness at greater tuberosity insertion; positive Neer's or Hawkins-Kennedy sign; imaging confirms tendon changes. TrP taut bands impose sustained enthesopathic tension \u2014 coexistence is common and causally related.", "action": "Treat supraspinatus TrPs first and reassess tendon findings after inactivation. Both conditions require treatment when identified together."}, {"condition": "Thoracic zygapophyseal (facet) joint pain", "confidence": "uncommon", "mimics": "Deep upper thoracic paraspinal pain in the same zone as multifidi TrP referral", "distinguishing_feature": "Pain reproduced by passive PA intersegmental pressures over the facet joints; hard end-feel on accessory movement testing. Multifidi TrPs and facet dysfunction coexist at the same segment and perpetuate each other \u2014 soft end-feel suggests TrP predominance.", "action": "Segmental accessory movement testing. Treat multifidi TrPs first \u2014 articular dysfunction often resolves with TrP inactivation. Mobilise the segment if hard end-feel persists."}, {"condition": "Herpes zoster \u2014 pre-eruptive and post-herpetic", "confidence": "uncommon", "mimics": "Unilateral burning upper thoracic pain before the rash appears \u2014 indistinguishable from rhomboid, multifidi, or intercostal TrP patterns at onset", "distinguishing_feature": "Dermatomal distribution; allodynia (light touch painful in a band); vesicles appear 1\u20134 days after pain onset. Age > 50 or immunocompromised increases suspicion. Post-herpetic: TrP pain coexists with neurogenic shooting pain.", "action": "Examine the skin carefully at every visit for unilateral burning thoracic pain. If vesicles present, refer urgently \u2014 antiviral window is 72 hours from rash onset."}, {"condition": "First rib dysfunction", "confidence": "rare", "mimics": "Upper thoracic and arm symptoms closely associated with scaleni TrP presentation \u2014 scaleni attach to the first rib and their taut bands directly elevate it", "distinguishing_feature": "First rib elevated and tender on posterior superior palpation; restricted first rib caudal glide on accessory movement testing. Scaleni TrPs are almost universally present concurrently.", "action": "First rib mobilisation in conjunction with scaleni TrP inactivation \u2014 the two conditions are mechanically linked and must be treated together."}, {"condition": "Winged scapula / serratus anterior weakness", "confidence": "rare", "mimics": "Medial scapular border pain and upper thoracic aching overlapping with rhomboid TrP presentation", "distinguishing_feature": "Scapular winging visible on push-up against wall or forward arm elevation; serratus anterior weakness on manual muscle testing. Rhomboid TrPs develop secondarily to serratus anterior inhibition \u2014 the rhomboids are overloaded trying to retract a scapula that serratus cannot stabilise.", "action": "Serratus anterior rehabilitation is the primary treatment. Treat rhomboid TrPs concurrently but address serratus anterior as the underlying driver \u2014 rhomboid TrPs will recur without it."}, {"condition": "Glenohumeral osteoarthritis", "confidence": "uncommon", "mimics": "Diffuse shoulder girdle and upper thoracic pain, particularly in older patients with supraspinatus and biceps TrP patterns", "distinguishing_feature": "Capsular pattern restriction (external rotation > abduction > internal rotation); crepitus on movement; radiological changes. Arthritis does not produce spot-tender taut bands. TrPs coexist and are independently treatable.", "action": "Shoulder radiograph. Inactivate TrPs concurrently with joint management \u2014 TrP treatment can substantially reduce pain even in the presence of established arthritis."}, {"condition": "Fibromyalgia", "confidence": "uncommon", "mimics": "Widespread upper back and shoulder girdle tenderness overlapping with all muscles in this algorithm simultaneously", "distinguishing_feature": "Widespread pain \u2265 3 months across multiple body regions; diffuse tenderness without specific TrP referral patterns; fatigue and non-restorative sleep. TrPs produce specific referred pain patterns and are a treatable component of fibromyalgia.", "action": "Systematic TrP examination alongside fibromyalgia management. Treating active TrPs reduces the overall pain burden independently \u2014 do not withhold TrP treatment because fibromyalgia is also present."}, {"condition": "Residual pain after spinal manipulation or injection", "confidence": "atypical", "mimics": "Persistent upper thoracic pain after a spinal procedure \u2014 may be re-attributed to the joint when myofascial TrPs are the active pain source", "distinguishing_feature": "After a spinal intervention, pain persisting at a lower level or shifting in character indicates TrPs masked by dominant segmental pain are now the primary source. The TrPs were pre-existing \u2014 not created by the procedure.", "action": "Re-examine muscles systematically after any spinal intervention. Most commonly harbouring residual TrPs: trapezius (upper and mid), levator scapulae, rhomboids, multifidi."}];
// ── RED FLAGS ──────────────────────────────────────────────────────
function buildRedFlags(){
function makeFlags(arr,id,isUrgent){
document.getElementById(id).innerHTML=arr.map(rf=>
'<div class="rf-item">'+
'<label class="rf-check-wrap" for="'+rf.id+'">'+
'<input type="checkbox" class="rf-cb" id="'+rf.id+'">'+
'</label>'+
'<div class="rf-body">'+
'<div class="rf-label">'+rf.label+'</div>'+
'<div class="rf-question">'+rf.question+'</div>'+
'</div></div>'
).join('');
}
makeFlags(RF_EMERGENCY,'rf-emergency-items',false);
makeFlags(RF_URGENT,'rf-urgent-items',true);
document.getElementById('btn-affirm').addEventListener('click',()=>{
document.getElementById('btn-affirm').style.display='none';
document.getElementById('affirmed-badge').classList.add('show');
const mg=document.getElementById('main-grid');
mg.classList.add('visible');
posteriors=getUpdatedPriors();advance();renderLearnPanel();
setTimeout(()=>mg.scrollIntoView({behavior:'smooth',block:'start'}),80);
});
}
// ── BROAD DIFFERENTIAL ─────────────────────────────────────────────
function buildBroadDiff(){
document.getElementById('broad-grid').innerHTML=BROAD_DIFF.map(d=>
'<div class="dt-diff-item">'+
'<div class="dt-diff-confidence '+d.confidence+'">'+d.confidence+'</div>'+
'<div class="dt-diff-name">'+d.condition+'</div>'+
'<div class="dt-diff-mimics">'+d.mimics+'</div>'+
'<div class="dt-diff-detail">'+
'<div class="dt-diff-distinguisher"><strong>Distinguishing features</strong>'+d.distinguishing_feature+'</div>'+
'<div class="dt-diff-action"><strong>Action</strong>'+d.action+'</div>'+
'</div></div>'
).join('');
// click to expand
document.getElementById('broad-grid').addEventListener('click',e=>{
const item=e.target.closest('.dt-diff-item');
if(item) item.classList.toggle('open');
});
// toggle visibility
document.getElementById('broad-toggle-btn').addEventListener('click',()=>{
const grid=document.getElementById('broad-grid');
const hidden=grid.classList.toggle('hidden');
document.getElementById('broad-toggle-btn').textContent=hidden?'Show':'Hide';
});
}
// ── LEARNING ───────────────────────────────────────────────────────
function loadCounts(){try{const r=localStorage.getItem(LS_KEY);if(r)return JSON.parse(r);}catch(e){}const c={};MUSCLE_IDS.forEach(m=>c[m]=0);return c;}
function saveCounts(c){try{localStorage.setItem(LS_KEY,JSON.stringify(c));}catch(e){}}
function incrementCount(mid){const c=loadCounts();c[mid]=(c[mid]||0)+1;saveCounts(c);return c;}
function getUpdatedPriors(){const counts=loadCounts(),p={};MUSCLE_IDS.forEach(m=>p[m]=SCHEMA.muscles[m].prior+(counts[m]||0)*0.02);return normalise(p);}
// ── STATE & MATH ───────────────────────────────────────────────────
let answers={},posteriors={},trail=[],pairwiseDone=new Set(),queueIdx=0,earlyDone=false;
function normalise(p){let t=0;MUSCLE_IDS.forEach(m=>t+=p[m]);const o={};MUSCLE_IDS.forEach(m=>o[m]=p[m]/t);return o;}
function applyLR(p,lr){const o={};MUSCLE_IDS.forEach(m=>o[m]=p[m]*(lr[m]||1.0));return normalise(o);}
function ranked(){return MUSCLE_IDS.map(m=>({id:m,p:posteriors[m],label:SCHEMA.muscles[m].label,subtitle:SCHEMA.muscles[m].subtitle||'',page:SCHEMA.muscles[m].page,note:SCHEMA.muscles[m].key_trp_note||null})).sort((a,b)=>b.p-a.p);}
function shouldEarlyExit(r){return r[0].p>=T.early_exit_posterior&&(r[0].p-r[1].p)>=T.early_exit_gap;}
function getPairwise(r){if((r[0].p-r[1].p)>=T.pairwise_trigger)return null;const s=new Set([r[0].id,r[1].id]);for(const pw of SCHEMA.pairwise)if(!pairwiseDone.has(pw.id)&&pw.pair.every(x=>s.has(x)))return pw;return null;}
function condMet(q){if(!q.condition)return true;return Object.entries(q.condition).every(([k,v])=>answers[k]===v);}
function nextQ(){for(let i=queueIdx;i<SCHEMA.questions.length;i++){const q=SCHEMA.questions[i];if(answers[q.id]!==undefined)continue;if(!condMet(q))continue;queueIdx=i;return q;}return null;}
function buildTreatmentOrder(r){const active=r.filter(m=>m.p>=0.05).map(m=>m.id);if(!active.length)return[];const steps=[],placed=new Set(),eL=DAG.edge_type_labels;active.forEach(mid=>{const out=DAG.edges.filter(e=>e.from===mid&&e.type==='key_satellite'&&active.includes(e.to));if(out.length&&!placed.has(mid)){const m=r.find(x=>x.id===mid);steps.push({label:m.label,page:m.page,edgeType:'key_satellite',edgeLabel:eL['key_satellite'],note:out.map(e=>e.label).join('; ')});placed.add(mid);}});active.filter(m=>!placed.has(m)).forEach(mid=>{const m=r.find(x=>x.id===mid);const inE=DAG.edges.find(e=>e.to===mid&&placed.has(e.from));const wE=DAG.edges.find(e=>e.type==='antagonist_risk'&&((e.from===mid&&active.includes(e.to))||(e.to===mid&&active.includes(e.from))));const et=wE?'antagonist_risk':(inE?inE.type:'functional_unit');steps.push({label:m.label,page:m.page,edgeType:et,edgeLabel:eL[et]||'Treat in session',note:wE?wE.label:(inE?inE.label:'')});placed.add(mid);});return steps;}
// ── RENDER ─────────────────────────────────────────────────────────
function renderDiff(){const r=ranked();document.getElementById('diff-rows').innerHTML=r.map((m,i)=>{const rank=i+1,w=(m.p*100).toFixed(1),bw=Math.max(0.5,m.p*100).toFixed(1),dim=rank>6?' dimmed':'',rc=rank<=3?' r'+rank:'';return'<div class="dt-diff-bar-row'+rc+dim+'"><div class="dt-diff-bar-rank">'+rank+'</div><div class="dt-diff-bar-name">'+m.label+'</div><div class="dt-diff-bar-track"><div class="dt-diff-bar-fill" style="width:'+bw+'%"></div></div><div class="dt-diff-bar-weight">'+w+'</div></div>';}).join('');}
function renderTrail(){const el=document.getElementById('trail-panel');if(!trail.length){el.classList.remove('visible');return;}el.classList.add('visible');document.getElementById('trail-counter').textContent=trail.length+' feature'+(trail.length>1?'s':'')+' recorded';document.getElementById('trail-items').innerHTML=trail.map(t=>'<div class="dt-trail-item"><div class="dt-trail-q">'+t.q+'</div><div class="dt-trail-a">↳ '+t.a+'</div></div>').join('');}
function renderCounter(){const tot=SCHEMA.questions.length,ans=Object.keys(answers).length;document.getElementById('q-counter').textContent=ans===0?'Prior weights':ans+'\u202f/\u202f'+tot+' features';}
function renderLearnPanel(){const counts=loadCounts(),total=Object.values(counts).reduce((a,b)=>a+b,0);const el=document.getElementById('learn-panel');if(!total){el.classList.remove('visible');return;}el.classList.add('visible');document.getElementById('learn-total').textContent=total+' confirmation'+(total>1?'s':'');document.getElementById('learn-rows').innerHTML=MUSCLE_IDS.filter(m=>counts[m]>0).sort((a,b)=>counts[b]-counts[a]).map(m=>'<div class="learn-row"><div class="learn-muscle">'+SCHEMA.muscles[m].label+'</div><div class="learn-count">'+counts[m]+'×</div></div>').join('');}
function renderQuestion(q,isPW){const qText=q.text||q.question;const label=isPW?'<div class="dt-card-label pairwise">⚡ Tiebreaker</div>':'<div class="dt-card-label">Clinical Feature '+(trail.length+1)+'</div>';const sub=q.sublabel?'<div class="dt-rationale">'+q.sublabel+'</div>':'';const btns='<div class="dt-answers"><button class="dt-answer dt-answer-yes" data-qid="'+(isPW?'__pw__'+q.id:q.id)+'" data-aid="yes" data-qlabel="'+encodeURIComponent(qText)+'" data-alabel="'+encodeURIComponent('Yes')+'">Yes</button><button class="dt-answer dt-answer-no" data-qid="'+(isPW?'__pw__'+q.id:q.id)+'" data-aid="no" data-qlabel="'+encodeURIComponent(qText)+'" data-alabel="'+encodeURIComponent('No')+'">No</button></div>';document.getElementById('question-area').innerHTML='<div class="dt-card"><div class="dt-card-head">'+label+'</div><div class="dt-question">'+qText+'</div>'+sub+btns+'</div>';}
// Fix: answer buttons need correct aid from schema
function renderQuestionFull(q,isPW){
const qText=q.text||q.question;
const label=isPW?'<div class="dt-card-label pairwise">⚡ Tiebreaker</div>':'<div class="dt-card-label">Clinical Feature '+(trail.length+1)+'</div>';
const sub=q.sublabel?'<div class="dt-rationale" style="font-size:.78rem;color:#57534e;font-style:italic;margin-bottom:.55rem;line-height:1.5">'+q.sublabel+'</div>':'';
const btns=q.answers.map(a=>{
const asub=a.sublabel?'<div style="font-size:.72rem;color:#57534e;font-weight:400;margin-top:2px">'+a.sublabel+'</div>':'';
const cls=q.answers.length===2
? (q.answers.indexOf(a)===0?'dt-answer dt-answer-yes':'dt-answer dt-answer-no')
: 'dt-answer dt-answer-yes';
return'<button class="'+cls+'" style="'+(q.answers.length>2?'flex:none;flex-direction:column;align-items:flex-start;':'')+'" data-qid="'+(isPW?'__pw__'+q.id:q.id)+'" data-aid="'+a.id+'" data-qlabel="'+encodeURIComponent(qText)+'" data-alabel="'+encodeURIComponent(a.label)+'">'+a.label+asub+'</button>';
}).join('');
const ansWrap=q.answers.length>2
?'<div style="display:flex;flex-direction:column;gap:.5rem;margin-top:1rem">'+btns+'</div>'
:'<div class="dt-answers">'+btns+'</div>';
document.getElementById('question-area').innerHTML='<div class="dt-card"><div class="dt-card-head">'+label+'</div><div class="dt-question">'+qText+'</div>'+sub+ansWrap+'</div>';
}
function renderEarlyExit(r){earlyDone=true;const top=r[0];document.getElementById('question-area').innerHTML='<div class="dt-card"><div class="dt-early-badge">Confident result available</div><div class="dt-early-muscle">'+top.label+'</div><div class="dt-early-sub">'+top.subtitle+'<br>Relative weight '+(top.p*100).toFixed(0)+' — accept to view result, or continue refining.</div><div class="dt-early-btns"><button class="dt-btn-primary" id="btn-accept">Accept result</button><button class="dt-btn-secondary" id="btn-continue">Continue refining</button></div></div>';document.getElementById('btn-accept').addEventListener('click',showResult);document.getElementById('btn-continue').addEventListener('click',()=>{earlyDone=true;advance();});}
function buildAddConfirm(r){const rem=r.slice(3);if(!rem.length)return'';const opts=rem.map(m=>'<option value="'+m.id+'">'+m.label+' (rank '+(r.indexOf(m)+1)+', weight '+(m.p*100).toFixed(1)+')</option>').join('');return'<div class="dt-add-confirm"><div class="dt-add-confirm-label">Also confirm a muscle not in the top three</div><div class="dt-add-confirm-row"><select class="dt-add-select" id="add-select"><option value="">— select muscle —</option>'+opts+'</select><button class="dt-btn-add-confirm" id="btn-add-confirm-ok">Confirm</button></div><div class="dt-add-confirm-done" id="add-confirm-done">✓ Confirmed and model updated</div></div>';}
function showResult(){
const r=ranked();
const ranks=['Most likely','2nd','3rd'],rk=['rk1','rk2','rk3'],card_cls=['dt-card-result','dt-card','dt-card'];
const cards=r.slice(0,3).map((m,i)=>{
const w=(m.p*100).toFixed(1);
const note=m.note?'<div class="dt-key-note"><div class="dt-key-note-label">Key TrP relationship</div>'+m.note+'</div>':'';
return'<div class="dt-card '+card_cls[i]+'" style="margin-bottom:10px">'+
'<div class="dt-result-rank '+rk[i]+'">'+ranks[i]+'</div>'+
'<div class="dt-result-name">'+m.label+'</div>'+
'<div class="dt-result-sub">'+m.subtitle+'</div>'+
'<div class="dt-result-weight">Relative weight: '+w+'</div>'+
'<div class="dt-result-actions">'+
'<a class="dt-wiki-link" href="https://painwiki.com/wiki/index.php?title='+m.page+'" target="_blank" rel="noopener">'+m.label+' on PainWiki</a>'+
'<button class="dt-confirm-btn" data-muscle="'+m.id+'" data-label="'+encodeURIComponent(m.label)+'">Confirm this muscle</button>'+
'</div>'+note+'</div>';
}).join('');
const addSec=buildAddConfirm(r);
document.getElementById('question-area').innerHTML=
'<div style="font-family:\'DM Mono\',monospace;font-size:.65rem;letter-spacing:.12em;text-transform:uppercase;color:#a8a29e;margin-bottom:8px">Differential — final weights</div>'+
cards+
(addSec?'<div class="dt-card" style="margin-top:4px"><div style="font-family:\'DM Mono\',monospace;font-size:.65rem;letter-spacing:.1em;text-transform:uppercase;color:#a8a29e;margin-bottom:10px">Additional Confirmation</div>'+addSec+'</div>':'');
const addBtn=document.getElementById('btn-add-confirm-ok');
if(addBtn){addBtn.addEventListener('click',()=>{const sel=document.getElementById('add-select'),mid=sel.value;if(!mid)return;incrementCount(mid);addBtn.disabled=true;sel.disabled=true;document.getElementById('add-confirm-done').style.display='block';renderLearnPanel();});}
const steps=buildTreatmentOrder(r);
if(steps.length){
const tp=document.getElementById('treat-panel');tp.classList.add('visible');
document.getElementById('treat-items').innerHTML=steps.map((s,i)=>{const isW=s.edgeType==='antagonist_risk';return'<div class="dt-treat-item"><div class="dt-treat-step">'+(i+1)+'</div><div><div class="dt-treat-muscle">'+s.label+'</div><div class="dt-treat-edge'+(isW?' warn':'')+'">'+s.edgeLabel+'</div>'+(s.note&&!isW?'<div class="dt-treat-edge" style="margin-top:2px;font-size:10px;color:#a8a29e">'+s.note+'</div>':'')+'</div></div>';}).join('');
}
renderLearnPanel();
}
function advance(){renderDiff();renderTrail();renderCounter();const r=ranked(),pw=getPairwise(r);if(pw){pairwiseDone.add(pw.id);renderQuestionFull(pw,true);return;}if(!earlyDone&&shouldEarlyExit(r)){renderEarlyExit(r);return;}const q=nextQ();if(q){queueIdx++;renderQuestionFull(q,false);return;}showResult();}
document.addEventListener('click',e=>{
const ab=e.target.closest('.dt-answer,.answer-btn');
if(ab&&ab.dataset.qid){
const qid=ab.dataset.qid,aid=ab.dataset.aid,qLabel=decodeURIComponent(ab.dataset.qlabel),aLabel=decodeURIComponent(ab.dataset.alabel);
if(qid.startsWith('__pw__')){const pw=SCHEMA.pairwise.find(x=>x.id===qid.replace('__pw__',''));const a=pw.answers.find(x=>x.id===aid);posteriors=applyLR(posteriors,a.lr);trail.push({q:qLabel,a:aLabel});earlyDone=false;}
else{const q=SCHEMA.questions.find(x=>x.id===qid),a=q.answers.find(x=>x.id===aid);answers[qid]=aid;posteriors=applyLR(posteriors,a.lr);trail.push({q:qLabel,a:aLabel});queueIdx++;}
advance();return;
}
const cb=e.target.closest('.dt-confirm-btn');
if(cb&&!cb.classList.contains('confirmed')){const mid=cb.dataset.muscle,label=decodeURIComponent(cb.dataset.label);incrementCount(mid);cb.classList.add('confirmed');cb.textContent='\u2713\u2713 '+label+' confirmed';renderLearnPanel();return;}
});
document.getElementById('reset-btn').addEventListener('click',()=>{answers={};trail=[];pairwiseDone=new Set();queueIdx=0;earlyDone=false;posteriors=getUpdatedPriors();document.getElementById('trail-panel').classList.remove('visible');document.getElementById('treat-panel').classList.remove('visible');advance();});
document.getElementById('learning-toggle-btn').addEventListener('click',()=>{const h=document.body.classList.toggle('hide-learning');document.getElementById('learning-toggle-btn').textContent='Learning: '+(h?'OFF':'ON');});
buildRedFlags();
buildBroadDiff();
/* ══════════════════════════════════════════════════════
BOOT — called after schema is loaded from wiki data page
Injects interface HTML then wires all event listeners
══════════════════════════════════════════════════════ */
function bootScoringInterface( hostEl, SCHEMA ) {
// 1. Set schema-derived globals FIRST — functions depend on these
T = SCHEMA.thresholds;
MUSCLE_IDS = Object.keys( SCHEMA.muscles );
DAG = SCHEMA.treatment_dag;
// 2. Rebuild posteriors from schema priors + any stored confirmations
posteriors = getUpdatedPriors();
// 3. Run the interface build functions — DOM elements now exist
buildRedFlags();
buildBroadDiff();
advance();
renderLearnPanel();
}
/* ══════════════════════════════════════════════════════
SCHEMA LOADER — fetches JSON from wiki data page
══════════════════════════════════════════════════════ */
function bootHost( hostEl ) {
var treePage = hostEl.getAttribute( 'data-tree-page' );
if ( !treePage ) return;
hostEl.innerHTML =
'<div style="padding:1.5em;font-family:DM Mono,monospace;' +
'font-size:0.8em;color:#a8a29e;text-align:center">' +
'Loading scoring model…</div>';
var api = new mw.Api();
api.get( {
action: 'query',
titles: treePage,
prop: 'revisions',
rvprop: 'content',
rvslots: 'main',
format: 'json'
} ).done( function ( data ) {
var pages = data.query.pages;
var pageId = Object.keys( pages )[0];
if ( pageId === '-1' ) {
hostEl.innerHTML =
'<div style="color:#b91c1c;padding:1em;font-family:monospace">' +
'Scoring model not found: ' + treePage + '</div>';
return;
}
var raw = pages[ pageId ].revisions[0].slots.main['*'];
var schema;
try {
schema = JSON.parse( raw );
} catch ( e ) {
hostEl.innerHTML =
'<div style="color:#b91c1c;padding:1em;font-family:monospace">' +
'Invalid JSON in ' + treePage + ': ' + e.message + '</div>';
return;
}
// Render the interface HTML into the host element
// then boot the scoring engine
renderInterfaceHTML( hostEl );
bootScoringInterface( hostEl, schema );
} ).fail( function () {
hostEl.innerHTML =
'<div style="color:#b91c1c;padding:1em;font-family:monospace">' +
'Failed to load: ' + treePage + '</div>';
} );
}
/* ══════════════════════════════════════════════════════
INTERFACE HTML RENDERER
Builds the shell, red flag panels, grid, and broad diff
into the host element — without any JS execution
══════════════════════════════════════════════════════ */
function renderInterfaceHTML( hostEl ) {
hostEl.innerHTML = [
'<div class="proto-shell">',
// Masthead
'<header class="proto-masthead">',
' <div class="proto-logo">Pain<span>Wiki</span></div>',
' <div class="proto-right">',
' <span class="proto-region">Diagnostic Algorithm · Upper Thoracic Back Pain</span>',
' <button class="learning-toggle" id="learning-toggle-btn">Learning: ON</button>',
' </div>',
'</header>',
// Red flags
'<div class="rf-outer">',
' <div class="rf-panel-wrap" id="rf-emergency-panel">',
' <div class="rf-col-header">⚠ Emergency — stop and act if any are present</div>',
' <div class="rf-items" id="rf-emergency-items"></div>',
' </div>',
' <div class="rf-panel-wrap urgent" id="rf-urgent-panel">',
' <div class="rf-col-header">⚠ Urgent — refer before myofascial assessment</div>',
' <div class="rf-items" id="rf-urgent-items"></div>',
' <div class="rf-affirm">',
' <div class="rf-affirm-note">All emergency and urgent flags screened and negative — or appropriate action taken.</div>',
' <button class="btn-affirm" id="btn-affirm">Proceed to clinical interview →</button>',
' <div class="affirmed-badge" id="affirmed-badge">Red flags cleared</div>',
' </div>',
' </div>',
'</div>',
// Main grid — hidden until red flags affirmed
'<div id="main-grid">',
' <div class="col-left">',
' <div id="question-area"></div>',
' <div class="dt-trail-panel" id="trail-panel">',
' <div class="dt-trail-head">',
' <span class="dt-trail-title">Clinical Interview Record</span>',
' <span class="dt-trail-meta" id="trail-counter"></span>',
' </div>',
' <div id="trail-items"></div>',
' </div>',
' </div>',
' <div class="col-right">',
' <div class="dt-running-panel">',
' <div class="dt-running-head">',
' <span class="dt-running-title">Running Differential</span>',
' <span class="dt-running-meta" id="q-counter">Prior weights</span>',
' </div>',
' <div id="diff-rows"></div>',
' <div class="dt-diff-caption">Diagnostic weights are relative, not absolute probabilities.</div>',
' </div>',
' <div class="dt-treat-panel" id="treat-panel">',
' <div class="dt-treat-head"><span class="dt-treat-title">Suggested Treatment Order</span></div>',
' <div id="treat-items"></div>',
' </div>',
' <div class="learn-panel" id="learn-panel">',
' <div class="learn-head">',
' <span class="learn-title">Confirmed Cases — This Device</span>',
' <span class="learn-meta" id="learn-total"></span>',
' </div>',
' <div class="learn-intro"><strong>Multiple muscles can be confirmed per case.</strong></div>',
' <div id="learn-rows"></div>',
' </div>',
' </div>',
// Broad differential — full width
' <div style="grid-column:1/-1">',
' <div class="dt-broad-panel" id="broad-panel">',
' <div class="dt-broad-header">',
' <div class="dt-broad-header-top">',
' <div class="dt-broad-title">■ Broad Differential Diagnosis</div>',
' <button class="dt-broad-toggle" id="broad-toggle-btn">Hide</button>',
' </div>',
' <div class="dt-epigraph">',
' “If he does not expect the unexpected, he will not discover it —',
' for it is difficult to discover and intractable.”',
' <cite>— Heraclitus, Fr. 18</cite>',
' </div>',
' </div>',
' <div class="dt-broad-grid" id="broad-grid"></div>',
' </div>',
' </div>',
'</div>', // end main-grid
// Footer
'<div class="proto-foot">',
' <div class="proto-disclaimer"><strong>Not validated for clinical use.</strong> Research prototype.</div>',
' <button class="btn-reset" id="reset-btn">↻ Start over</button>',
'</div>',
'</div>' // end proto-shell
].join( '
' );
// Wire reset and toggle — these elements now exist
var resetBtn = document.getElementById( 'reset-btn' );
var learnBtn = document.getElementById( 'learning-toggle-btn' );
if ( resetBtn ) {
resetBtn.addEventListener( 'click', function () {
answers = {}; trail = [];
pairwiseDone = new Set();
queueIdx = 0; earlyDone = false;
posteriors = getUpdatedPriors();
var tp = document.getElementById( 'trail-panel' );
var tp2 = document.getElementById( 'treat-panel' );
if ( tp ) tp.classList.remove( 'visible' );
if ( tp2 ) tp2.classList.remove( 'visible' );
advance();
} );
}
if ( learnBtn ) {
learnBtn.addEventListener( 'click', function () {
var h = document.body.classList.toggle( 'hide-learning' );
learnBtn.textContent = 'Learning: ' + ( h ? 'OFF' : 'ON' );
} );
}
}
/* ══════════════════════════════════════════════════════
MEDIAWIKI ENTRY POINT
══════════════════════════════════════════════════════ */
function init() {
document.querySelectorAll( '.scoring-tree-host' ).forEach( function ( el ) {
bootHost( el );
} );
}
if ( typeof mw !== 'undefined' ) {
mw.hook( 'wikipage.content' ).add( init );
}
}() );