pediatric-ai-scribe-v3/public/js/memories.js
Daniel Onyejesi 5f066bb760 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

217 lines
9.3 KiB
JavaScript

// ============================================================
// MEMORIES.JS — User template/memory storage + settings UI
// ============================================================
(function() {
var _memories = [];
var _editingId = null;
var CATEGORY_LABELS = {
physical_exam: 'Physical Exam',
ros: 'Review of Systems',
encounter_format: 'Encounter Format',
family_history: 'Family History',
assessment_plan: 'Assessment & Plan',
template_soap: 'SOAP Template',
template_hpi: 'HPI Template',
template_wellvisit: 'Well Visit Template',
template_sickvisit: 'Sick Visit 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() {
fetch('/api/memories', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
_memories = data.memories || [];
renderMemoryList();
})
.catch(function(err) { console.error('[Memories] Load failed:', err); });
}
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) {
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">No templates saved yet. Add one above.</p>';
return;
}
container.innerHTML = templates.map(function(m) {
var catLabel = CATEGORY_LABELS[m.category] || m.category;
var preview = (m.content || '').substring(0, 100).replace(/\n/g, ' ');
return '<div class="mem-item" data-id="' + m.id + '">' +
'<div class="mem-item-info">' +
'<div style="display:flex;align-items:center;gap:6px;margin-bottom:2px;">' +
'<span class="mem-item-name">' + esc(m.name) + '</span>' +
'<span class="mem-item-cat">' + esc(catLabel) + '</span>' +
'</div>' +
'<div class="mem-item-preview">' + esc(preview) + (m.content && m.content.length > 100 ? '...' : '') + '</div>' +
'</div>' +
'<button class="btn-sm btn-ghost mem-edit-btn" data-id="' + m.id + '" title="Edit"><i class="fas fa-pen"></i></button>' +
'<button class="btn-sm btn-ghost mem-delete-btn" data-id="' + m.id + '" style="color:var(--red);" title="Delete"><i class="fas fa-trash"></i></button>' +
'</div>';
}).join('');
container.querySelectorAll('.mem-edit-btn').forEach(function(btn) {
btn.addEventListener('click', function() { editMemory(parseInt(btn.dataset.id)); });
});
container.querySelectorAll('.mem-delete-btn').forEach(function(btn) {
btn.addEventListener('click', function() { deleteMemory(parseInt(btn.dataset.id)); });
});
}
function renderCorrectionsList(corrections) {
var container = document.getElementById('corrections-list');
if (!container) return;
if (corrections.length === 0) {
container.innerHTML = '<p style="color:var(--g400);font-size:13px;">No corrections yet. Edit AI-generated notes and save to start learning.</p>';
return;
}
container.innerHTML = corrections.map(function(m) {
var catLabel = CORRECTION_LABELS[m.category] || m.category;
var preview = (m.content || '').substring(0, 120).replace(/\n/g, ' ');
return '<div class="mem-item" data-id="' + m.id + '">' +
'<div class="mem-item-info">' +
'<div style="display:flex;align-items:center;gap:6px;margin-bottom:2px;">' +
'<span class="mem-item-cat">' + esc(catLabel) + '</span>' +
'</div>' +
'<div class="mem-item-preview">' + esc(preview) + (m.content && m.content.length > 120 ? '...' : '') + '</div>' +
'</div>' +
'<button class="btn-sm btn-ghost correction-delete-btn" data-id="' + m.id + '" style="color:var(--red);" title="Delete"><i class="fas fa-trash"></i></button>' +
'</div>';
}).join('');
container.querySelectorAll('.correction-delete-btn').forEach(function(btn) {
btn.addEventListener('click', function() { deleteMemory(parseInt(btn.dataset.id)); });
});
}
// ── Save memory ──────────────────────────────────────────────────────────
function saveMemory() {
var name = document.getElementById('mem-name').value.trim();
var category = document.getElementById('mem-category').value;
var content = document.getElementById('mem-content').value.trim();
if (!name) { showToast('Enter a template name', 'error'); return; }
if (!content) { showToast('Enter template content', 'error'); return; }
var url = _editingId ? '/api/memories/' + _editingId : '/api/memories';
var method = _editingId ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: getAuthHeaders(),
body: JSON.stringify({ name: name, category: category, content: content })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
showToast(_editingId ? 'Template updated' : 'Template saved', 'success');
cancelEdit();
loadMemories();
} else {
showToast(data.error || 'Save failed', 'error');
}
})
.catch(function() { showToast('Save failed', 'error'); });
}
function editMemory(id) {
var mem = _memories.find(function(m) { return m.id === id; });
if (!mem) return;
_editingId = id;
document.getElementById('mem-name').value = mem.name;
document.getElementById('mem-category').value = mem.category;
document.getElementById('mem-content').value = mem.content;
var btn = document.getElementById('btn-mem-save');
if (btn) btn.innerHTML = '<i class="fas fa-check"></i> Update Template';
// Add cancel button if not present
if (!document.getElementById('btn-mem-cancel')) {
var cancel = document.createElement('button');
cancel.id = 'btn-mem-cancel';
cancel.className = 'btn-sm btn-ghost';
cancel.innerHTML = '<i class="fas fa-times"></i> Cancel';
cancel.style.marginLeft = '6px';
cancel.addEventListener('click', cancelEdit);
if (btn && btn.parentNode) btn.parentNode.appendChild(cancel);
}
document.getElementById('mem-name').focus();
}
function cancelEdit() {
_editingId = null;
document.getElementById('mem-name').value = '';
document.getElementById('mem-content').value = '';
var btn = document.getElementById('btn-mem-save');
if (btn) btn.innerHTML = '<i class="fas fa-plus"></i> Add Template';
var cancel = document.getElementById('btn-mem-cancel');
if (cancel) cancel.remove();
}
function deleteMemory(id) {
var mem = _memories.find(function(m) { return m.id === id; });
if (!confirm('Delete template "' + (mem ? mem.name : '') + '"?')) return;
fetch('/api/memories/' + id, { method: 'DELETE', headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) { showToast('Template deleted', 'info'); loadMemories(); }
else showToast(data.error || 'Delete failed', 'error');
})
.catch(function() { showToast('Delete failed', 'error'); });
}
// ── Event wiring ─────────────────────────────────────────────────────────
// Expose so auth.js can call on settings tab navigation
window.loadMemories = loadMemories;
document.addEventListener('click', function(e) {
// Settings button or settings tab — load memories
if (e.target.closest('#btn-settings') || (e.target.closest('.tab-btn') && e.target.closest('.tab-btn[data-tab="settings"]'))) {
setTimeout(loadMemories, 100);
}
// Save memory button
if (e.target.closest('#btn-mem-save')) {
saveMemory();
}
});
// ── Expose context for AI generation ────────────────────────────────────
// Call this before any AI generation to get user memory context.
// Always fetches from the API so it works even if _memories cache is empty.
window.getUserMemoryContext = function() {
return fetch('/api/memories/context', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) { return data.success ? (data.context || '') : ''; })
.catch(function() { return ''; });
};
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
console.log('✅ Memories module loaded');
})();