Suggestion only. Verify against your institution\'s coding guidelines.
';
+ text.innerHTML = html;
+ card.classList.remove('hidden');
+ card.scrollIntoView({ behavior: 'smooth' });
+ }
+
+ // ── Save draft (manual / auto on stage advance) ──────────────────────
+ function autoSaveDraft() {
+ var label = getVal('ed-label');
+ if (!label.trim()) return; // need a label to save; skip silently
+ if (typeof saveEncounter !== 'function') return;
+ var fullTranscript = _state.stages.map(function(s) { return s.transcript || ''; }).join('\n\n');
+ var partial = { stages: _state.stages, finalized: false };
+ saveEncounter({
+ id: window._savedEncId_ed || null,
+ label: label,
+ enc_type: 'ed',
+ transcript: fullTranscript,
+ generated_note: lastStage() ? lastStage().note : '',
+ partial_data: partial,
+ status: 'draft',
+ idempotency_key: 'ed-draft-' + (window._savedEncId_ed || 'new'),
+ onSaved: function(id) {
+ window._savedEncId_ed = id;
+ try { sessionStorage.setItem('_savedEncId_ed', id); } catch(e) {}
+ }
+ });
+ }
+
+ function manualSaveDraft() {
+ if (!getVal('ed-label').trim()) { showToast('Enter a patient label first', 'error'); return; }
+ autoSaveDraft();
+ showToast('Draft saved', 'success');
+ }
+
+ // ── Reset / new patient ──────────────────────────────────────────────
+ function resetEncounter() {
+ _state = freshState();
+ setVal('ed-label', '');
+ setVal('ed-age', '');
+ setVal('ed-gender', '');
+ setVal('ed-cc', '');
+ setText('ed-transcript', '');
+ setText('ed-note-text', '');
+ _liveTranscript = '';
+ hideEl('ed-note-output');
+ hideEl('ed-dontmiss-card');
+ hideEl('ed-mdm-card');
+ var modelTag = document.getElementById('ed-note-model-tag');
+ if (modelTag) modelTag.textContent = '';
+ window._savedEncId_ed = null;
+ try { sessionStorage.removeItem('_savedEncId_ed'); } catch(e) {}
+ clearLocal();
+ updateStageTags();
+ showToast('ED encounter cleared for new patient', 'info');
+ }
+
+ // ── Wire click handlers ──────────────────────────────────────────────
+ document.addEventListener('click', function(e) {
+ if (e.target.closest('#btn-ed-generate')) generateStage();
+ if (e.target.closest('#btn-ed-add-more')) advanceStage();
+ if (e.target.closest('#btn-ed-finalize')) finalize();
+ if (e.target.closest('#btn-ed-new')) resetEncounter();
+ if (e.target.closest('#btn-ed-save')) manualSaveDraft();
+ if (e.target.closest('#ed-refine-btn')) refineDocument('ed-note-text', 'ed-refine-input');
+ if (e.target.closest('#ed-shorten-btn')) shortenDocument('ed-note-text');
+ });
+
+ // Persist on input changes
+ document.addEventListener('input', function(e) {
+ if (!e.target) return;
+ var id = e.target.id;
+ if (id === 'ed-label' || id === 'ed-age' || id === 'ed-gender' || id === 'ed-cc' || id === 'ed-transcript' || id === 'ed-note-text') {
+ persistLocal();
+ }
+ });
+
+ // ── Load handler (resume from saved encounters list) ────────────────
+ if (typeof registerEncounterLoadHandler === 'function') {
+ registerEncounterLoadHandler('ed', function(enc) {
+ // Reset and restore from the row
+ _state = freshState();
+ setVal('ed-label', enc.label || '');
+ var partial = enc.partial_data;
+ try { if (typeof partial === 'string') partial = JSON.parse(partial); } catch (e) { partial = null; }
+ if (partial && partial.stages) {
+ _state.stages = partial.stages;
+ _state.stage = partial.stages.length || 1;
+ _state.finalized = !!partial.finalized;
+ _state.mdm = partial.mdm || null;
+ var last = lastStage();
+ if (last) {
+ setText('ed-note-text', last.note || '');
+ renderDontMiss(last.dontMiss || []);
+ showOutput();
+ }
+ if (_state.mdm) renderMdm(_state.mdm);
+ } else if (enc.generated_note) {
+ setText('ed-note-text', enc.generated_note);
+ showOutput();
+ }
+ if (enc.transcript) setText('ed-transcript', enc.transcript);
+ updateStageTags();
+ });
+ }
+
+ // ── Init on tab activation ───────────────────────────────────────────
+ var _inited = false;
+ document.addEventListener('tabChanged', function(e) {
+ if (e.detail.tab !== 'ed' || _inited) return;
+ _inited = true;
+ initRecording();
+ loadLocal();
+ updateStageTags();
+ });
+
+ console.log('ED encounters module loaded');
+})();
diff --git a/public/js/encounters.js b/public/js/encounters.js
index 657a85a..5ae8844 100644
--- a/public/js/encounters.js
+++ b/public/js/encounters.js
@@ -95,7 +95,7 @@
// Restore saved encounter IDs from sessionStorage (survive page refresh, cleared on tab close)
(function() {
- ['encounter','dictation','hospital','chart','wellvisit','sickvisit','soap'].forEach(function(t) {
+ ['encounter','dictation','ed','hospital','chart','wellvisit','sickvisit','soap'].forEach(function(t) {
try {
var id = sessionStorage.getItem('_savedEncId_' + t);
if (id) window['_savedEncId_' + t] = id;
@@ -306,7 +306,8 @@
encounter: 'encounter', dictation: 'dictation',
hospital: 'hospital', chart: 'chart',
soap: 'soap', milestones: 'milestones',
- wellvisit: 'wellvisit', sickvisit: 'sickvisit'
+ wellvisit: 'wellvisit', sickvisit: 'sickvisit',
+ ed: 'ed'
};
var tabName = tabMap[enc.enc_type] || 'encounter';
var tabBtn = document.querySelector('.tab-btn[data-tab="' + tabName + '"]');
@@ -443,12 +444,6 @@
};
var noteEl = document.getElementById(noteIdMap[type] || (prefix + '-hpi-text'));
- // Save any corrections (Dragon-like learning) before saving encounter
- var noteElId = noteIdMap[type] || (prefix + '-hpi-text');
- if (typeof saveCorrection === 'function') {
- saveCorrection(noteElId, type);
- }
-
window.saveEncounter({
id: savedId,
label: label,
diff --git a/public/js/liveEncounter.js b/public/js/liveEncounter.js
index 2a0bfd3..4554cfd 100644
--- a/public/js/liveEncounter.js
+++ b/public/js/liveEncounter.js
@@ -209,7 +209,6 @@
if (data.success) {
setOutputText(hpiText, data.hpi);
storeSourceContext('enc-hpi-text', transcript.innerText.trim());
- if (typeof trackAIOutput === 'function') trackAIOutput('enc-hpi-text', data.hpi);
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });
diff --git a/public/js/memories.js b/public/js/memories.js
index f4643c3..ef297bc 100644
--- a/public/js/memories.js
+++ b/public/js/memories.js
@@ -17,18 +17,10 @@
template_hpi: 'HPI Template',
template_wellvisit: 'Well Visit Template',
template_sickvisit: 'Sick Visit Template',
+ template_ed: 'ED Template',
custom: 'Custom'
};
- // Correction categories (hidden from manual editing, managed by correction tracker)
- var CORRECTION_LABELS = {
- correction_soap: 'SOAP Correction',
- correction_hpi: 'HPI Correction',
- correction_encounter: 'Encounter Correction',
- correction_wellvisit: 'Well Visit Correction',
- correction_sickvisit: 'Sick Visit Correction'
- };
-
// ── Load memories ────────────────────────────────────────────────────────
function loadMemories() {
@@ -45,16 +37,11 @@
function renderMemoryList() {
var container = document.getElementById('mem-list');
if (!container) return;
- // Separate templates from corrections
- var templates = _memories.filter(function(m) { return !m.category.startsWith('correction_'); });
- var corrections = _memories.filter(function(m) { return m.category.startsWith('correction_'); });
- // Render corrections list
- renderCorrectionsList(corrections);
- if (templates.length === 0) {
+ if (_memories.length === 0) {
container.innerHTML = 'No templates saved yet. Add one above.
';
return;
}
- container.innerHTML = templates.map(function(m) {
+ container.innerHTML = _memories.map(function(m) {
var catLabel = CATEGORY_LABELS[m.category] || m.category;
var preview = (m.content || '').substring(0, 100).replace(/\n/g, ' ');
return '' +
@@ -78,75 +65,6 @@
});
}
- function renderCorrectionsList(corrections) {
- var container = document.getElementById('corrections-list');
- if (!container) return;
- if (corrections.length === 0) {
- container.innerHTML = '
No corrections yet. Edit AI-generated notes and save to start learning.
';
- return;
- }
- container.innerHTML = corrections.map(function(m) {
- var catLabel = CORRECTION_LABELS[m.category] || m.category;
- var preview = (m.name || '').replace(/\n/g, ' ');
- // Parse original and corrected from content
- var parts = parseCorrection(m.content || '');
- var date = m.created_at ? new Date(m.created_at).toLocaleDateString() : '';
- return '
' +
- '' +
- '
' +
- '
' +
- '
Original (AI generated):
' +
- '
' + esc(parts.original) + '
' +
- '
' +
- '
' +
- '
Corrected to:
' +
- '
' + esc(parts.corrected) + '
' +
- '
' +
- '
' +
- '
';
- }).join('');
-
- // Toggle expand/collapse
- container.querySelectorAll('.correction-header').forEach(function(header) {
- header.addEventListener('click', function(e) {
- if (e.target.closest('.correction-delete-btn')) return;
- var id = header.dataset.toggle;
- var detail = document.getElementById('detail-' + id);
- var arrow = document.getElementById('arrow-' + id);
- if (detail) {
- detail.classList.toggle('hidden');
- if (arrow) arrow.style.transform = detail.classList.contains('hidden') ? '' : 'rotate(90deg)';
- }
- });
- });
-
- container.querySelectorAll('.correction-delete-btn').forEach(function(btn) {
- btn.addEventListener('click', function(e) {
- e.stopPropagation();
- deleteMemory(parseInt(btn.dataset.id));
- });
- });
- }
-
- function parseCorrection(content) {
- var original = '';
- var corrected = '';
- var idx = content.indexOf('\nCORRECTED TO: ');
- if (idx !== -1) {
- original = content.substring(0, idx).replace(/^ORIGINAL:\s*/i, '');
- corrected = content.substring(idx + '\nCORRECTED TO: '.length);
- } else {
- original = content;
- }
- return { original: original.trim(), corrected: corrected.trim() };
- }
-
// ── Save memory ──────────────────────────────────────────────────────────
function saveMemory() {
diff --git a/public/js/notes.js b/public/js/notes.js
index 04a6c32..d823f0a 100644
--- a/public/js/notes.js
+++ b/public/js/notes.js
@@ -595,11 +595,13 @@
updateStatus(msg, 'err'); setRecUI('idle'); return;
}
updateStatus('Generating note…', 'saving');
+ var modelEl = document.getElementById('notes-model-select');
+ var selectedModel = modelEl ? modelEl.value : '';
return fetch('/api/notes/from-voice', {
method: 'POST',
credentials: 'include',
headers: Object.assign({ 'Content-Type': 'application/json' }, getAuthHeaders()),
- body: JSON.stringify({ transcript: resp.text }),
+ body: JSON.stringify({ transcript: resp.text, model: selectedModel || undefined }),
})
.then(function(r) { return r.json(); })
.then(function(data) {
diff --git a/public/js/shadess.js b/public/js/shadess.js
index 72e3e76..c094674 100644
--- a/public/js/shadess.js
+++ b/public/js/shadess.js
@@ -911,7 +911,6 @@
var outCard = document.getElementById('wv-note-output');
var tag = document.getElementById('wv-note-model-tag');
if (noteEl) setOutputText(noteEl, data.note);
- if (typeof trackAIOutput === 'function') trackAIOutput('wv-note-text', data.note);
// Store source for refine — collect all well visit inputs
var wvSource = [];
var wvTranscript = document.getElementById('wv-transcript');
diff --git a/public/js/sickVisit.js b/public/js/sickVisit.js
index 00c2aa5..660b3c5 100644
--- a/public/js/sickVisit.js
+++ b/public/js/sickVisit.js
@@ -333,7 +333,6 @@
var outCard = document.getElementById('sick-note-output');
var tag = document.getElementById('sick-note-model-tag');
if (noteEl) setOutputText(noteEl, data.note);
- if (typeof trackAIOutput === 'function') trackAIOutput('sick-note-text', data.note);
var sickTranscript = document.getElementById('sick-transcript');
if (sickTranscript) storeSourceContext('sick-note-text', sickTranscript.innerText.trim());
if (tag) tag.textContent = (data.model || '').split('/').pop();
diff --git a/public/js/soap.js b/public/js/soap.js
index eb69ec3..4f89838 100644
--- a/public/js/soap.js
+++ b/public/js/soap.js
@@ -190,7 +190,6 @@
if (data.success) {
setOutputText(soapText, data.soap);
storeSourceContext('soap-text', transcript.innerText.trim());
- if (typeof trackAIOutput === 'function') trackAIOutput('soap-text', data.soap);
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });
diff --git a/public/js/voiceDictation.js b/public/js/voiceDictation.js
index faa1b53..980c76c 100644
--- a/public/js/voiceDictation.js
+++ b/public/js/voiceDictation.js
@@ -183,7 +183,6 @@
if (data.success) {
setOutputText(hpiText, data.hpi || data.soap);
storeSourceContext('dict-hpi-text', transcript.innerText.trim());
- if (typeof trackAIOutput === 'function') trackAIOutput('dict-hpi-text', data.hpi || data.soap);
modelTag.textContent = (data.model || '').split('/').pop();
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });
diff --git a/scripts/lint-references.js b/scripts/lint-references.js
index 4eeb61e..d572237 100755
--- a/scripts/lint-references.js
+++ b/scripts/lint-references.js
@@ -102,7 +102,6 @@ const DYNAMIC_ID_PREFIXES = [
'nc-', // nextcloud settings
'mem-', // memories
'doc-', // documents
- 'corrections-', // corrections list
'audio-', // audio backups
'saved-', // saved encounters
'ext-', // extensions CRUD
diff --git a/server.js b/server.js
index 2dce218..d05152c 100644
--- a/server.js
+++ b/server.js
@@ -297,6 +297,7 @@ app.use('/api', require('./src/routes/billing'));
app.use('/api/sessions', require('./src/routes/sessions'));
app.use('/api', require('./src/routes/wellVisit'));
app.use('/api', require('./src/routes/sickVisit'));
+app.use('/api', require('./src/routes/edEncounters'));
app.use('/api/user', require('./src/routes/userPreferences'));
app.use('/api/admin/learning', require('./src/routes/learningAI'));
diff --git a/src/routes/edEncounters.js b/src/routes/edEncounters.js
new file mode 100644
index 0000000..42257b3
--- /dev/null
+++ b/src/routes/edEncounters.js
@@ -0,0 +1,161 @@
+// ============================================================
+// ED ENCOUNTER ROUTE — multi-stage emergency department note
+// generation with don't-miss tooltips and 2023 E/M MDM finalize.
+//
+// Two endpoints, both auth-gated:
+// POST /api/ed-encounters/generate — per-stage note + tooltips
+// POST /api/ed-encounters/finalize — MDM block for billing
+//
+// Stage state lives client-side (localStorage + draft row in
+// saved_encounters via the existing encounters route). This file
+// is purely AI plumbing — it does not own persistence.
+// ============================================================
+
+var express = require('express');
+var router = express.Router();
+var { callAI } = require('../utils/ai');
+var PROMPTS = require('../utils/prompts');
+var { authMiddleware } = require('../middleware/auth');
+var logger = require('../utils/logger');
+var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
+
+router.use(authMiddleware);
+
+// Models sometimes wrap JSON in ```json fences or prepend prose. Strip the
+// fence and recover the JSON object between the first { and last }.
+function extractJson(raw) {
+ var t = String(raw || '').trim();
+ t = t.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
+ var s = t.indexOf('{');
+ if (s > 0) t = t.substring(s);
+ var e = t.lastIndexOf('}');
+ if (e > -1 && e < t.length - 1) t = t.substring(0, e + 1);
+ try { return JSON.parse(t); } catch (err) { return null; }
+}
+
+// ── POST /ed-encounters/generate — one stage of an ED encounter ─────
+// Body:
+// stage (1, 2, ...) — informational, drives "previous note" handling
+// transcript — current stage's voice transcript (required)
+// chiefComplaint — string, required
+// patientAge — string, optional but strongly preferred
+// patientGender — string, optional
+// previousNote — string, only on stage > 1, the last generated note
+// physicianMemories — string from /api/memories/context, optional
+// model — admin/user-selected model id, optional
+// Returns: { success, note, dontMiss: [{point, why}], model }
+router.post('/ed-encounters/generate', async function (req, res) {
+ var start = Date.now();
+ try {
+ var {
+ stage, transcript, chiefComplaint,
+ patientAge, patientGender,
+ previousNote, physicianMemories, model
+ } = req.body;
+
+ if (!chiefComplaint || !chiefComplaint.trim()) {
+ return res.status(400).json({ error: 'Chief complaint is required' });
+ }
+ if (!transcript || !transcript.trim()) {
+ return res.status(400).json({ error: 'Transcript is required' });
+ }
+
+ var stageNum = parseInt(stage, 10) || 1;
+
+ var context = 'ED ENCOUNTER — STAGE ' + stageNum + '\n';
+ context += 'Patient: ' + (patientAge || 'Unknown age') + ', ' + (patientGender || 'Unknown gender') + '\n';
+ context += 'Chief Complaint: ' + wrapUserText('chief_complaint', chiefComplaint) + '\n\n';
+ context += 'CURRENT STAGE TRANSCRIPT (may include direct physician asides — preserve and route them per the prompt rules):\n'
+ + wrapUserText('transcript', transcript) + '\n\n';
+
+ if (previousNote && previousNote.trim() && stageNum > 1) {
+ context += 'PREVIOUS-STAGE NOTE (baseline to integrate on top of — do not start fresh):\n'
+ + wrapUserText('previous_note', previousNote) + '\n\n';
+ }
+ if (physicianMemories && physicianMemories.trim()) {
+ context += physicianMemories + '\n\n';
+ }
+
+ var result = await callAI([
+ { role: 'system', content: PROMPTS.edEncounterStaged + INJECTION_GUARD },
+ { role: 'user', content: context }
+ ], { model: model, maxTokens: 4000 });
+
+ var parsed = extractJson(result.content);
+ if (!parsed || typeof parsed.note !== 'string') {
+ // Recovery: if the model returned plain prose, treat the whole response
+ // as the note and return an empty don't-miss list rather than 500-ing.
+ parsed = { note: String(result.content || '').trim(), dontMiss: [] };
+ }
+ if (!Array.isArray(parsed.dontMiss)) parsed.dontMiss = [];
+ parsed.dontMiss = parsed.dontMiss
+ .filter(function (d) { return d && d.point; })
+ .map(function (d) { return { point: String(d.point).trim(), why: String(d.why || '').trim() }; });
+
+ var dur = Date.now() - start;
+ logger.apiCall(req.user.id, '/api/ed-encounters/generate', {
+ model: result.model,
+ tokensInput: result.usage && result.usage.input_tokens,
+ tokensOutput: result.usage && result.usage.output_tokens,
+ duration: dur,
+ statusCode: 200
+ });
+ logger.audit(req.user.id, 'generate_ed_encounter', 'ED encounter stage ' + stageNum, req, { category: 'clinical' });
+
+ res.json({ success: true, note: parsed.note, dontMiss: parsed.dontMiss, model: result.model });
+ } catch (e) {
+ logger.error('[edEncounters] generate failed', e.message);
+ res.status(500).json({ error: 'Request failed' });
+ }
+});
+
+// ── POST /ed-encounters/finalize — generate 2023 E/M MDM block ─────
+// Body:
+// note — final clinical note (required)
+// transcript — concatenated transcript across all stages (optional but recommended)
+// patientAge — string, optional
+// model — optional override
+// Returns: { success, mdm: {...}, model }
+router.post('/ed-encounters/finalize', async function (req, res) {
+ var start = Date.now();
+ try {
+ var { note, transcript, patientAge, model } = req.body;
+
+ if (!note || !note.trim()) {
+ return res.status(400).json({ error: 'Final note is required' });
+ }
+
+ var context = 'PATIENT: ' + (patientAge || 'Unknown age') + '\n\n';
+ context += 'FINAL CLINICAL NOTE:\n' + wrapUserText('note', note) + '\n\n';
+ if (transcript && transcript.trim()) {
+ context += 'FULL ENCOUNTER TRANSCRIPT (all stages):\n' + wrapUserText('transcript', transcript) + '\n';
+ }
+
+ var result = await callAI([
+ { role: 'system', content: PROMPTS.edFinalize + INJECTION_GUARD },
+ { role: 'user', content: context }
+ ], { model: model, maxTokens: 2000 });
+
+ var parsed = extractJson(result.content);
+ if (!parsed || !parsed.mdm) {
+ return res.status(502).json({ error: 'AI did not return a parseable MDM block', raw: result.content });
+ }
+
+ var dur = Date.now() - start;
+ logger.apiCall(req.user.id, '/api/ed-encounters/finalize', {
+ model: result.model,
+ tokensInput: result.usage && result.usage.input_tokens,
+ tokensOutput: result.usage && result.usage.output_tokens,
+ duration: dur,
+ statusCode: 200
+ });
+ logger.audit(req.user.id, 'finalize_ed_encounter', 'ED encounter MDM finalized', req, { category: 'clinical' });
+
+ res.json({ success: true, mdm: parsed.mdm, model: result.model });
+ } catch (e) {
+ logger.error('[edEncounters] finalize failed', e.message);
+ res.status(500).json({ error: 'Request failed' });
+ }
+});
+
+module.exports = router;
diff --git a/src/routes/memories.js b/src/routes/memories.js
index ed5c321..503f6d1 100644
--- a/src/routes/memories.js
+++ b/src/routes/memories.js
@@ -22,17 +22,17 @@ function decryptMemory(row) {
var VALID_CATEGORIES = [
'physical_exam', 'ros', 'encounter_format', 'family_history', 'assessment_plan', 'custom',
- 'template_soap', 'template_hpi', 'template_wellvisit', 'template_sickvisit',
- 'correction_soap', 'correction_hpi', 'correction_encounter', 'correction_wellvisit', 'correction_sickvisit'
+ 'template_soap', 'template_hpi', 'template_wellvisit', 'template_sickvisit', 'template_ed'
];
// ── GET all memories for current user ───────────────────────────────────
router.get('/memories', async function(req, res) {
try {
- // Cannot ORDER BY encrypted `name`; order by id as a stable proxy after
- // per-category sort. Frontend can re-sort alphabetically client-side.
+ // Legacy correction_* rows from the removed Dragon-style learning feature
+ // are filtered out — invisible to UI and to the AI context endpoint, but
+ // not dropped from the table.
var rows = await db.all(
- 'SELECT id, category, name, content, created_at, updated_at FROM user_memories WHERE user_id = $1 ORDER BY category, id',
+ "SELECT id, category, name, content, created_at, updated_at FROM user_memories WHERE user_id = $1 AND category NOT LIKE 'correction_%' ORDER BY category, id",
[req.user.id]
);
rows.forEach(decryptMemory);
@@ -93,89 +93,25 @@ router.delete('/memories/:id', async function(req, res) {
});
// ── GET memories as prompt context (for AI generation) ──────────────────
+// Returns user templates concatenated as system-prompt context. Multiple
+// templates per category are allowed; we order by id ASC so the newest
+// row lands last in the prompt — models weight later context more heavily,
+// which gives the most recently saved template implicit predominance.
router.get('/memories/context', async function(req, res) {
try {
var rows = await db.all(
- 'SELECT category, name, content FROM user_memories WHERE user_id = $1 ORDER BY category',
+ "SELECT category, name, content FROM user_memories WHERE user_id = $1 AND category NOT LIKE 'correction_%' ORDER BY category, id",
[req.user.id]
);
if (rows.length === 0) return res.json({ success: true, context: '' });
rows.forEach(decryptMemory);
- var templates = [];
- var corrections = [];
+ var context = '\n\nPHYSICIAN TEMPLATES AND PREFERENCES:\n';
rows.forEach(function(r) {
- if (r.category.startsWith('correction_')) corrections.push(r);
- else templates.push(r);
+ context += '--- ' + r.category.toUpperCase().replace(/_/g, ' ') + ': ' + r.name + ' ---\n' + r.content + '\n\n';
});
-
- var context = '';
- if (templates.length > 0) {
- context += '\n\nPHYSICIAN TEMPLATES AND PREFERENCES:\n';
- templates.forEach(function(r) {
- context += '--- ' + r.category.toUpperCase().replace(/_/g, ' ') + ': ' + r.name + ' ---\n' + r.content + '\n\n';
- });
- }
- if (corrections.length > 0) {
- context += '\n\nSTYLE CORRECTIONS (these are examples of past edits — use only the writing style, never the clinical content):\n';
- corrections.slice(-10).forEach(function(r) {
- // Trim to just the key style difference, not full encounter text
- var lines = r.content.split('\n').filter(function(l) { return l.trim(); });
- var orig = '', corr = '';
- var inCorrected = false;
- lines.forEach(function(l) {
- if (l.startsWith('CORRECTED TO:')) { inCorrected = true; corr = l.replace('CORRECTED TO:', '').trim(); }
- else if (l.startsWith('ORIGINAL:')) { orig = l.replace('ORIGINAL:', '').trim(); }
- else if (inCorrected) { corr += ' ' + l.trim(); }
- else { orig += ' ' + l.trim(); }
- });
- // Keep only first 200 chars of each to avoid flooding the prompt
- orig = orig.substring(0, 200);
- corr = corr.substring(0, 200);
- if (orig && corr) {
- context += '- Before: "' + orig + '..."\n After: "' + corr + '..."\n';
- }
- });
- }
res.json({ success: true, context: context.trim() });
} catch (e) { logger.error('GET /memories/context', e.message); res.status(500).json({ error: 'Request failed' }); }
});
-// ── POST auto-save correction (Dragon-like learning) ──────────────────
-router.post('/memories/correction', async function(req, res) {
- try {
- var { section, original_snippet, corrected_snippet } = req.body;
- if (!section || !original_snippet || !corrected_snippet) {
- return res.status(400).json({ error: 'section, original_snippet, and corrected_snippet required' });
- }
- if (original_snippet.trim() === corrected_snippet.trim()) {
- return res.json({ success: true, skipped: true });
- }
- var cat = 'correction_' + section;
- if (!VALID_CATEGORIES.includes(cat)) cat = 'correction_encounter';
-
- // Limit corrections per category: keep only latest 20
- var existing = await db.all(
- 'SELECT id FROM user_memories WHERE user_id = $1 AND category = $2 ORDER BY created_at ASC',
- [req.user.id, cat]
- );
- if (existing.length >= 20) {
- // Delete oldest to make room
- var toDelete = existing.slice(0, existing.length - 19);
- for (var i = 0; i < toDelete.length; i++) {
- await db.run('DELETE FROM user_memories WHERE id = $1 AND user_id = $2', [toDelete[i].id, req.user.id]);
- }
- }
-
- var name = original_snippet.substring(0, 60).replace(/\n/g, ' ') + '...';
- var content = 'ORIGINAL: ' + original_snippet.substring(0, 2000) + '\nCORRECTED TO: ' + corrected_snippet.substring(0, 2000);
-
- await db.run(
- 'INSERT INTO user_memories (user_id, category, name, content) VALUES ($1,$2,$3,$4)',
- [req.user.id, cat, cryptoUtil.encryptString(name), cryptoUtil.encryptString(content)]
- );
- res.json({ success: true });
- } catch (e) { logger.error('POST /memories/correction', e.message); res.status(500).json({ error: 'Request failed' }); }
-});
-
module.exports = router;
diff --git a/src/routes/notes.js b/src/routes/notes.js
index a46b433..dc45afa 100644
--- a/src/routes/notes.js
+++ b/src/routes/notes.js
@@ -227,27 +227,28 @@ router.post('/notes/from-voice', async function (req, res) {
if (!transcript) return res.status(400).json({ error: 'No transcript provided' });
var systemPrompt =
- 'You are a medical scribe turning a physician\'s dictated notes into a clean personal note.\n' +
+ 'You turn a voice dictation into a clean, well-structured personal note.\n' +
+ 'The note may be anything: a clinical observation, a shopping list, a reminder, a travel idea, a journal entry. Match the dictation — do not assume it is medical and do not impose clinical structure (assessment/plan, SOAP, etc.) on non-clinical content.\n' +
'Output STRICT JSON only — no preamble, no code fences, no commentary.\n' +
'Shape: {"title": "
", "body": ""}.\n' +
'The body must be HTML using only these tags: ,