Fix: admin models loading, clear refine/instructions on New, bigger HPI areas, unique labels

- Admin models: reset modelsLoaded flag on error so retry works
- Admin default model: fix redundant fetch race condition
- clearTab: now clears refine inputs, instructions, and demographic fields
- Encounter/dictation clear buttons: also clear refine input
- SOAP: instructions textarea already cleared by clearTab (soap-instructions)
- Encounter HPI: bigger transcript (400px) and output (600px) text areas
- Unique label enforcement: 409 error if saving with duplicate label
This commit is contained in:
ifedan-ed 2026-03-29 01:35:45 +00:00
parent 50492ff30c
commit 51fc818e4a
7 changed files with 47 additions and 13 deletions

View file

@ -56,7 +56,7 @@
<div class="card">
<div class="card-header"><h3><i class="fas fa-file-lines"></i> Transcript</h3><button id="enc-clear" class="btn-sm btn-ghost"><i class="fas fa-eraser"></i> Clear</button></div>
<div id="enc-transcript" class="editable-box" contenteditable="true" data-placeholder="Transcript appears here or type/paste directly..."></div>
<div id="enc-transcript" class="editable-box editable-box-large" contenteditable="true" data-placeholder="Transcript appears here or type/paste directly..."></div>
</div>
<button id="enc-generate-btn" class="btn-generate"><i class="fas fa-wand-magic-sparkles"></i> Generate HPI</button>
@ -71,7 +71,7 @@
<button class="btn-sm btn-ghost" data-action="nc-export" data-target="enc-hpi-text" data-label="hpi-encounter"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="enc-hpi-text" class="output-text" contenteditable="true"></div>
<div id="enc-hpi-text" class="output-text output-text-large" contenteditable="true"></div>
<div class="refine-bar">
<textarea id="enc-refine-input" class="refine-input" rows="2" placeholder="Tell AI to modify (e.g., 'make it shorter', 'add that patient has asthma history')"></textarea>
<button class="btn-sm btn-primary" id="enc-refine-btn"><i class="fas fa-edit"></i> Refine</button>

View file

@ -153,6 +153,8 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(--
.output-header{background:var(--green-light);border-bottom-color:var(--green);}
.output-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
.output-text{padding:20px 24px;font-size:13px;line-height:1.9;outline:none;min-height:480px;white-space:pre-wrap;overflow-y:auto;}
.output-text-large{min-height:600px;}
.editable-box-large{min-height:400px;}
.model-tag{font-size:10px;padding:2px 7px;background:var(--g200);border-radius:4px;color:var(--g600);}
/* REFINE BAR */

View file

@ -571,7 +571,7 @@
fetch('/api/admin/config/models', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
if (!data.success) { modelsLoaded = false; return; }
window._adminModelsData = data;
var providerEl = document.getElementById('admin-model-provider');
if (providerEl) providerEl.textContent = (data.provider || 'openrouter').toUpperCase();
@ -579,7 +579,7 @@
renderCustomModels(data.custom || []);
populateDefaultModelSelect(data.models || [], data.custom || []);
})
.catch(function(err) { console.error('[Admin] Models load failed:', err); });
.catch(function(err) { modelsLoaded = false; console.error('[Admin] Models load failed:', err); });
}
function renderModelTable(models) {
@ -668,15 +668,22 @@
sel.appendChild(opt);
});
// Load current default
fetch('/api/admin/config', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
var cfg = {};
(data.config || []).forEach(function(row) { cfg[row.key] = row.value; });
if (cfg['models.default']) sel.value = cfg['models.default'];
});
// Load current default from already-loaded config, or fetch if needed
var cfg = window._adminEmailConfig || {};
if (cfg['models.default']) {
sel.value = cfg['models.default'];
} else {
// Fetch only the default model setting
fetch('/api/admin/config', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) return;
(data.config || []).forEach(function(row) {
if (row.key === 'models.default' && row.value) sel.value = row.value;
});
})
.catch(function() {});
}
}
function addCustomModel() {

View file

@ -468,6 +468,17 @@
// Hide output card
var out = document.getElementById(outputElMap[type]);
if (out) out.classList.add('hidden');
// Clear refine input (textarea or input) for every tab
var refineEl = document.getElementById(pfx + '-refine-input');
if (refineEl) refineEl.value = '';
// Clear instructions textarea (SOAP, hospital, etc.)
var instrEl = document.getElementById(pfx + '-instructions');
if (instrEl) instrEl.value = '';
// Clear demographic fields
var ageEl = document.getElementById(pfx + '-age');
if (ageEl) ageEl.value = '';
var genderEl = document.getElementById(pfx + '-gender');
if (genderEl) genderEl.value = '';
// Reset saved ID and idempotency key (memory + sessionStorage)
window['_savedEncId_' + type] = null;
try { sessionStorage.removeItem('_savedEncId_' + type); } catch(e) {}

View file

@ -131,6 +131,8 @@
window._savedEncId_encounter = null;
var labelEl = document.getElementById('enc-label');
if (labelEl) labelEl.value = '';
var refineEl = document.getElementById('enc-refine-input');
if (refineEl) refineEl.value = '';
});
generateBtn.addEventListener('click', function() {

View file

@ -126,6 +126,8 @@
window._savedEncId_dictation = null;
var labelEl = document.getElementById('dict-label');
if (labelEl) labelEl.value = '';
var refineEl = document.getElementById('dict-refine-input');
if (refineEl) refineEl.value = '';
});
generateBtn.addEventListener('click', function() {

View file

@ -56,6 +56,16 @@ router.post('/encounters/saved', async function(req, res) {
);
res.json({ success: true, id: id });
} else {
// Enforce unique label per user (within active/non-expired encounters)
if (label && label.trim()) {
var labelDup = await db.get(
"SELECT id FROM saved_encounters WHERE user_id = $1 AND LOWER(label) = LOWER($2) AND expires_at > NOW()",
[req.user.id, label.trim()]
);
if (labelDup) {
return res.status(409).json({ error: 'An encounter with this label already exists. Use a unique label or load the existing one.' });
}
}
// Check for duplicate via idempotency_key
if (idempotency_key) {
var dup = await db.get(