MediaWiki:Gadget-DiagnosticTree.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* DiagnosticTree.js
* MediaWiki Gadget — Clinical Decision Support Tree
*
* INSTALLATION:
* 1. Copy this file to MediaWiki:Gadget-DiagnosticTree.js
* 2. Copy DiagnosticTree.css to MediaWiki:Gadget-DiagnosticTree.css
* 3. Add to MediaWiki:Gadgets-definition:
* * DiagnosticTree[ResourceLoader|default|type=general]|DiagnosticTree.js|DiagnosticTree.css
* 4. On any wiki page, embed a tree with:
* <div class="diagnostic-tree-host" data-tree-page="Data:DiagnosticTree/EarTMJ"></div>
* where data-tree-page is a wiki page containing the JSON tree definition.
*
* TESTING (standalone, without MediaWiki):
* Open DiagnosticTree_test.html in a browser.
*/
( function () {
'use strict';
/* ─── Colour / icon map by node type ──────────────────────────────────── */
var NODE_META = {
emergency: { color: '#c0392b', bg: '#fdf2f2', icon: '🚨', label: 'Emergency' },
urgent: { color: '#e67e22', bg: '#fef9f0', icon: '⚠️', label: 'Urgent Referral' },
rom: { color: '#2980b9', bg: '#f0f7fd', icon: '🔄', label: 'Movement Screen' },
nerve_entrapment:{ color: '#8e44ad', bg: '#f9f4fd', icon: '⚡', label: 'Nerve Screen' },
symptom: { color: '#16a085', bg: '#f0faf7', icon: '💬', label: 'Patient Symptom' },
examination: { color: '#27ae60', bg: '#f2faf4', icon: '🩺', label: 'Clinical Examination' },
result: { color: '#27ae60', bg: '#f2faf4', icon: '✅', label: 'Diagnosis' },
referral: { color: '#c0392b', bg: '#fdf2f2', icon: '🏥', label: 'Referral' },
overlap: { color: '#7f8c8d', bg: '#f8f9f9', icon: '🔍', label: 'Inconclusive' }
};
/* ─── History stack for Back button ───────────────────────────────────── */
function TreeState( tree ) {
this.tree = tree;
this.history = [];
this.current = tree.start;
}
TreeState.prototype.go = function ( nodeId ) {
this.history.push( this.current );
this.current = nodeId;
};
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 ];
};
/* ─── Renderer ─────────────────────────────────────────────────────────── */
function DiagnosticTree( hostEl, treeData ) {
this.host = hostEl;
this.state = new TreeState( treeData );
this.render();
}
DiagnosticTree.prototype.render = function () {
var self = this;
var node = this.state.node();
var meta = NODE_META[ node.type ] || NODE_META.symptom;
var canBack = this.state.history.length > 0;
/* Progress breadcrumb */
var progressHtml = this._renderProgress();
/* Node card */
var cardHtml = '';
if ( node.type === 'result' ) {
cardHtml = self._renderResult( node );
} else if ( node.type === 'referral' ) {
cardHtml = self._renderReferral( node );
} else if ( node.type === 'overlap' ) {
cardHtml = self._renderOverlap( node );
} else {
cardHtml = self._renderQuestion( node, meta );
}
/* Shell */
this.host.innerHTML =
'<div class="dt-shell">' +
'<div class="dt-header">' +
'<span class="dt-region">' + ( this.state.tree.region || 'Diagnostic Tree' ) + '</span>' +
'<div class="dt-controls">' +
( canBack ? '<button class="dt-btn dt-btn-back" id="dt-back">← Back</button>' : '' ) +
'<button class="dt-btn dt-btn-reset" id="dt-reset">↺ Restart</button>' +
'</div>' +
'</div>' +
progressHtml +
cardHtml +
'</div>';
/* Bind buttons */
var backBtn = this.host.querySelector( '#dt-back' );
var resetBtn = this.host.querySelector( '#dt-reset' );
if ( backBtn ) {
backBtn.addEventListener( 'click', function () {
self.state.back();
self.render();
} );
}
if ( resetBtn ) {
resetBtn.addEventListener( 'click', function () {
self.state.reset();
self.render();
} );
}
/* Yes / No answer buttons */
var yesBtn = this.host.querySelector( '#dt-yes' );
var noBtn = this.host.querySelector( '#dt-no' );
if ( yesBtn ) {
yesBtn.addEventListener( 'click', function () {
var node = self.state.node();
if ( node.yes ) { self.state.go( node.yes ); self.render(); }
} );
}
if ( noBtn ) {
noBtn.addEventListener( 'click', function () {
var node = self.state.node();
if ( node.no ) { self.state.go( node.no ); self.render(); }
} );
}
/* Result wiki link */
var wikiLink = this.host.querySelector( '.dt-wiki-link' );
if ( wikiLink ) {
wikiLink.addEventListener( 'click', function () {
var page = wikiLink.getAttribute( 'data-page' );
if ( page ) {
/* In MediaWiki context use mw.util.getUrl; in test use hash */
if ( typeof mw !== 'undefined' ) {
window.location.href = mw.util.getUrl( page );
} else {
alert( 'Would navigate to wiki page: ' + page );
}
}
} );
}
};
DiagnosticTree.prototype._renderProgress = function () {
var steps = this.state.history.length + 1;
var tree = this.state.tree;
var total = Object.keys( tree.nodes ).length;
var pct = Math.min( Math.round( ( steps / total ) * 100 ), 95 );
return '<div class="dt-progress-wrap">' +
'<div class="dt-progress-bar" style="width:' + pct + '%"></div>' +
'</div>' +
'<div class="dt-step-label">Step ' + steps + '</div>';
};
DiagnosticTree.prototype._renderQuestion = function ( node, meta ) {
var typeLabel = meta.label;
var rationale = node.clinical_rationale || node.distinguishing_feature || '';
var implicated = node.muscles_implicated ? node.muscles_implicated.join( ', ' ) : '';
var excluded = node.muscles_excluded ? node.muscles_excluded.join( ', ' ) : '';
var landmark = node.landmark ? '<div class="dt-landmark">📍 ' + node.landmark + '</div>' : '';
var movement = node.movement ? '<div class="dt-tag dt-tag-blue">Movement: ' + node.movement + ' (' + ( node.direction || '' ) + ')</div>' : '';
var nerve = node.nerve ? '<div class="dt-tag dt-tag-purple">Nerve: ' + node.nerve + '</div>' : '';
var examType = node.exam_type ? '<div class="dt-tag dt-tag-green">Test type: ' + node.exam_type.replace(/_/g,' ') + '</div>' : '';
var positiveFind = node.positive_finding ? '<div class="dt-positive">Positive finding: ' + node.positive_finding + '</div>' : '';
return '<div class="dt-card" style="border-color:' + meta.color + '; background:' + meta.bg + '">' +
'<div class="dt-type-badge" style="background:' + meta.color + '">' + meta.icon + ' ' + typeLabel + '</div>' +
'<div class="dt-question">' + node.question + '</div>' +
( movement || nerve || examType ? '<div class="dt-tags">' + movement + nerve + examType + '</div>' : '' ) +
landmark +
positiveFind +
( rationale ? '<div class="dt-rationale">' + rationale + '</div>' : '' ) +
( implicated ? '<div class="dt-muscle-hint dt-implicated">Suggests: ' + implicated + '</div>' : '' ) +
( excluded ? '<div class="dt-muscle-hint dt-excluded">Argues against: ' + excluded + '</div>' : '' ) +
'<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._renderResult = function ( node ) {
var confidence = node.confidence || 'moderate';
var confColor = { high: '#27ae60', moderate: '#e67e22', low: '#e74c3c' }[ confidence ] || '#7f8c8d';
var alsoHtml = node.also_consider && node.also_consider.length
? '<div class="dt-also">Also consider: ' + node.also_consider.join( ', ' ) + '</div>'
: '';
return '<div class="dt-card dt-card-result">' +
'<div class="dt-type-badge" style="background:#27ae60">✅ Likely Diagnosis</div>' +
'<div class="dt-result-name">' + node.diagnosis + '</div>' +
( node.confidence ? '<div class="dt-confidence" style="color:' + confColor + '">Confidence: ' + confidence + '</div>' : '' ) +
( 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>' : '' ) +
alsoHtml +
( node.wiki_page
? '<button class="dt-wiki-link" data-page="' + node.wiki_page + '">Open Wiki Page →</button>'
: '' ) +
'</div>';
};
DiagnosticTree.prototype._renderReferral = function ( node ) {
var isEmergency = node.urgency === 'emergency';
var color = isEmergency ? '#c0392b' : '#e67e22';
var bg = isEmergency ? '#fdf2f2' : '#fef9f0';
var icon = isEmergency ? '🚨' : '⚠️';
var label = isEmergency ? 'EMERGENCY — Act Now' : 'Urgent Referral Required';
return '<div class="dt-card dt-card-referral" style="border-color:' + color + ';background:' + bg + '">' +
'<div class="dt-type-badge" style="background:' + color + '">' + icon + ' ' + label + '</div>' +
'<div class="dt-referral-text">' + ( node.text || '' ) + '</div>' +
( node.action ? '<div class="dt-referral-action">' + node.action + '</div>' : '' ) +
( node.flag_label ? '<div class="dt-flag-label">' + node.flag_label + '</div>' : '' ) +
'</div>';
};
DiagnosticTree.prototype._renderOverlap = function ( node ) {
var screenList = 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:#7f8c8d">🔍 Inconclusive</div>' +
'<div class="dt-overlap-text">' + ( node.text || 'Findings inconclusive — multi-muscle involvement likely.' ) + '</div>' +
screenList +
( node.wiki_page
? '<button class="dt-wiki-link" data-page="' + node.wiki_page + '">Open Differential Page →</button>'
: '' ) +
'</div>';
};
/* ─── Loader: fetch JSON from a wiki page, then boot ───────────────────── */
function loadTreeFromWikiPage( pageName, hostEl ) {
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' ) {
hostEl.innerHTML = '<div class="dt-error">Tree data page not found: <code>' + pageName + '</code></div>';
return;
}
var content = pages[ pageId ].revisions[ 0 ].slots.main[ '*' ];
try {
var treeData = JSON.parse( content );
new DiagnosticTree( hostEl, treeData );
} catch ( e ) {
hostEl.innerHTML = '<div class="dt-error">Invalid JSON in <code>' + pageName + '</code>: ' + e.message + '</div>';
}
} ).fail( function () {
hostEl.innerHTML = '<div class="dt-error">Failed to load tree data.</div>';
} );
}
/* ─── MediaWiki entry point ────────────────────────────────────────────── */
function init() {
var hosts = document.querySelectorAll( '.diagnostic-tree-host' );
if ( !hosts.length ) { return; }
hosts.forEach( function ( el ) {
var treePage = el.getAttribute( 'data-tree-page' );
if ( treePage ) {
loadTreeFromWikiPage( treePage, el );
} else {
/* Inline JSON support: <div data-tree-inline='{"tree_id":...}'> */
var inlineData = el.getAttribute( 'data-tree-inline' );
if ( inlineData ) {
try {
new DiagnosticTree( el, JSON.parse( inlineData ) );
} catch ( e ) {
el.innerHTML = '<div class="dt-error">Invalid inline tree JSON: ' + e.message + '</div>';
}
}
}
} );
}
if ( typeof mw !== 'undefined' ) {
mw.hook( 'wikipage.content' ).add( init );
} else {
/* Standalone test mode */
window.DiagnosticTree = DiagnosticTree;
}
}() );