🏠 Home

PatientPower

Practitioner

🧐PRACTITIONER-01 — Practitioner Coach

Uses your care team + appointments + cross-portal data · view prime directive
Hi — I'm your practitioner coach. I can help you prepare for upcoming visits, draft messages to your providers, suggest follow-ups, and assemble visit-prep checklists that pull from your tracked conditions, labs, and imaging.
PRACTITIONER-01 v1.0 · educational, not medical advice Close
PRACTITIONER-01 · Practitioner Coach
Tap to talk about your care team + visits
Ask anything. Or attach a picture (referral letter, visit summary).
My care team

Your providers, your appointments, your prep — in one place.

Track every Healthcare Provider on your care team. Log upcoming visits. Generate a visit-prep checklist that auto-pulls from your tracked conditions, labs, and imaging history across the other portals.

🔗Share with a physician

Create a secure invite link for a Healthcare Provider. Pick exactly what they can see. Expires automatically; you can revoke anytime.

👥My care team

Add every provider you see — PCP, specialists, dentist, mental health, etc. Stored on this device.

📅Upcoming appointments

Log appointments so the Visit Prep generator can target the right provider + visit type.

📝Visit prep generator

Pick a provider + visit type → generates a prioritized question checklist pulling from your tracked conditions, labs (Lab Decoder portal), and imaging history (X-rays portal). Print or copy for your visit.

Visit Prep - ' + provider + '

' + '
' + visitType + ' - generated ' + new Date().toLocaleDateString() + '
' + '
' + escapeHtml(content) + '
FEEDBACK
Beta Tester Feedback syncing…
Highlight anything → pick a type → post your feedback
How to give feedback: Highlight any text or feature, click the "+ Comment" bubble, pick a type, and post. Feedback is tagged with the page you posted from so we can triage by area.
Replying to
+ Comment
'); w.document.close(); w.print(); } function copyVisitPrep() { var content = document.getElementById('prep-output').innerText; try { navigator.clipboard.writeText(content).then(function(){ alert('Visit prep copied to clipboard.'); }); } catch(e) { alert('Copy failed. Select the text manually.'); } } function escapeHtml(s) { return (s || '').toString().replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // ═══════════════════════════════════════════════════════════════════ // PRACTITIONER-01 — Practitioner Coach Agent // Spec: ../../agents/PRACTITIONER-01/PRIME-DIRECTIVE.md // ═══════════════════════════════════════════════════════════════════ var PRAC01_ENDPOINT = '/.netlify/functions/chat'; var coachThread = []; function buildPractitioner01Prompt() { var providers = getProviders(); var appts = getAppointments(); var conds = getConds(); var labs = getLabs(); var imaging = getImaging(); var p = 'You are PRACTITIONER-01, the Practitioner Coach Agent on the PatientPower Practitioner portal. You are an evidence-grounded educator — not a Healthcare Provider.\n\n'; p += 'MISSION: help the user prepare for upcoming visits, draft messages to specific providers, identify missing specialists for their tracked conditions, and recommend follow-up appointment cadence.\n\n'; p += 'MANDATORY: cite the specific provider by name when discussing them, cite the specific appointment date when discussing prep, cite the user\'s actual tracked conditions/labs/imaging when generating prep questions.\n\n'; p += '── USER\'S CARE TEAM (' + providers.length + ' providers) ──\n'; if (!providers.length) p += ' (none added yet — nudge user to add their PCP first)\n'; else providers.forEach(function(pp) { p += ' - ' + pp.name + ' [' + pp.specialty + ']' + (pp.clinic ? ' @ ' + pp.clinic : '') + '\n'; }); p += '\n── UPCOMING APPOINTMENTS (' + appts.length + ') ──\n'; if (!appts.length) p += ' (none scheduled)\n'; else appts.slice().sort(function(a,b){return a.datetime.localeCompare(b.datetime);}).forEach(function(a) { var d = new Date(a.datetime); p += ' - ' + d.toISOString().slice(0,16).replace('T',' ') + ' · ' + a.provider + ' (' + a.type + ')' + (a.notes ? ' — ' + a.notes : '') + '\n'; }); p += '\n── CROSS-PORTAL CONTEXT ──\n'; p += ' Tracked conditions: ' + (conds.length ? conds.map(function(c){return c.name + ' [' + c.status + ']'}).join(', ') : 'none') + '\n'; p += ' Saved lab values: ' + labs.length + ' (latest tests: ' + [...new Set(labs.map(function(l){return l.test}))].slice(0,8).join(', ') + ')\n'; p += ' Imaging studies analyzed: ' + imaging.length + '\n'; p += '\nSPECIALIST-MATCH HEURISTICS (use to identify gaps):\n'; p += '- Diabetes / endocrine → Endocrinology + Primary Care\n'; p += '- Hypertension / cardiovascular / atherosclerosis → Cardiology + Primary Care\n'; p += '- Anxiety / depression / mental health → Psychiatry or Psychology + Primary Care\n'; p += '- RA / autoimmune / fibromyalgia → Rheumatology + Primary Care\n'; p += '- Parkinson\'s / dementia / ALS / MSA → Neurology (movement-disorder specialist if applicable)\n'; p += '- Cancer → Oncology + multidisciplinary team\n'; p += '- Kidney → Nephrology · Liver → Hepatology · Lymphedema → CLT · Pain → Pain Medicine + PT\n'; p += '\nOUTPUT FORMAT (brief markdown):\n'; p += '- For visit prep: a numbered list of 3-7 questions tied to user\'s actual data\n'; p += '- For specialist gaps: name the missing specialty + the condition it covers, with rationale\n'; p += '- For message drafts: a complete copy-pasteable message, professional tone, with subject line\n'; p += '- For follow-up cadence: per-condition recommended interval (e.g. "Diabetes: HbA1c every 3 months; annual eye + foot exam")\n\n'; p += 'SAFETY RAILS (NEVER do):\n'; p += '- Diagnose conditions\n'; p += '- Recommend specific medications or doses\n'; p += '- Claim a provider is "wrong" or recommend abandoning a provider\n'; p += '- Recommend skipping appointments\n'; p += '- Draft messages that demand specific treatments — frame as questions instead\n'; p += '- For acute symptoms (chest pain, sudden weakness, severe headache, bleeding, suicidal thoughts) → recommend ED / 911 / crisis line, not waiting for appointment\n'; return p; } function openCoach() { document.getElementById('coach-overlay').classList.add('open'); setTimeout(function(){ document.getElementById('coach-input').focus(); }, 50); } function closeCoach() { document.getElementById('coach-overlay').classList.remove('open'); } function coachQuick(text) { document.getElementById('coach-input').value = text; coachAsk(); } async function coachAsk() { var input = document.getElementById('coach-input'); var question = (input.value || '').trim(); if (!question) return; coachThread.push({ role:'user', content: question }); input.value = ''; renderCoachThread('thinking…'); var btn = document.getElementById('coach-send-btn'); btn.disabled = true; try { var resp = await fetch(PRAC01_ENDPOINT, { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ model:'claude-sonnet-4-6', max_tokens: 900, system: buildPractitioner01Prompt(), messages: coachThread.map(function(m){ return { role:m.role, content:m.content }; }) }) }); if (!resp.ok) { var errText = await resp.text(); var errMsg; try { errMsg = JSON.parse(errText).error || ('HTTP ' + resp.status); } catch(e) { errMsg = errText.startsWith('<') ? 'Server returned HTML' : ('HTTP ' + resp.status); } throw new Error(errMsg); } var data = await resp.json(); var raw = (data.content || []).filter(function(b){ return b.type === 'text' }).map(function(b){ return b.text }).join('') || '(no response)'; coachThread.push({ role:'assistant', content: raw }); if (window.PP_VOICE && PP_VOICE.speak) PP_VOICE.speak(raw); renderCoachThread(); } catch (e) { coachThread.push({ role:'assistant', content: '⚠️ Error: ' + (e.message || String(e)) }); renderCoachThread(); } finally { btn.disabled = false; } } function renderCoachThread(pendingHtml) { var thread = document.getElementById('coach-thread'); if (coachThread.length === 0 && !pendingHtml) { var em = document.getElementById('coach-empty'); if (em) em.style.display = ''; return; } var em = document.getElementById('coach-empty'); if (em) em.style.display = 'none'; var html = coachThread.map(function(m){ return '
' + mdLite(m.content) + '
'; }).join(''); if (pendingHtml) html += '
' + pendingHtml + '
'; thread.innerHTML = html; thread.scrollTop = thread.scrollHeight; } function mdLite(s) { return (s || '').replace(/&/g,'&').replace(//g,'>') .replace(/\*\*(.+?)\*\*/g,'$1') .replace(/\*(.+?)\*/g,'$1') .replace(/^\s*[-*]\s+(.+)$/gm,'
• $1
') .replace(/^\s*(\d+)\.\s+(.+)$/gm,'
$1. $2
') .replace(/\n/g,'
'); } // Init renderProviders(); updateProviderSelects(); renderAppointments(); // ═══════════════════════════════════════════════════════════════════ // Avatar-dominant UX: data toggle + picture attach → coach // ═══════════════════════════════════════════════════════════════════ function toggleDataSections(btn) { var sec = document.getElementById('data-sections'); var label = document.getElementById('data-toggle-label'); if (sec.classList.contains('hidden')) { sec.classList.remove('hidden'); btn.classList.add('open'); label.textContent = 'Hide page details'; } else { sec.classList.add('hidden'); btn.classList.remove('open'); label.textContent = 'Show page details'; } } var _portalVisionB64 = null, _portalVisionMime = null; function onPortalVisionPick(input) { if (!input.files || !input.files[0]) return; var file = input.files[0]; if (file.size > 20 * 1024 * 1024) { alert('Image too large (>20 MB).'); return; } var reader = new FileReader(); reader.onload = function(e) { var dataUrl = e.target.result, parts = dataUrl.split(','); _portalVisionB64 = parts[1]; _portalVisionMime = parts[0].match(/:(.*?);/)[1]; openCoach(); coachThread.push({ role:'user', content: '[Attached an image — please analyze it.]' }); renderCoachThread('analyzing image…'); coachAskWithImage(); }; reader.onerror = function() { alert('Failed to read image file.'); }; reader.readAsDataURL(file); input.value = ''; } async function coachAskWithImage() { if (!_portalVisionB64) return; var btn = document.getElementById('coach-send-btn'); if (btn) btn.disabled = true; try { var compressed = await compressVisionImage(_portalVisionB64, _portalVisionMime, 1568, 0.85); var visionSystemPrompt = buildPractitioner01Prompt() + '\n\nIMAGE CONTEXT: The user has attached a photo. Describe what you see in 1 sentence, then respond as Practitioner Coach. If you see anything concerning, surface it in the first sentence.'; var resp = await fetch(PRAC01_ENDPOINT, { method:'POST', headers:{ 'Content-Type':'application/json' }, body: JSON.stringify({ model:'claude-sonnet-4-6', max_tokens: 900, system: visionSystemPrompt, messages: [{ role:'user', content: [ { type:'image', source:{ type:'base64', media_type: compressed.mediaType, data: compressed.base64 } }, { type:'text', text: 'Please look at this image and tell me what you see and what I should do or know.' } ] }] }) }); if (!resp.ok) { var t = await resp.text(); throw new Error(t.startsWith('<') ? 'Server error' : ('HTTP ' + resp.status)); } var data = await resp.json(); var raw = (data.content || []).filter(function(b){ return b.type === 'text' }).map(function(b){ return b.text }).join('') || '(no response)'; coachThread.push({ role:'assistant', content: raw }); if (window.PP_VOICE && PP_VOICE.speak) PP_VOICE.speak(raw); renderCoachThread(); } catch (e) { coachThread.push({ role:'assistant', content: '⚠️ Error: ' + (e.message || String(e)) }); renderCoachThread(); } finally { if (btn) btn.disabled = false; _portalVisionB64 = null; _portalVisionMime = null; } } function compressVisionImage(base64, mediaType, maxDim, quality) { return new Promise(function(resolve) { var img = new Image(); img.onload = function() { var w = img.width, h = img.height; if (w <= maxDim && h <= maxDim && base64.length < 4 * 1024 * 1024) { resolve({ base64: base64, mediaType: mediaType }); return; } var scale = Math.min(maxDim / w, maxDim / h, 1); var canvas = document.createElement('canvas'); canvas.width = Math.round(w * scale); canvas.height = Math.round(h * scale); canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height); var compressed = canvas.toDataURL('image/jpeg', quality || 0.85); var parts = compressed.split(','); resolve({ base64: parts[1], mediaType: 'image/jpeg' }); }; img.onerror = function() { resolve({ base64: base64, mediaType: mediaType }); }; img.src = 'data:' + mediaType + ';base64,' + base64; }); }
FEEDBACK
Beta Tester Feedback syncing…
Highlight anything → pick a type → post your feedback
How to give feedback: Highlight any text or feature, click the "+ Comment" bubble, pick a type, and post. Feedback is tagged with the page you posted from so we can triage by area.
Replying to
+ Comment