MediaWiki:Gadget-DiagnosticTree.js: Difference between revisions

From Painwiki
Jump to navigation Jump to search
No edit summary
No edit summary
 
Line 30: Line 30:
     ══════════════════════════════════════════════════════ */
     ══════════════════════════════════════════════════════ */
   var NODE_META = {
   var NODE_META = {
     rom:              { color:'#1d4ed8', icon:'🔄', label:'Movement Screen' },
     rom:              { color:'#1d4ed8', icon:'', label:'Movement Screen' },
     nerve_entrapment: { color:'#6d28d9', icon:'', label:'Nerve Screen' },
     nerve_entrapment: { color:'#6d28d9', icon:'', label:'Nerve Screen' },
     symptom:          { color:'#0f766e', icon:'💬', label:'Patient Symptom' },
     symptom:          { color:'#0f766e', icon:'', label:'Patient Symptom' },
     examination:      { color:'#15803d', icon:'🩺', label:'Examination' },
     examination:      { color:'#15803d', icon:'', label:'Examination' },
     result:          { color:'#15803d', icon:'', label:'Diagnosis' },
     result:          { color:'#15803d', icon:'', label:'Diagnosis' },
     overlap:          { color:'#78716c', icon:'🔍', label:'Inconclusive' },
     overlap:          { color:'#78716c', icon:'', label:'Inconclusive' },
     neuro_referral:  { color:'#b91c1c', icon:'🧠', label:'Neurological Referral' }
     neuro_referral:  { color:'#b91c1c', icon:'', label:'Neurological Referral' }
   };
   };


Line 415: Line 415:
         : '' ) +
         : '' ) +
       '<div class="dt-broad-reminder">' +
       '<div class="dt-broad-reminder">' +
         '<span class="dt-broad-reminder-icon">🔭</span>' +
         '<span class="dt-broad-reminder-icon"></span>' +
         '<div><strong>Before concluding</strong>' +
         '<div><strong>Before concluding</strong>' +
         'Review the broad differential panel below — several conditions in this region closely mimic this presentation.</div>' +
         'Review the broad differential panel below — several conditions in this region closely mimic this presentation.</div>' +
Line 471: Line 471:
       '<button class="dt-key-tab nerve-key' + ( hasNerveTree ? '' : ' coming-soon' ) + '" id="dt-tab-nerve"' +
       '<button class="dt-key-tab nerve-key' + ( hasNerveTree ? '' : ' coming-soon' ) + '" id="dt-tab-nerve"' +
         ( hasNerveTree ? '' : ' title="Nerve entrapment key coming soon"' ) + '>' +
         ( hasNerveTree ? '' : ' title="Nerve entrapment key coming soon"' ) + '>' +
         '<span class="tab-icon"></span> Nerve Entrapment Key' +
         '<span class="tab-icon"></span> Nerve Entrapment Key' +
         ( hasNerveTree ? '' : '<span class="tab-soon">coming soon</span>' ) +
         ( hasNerveTree ? '' : '<span class="tab-soon">coming soon</span>' ) +
       '</button>';
       '</button>';
Line 485: Line 485:
     algoSection.innerHTML =
     algoSection.innerHTML =
       '<div class="dt-algo-header">' +
       '<div class="dt-algo-header">' +
         '<div class="dt-algo-header-left">🩺 Pain Pattern Algorithm</div>' +
         '<div class="dt-algo-header-left"> Pain Pattern Algorithm</div>' +
         '<div class="dt-algo-header-right">Complete checklist above to unlock</div>' +
         '<div class="dt-algo-header-right">Complete checklist above to unlock</div>' +
       '</div>' +
       '</div>' +
Line 500: Line 500:
         '<div class="dt-algo-section" style="border-color:var(--nerve-header-border)">' +
         '<div class="dt-algo-section" style="border-color:var(--nerve-header-border)">' +
           '<div class="dt-algo-header" style="background:var(--nerve-header)">' +
           '<div class="dt-algo-header" style="background:var(--nerve-header)">' +
             '<div class="dt-algo-header-left">Nerve Entrapment Key</div>' +
             '<div class="dt-algo-header-left"> Nerve Entrapment Key</div>' +
           '</div>' +
           '</div>' +
           '<div class="dt-algo-body">' +
           '<div class="dt-algo-body">' +

Latest revision as of 23:31, 13 April 2026

/**
 * DiagnosticTree.js — v6
 * Copy entire contents to: MediaWiki:Gadget-DiagnosticTree.js
 *
 * EMBED ON ANY WIKI PAGE:
 *   <div class="diagnostic-tree-host"
 *        data-tree-page="DiagnosticTree/EarTMJ"
 *        data-nerve-tree-page="DiagnosticTree/EarTMJ_nerve">
 *   </div>
 *
 *   data-nerve-tree-page is optional — omit it and the nerve tab
 *   shows as "coming soon" automatically.
 *
 * JSON PAGE STRUCTURE (data-tree-page):
 *   {
 *     "tree_id":   "...",
 *     "region":    "...",
 *     "start":     "first-node-id",
 *     "redflags":  { "emergency": [...], "urgent": [...] },
 *     "broad_differential": [...],
 *     "nodes":     { ... }
 *   }
 */

( function () {
  'use strict';

  /* ══════════════════════════════════════════════════════
     NODE META
     ══════════════════════════════════════════════════════ */
  var NODE_META = {
    rom:              { color:'#1d4ed8', icon:'', label:'Movement Screen' },
    nerve_entrapment: { color:'#6d28d9', icon:'', label:'Nerve Screen' },
    symptom:          { color:'#0f766e', icon:'', label:'Patient Symptom' },
    examination:      { color:'#15803d', icon:'', label:'Examination' },
    result:           { color:'#15803d', icon:'', label:'Diagnosis' },
    overlap:          { color:'#78716c', icon:'', label:'Inconclusive' },
    neuro_referral:   { color:'#b91c1c', icon:'', label:'Neurological Referral' }
  };

  /* ══════════════════════════════════════════════════════
     RED FLAG PANEL
     ══════════════════════════════════════════════════════ */
  function RedFlagPanel( el, data, onCleared ) {
    this.el = el; this.data = data; this.onCleared = onCleared;
    this.state = {}; this.expanded = null;
    data.emergency.forEach( function ( f ) { this.state[ f.id ] = 'pending'; }, this );
    data.urgent.forEach(    function ( f ) { this.state[ f.id ] = 'pending'; }, this );
    this.render();
  }

  RedFlagPanel.prototype.allCleared = function () {
    return Object.keys( this.state ).every( function ( k ) {
      return this.state[ k ] === 'cleared';
    }, this );
  };

  RedFlagPanel.prototype.render = function () {
    var s = this;
    var eC = this.data.emergency.every( function ( f ) { return s.state[ f.id ] === 'cleared'; } );
    var uC = this.data.urgent.every(    function ( f ) { return s.state[ f.id ] === 'cleared'; } );

    this.el.innerHTML =
      '<div class="dt-redflag-panel">' +
        this._col( 'emergency', this.data.emergency, eC ) +
        this._col( 'urgent',    this.data.urgent,    uC ) +
      '</div>';

    this.el.querySelectorAll( '.dt-flag-item' ).forEach( function ( el ) {
      var id = el.getAttribute( 'data-id' );
      el.addEventListener( 'click', function ( e ) {
        if ( e.target.classList.contains( 'dt-flag-btn' ) ) return;
        if ( s.state[ id ] === 'cleared' ) return;
        s.expanded = s.expanded === id ? null : id;
        s.render();
      } );
    } );

    this.el.querySelectorAll( '.dt-flag-btn' ).forEach( function ( btn ) {
      btn.addEventListener( 'click', function ( e ) {
        e.stopPropagation();
        var id  = btn.getAttribute( 'data-id' );
        var act = btn.getAttribute( 'data-action' );
        s.state[ id ] = act === 'present' ? 'present' : 'cleared';
        s.expanded    = act === 'present' ? id : null;
        s.render();
        if ( s.allCleared() ) s.onCleared();
      } );
    } );
  };

  RedFlagPanel.prototype._col = function ( type, flags, allCleared ) {
    var s = this, isE = type === 'emergency';
    var cc = 'dt-flag-column' + ( isE ? '' : ' urgent' ) + ( allCleared ? ' all-cleared' : '' );
    var items = flags.map( function ( f ) { return s._item( f ); } ).join( '' );
    var refs = '';
    flags.forEach( function ( f ) {
      if ( s.state[ f.id ] === 'present' ) {
        refs += '<div class="dt-flag-referral ' + ( isE ? '' : 'urgent-ref' ) + ' visible">' +
          '<strong>⛔ ' + ( isE ? 'EMERGENCY ACTION' : 'URGENT REFERRAL' ) + '</strong>' +
          f.action + '</div>';
      }
    } );
    return '<div class="' + cc + '">' +
      '<div class="dt-flag-col-header"><span class="header-icon">' + ( isE ? '🚨' : '⚠️' ) + '</span>' +
      ( isE ? 'Emergency' : 'Urgent Referral' ) + '</div>' +
      '<div class="dt-flag-items">' + items + '</div>' + refs +
      '<div class="dt-panel-cleared-msg">✓ All ' + ( isE ? 'emergency' : 'urgent' ) + ' flags cleared</div>' +
    '</div>';
  };

  RedFlagPanel.prototype._item = function ( f ) {
    var isCl = this.state[ f.id ] === 'cleared';
    var isEx = this.expanded === f.id;
    var cls  = 'dt-flag-item' + ( isCl ? ' cleared' : '' ) + ( isEx ? ' expanded' : '' );
    var acts = isCl ? '' :
      '<div class="dt-flag-actions">' +
        '<button class="dt-flag-btn present" data-id="' + f.id + '" data-action="present">Present</button>' +
        '<button class="dt-flag-btn absent"  data-id="' + f.id + '" data-action="absent">Absent — cleared</button>' +
      '</div>';
    return '<div class="' + cls + '" data-id="' + f.id + '">' +
      '<div class="dt-flag-checkbox"></div>' +
      '<div class="dt-flag-item-body">' +
        '<div class="dt-flag-label">' + f.label + '</div>' +
        '<div class="dt-flag-question">' + f.question +
          '<div class="dt-flag-rationale">' + f.rationale + '</div>' +
        '</div>' + acts +
      '</div></div>';
  };

  /* ══════════════════════════════════════════════════════
     BROAD DIFFERENTIAL PANEL
     ══════════════════════════════════════════════════════ */
  function BroadDiffPanel( el, data ) {
    this.el = el; this.data = data;
    this.open = null; this.collapsed = false; this.locked = true;
    this.render();
  }

  BroadDiffPanel.prototype.unlock = function () { this.locked = false; this.render(); };

  BroadDiffPanel.prototype.render = function () {
    var s = this;
    var lockedSection = this.locked
      ? '<div class="dt-broad-locked-msg">⟳ Complete the emergency &amp; urgent checklist above to unlock</div>'
      : '<div class="dt-broad-grid' + ( this.collapsed ? ' hidden' : '' ) + '" id="bd-grid">' +
          this.data.map( function ( d ) { return s._card( d ); } ).join( '' ) +
        '</div>';

    this.el.innerHTML =
      '<div class="dt-broad-panel">' +
        '<div class="dt-broad-header">' +
          '<div class="dt-broad-header-top">' +
            '<div class="dt-broad-title"><span>🔭</span> Broad Differential — Also Consider</div>' +
            ( !this.locked
              ? '<button class="dt-broad-toggle" id="bd-toggle">' + ( this.collapsed ? 'Show ▾' : '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>' +
        lockedSection +
      '</div>';

    var tog = this.el.querySelector( '#bd-toggle' );
    if ( tog ) tog.addEventListener( 'click', function () { s.collapsed = !s.collapsed; s.render(); } );

    this.el.querySelectorAll( '.dt-diff-item' ).forEach( function ( el ) {
      var id = el.getAttribute( 'data-id' );
      el.addEventListener( 'click', function ( e ) {
        if ( e.target.classList.contains( 'dt-wiki-link' ) ) return;
        s.open = s.open === id ? null : id;
        s.render();
      } );
    } );
  };

  BroadDiffPanel.prototype._card = function ( d ) {
    var isOpen = this.open === d.id;
    var detail = isOpen
      ? '<div class="dt-diff-detail">' +
          '<div class="dt-diff-distinguisher"><strong>Distinguishing feature</strong>' + d.distinguishing_feature + '</div>' +
          '<div class="dt-diff-action"><strong>If suspected</strong>' + d.action + '</div>' +
        '</div>' : '';
    return '<div class="dt-diff-item' + ( isOpen ? ' open' : '' ) + '" data-id="' + d.id + '">' +
      '<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>' +
      detail + '</div>';
  };

  /* ══════════════════════════════════════════════════════
     TREE STATE
     ══════════════════════════════════════════════════════ */
  function TreeState( tree ) {
    this.tree = tree; this.history = []; this.current = tree.start;
  }
  TreeState.prototype.go    = function ( id ) { this.history.push( this.current ); this.current = id; };
  TreeState.prototype.back  = function ()     { if ( this.history.length ) this.current = this.history.pop(); };
  TreeState.prototype.reset = function ()     { this.history = []; this.current = this.tree.start; };
  TreeState.prototype.node  = function ()     { return this.tree.nodes[ this.current ]; };

  /* ══════════════════════════════════════════════════════
     DIAGNOSTIC TREE ENGINE
     ══════════════════════════════════════════════════════ */
  function DiagnosticTree( el, data ) {
    this.el = el; this.state = new TreeState( data ); this.render();
  }

  DiagnosticTree.prototype.render = function () {
    var s = this, node = this.state.node(), meta = NODE_META[ node.type ] || NODE_META.symptom;
    var canBack = this.state.history.length > 0;
    var steps = this.state.history.length + 1;
    var total = Object.keys( this.state.tree.nodes ).length;
    var pct   = Math.min( Math.round( ( steps / total ) * 100 ), 95 );

    var cardHtml;
    if      ( node.type === 'result'        ) cardHtml = this._result( node );
    else if ( node.type === 'overlap'       ) cardHtml = this._overlap( node );
    else if ( node.type === 'neuro_referral') cardHtml = this._neuroRef( node );
    else                                      cardHtml = this._question( node, meta );

    this.el.innerHTML =
      '<div class="dt-tree-header">' +
        '<span class="dt-region-label">Diagnostic algorithm</span>' +
        '<div class="dt-controls">' +
          ( canBack ? '<button class="dt-btn dt-btn-back" id="dt-back">← Back</button>' : '' ) +
          '<button class="dt-btn" id="dt-reset">↺ Restart</button>' +
        '</div>' +
      '</div>' +
      '<div class="dt-progress-wrap"><div class="dt-progress-bar" style="width:' + pct + '%"></div></div>' +
      '<div class="dt-step-label">Step ' + steps + '</div>' +
      cardHtml;

    var b = function ( sel, fn ) {
      var el = s.el.querySelector( sel );
      if ( el ) el.addEventListener( 'click', fn );
    };

    b( '#dt-back',  function () { s.state.back();  s.render(); } );
    b( '#dt-reset', function () { s.state.reset(); s.render(); } );
    b( '#dt-yes',   function () { var n = s.state.node(); if ( n.yes ) { s.state.go( n.yes ); s.render(); } } );
    b( '#dt-no',    function () { var n = s.state.node(); if ( n.no  ) { s.state.go( n.no  ); s.render(); } } );

    this.el.querySelectorAll( '.dt-division-tab' ).forEach( function ( tab ) {
      tab.addEventListener( 'click', function () {
        var panel = tab.getAttribute( 'data-panel' );
        s.el.querySelectorAll( '.dt-division-tab'   ).forEach( function ( t ) { t.classList.remove( 'active' ); } );
        s.el.querySelectorAll( '.dt-division-panel' ).forEach( function ( p ) { p.classList.remove( 'active' ); } );
        tab.classList.add( 'active' );
        var target = s.el.querySelector( '#' + panel );
        if ( target ) target.classList.add( 'active' );
      } );
    } );

    this.el.querySelectorAll( '.dt-wiki-link' ).forEach( function ( btn ) {
      btn.addEventListener( 'click', function ( e ) {
        e.stopPropagation();
        var page = btn.getAttribute( 'data-page' );
        if ( page ) window.location.href = mw.util.getUrl( page );
      } );
    } );
  };

  DiagnosticTree.prototype._question = function ( node, meta ) {
    var mv  = node.movement        ? '<div class="dt-tag dt-tag-blue">'   + node.movement  + ' (' + ( node.direction || '' ) + ')</div>' : '';
    var nv  = node.nerve           ? '<div class="dt-tag dt-tag-purple">Nerve: ' + node.nerve + '</div>' : '';
    var et  = node.exam_type       ? '<div class="dt-tag dt-tag-green">'  + node.exam_type.replace( /_/g, ' ' ) + '</div>' : '';
    var rat = node.clinical_rationale || node.distinguishing_feature || '';
    var lm  = node.landmark        ? '<div class="dt-landmark">📍 '       + node.landmark        + '</div>' : '';
    var pf  = node.positive_finding? '<div class="dt-positive">✓ Positive: ' + node.positive_finding + '</div>' : '';
    var nf  = node.negative_finding? '<div class="dt-negative">✗ Negative: ' + node.negative_finding + '</div>' : '';
    var imp = node.muscles_implicated ? '<div class="dt-muscle-hint dt-implicated">Suggests: '       + node.muscles_implicated.join( ', ' ) + '</div>' : '';
    var exc = node.muscles_excluded   ? '<div class="dt-muscle-hint dt-excluded">Argues against: '  + node.muscles_excluded.join( ', ' )   + '</div>' : '';

    return '<div class="dt-card" style="border-color:' + meta.color + '22">' +
      '<div class="dt-type-badge" style="background:' + meta.color + '">' + meta.icon + ' ' + meta.label + '</div>' +
      '<div class="dt-question">' + node.question + '</div>' +
      ( ( mv || nv || et ) ? '<div class="dt-tags">' + mv + nv + et + '</div>' : '' ) +
      lm + pf + nf +
      ( rat ? '<div class="dt-rationale">' + rat + '</div>' : '' ) +
      imp + exc +
      '<div class="dt-answers">' +
        '<button class="dt-answer dt-answer-yes" id="dt-yes">Yes</button>' +
        '<button class="dt-answer dt-answer-no"  id="dt-no">No</button>' +
      '</div>' +
    '</div>';
  };

  DiagnosticTree.prototype._result = function ( node ) {
    var conf = node.confidence || 'moderate';
    var cc   = { high: '#15803d', moderate: '#b45309', low: '#b91c1c' }[ conf ] || '#78716c';
    var also = node.also_consider && node.also_consider.length
      ? '<div class="dt-also">Also consider: ' + node.also_consider.join( ', ' ) + '</div>' : '';

    /* Confirmatory checklist */
    var confirmatoryHtml = '';
    if ( node.confirmatory && node.confirmatory.length ) {
      confirmatoryHtml =
        '<div class="dt-confirmatory">' +
          '<div class="dt-confirmatory-header">✓ Confirm at point of diagnosis</div>' +
          '<div class="dt-confirmatory-items">' +
            node.confirmatory.map( function ( c ) {
              return '<div class="dt-confirmatory-item"><div class="dt-confirmatory-dot"></div>' + c + '</div>';
            } ).join( '' ) +
          '</div>' +
        '</div>';
    }

    /* Division tabs — SCM */
    var divisionHtml = '';
    if ( node.division ) {
      divisionHtml =
        '<div class="dt-division-tabs">' +
          '<button class="dt-division-tab active" data-panel="div-sternal">Sternal Division</button>' +
          '<button class="dt-division-tab" data-panel="div-clavicular">Clavicular Division</button>' +
        '</div>' +
        '<div class="dt-division-panel active" id="div-sternal">' +
          '<div class="dt-division-content">' +
            '<strong>Primary symptoms:</strong> Cheek, temple, orbit, and supraorbital pain. Sore neck sometimes misattributed to lymphadenopathy.' +
            '<ul>' +
              '<li>Profuse tearing — often more alarming to patient than pain</li>' +
              '<li>Conjunctival redness, rhinitis</li>' +
              '<li>Apparent ptosis via palpebral fissure narrowing on TrP side</li>' +
              '<li>Visual disturbance with strongly contrasted vertical lines (e.g. venetian blinds)</li>' +
              '<li>Head tilts to side of TrPs — pain on holding head upright</li>' +
              '<li>Patient prefers to lie on sore side with pillow so face does not bear weight</li>' +
            '</ul>' +
          '</div>' +
        '</div>' +
        '<div class="dt-division-panel" id="div-clavicular">' +
          '<div class="dt-division-content">' +
            '<strong>Any one of three may dominate:</strong> Frontal headache, postural dizziness/imbalance, or dysmetria.' +
            '<ul>' +
              '<li>Dizziness worsens on changing head load, lying without pillow, or quick rotation</li>' +
              '<li>Turning over in bed — patient should roll head on pillow, not lift it</li>' +
              '<li>Adds to car sickness or sea sickness; may report nausea or anorexia</li>' +
              '<li>Transient loss of equilibrium after vigorous or quick head/neck rotation</li>' +
              '<li>Rarely: hearing impaired on same side as clavicular TrPs</li>' +
            '</ul>' +
          '</div>' +
        '</div>';
    }

    /* Horner confirmation */
    var hornerHtml = node.division
      ? '<div class="dt-confirm-panel">' +
          '<div class="dt-confirm-header">🔍 Confirmatory — Horner Syndrome Exclusion</div>' +
          '<div class="dt-confirm-items">' +
            '<div class="dt-confirm-item"><div class="dt-confirm-dot"></div>Pupils equal and reactive — no miosis</div>' +
            '<div class="dt-confirm-item"><div class="dt-confirm-dot"></div>No enophthalmos — apparent ptosis is from palpebral fissure narrowing only</div>' +
            '<div class="dt-confirm-item"><div class="dt-confirm-dot"></div>Ciliospinal reflex present — pinch skin on back of neck → ipsilateral pupil dilates</div>' +
            '<div class="dt-confirm-item"><div class="dt-confirm-dot"></div>Extraocular movements full — no paralysis; symptoms not conversion hysteria</div>' +
          '</div>' +
        '</div>' : '';

    /* Satellite TrPs */
    var satelliteHtml = '';
    if ( node.satellite_trps && node.satellite_trps.length ) {
      satelliteHtml =
        '<div class="dt-satellite-section">' +
          '<div class="dt-satellite-header">Satellite TrPs — may activate from or with this muscle</div>' +
          '<div class="dt-satellite-grid">' +
            node.satellite_trps.map( function ( t ) {
              return '<span class="dt-satellite-tag">' + t + '</span>';
            } ).join( '' ) +
          '</div>' +
        '</div>';
    }

    /* Landing page signposting */
    var landingHtml = '';
    if ( node.landing_page_topics && node.landing_page_topics.length ) {
      landingHtml =
        '<div class="dt-landing-panel">' +
          '<div class="dt-landing-header">📋 Further assessment — muscle landing page</div>' +
          '<div class="dt-landing-body">' +
            'The following clinical detail goes beyond the scope of this algorithm and is covered on the muscle page:' +
            '<ul class="dt-landing-list">' +
              node.landing_page_topics.map( function ( t ) { return '<li>' + t + '</li>'; } ).join( '' ) +
            '</ul>' +
          '</div>' +
        '</div>';
    }

    /* Related page links */
    var relatedHtml = node.related_pages && node.related_pages.length
      ? node.related_pages.map( function ( p ) {
          return '<button class="dt-wiki-link satellite" data-page="' + p.page + '">' + p.label + '</button>';
        } ).join( '' ) : '';

    return '<div class="dt-card dt-card-result">' +
      '<div class="dt-type-badge" style="background:#15803d">✅ Likely Diagnosis</div>' +
      '<div class="dt-result-name">' + node.diagnosis + '</div>' +
      '<div class="dt-confidence" style="color:' + cc + '">Confidence: ' + conf + '</div>' +
      divisionHtml +
      ( node.notes          ? '<div class="dt-notes">'     + node.notes          + '</div>' : '' ) +
      ( node.treatment_hint ? '<div class="dt-treatment">Treatment: ' + node.treatment_hint + '</div>' : '' ) +
      ( node.chapter_ref    ? '<div class="dt-chapter">📖 ' + node.chapter_ref   + '</div>' : '' ) +
      also + confirmatoryHtml + hornerHtml + satelliteHtml + landingHtml +
      ( node.wiki_page ? '<button class="dt-wiki-link" data-page="' + node.wiki_page + '">Open Wiki Page →</button>' : '' ) +
      relatedHtml +
      ( node.less_likely && node.less_likely.length
        ? '<div class="dt-less-likely">' +
            '<div class="dt-less-likely-header">Also possible — less likely on this path</div>' +
            node.less_likely.map( function ( l ) {
              return '<div class="dt-less-likely-item">' +
                '<span class="dt-less-likely-name">' + l.muscle + '</span>' +
                '<span class="dt-less-likely-reason">' + l.reason + '</span>' +
              '</div>';
            } ).join( '' ) +
          '</div>'
        : '' ) +
      '<div class="dt-broad-reminder">' +
        '<span class="dt-broad-reminder-icon"></span>' +
        '<div><strong>Before concluding</strong>' +
        'Review the broad differential panel below — several conditions in this region closely mimic this presentation.</div>' +
      '</div>' +
    '</div>';
  };

  DiagnosticTree.prototype._neuroRef = function ( node ) {
    var isE   = node.urgency === 'emergency';
    var color = isE ? 'var(--emergency)' : 'var(--urgent)';
    var bdrCl = isE ? 'var(--emergency-border)' : 'var(--urgent-border)';
    var bgCl  = isE ? 'var(--emergency-bg)'     : 'var(--urgent-bg)';
    return '<div class="dt-card dt-card-neuro-ref" style="border-color:' + bdrCl + ';background:' + bgCl + '">' +
      '<div class="dt-type-badge" style="background:' + color + '">' + ( isE ? '🚨 Emergency' : '⚠️ Urgent Referral' ) + '</div>' +
      '<div class="dt-neuro-title">' + node.title  + '</div>' +
      '<div class="dt-neuro-body">'  + node.body   + '</div>' +
      '<div class="dt-treatment">'   + node.action + '</div>' +
    '</div>';
  };

  DiagnosticTree.prototype._overlap = function ( node ) {
    var sl = node.screen_these && node.screen_these.length
      ? '<ul class="dt-screen-list">' +
          node.screen_these.map( function ( m ) { return '<li>' + m + '</li>'; } ).join( '' ) +
        '</ul>' : '';
    return '<div class="dt-card dt-card-overlap">' +
      '<div class="dt-type-badge" style="background:#78716c">🔍 Inconclusive</div>' +
      '<div class="dt-overlap-text">' + ( node.text || '' ) + '</div>' + sl +
      ( node.wiki_page ? '<button class="dt-wiki-link" data-page="' + node.wiki_page + '">Open Differential Page →</button>' : '' ) +
      '<div class="dt-broad-reminder">' +
        '<span class="dt-broad-reminder-icon">🔭</span>' +
        '<div><strong>Before concluding</strong>' +
        'Review the broad differential panel below — several conditions in this region closely mimic these findings.</div>' +
      '</div>' +
    '</div>';
  };

  /* ══════════════════════════════════════════════════════
     BOOT SHELL — builds key toggle, red flags, algo, broad diff
     ══════════════════════════════════════════════════════ */
  function bootShell( hostEl, treeData ) {
    var shell = document.createElement( 'div' );
    shell.className = 'dt-shell';
    hostEl.appendChild( shell );

    var hasNerveTree = !!hostEl.getAttribute( 'data-nerve-tree-page' );

    /* 0 — Key toggle tabs */
    var keyToggle = document.createElement( 'div' );
    keyToggle.className = 'dt-key-toggle';
    keyToggle.innerHTML =
      '<button class="dt-key-tab pain-key active" id="dt-tab-pain">' +
        '<span class="tab-icon">🩺</span> Pain Pattern Key' +
      '</button>' +
      '<button class="dt-key-tab nerve-key' + ( hasNerveTree ? '' : ' coming-soon' ) + '" id="dt-tab-nerve"' +
        ( hasNerveTree ? '' : ' title="Nerve entrapment key coming soon"' ) + '>' +
        '<span class="tab-icon"></span> Nerve Entrapment Key' +
        ( hasNerveTree ? '' : '<span class="tab-soon">coming soon</span>' ) +
      '</button>';
    shell.appendChild( keyToggle );

    /* 1 — Red flag panel (shared) */
    var panelEl = document.createElement( 'div' );
    shell.appendChild( panelEl );

    /* 2 — Pain algorithm section */
    var algoSection = document.createElement( 'div' );
    algoSection.className = 'dt-algo-section locked';
    algoSection.innerHTML =
      '<div class="dt-algo-header">' +
        '<div class="dt-algo-header-left"> Pain Pattern Algorithm</div>' +
        '<div class="dt-algo-header-right">Complete checklist above to unlock</div>' +
      '</div>' +
      '<div class="dt-algo-body" id="dt-algo-body">' +
        '<div class="dt-tree-locked-msg">Work through all emergency and urgent flags above before proceeding.</div>' +
      '</div>';
    shell.appendChild( algoSection );

    /* 2b — Nerve section (placeholder or loaded) */
    var nerveSection = document.createElement( 'div' );
    nerveSection.style.display = 'none';
    if ( !hasNerveTree ) {
      nerveSection.innerHTML =
        '<div class="dt-algo-section" style="border-color:var(--nerve-header-border)">' +
          '<div class="dt-algo-header" style="background:var(--nerve-header)">' +
            '<div class="dt-algo-header-left"> Nerve Entrapment Key</div>' +
          '</div>' +
          '<div class="dt-algo-body">' +
            '<div class="dt-tree-locked-msg" style="color:#6d28d9">' +
              'The nerve entrapment key for this region is under development.<br>' +
              'It will differentiate muscles by their nerve entrapment signatures.<br><br>' +
              '<small>Refer to individual muscle pages for entrapment detail in the meantime.</small>' +
            '</div>' +
          '</div>' +
        '</div>';
    }
    shell.appendChild( nerveSection );

    /* Tab switching */
    var tabPain  = shell.querySelector( '#dt-tab-pain' );
    var tabNerve = shell.querySelector( '#dt-tab-nerve' );

    tabPain.addEventListener( 'click', function () {
      tabPain.classList.add( 'active' );
      tabNerve.classList.remove( 'active' );
      algoSection.style.display  = '';
      nerveSection.style.display = 'none';
    } );

    tabNerve.addEventListener( 'click', function () {
      tabNerve.classList.add( 'active' );
      tabPain.classList.remove( 'active' );
      algoSection.style.display  = 'none';
      nerveSection.style.display = '';
    } );

    /* 3 — Broad differential (shared) */
    var div2 = document.createElement( 'div' );
    div2.className   = 'dt-divider';
    div2.textContent = 'Low Probability — Broad Differential';
    shell.appendChild( div2 );

    var broadEl = document.createElement( 'div' );
    shell.appendChild( broadEl );
    var broadPanel = new BroadDiffPanel( broadEl, treeData.broad_differential || [] );

    /* 4 — Unlock on red flags cleared */
    var tree = null;
    new RedFlagPanel( panelEl, treeData.redflags, function () {
      algoSection.classList.remove( 'locked' );
      algoSection.classList.add( 'unlocked' );
      var body = document.getElementById( 'dt-algo-body' );
      body.innerHTML = '';
      if ( !tree ) tree = new DiagnosticTree( body, treeData );
      broadPanel.unlock();
    } );
  }

  /* ══════════════════════════════════════════════════════
     API LOADER — fetch JSON from wiki page
     ══════════════════════════════════════════════════════ */
  function loadPage( pageName, callback ) {
    var api = new mw.Api();
    api.get( {
      action:  'query',
      titles:  pageName,
      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' ) { callback( null, 'Page not found: ' + pageName ); return; }
      var content = pages[ pageId ].revisions[ 0 ].slots.main[ '*' ];
      /* Strip any wiki markup after the closing } (e.g. category tags) */
      var jsonEnd = content.lastIndexOf( '}' );
      try {
        callback( JSON.parse( content.substring( 0, jsonEnd + 1 ) ), null );
      } catch ( e ) {
        callback( null, 'Invalid JSON in ' + pageName + ': ' + e.message );
      }
    } ).fail( function () {
      callback( null, 'API request failed for: ' + pageName );
    } );
  }

  function bootHost( hostEl ) {
    var treePage = hostEl.getAttribute( 'data-tree-page' );
    if ( !treePage ) return;

    hostEl.innerHTML = '<div style="padding:1em;font-family:monospace;font-size:0.8em;color:#999">Loading...</div>';

    loadPage( treePage, function ( treeData, err ) {
      hostEl.innerHTML = '';
      if ( err ) {
        hostEl.innerHTML = '<div class="dt-error">' + err + '</div>';
        return;
      }
      bootShell( hostEl, treeData );
    } );
  }

  /* ══════════════════════════════════════════════════════
     MEDIAWIKI ENTRY POINT
     ══════════════════════════════════════════════════════ */
  function init() {
    document.querySelectorAll( '.diagnostic-tree-host' ).forEach( function ( el ) {
      bootHost( el );
    } );
  }

  if ( typeof mw !== 'undefined' ) {
    mw.hook( 'wikipage.content' ).add( init );
  }

}() );