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.
117 lines
5.6 KiB
JavaScript
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;
|