pediatric-ai-scribe-v3/src/routes/memories.js
Daniel Onyejesi 08a8fb26c4 v9: Major feature update — audio backup, SOAP save, Dragon memory, S3 docs, CI/CD, APK
Phase 1 — Critical Fixes:
- Fix SOAP instructions not clearing on Clear button
- Show transcription provider (AWS/OpenAI) in UI toast
- Fix silent transcription failures in dictation and SOAP modules
- Add IndexedDB audio backup system (24hr retention, retry from Settings)
- Prevent duplicate encounter saves with idempotency keys
- Add Save/Load/New bar to SOAP note generator

Phase 2 — Features:
- Dragon-like AI memory: auto-track user corrections, inject into prompts
- Per-section template categories (SOAP, HPI, well visit, sick visit)
- Bigger textarea for SOAP instructions
- S3 document upload/management (AWS S3, Backblaze B2, MinIO compatible)
- Faster transcription via lower bitrate recording (16kbps opus)

Phase 3 — APK & CI/CD:
- GitHub Actions: Docker build+push on version tags
- GitHub Actions: TWA APK build for Obtainium auto-updates
- Android TWA project with foreground service for background recording
- Enhanced PWA manifest with shortcuts and maskable icons
2026-03-28 21:08:32 +00:00

147 lines
6.8 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');
router.use(authMiddleware);
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'
];
// ── GET all memories for current user ───────────────────────────────────
router.get('/memories', async function(req, res) {
try {
var rows = await db.all(
'SELECT id, category, name, content, created_at, updated_at FROM user_memories WHERE user_id = $1 ORDER BY category, name',
[req.user.id]
);
res.json({ success: true, memories: rows });
} catch (e) { logger.error('GET /memories', e.message); res.status(500).json({ error: e.message }); }
});
// ── 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, name.trim().substring(0, 100), 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: e.message }); }
});
// ── 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',
[
(name || '').trim().substring(0, 100),
cat,
(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: e.message }); }
});
// ── 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: e.message }); }
});
// ── GET memories as prompt context (for AI generation) ──────────────────
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',
[req.user.id]
);
if (rows.length === 0) return res.json({ success: true, context: '' });
var templates = [];
var corrections = [];
rows.forEach(function(r) {
if (r.category.startsWith('correction_')) corrections.push(r);
else templates.push(r);
});
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\nPHYSICIAN CORRECTION HISTORY (learn from these preferences — apply similar corrections to future outputs):\n';
corrections.slice(-20).forEach(function(r) {
context += '--- CORRECTION (' + r.category.replace('correction_', '').toUpperCase() + '): ' + 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: e.message }); }
});
// ── 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, name, content]
);
res.json({ success: true });
} catch (e) { logger.error('POST /memories/correction', e.message); res.status(500).json({ error: e.message }); }
});
module.exports = router;