MediaWiki:Gadget-DiagnosticTree.js: Difference between revisions

From Painwiki
Jump to navigation Jump to search
No edit summary
No edit summary
Line 1: Line 1:
/**
/**
  * DiagnosticTree.js — v6
  * DiagnosticTree.js — v6 PATCH: choice node support
* Copy entire contents to: MediaWiki:Gadget-DiagnosticTree.js
  *
  *
  * EMBED ON ANY WIKI PAGE:
  * THREE changes required. Search for each FIND block and replace
  *   <div class="diagnostic-tree-host"
  * with the corresponding REPLACE block. Nothing else changes.
*        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.
* CHANGE 1 of 3 — Add 'choice' to NODE_META
  * ─────────────────────────────────────────────────────────────────
  *
  *
  * JSON PAGE STRUCTURE (data-tree-page):
  * FIND (the last entry in NODE_META, around line 13):
  *  {
  *
  *    "tree_id":  "...",
*    neuro_referral:   { color:'#b91c1c', icon:'', label:'Neurological Referral' }
  *    "region":    "...",
*
  *    "start":    "first-node-id",
* REPLACE WITH:
  *    "redflags": { "emergency": [...], "urgent": [...] },
*
  *    "broad_differential": [...],
  *    neuro_referral:  { color:'#b91c1c', icon:'', label:'Neurological Referral' },
  *     "nodes":    { ... }
*    choice:          { color:'#7c3aed', icon:'', label:'Select Best Match' }
  *   }
*
*
* ─────────────────────────────────────────────────────────────────
* CHANGE 2 of 3 — Route choice nodes to _choice() in render()
* ─────────────────────────────────────────────────────────────────
*
* FIND (inside DiagnosticTree.prototype.render, around line 120):
*
*    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 );
*
* REPLACE WITH:
*
  *    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 if ( node.type === 'choice'        ) cardHtml = this._choice( node, meta );
  *    else                                      cardHtml = this._question( node, meta );
*
*
* ─────────────────────────────────────────────────────────────────
* CHANGE 3 of 3 — Wire option button clicks in render()
*                AND add the _choice() method
* ─────────────────────────────────────────────────────────────────
*
* FIND (inside DiagnosticTree.prototype.render, the yes/no wiring
*      block near the bottom, around line 140):
*
*    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(); } } );
*
* REPLACE WITH:
*
  *    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-option-btn' ).forEach( function ( btn ) {
*      btn.addEventListener( 'click', function () {
*        var next = btn.getAttribute( 'data-next' );
  *         if ( next ) { s.state.go( next ); s.render(); }
*      } );
  *     } );
*
*
* ─────────────────────────────────────────────────────────────────
* Then add _choice() as a new method.
*
* FIND (the opening line of the existing _question method):
*
*    DiagnosticTree.prototype._question = function ( node, meta ) {
*
* INSERT the entire _choice() method ABOVE that line:
* ─────────────────────────────────────────────────────────────────
  */
  */


( function () {
// ─── INSERT THIS ENTIRE BLOCK above _question ───────────────────
  '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 ) {
DiagnosticTree.prototype._choice = function ( node, meta ) {
      btn.addEventListener( 'click', function ( e ) {
  var rat = node.clinical_rationale || '';
        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 opts = node.options.map( function ( opt, i ) {
    var s = this, isE = type === 'emergency';
     return '<button class="dt-option-btn" data-opt-index="' + i + '" data-next="' + opt.next + '">' +
    var cc = 'dt-flag-column' + ( isE ? '' : ' urgent' ) + ( allCleared ? ' all-cleared' : '' );
       '<div class="dt-option-content">' +
    var items = flags.map( function ( f ) { return s._item( f ); } ).join( '' );
         '<span class="dt-option-label">' + opt.label + '</span>' +
    var refs = '';
         ( opt.sublabel
    flags.forEach( function ( f ) {
           ? '<span class="dt-option-sublabel">' + opt.sublabel + '</span>'
      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>' +
      '<div class="dt-progress-wrap"><div class="dt-progress-bar" style="width:' + pct + '%"></div></div>' +
    '</button>';
      '<div class="dt-step-label">Step ' + steps + '</div>' +
  } ).join( '' );
      cardHtml;


     var b = function ( sel, fn ) {
  return '<div class="dt-card" style="border-color:' + meta.color + '22">' +
       var el = s.el.querySelector( sel );
     '<div class="dt-type-badge" style="background:' + meta.color + '">' +
      if ( el ) el.addEventListener( 'click', fn );
       meta.icon + ' ' + meta.label +
    };
    '</div>' +
    '<div class="dt-question">' + node.question + '</div>' +
    ( rat ? '<div class="dt-rationale">' + rat + '</div>' : '' ) +
    '<div class="dt-options">' + opts + '</div>' +
  '</div>';
};


    b( '#dt-back',  function () { s.state.back();  s.render(); } );
// ─── END INSERT ──────────────────────────────────────────────────
    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();
  * CSS ADDITIONS — append to MediaWiki:Gadget-DiagnosticTree.css
        var page = btn.getAttribute( 'data-page' );
  * ─────────────────────────────────────────────────────────────────
        if ( page ) window.location.href = mw.util.getUrl( page );
  *
      } );
  * Add the following rules at the end of the existing CSS file.
    } );
  * They use only existing CSS variables — no new variables needed.
  };
  */
 
  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 ) {
.dt-options {
    var conf = node.confidence || 'moderate';
  display: flex;
    var cc   = { high: '#15803d', moderate: '#b45309', low: '#b91c1c' }[ conf ] || '#78716c';
   flex-direction: column;
    var also = node.also_consider && node.also_consider.length
  gap: 0.5rem;
      ? '<div class="dt-also">Also consider: ' + node.also_consider.join( ', ' ) + '</div>' : '';
  margin-top: 1rem;
}


    /* Confirmatory checklist */
.dt-option-btn {
    var confirmatoryHtml = '';
  display: flex;
    if ( node.confirmatory && node.confirmatory.length ) {
  align-items: flex-start;
      confirmatoryHtml =
  gap: 0.85rem;
        '<div class="dt-confirmatory">' +
  width: 100%;
          '<div class="dt-confirmatory-header">✓ Confirm at point of diagnosis</div>' +
  padding: 0.75rem 0.9rem;
          '<div class="dt-confirmatory-items">' +
  background: var(--paper-2);
            node.confirmatory.map( function ( c ) {
  border: 1.5px solid var(--rule);
              return '<div class="dt-confirmatory-item"><div class="dt-confirmatory-dot"></div>' + c + '</div>';
  border-radius: 8px;
            } ).join( '' ) +
  cursor: pointer;
          '</div>' +
  text-align: left;
        '</div>';
  font-family: 'DM Sans', sans-serif;
    }
  position: relative;
  overflow: hidden;
  transition: border-color 0.15s, background 0.15s, transform 0.1s, box-shadow 0.15s;
}


    /* Division tabs — SCM */
.dt-option-btn::before {
    var divisionHtml = '';
  content: '';
    if ( node.division ) {
  position: absolute;
      divisionHtml =
  left: 0; top: 0; bottom: 0;
        '<div class="dt-division-tabs">' +
  width: 4px;
          '<button class="dt-division-tab active" data-panel="div-sternal">Sternal Division</button>' +
  background: var(--opt-color, var(--blue));
          '<button class="dt-division-tab" data-panel="div-clavicular">Clavicular Division</button>' +
  border-radius: 8px 0 0 8px;
        '</div>' +
  transition: width 0.15s;
        '<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 */
.dt-option-btn:hover {
    var hornerHtml = node.division
  border-color: var(--opt-color, var(--blue));
      ? '<div class="dt-confirm-panel">' +
  background: #fff;
          '<div class="dt-confirm-header">🔍 Confirmatory — Horner Syndrome Exclusion</div>' +
  transform: translateY(-1px);
          '<div class="dt-confirm-items">' +
  box-shadow: 0 3px 10px rgba(0,0,0,0.08);
            '<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 */
.dt-option-btn:hover::before { width: 6px; }
    var satelliteHtml = '';
.dt-option-btn:active        { transform: translateY(0); box-shadow: none; }
    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 */
.dt-option-btn[data-opt-index="0"] { --opt-color: var(--blue);   }
    var landingHtml = '';
.dt-option-btn[data-opt-index="1"] { --opt-color: var(--teal);  }
    if ( node.landing_page_topics && node.landing_page_topics.length ) {
.dt-option-btn[data-opt-index="2"] { --opt-color: var(--slate); }
      landingHtml =
.dt-option-btn[data-opt-index="3"] { --opt-color: var(--urgent); }
        '<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 */
.dt-option-btn[data-opt-index="0"]:hover { background: #eff6ff; border-color: var(--blue);  }
    var relatedHtml = node.related_pages && node.related_pages.length
.dt-option-btn[data-opt-index="1"]:hover { background: #f0fdfa; border-color: var(--teal);  }
      ? node.related_pages.map( function ( p ) {
.dt-option-btn[data-opt-index="2"]:hover { background: #f8fafc; border-color: var(--slate);  }
          return '<button class="dt-wiki-link satellite" data-page="' + p.page + '">' + p.label + '</button>';
.dt-option-btn[data-opt-index="3"]:hover { background: #fffbeb; border-color: var(--urgent); }
        } ).join( '' ) : '';


    return '<div class="dt-card dt-card-result">' +
.dt-option-content {
      '<div class="dt-type-badge" style="background:#15803d">✅ Likely Diagnosis</div>' +
  display: flex;
      '<div class="dt-result-name">' + node.diagnosis + '</div>' +
  flex-direction: column;
      '<div class="dt-confidence" style="color:' + cc + '">Confidence: ' + conf + '</div>' +
  gap: 0.15rem;
      divisionHtml +
   padding-left: 0.25rem;
      ( 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 ) {
.dt-option-label {
    var isE   = node.urgency === 'emergency';
   font-size: 0.9rem;
    var color = isE ? 'var(--emergency)' : 'var(--urgent)';
  font-weight: 600;
    var bdrCl = isE ? 'var(--emergency-border)' : 'var(--urgent-border)';
  color: var(--ink);
    var bgCl  = isE ? 'var(--emergency-bg)'    : 'var(--urgent-bg)';
  line-height: 1.35;
    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 ) {
.dt-option-sublabel {
    var sl = node.screen_these && node.screen_these.length
  font-size: 0.75rem;
      ? '<ul class="dt-screen-list">' +
  color: var(--ink-soft);
          node.screen_these.map( function ( m ) { return '<li>' + m + '</li>'; } ).join( '' ) +
  line-height: 1.4;
        '</ul>' : '';
  font-weight: 400;
    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 */
* SUMMARY OF ALL CHANGES
    var keyToggle = document.createElement( 'div' );
* ─────────────────────────────────────────────────────────────────
    keyToggle.className = 'dt-key-toggle';
*
    keyToggle.innerHTML =
* DiagnosticTree.js — 3 edits:
      '<button class="dt-key-tab pain-key active" id="dt-tab-pain">' +
*  1. Add 'choice' entry to NODE_META           (~1 line)
        '<span class="tab-icon">🩺</span> Pain Pattern Key' +
*   2. Add choice branch in render() dispatcher  (~1 line)
      '</button>' +
*  3. Add option button click wiring in render() (~5 lines)
      '<button class="dt-key-tab nerve-key' + ( hasNerveTree ? '' : ' coming-soon' ) + '" id="dt-tab-nerve"' +
  *  4. Add _choice() method above _question()   (~20 lines)
        ( hasNerveTree ? '' : ' title="Nerve entrapment key coming soon"' ) + '>' +
  *
        '<span class="tab-icon"></span> Nerve Entrapment Key' +
* DiagnosticTree.css — 1 edit:
        ( hasNerveTree ? '' : '<span class="tab-soon">coming soon</span>' ) +
*   Append ~50 lines of new rules at the end of the file.
      '</button>';
*   All rules use only existing CSS variables.
    shell.appendChild( keyToggle );
*  No existing rules are modified.
 
*
    /* 1 — Red flag panel (shared) */
* Backward compatibility:
    var panelEl = document.createElement( 'div' );
*  All existing binary (yes/no) nodes are completely unchanged.
    shell.appendChild( panelEl );
*   The new code only activates when node.type === 'choice'.
 
*   Existing EarTMJ and FrontOfChest trees work identically.
    /* 2 — Pain algorithm section */
*
    var algoSection = document.createElement( 'div' );
* JSON schema for a choice node:
    algoSection.className = 'dt-algo-section locked';
*  {
    algoSection.innerHTML =
*     "type": "choice",
      '<div class="dt-algo-header">' +
  *    "question": "...",
        '<div class="dt-algo-header-left"> Pain Pattern Algorithm</div>' +
  *    "clinical_rationale": "...",   <- optional
        '<div class="dt-algo-header-right">Complete checklist above to unlock</div>' +
  *     "options": [
      '</div>' +
  *       { "label": "...", "sublabel": "...", "next": "node-id" },
      '<div class="dt-algo-body" id="dt-algo-body">' +
*       { "label": "...", "sublabel": "...", "next": "node-id" },
        '<div class="dt-tree-locked-msg">Work through all emergency and urgent flags above before proceeding.</div>' +
*      { "label": "...", "sublabel": "...", "next": "node-id" }
      '</div>';
*     ]
    shell.appendChild( algoSection );
*   }
 
*  sublabel is optional. Maximum 4 options (index 0–3 are styled).
    /* 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 );
  }
 
}() );

Revision as of 12:21, 9 May 2026

/**
 * DiagnosticTree.js — v6 PATCH: choice node support
 *
 * THREE changes required. Search for each FIND block and replace
 * with the corresponding REPLACE block. Nothing else changes.
 *
 * ─────────────────────────────────────────────────────────────────
 * CHANGE 1 of 3 — Add 'choice' to NODE_META
 * ─────────────────────────────────────────────────────────────────
 *
 * FIND (the last entry in NODE_META, around line 13):
 *
 *     neuro_referral:   { color:'#b91c1c', icon:'', label:'Neurological Referral' }
 *
 * REPLACE WITH:
 *
 *     neuro_referral:   { color:'#b91c1c', icon:'', label:'Neurological Referral' },
 *     choice:           { color:'#7c3aed', icon:'', label:'Select Best Match' }
 *
 *
 * ─────────────────────────────────────────────────────────────────
 * CHANGE 2 of 3 — Route choice nodes to _choice() in render()
 * ─────────────────────────────────────────────────────────────────
 *
 * FIND (inside DiagnosticTree.prototype.render, around line 120):
 *
 *     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 );
 *
 * REPLACE WITH:
 *
 *     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 if ( node.type === 'choice'        ) cardHtml = this._choice( node, meta );
 *     else                                      cardHtml = this._question( node, meta );
 *
 *
 * ─────────────────────────────────────────────────────────────────
 * CHANGE 3 of 3 — Wire option button clicks in render()
 *                 AND add the _choice() method
 * ─────────────────────────────────────────────────────────────────
 *
 * FIND (inside DiagnosticTree.prototype.render, the yes/no wiring
 *       block near the bottom, around line 140):
 *
 *     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(); } } );
 *
 * REPLACE WITH:
 *
 *     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-option-btn' ).forEach( function ( btn ) {
 *       btn.addEventListener( 'click', function () {
 *         var next = btn.getAttribute( 'data-next' );
 *         if ( next ) { s.state.go( next ); s.render(); }
 *       } );
 *     } );
 *
 *
 * ─────────────────────────────────────────────────────────────────
 * Then add _choice() as a new method.
 *
 * FIND (the opening line of the existing _question method):
 *
 *     DiagnosticTree.prototype._question = function ( node, meta ) {
 *
 * INSERT the entire _choice() method ABOVE that line:
 * ─────────────────────────────────────────────────────────────────
 */

// ─── INSERT THIS ENTIRE BLOCK above _question ───────────────────

DiagnosticTree.prototype._choice = function ( node, meta ) {
  var rat = node.clinical_rationale || '';

  var opts = node.options.map( function ( opt, i ) {
    return '<button class="dt-option-btn" data-opt-index="' + i + '" data-next="' + opt.next + '">' +
      '<div class="dt-option-content">' +
        '<span class="dt-option-label">' + opt.label + '</span>' +
        ( opt.sublabel
          ? '<span class="dt-option-sublabel">' + opt.sublabel + '</span>'
          : '' ) +
      '</div>' +
    '</button>';
  } ).join( '' );

  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>' +
    ( rat ? '<div class="dt-rationale">' + rat + '</div>' : '' ) +
    '<div class="dt-options">' + opts + '</div>' +
  '</div>';
};

// ─── END INSERT ──────────────────────────────────────────────────


/**
 * ─────────────────────────────────────────────────────────────────
 * CSS ADDITIONS — append to MediaWiki:Gadget-DiagnosticTree.css
 * ─────────────────────────────────────────────────────────────────
 *
 * Add the following rules at the end of the existing CSS file.
 * They use only existing CSS variables — no new variables needed.
 */

/*

.dt-options {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 1rem;
}

.dt-option-btn {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  width: 100%;
  padding: 0.75rem 0.9rem;
  background: var(--paper-2);
  border: 1.5px solid var(--rule);
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  font-family: 'DM Sans', sans-serif;
  position: relative;
  overflow: hidden;
  transition: border-color 0.15s, background 0.15s, transform 0.1s, box-shadow 0.15s;
}

.dt-option-btn::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 4px;
  background: var(--opt-color, var(--blue));
  border-radius: 8px 0 0 8px;
  transition: width 0.15s;
}

.dt-option-btn:hover {
  border-color: var(--opt-color, var(--blue));
  background: #fff;
  transform: translateY(-1px);
  box-shadow: 0 3px 10px rgba(0,0,0,0.08);
}

.dt-option-btn:hover::before { width: 6px; }
.dt-option-btn:active        { transform: translateY(0); box-shadow: none; }

.dt-option-btn[data-opt-index="0"] { --opt-color: var(--blue);   }
.dt-option-btn[data-opt-index="1"] { --opt-color: var(--teal);   }
.dt-option-btn[data-opt-index="2"] { --opt-color: var(--slate);  }
.dt-option-btn[data-opt-index="3"] { --opt-color: var(--urgent); }

.dt-option-btn[data-opt-index="0"]:hover { background: #eff6ff; border-color: var(--blue);   }
.dt-option-btn[data-opt-index="1"]:hover { background: #f0fdfa; border-color: var(--teal);   }
.dt-option-btn[data-opt-index="2"]:hover { background: #f8fafc; border-color: var(--slate);  }
.dt-option-btn[data-opt-index="3"]:hover { background: #fffbeb; border-color: var(--urgent); }

.dt-option-content {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding-left: 0.25rem;
}

.dt-option-label {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  line-height: 1.35;
}

.dt-option-sublabel {
  font-size: 0.75rem;
  color: var(--ink-soft);
  line-height: 1.4;
  font-weight: 400;
}

*/

/**
 * ─────────────────────────────────────────────────────────────────
 * SUMMARY OF ALL CHANGES
 * ─────────────────────────────────────────────────────────────────
 *
 * DiagnosticTree.js — 3 edits:
 *   1. Add 'choice' entry to NODE_META           (~1 line)
 *   2. Add choice branch in render() dispatcher  (~1 line)
 *   3. Add option button click wiring in render() (~5 lines)
 *   4. Add _choice() method above _question()    (~20 lines)
 *
 * DiagnosticTree.css — 1 edit:
 *   Append ~50 lines of new rules at the end of the file.
 *   All rules use only existing CSS variables.
 *   No existing rules are modified.
 *
 * Backward compatibility:
 *   All existing binary (yes/no) nodes are completely unchanged.
 *   The new code only activates when node.type === 'choice'.
 *   Existing EarTMJ and FrontOfChest trees work identically.
 *
 * JSON schema for a choice node:
 *   {
 *     "type": "choice",
 *     "question": "...",
 *     "clinical_rationale": "...",   <- optional
 *     "options": [
 *       { "label": "...", "sublabel": "...", "next": "node-id" },
 *       { "label": "...", "sublabel": "...", "next": "node-id" },
 *       { "label": "...", "sublabel": "...", "next": "node-id" }
 *     ]
 *   }
 *   sublabel is optional. Maximum 4 options (index 0–3 are styled).
 * ─────────────────────────────────────────────────────────────────
 */