pediatric-ai-scribe-v3/src/routes/memories.js
Daniel f8e883e969 feat: ED encounters + notes model selector; remove AI corrections; fix notes framing
Four changes batched:

1. ED Encounters tab (new) — multi-stage emergency note with don't-miss
   tooltips and 2023 E/M MDM finalize. New route /api/ed-encounters
   (generate per-stage + finalize MDM), new ed-encounters.js owning all
   client logic, new ed-encounter.html component, new template_ed memory
   category. Persists draft to localStorage every keystroke and to
   saved_encounters on stage advance. encounters.js touched only to
   register the new tab in sessionStorage restore + tabMap (save and
   idempotency code untouched).

2. Notes model selector — /notes/from-voice now accepts a client-supplied
   model (validated by the existing callAI allow-list); falls back to the
   admin default. Added <select class="tab-model-select"> to notes.html
   so the existing app.js populator handles options + default.

3. Remove AI-learning-from-corrections — deleted correctionTracker.js,
   POST /memories/correction, the corrections branch in
   /memories/context, the settings UI section, the FAQ entry, and all
   dead trackAIOutput/saveCorrection guards in callers. Legacy
   correction_* DB rows are filtered (NOT LIKE) rather than dropped, so
   no destructive migration.

4. Fix notes AI framing — /notes/from-voice prompt no longer assumes
   "physician dictation". Plain notes (shopping lists, reminders,
   ideas) now match the dictation tone instead of being forced into
   clinical structure.

All 46 tests pass.
2026-04-28 03:09:38 +02:00

117 lines
5.6 KiB
JavaScript

// ============================================================
// MEMORIES ROUTES — User template/memory storage
// ============================================================
var express = require('express');
var router = express.Router();
var db = require('../db/database');
var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
var cryptoUtil = require('../utils/crypto');
router.use(authMiddleware);
// Decrypt a row's user-facing fields. Safe against legacy plaintext rows —
// cryptoUtil.decryptString passes through values without the "enc1:" prefix.
function decryptMemory(row) {
if (!row) return row;
try { row.name = cryptoUtil.decryptString(row.name); } catch (e) {}
try { row.content = cryptoUtil.decryptString(row.content); } catch (e) {}
return row;
}
var VALID_CATEGORIES = [
'physical_exam', 'ros', 'encounter_format', 'family_history', 'assessment_plan', 'custom',
'template_soap', 'template_hpi', 'template_wellvisit', 'template_sickvisit', 'template_ed'
];
// ── GET all memories for current user ───────────────────────────────────
router.get('/memories', async function(req, res) {
try {
// 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 AND category NOT LIKE 'correction_%' ORDER BY category, id",
[req.user.id]
);
rows.forEach(decryptMemory);
res.json({ success: true, memories: rows });
} catch (e) { logger.error('GET /memories', e.message); res.status(500).json({ error: 'Request failed' }); }
});
// ── POST create memory ───────────────────────────────────────────────────
router.post('/memories', async function(req, res) {
try {
var { name, category, content } = req.body;
if (!name || !name.trim()) return res.status(400).json({ error: 'Name required' });
if (!content || !content.trim()) return res.status(400).json({ error: 'Content required' });
var cat = VALID_CATEGORIES.includes(category) ? category : 'custom';
// Limit per user
var count = await db.get('SELECT COUNT(*) as cnt FROM user_memories WHERE user_id = $1', [req.user.id]);
if (count && parseInt(count.cnt) >= 200) return res.status(400).json({ error: 'Maximum 200 memories per user' });
var result = await db.run(
'INSERT INTO user_memories (user_id, category, name, content) VALUES ($1,$2,$3,$4)',
[
req.user.id,
cat,
cryptoUtil.encryptString(name.trim().substring(0, 100)),
cryptoUtil.encryptString(content.trim().substring(0, 5000))
]
);
res.json({ success: true, id: result.lastInsertRowid });
} catch (e) { logger.error('POST /memories', e.message); res.status(500).json({ error: 'Request failed' }); }
});
// ── PUT update memory ────────────────────────────────────────────────────
router.put('/memories/:id', async function(req, res) {
try {
var { name, category, content } = req.body;
var cat = VALID_CATEGORIES.includes(category) ? category : 'custom';
await db.run(
'UPDATE user_memories SET name=$1, category=$2, content=$3, updated_at=NOW() WHERE id=$4 AND user_id=$5',
[
cryptoUtil.encryptString((name || '').trim().substring(0, 100)),
cat,
cryptoUtil.encryptString((content || '').trim().substring(0, 5000)),
req.params.id,
req.user.id
]
);
res.json({ success: true });
} catch (e) { logger.error('PUT /memories/:id', e.message); res.status(500).json({ error: 'Request failed' }); }
});
// ── DELETE memory ────────────────────────────────────────────────────────
router.delete('/memories/:id', async function(req, res) {
try {
await db.run('DELETE FROM user_memories WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
res.json({ success: true });
} catch (e) { logger.error('DELETE /memories/:id', e.message); res.status(500).json({ error: 'Request failed' }); }
});
// ── 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 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 context = '\n\nPHYSICIAN TEMPLATES AND PREFERENCES:\n';
rows.forEach(function(r) {
context += '--- ' + r.category.toUpperCase().replace(/_/g, ' ') + ': ' + r.name + ' ---\n' + r.content + '\n\n';
});
res.json({ success: true, context: context.trim() });
} catch (e) { logger.error('GET /memories/context', e.message); res.status(500).json({ error: 'Request failed' }); }
});
module.exports = router;