fix: OWUI-style polish — fullscreen workspace replaces main menu, row-click saved chats (delete stays as hover), markdown-rendered patient handout, admin-only raw transcript, /api TTS URL, scrollable unclipped tables, action buttons hidden in exports, autosave-only copy
This commit is contained in:
parent
2e0951995c
commit
da86c7db79
17 changed files with 129 additions and 146 deletions
|
|
@ -19,18 +19,9 @@
|
|||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-bookmark"></i> Saved Chats</h3>
|
||||
<button id="btn-assistant-save" class="btn-sm btn-ghost" type="button"><i class="fas fa-floppy-disk"></i> Save</button>
|
||||
</div>
|
||||
<div id="assistant-saved-chats" class="assistant-saved-chats">
|
||||
<p class="assistant-muted">Saved chats appear here after you click Save chat.</p>
|
||||
</div>
|
||||
<div id="assistant-save-panel" class="assistant-save-panel" hidden>
|
||||
<label for="assistant-save-title">Save chat as</label>
|
||||
<input id="assistant-save-title" type="text" maxlength="160" autocomplete="off">
|
||||
<div class="assistant-save-actions">
|
||||
<button id="btn-assistant-save-confirm" class="btn-sm btn-primary" type="button">Save</button>
|
||||
<button id="btn-assistant-save-cancel" class="btn-sm btn-ghost" type="button">Cancel</button>
|
||||
</div>
|
||||
<p class="assistant-muted">Chats save automatically as you go.</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
.assistant-msg.user .assistant-bubble pre { background:rgba(0,0,0,.35); }
|
||||
.assistant-msg.user .assistant-bubble blockquote { border-left-color:rgba(255,255,255,.6); background:rgba(255,255,255,.15); color:#fff; }
|
||||
.assistant-msg.user .assistant-bubble hr { border-top-color:rgba(255,255,255,.35); }
|
||||
.assistant-table-scroll { position:relative; max-width:100%; overflow-x:auto; overflow-y:hidden; -webkit-overflow-scrolling:touch; margin:12px 0; border:1px solid var(--g200); border-radius:12px; background:white; box-shadow:inset 0 -1px 0 rgba(0,0,0,.03); scrollbar-width:none; }
|
||||
.assistant-table-scroll { position:relative; max-width:100%; max-height:60vh; overflow:auto; -webkit-overflow-scrolling:touch; margin:12px 0; border:1px solid var(--g200); border-radius:12px; background:white; box-shadow:inset 0 -1px 0 rgba(0,0,0,.03); scrollbar-width:none; }
|
||||
.assistant-table-scroll::-webkit-scrollbar { display:none; }
|
||||
.assistant-table-scroll table { width:max-content; min-width:100%; max-width:none; border-collapse:separate; border-spacing:0; margin:0; border:0; border-radius:0; font-size:12px; }
|
||||
.assistant-table-scroll::after { content:'Swipe table'; display:none; position:sticky; left:0; bottom:0; padding:3px 9px; font-size:10px; font-weight:700; color:var(--g500); background:linear-gradient(90deg,rgba(255,255,255,.95),rgba(255,255,255,0)); pointer-events:none; }
|
||||
|
|
@ -131,16 +131,12 @@
|
|||
.assistant-image-modal-close { position:absolute; top:8px; right:8px; z-index:1; width:38px; height:38px; border:0; border-radius:999px; background:white; color:var(--g800); font-size:24px; line-height:1; cursor:pointer; box-shadow:var(--shadow); }
|
||||
.assistant-image-modal-cancel { justify-self:center; border:0; border-radius:999px; background:white; color:var(--g800); font-weight:700; padding:9px 14px; box-shadow:var(--shadow); cursor:pointer; }
|
||||
.assistant-sources { padding:10px 12px; display:grid; gap:8px; max-height:520px; overflow-y:auto; }
|
||||
.assistant-saved-chats { padding:10px 12px; display:grid; gap:8px; max-height:220px; overflow-y:auto; }
|
||||
.assistant-saved-chat { border:1px solid var(--g200); border-radius:10px; padding:8px; background:white; display:grid; gap:5px; }
|
||||
.assistant-saved-chats { flex:1 1 auto; overflow-y:auto; min-height:0; padding:10px 12px; display:flex; flex-direction:column; gap:8px; }
|
||||
.assistant-saved-chat { border:1px solid var(--g200); border-radius:10px; padding:8px 10px; background:white; display:flex; flex-direction:column; gap:3px; width:100%; text-align:left; cursor:pointer; color:inherit; font:inherit; }
|
||||
.assistant-saved-chat:hover { background:var(--g100, #f3f4f6); }
|
||||
.assistant-saved-chat.active { border-color:var(--accent, #0f766e); }
|
||||
.assistant-saved-chat-title { font-size:12px; font-weight:700; color:var(--g800); line-height:1.35; }
|
||||
.assistant-saved-chat-meta { font-size:11px; color:var(--g500); }
|
||||
.assistant-saved-chat-actions { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.assistant-save-panel { border-top:1px solid var(--g200); padding:10px 12px; display:grid; gap:7px; }
|
||||
.assistant-save-panel[hidden] { display:none; }
|
||||
.assistant-save-panel label { font-size:11px; font-weight:700; color:var(--g500); text-transform:uppercase; letter-spacing:.04em; }
|
||||
.assistant-save-panel input { width:100%; border:1.5px solid var(--g300); border-radius:9px; padding:8px 10px; font-size:12px; outline:none; }
|
||||
.assistant-save-panel input:focus { border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-light); }
|
||||
.assistant-save-actions { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.assistant-source { border:1px solid var(--g200); border-radius:10px; padding:9px; background:white; font-size:12px; line-height:1.5; }
|
||||
.assistant-source strong { color:var(--g800); }
|
||||
|
|
@ -186,7 +182,19 @@
|
|||
/* Patient take home modal + actions */
|
||||
.assistant-takehome-modal { position: fixed; inset: 0; background: rgba(15, 23, 42, .45); display: flex; align-items: center; justify-content: center; z-index: 120; }
|
||||
.assistant-takehome-modal .modal-content { width: min(640px, 92vw); max-height: 82vh; display: flex; flex-direction: column; }
|
||||
.assistant-takehome-result { white-space: pre-wrap; overflow-y: auto; font-size: .95rem; line-height: 1.55; }
|
||||
/* Take-home result uses the same markdown typography as chat bubbles */
|
||||
.assistant-takehome-result { overflow-y: auto; font-size: 13px; line-height: 1.75; overflow-wrap: anywhere; }
|
||||
.assistant-takehome-result h1, .assistant-takehome-result h2, .assistant-takehome-result h3 { margin:16px 0 8px; line-height:1.25; color:var(--g900); }
|
||||
.assistant-takehome-result h1:first-child, .assistant-takehome-result h2:first-child, .assistant-takehome-result h3:first-child { margin-top:0; }
|
||||
.assistant-takehome-result h1 { font-size:20px; }
|
||||
.assistant-takehome-result h2 { font-size:17px; border-bottom:1px solid var(--g200); padding-bottom:4px; }
|
||||
.assistant-takehome-result h3 { font-size:15px; }
|
||||
.assistant-takehome-result ul, .assistant-takehome-result ol { padding-left:20px; margin:8px 0; }
|
||||
.assistant-takehome-result p { margin:8px 0; }
|
||||
.assistant-takehome-result strong { font-weight:600; }
|
||||
.assistant-takehome-result table { border-collapse:collapse; margin:10px 0; font-size:12px; }
|
||||
.assistant-takehome-result th, .assistant-takehome-result td { border:1px solid var(--g200); padding:5px 8px; text-align:left; }
|
||||
.assistant-takehome-result code { background:var(--g100, #f3f4f6); border-radius:4px; padding:1px 5px; font-size:12px; }
|
||||
.assistant-takehome-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; border-top: 1px solid var(--border, #e5e7eb); padding-top: .75rem; }
|
||||
.assistant-takehome-email { display: flex; gap: .5rem; margin-left: auto; }
|
||||
.assistant-takehome-email input { width: 220px; }
|
||||
|
|
@ -202,3 +210,12 @@
|
|||
.assistant-voice-mic { width: 72px; height: 72px; border-radius: 50%; border: none; background: var(--accent, #0f766e); color: #fff; font-size: 1.6rem; cursor: pointer; }
|
||||
.assistant-voice-mic.recording { background: var(--danger, #dc2626); animation: assistant-pulse 1.2s infinite; }
|
||||
@keyframes assistant-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(220,38,38,.45); } 50% { box-shadow: 0 0 0 14px rgba(220,38,38,0); } }
|
||||
|
||||
/* Full-screen Open WebUI workspace: the assistant replaces the app chrome */
|
||||
body.assistant-workspace .assistant-layout { height: 100vh; }
|
||||
body.assistant-workspace .assistant-main { min-height: 0; }
|
||||
body.assistant-workspace #assistant-chat-view { min-height: 0; }
|
||||
body.assistant-workspace .assistant-learning-view { min-height: 0; height: 100vh; overflow-y: auto; }
|
||||
/* Saved chats occupy the full left rail and scroll inside it */
|
||||
.assistant-history { display:flex; flex-direction:column; height:100%; min-height:0; }
|
||||
.assistant-history .card { flex:1 1 auto; display:flex; flex-direction:column; min-height:0; overflow:hidden; }
|
||||
|
|
|
|||
|
|
@ -1204,3 +1204,10 @@ textarea.full-input{resize:vertical;}
|
|||
#admin-tab .cms-prompt-family select, #admin-tab .cms-prompt-family textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* Open WebUI-style assistant workspace: saved chats replace the main menu */
|
||||
body.assistant-workspace .app-header,
|
||||
body.assistant-workspace .sidebar,
|
||||
body.assistant-workspace .announcement-banner { display:none; }
|
||||
body.assistant-workspace .app-body { min-height:100vh; }
|
||||
body.assistant-workspace #assistant-tab.active { width:100%; }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
function authHeaders() {
|
||||
return window.getAuthHeaders ? window.getAuthHeaders() : { 'Content-Type': 'application/json' };
|
||||
var token = typeof localStorage === 'undefined' ? null : localStorage.getItem('auth_token');
|
||||
return token ? { Authorization: 'Bearer ' + token } : {};
|
||||
}
|
||||
|
||||
function parseJsonWithStatus(response) {
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ function isShareCancel(error) {
|
|||
}
|
||||
|
||||
function exportWindowCss() {
|
||||
return 'body{font-family:Arial,sans-serif;color:#111827;line-height:1.55;margin:36px;max-width:820px}.assistant-export-actions{display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px}.assistant-export-actions button{padding:8px 12px}h1{font-size:22px;margin:0 0 4px}h2{font-size:17px;margin-top:26px;border-bottom:1px solid #e5e7eb;padding-bottom:4px;break-after:avoid}h3{font-size:14px;margin:18px 0 8px;break-after:avoid}.meta,.question{font-size:12px;color:#6b7280;margin-bottom:12px}.answer{font-size:13px}.export-section{margin-top:20px;break-before:auto}.full-answer{page-break-before:auto}.export-image img{max-width:100%;border:1px solid #e5e7eb;border-radius:10px}.refs{font-size:12px;padding-left:20px;margin-top:8px;break-inside:auto}.refs li{margin:6px 0;break-inside:avoid}.assistant-cite{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 5px;margin:0 1px;border-radius:999px;background:#f3e8ff;color:#7c3aed;border:1px solid rgba(124,58,237,.22);font-size:9px;font-weight:800;line-height:16px;text-decoration:none;text-transform:uppercase;letter-spacing:.03em;vertical-align:baseline;white-space:nowrap;-webkit-print-color-adjust:exact;print-color-adjust:exact}.answer table{width:100%;border-collapse:collapse;table-layout:auto;margin:12px 0 18px;border:1px solid #e5e7eb;page-break-inside:auto}.answer th,.answer td{padding:7px 8px;border:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:normal;overflow-wrap:break-word}.answer th{background:#f9fafb;font-weight:700;-webkit-print-color-adjust:exact;print-color-adjust:exact}.answer th:last-child,.answer td:last-child{width:1%;white-space:nowrap}.answer tr{break-inside:avoid;page-break-inside:avoid}.answer thead{display:table-header-group}.answer tbody{display:table-row-group}.answer p{margin:8px 0}.answer ul,.answer ol{padding-left:20px}.answer li{margin:4px 0}@media print{.assistant-export-actions{display:none}body{margin:24mm}.export-section{break-inside:auto}.refs{break-before:avoid}}';
|
||||
return 'body{font-family:Arial,sans-serif;color:#111827;line-height:1.55;margin:36px;max-width:820px}.assistant-table-actions,.assistant-msg-actions,.assistant-code-copy,.assistant-translate-pop,.assistant-takehome-actions,.assistant-takehome-modal,.assistant-voice-overlay{display:none!important}.assistant-export-actions{display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px}.assistant-export-actions button{padding:8px 12px}h1{font-size:22px;margin:0 0 4px}h2{font-size:17px;margin-top:26px;border-bottom:1px solid #e5e7eb;padding-bottom:4px;break-after:avoid}h3{font-size:14px;margin:18px 0 8px;break-after:avoid}.meta,.question{font-size:12px;color:#6b7280;margin-bottom:12px}.answer{font-size:13px}.export-section{margin-top:20px;break-before:auto}.full-answer{page-break-before:auto}.export-image img{max-width:100%;border:1px solid #e5e7eb;border-radius:10px}.refs{font-size:12px;padding-left:20px;margin-top:8px;break-inside:auto}.refs li{margin:6px 0;break-inside:avoid}.assistant-cite{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 5px;margin:0 1px;border-radius:999px;background:#f3e8ff;color:#7c3aed;border:1px solid rgba(124,58,237,.22);font-size:9px;font-weight:800;line-height:16px;text-decoration:none;text-transform:uppercase;letter-spacing:.03em;vertical-align:baseline;white-space:nowrap;-webkit-print-color-adjust:exact;print-color-adjust:exact}.answer table{width:100%;border-collapse:collapse;table-layout:auto;margin:12px 0 18px;border:1px solid #e5e7eb;page-break-inside:auto}.answer th,.answer td{padding:7px 8px;border:1px solid #e5e7eb;text-align:left;vertical-align:top;word-break:normal;overflow-wrap:break-word}.answer th{background:#f9fafb;font-weight:700;-webkit-print-color-adjust:exact;print-color-adjust:exact}.answer th:last-child,.answer td:last-child{width:1%;white-space:nowrap}.answer tr{break-inside:avoid;page-break-inside:avoid}.answer thead{display:table-header-group}.answer tbody{display:table-row-group}.answer p{margin:8px 0}.answer ul,.answer ol{padding-left:20px}.answer li{margin:4px 0}@media print{.assistant-export-actions{display:none}body{margin:24mm}.export-section{break-inside:auto}.refs{break-before:avoid}}';
|
||||
}
|
||||
|
||||
function inlineExportCss() {
|
||||
|
|
@ -284,7 +284,7 @@ function inlineExportCss() {
|
|||
}
|
||||
|
||||
function exportTableScrollCss() {
|
||||
return '.answer [align=right],#assistant-export-modal .answer [align=right]{text-align:right}.answer [align=center],#assistant-export-modal .answer [align=center]{text-align:center}.assistant-table-scroll{max-width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;margin:12px 0 18px;border:1px solid #e5e7eb;border-radius:10px;background:white}.answer .assistant-table-scroll table,#assistant-export-modal .answer .assistant-table-scroll table{width:max-content;min-width:100%;max-width:none;margin:0;border:0;border-radius:0}.answer .assistant-table-scroll th,.answer .assistant-table-scroll td,#assistant-export-modal .answer .assistant-table-scroll th,#assistant-export-modal .answer .assistant-table-scroll td{min-width:120px;overflow-wrap:normal;word-break:normal}.assistant-table-scroll::after{content:"Swipe table";display:block;position:sticky;left:0;bottom:0;padding:3px 9px;font-size:10px;font-weight:700;color:#6b7280;background:linear-gradient(90deg,rgba(255,255,255,.95),rgba(255,255,255,0));pointer-events:none}@media print{.assistant-table-scroll{overflow:visible;border:0}.assistant-table-scroll::after{display:none}.answer .assistant-table-scroll table,#assistant-export-modal .answer .assistant-table-scroll table{width:100%;max-width:100%}}';
|
||||
return '.answer [align=right],#assistant-export-modal .answer [align=right]{text-align:right}.answer [align=center],#assistant-export-modal .answer [align=center]{text-align:center}.assistant-table-actions,.assistant-msg-actions,.assistant-code-copy,.assistant-translate-pop,.assistant-takehome-actions,.assistant-takehome-modal,.assistant-voice-overlay{display:none!important}.assistant-table-scroll{max-width:100%;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;margin:12px 0 18px;border:1px solid #e5e7eb;border-radius:10px;background:white}.answer .assistant-table-scroll table,#assistant-export-modal .answer .assistant-table-scroll table{width:max-content;min-width:100%;max-width:none;margin:0;border:0;border-radius:0}.answer .assistant-table-scroll th,.answer .assistant-table-scroll td,#assistant-export-modal .answer .assistant-table-scroll th,#assistant-export-modal .answer .assistant-table-scroll td{min-width:120px;overflow-wrap:normal;word-break:normal}.assistant-table-scroll::after{content:"Swipe table";display:block;position:sticky;left:0;bottom:0;padding:3px 9px;font-size:10px;font-weight:700;color:#6b7280;background:linear-gradient(90deg,rgba(255,255,255,.95),rgba(255,255,255,0));pointer-events:none}@media print{.assistant-table-scroll{overflow:visible;border:0}.assistant-table-scroll::after{display:none}.answer .assistant-table-scroll table,#assistant-export-modal .answer .assistant-table-scroll table{width:100%;max-width:100%}}';
|
||||
}
|
||||
|
||||
function collectExportItems(messages, lastAnswer, lastSources, presentMessage) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import {
|
|||
.catch(function() { /* keep the full static list on failure */ })
|
||||
.finally(function() {
|
||||
translateLanguagesLoading = false;
|
||||
if (typeof document === 'undefined') return;
|
||||
var open = document.querySelector('[data-assistant-translate-pop]');
|
||||
if (open) renderTranslateLanguages(open, open.assistantMessageRow);
|
||||
});
|
||||
|
|
@ -86,8 +87,16 @@ import {
|
|||
var SOURCE_EXCERPT_PREVIEW = 10000;
|
||||
|
||||
document.addEventListener('tabChanged', function (e) {
|
||||
if (e.detail && e.detail.tab === 'assistant') initIfNeeded();
|
||||
if (e.detail && e.detail.tab && e.detail.tab !== 'assistant' && e.detail.tab !== 'learning') lastNonAssistantTab = e.detail.tab;
|
||||
if (e.detail && e.detail.tab === 'assistant') {
|
||||
initIfNeeded();
|
||||
// Open WebUI-style: the assistant workspace replaces the main menu;
|
||||
// saved chats become the left navigation and Go back restores the app.
|
||||
document.body.classList.add('assistant-workspace');
|
||||
}
|
||||
if (e.detail && e.detail.tab && e.detail.tab !== 'assistant' && e.detail.tab !== 'learning') {
|
||||
lastNonAssistantTab = e.detail.tab;
|
||||
document.body.classList.remove('assistant-workspace');
|
||||
}
|
||||
});
|
||||
window.addEventListener('account-boundary', function () {
|
||||
cancelAutosave();
|
||||
|
|
@ -98,6 +107,10 @@ import {
|
|||
function initIfNeeded() {
|
||||
if (initialized) return;
|
||||
var root = document.getElementById('assistant-tab');
|
||||
if (typeof window !== 'undefined' && window._userRole && window._userRole !== 'admin') {
|
||||
var downloadBtn = document.getElementById('btn-assistant-download-chat');
|
||||
if (downloadBtn) downloadBtn.remove();
|
||||
}
|
||||
if (!root || !root.querySelector('#assistant-form')) return;
|
||||
initialized = true;
|
||||
bindEvents();
|
||||
|
|
@ -109,9 +122,6 @@ import {
|
|||
var form = document.getElementById('assistant-form');
|
||||
var clearBtn = document.getElementById('btn-assistant-clear');
|
||||
var cancelBtn = document.getElementById('btn-assistant-cancel');
|
||||
var saveBtn = document.getElementById('btn-assistant-save');
|
||||
var saveConfirmBtn = document.getElementById('btn-assistant-save-confirm');
|
||||
var saveCancelBtn = document.getElementById('btn-assistant-save-cancel');
|
||||
var exportBtn = document.getElementById('btn-assistant-export-pdf');
|
||||
var imageBtn = document.getElementById('btn-assistant-image');
|
||||
var imageClearBtn = document.getElementById('btn-assistant-image-clear');
|
||||
|
|
@ -129,9 +139,6 @@ import {
|
|||
if (learningViewBtn) learningViewBtn.addEventListener('click', openLearningView);
|
||||
if (input) input.addEventListener('input', updateConversationBudget);
|
||||
if (cancelBtn) cancelBtn.addEventListener('click', cancelAssistantSearch);
|
||||
if (saveBtn) saveBtn.addEventListener('click', showSavePanel);
|
||||
if (saveConfirmBtn) saveConfirmBtn.addEventListener('click', saveCurrentChat);
|
||||
if (saveCancelBtn) saveCancelBtn.addEventListener('click', hideSavePanel);
|
||||
if (exportBtn) exportBtn.addEventListener('click', exportAnswerPdf);
|
||||
var takehomeBtn = document.getElementById('btn-assistant-takehome');
|
||||
if (takehomeBtn) takehomeBtn.addEventListener('click', openPatientTakehome);
|
||||
|
|
@ -199,7 +206,7 @@ import {
|
|||
|
||||
if (conversationChars !== null && conversationSize(text) > conversationChars) {
|
||||
updateConversationBudget();
|
||||
if (typeof showToast === 'function') showToast('Conversation limit reached. Save or download this chat, then start a new chat.', 'error');
|
||||
if (typeof showToast === 'function') showToast('Conversation limit reached. This chat is saved automatically; start a new chat.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -860,12 +867,6 @@ import {
|
|||
loadSavedChat(loadBtn.getAttribute('data-assistant-load-chat'));
|
||||
return;
|
||||
}
|
||||
var deleteBtn = e.target.closest('[data-assistant-delete-chat]');
|
||||
if (deleteBtn) {
|
||||
e.preventDefault();
|
||||
deleteSavedChat(deleteBtn.getAttribute('data-assistant-delete-chat'));
|
||||
return;
|
||||
}
|
||||
var openBtn = e.target.closest('[data-assistant-open-image]');
|
||||
if (openBtn) {
|
||||
e.preventDefault();
|
||||
|
|
@ -1086,7 +1087,7 @@ import {
|
|||
if (st) st.textContent = msg || 'Tap to speak';
|
||||
};
|
||||
if (!text) { done(); return; }
|
||||
fetch('/text-to-speech', {
|
||||
fetch('/api/text-to-speech', {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(Object.assign({ 'Content-Type': 'application/json' })),
|
||||
body: JSON.stringify({ text: text })
|
||||
|
|
@ -1151,7 +1152,12 @@ import {
|
|||
var result = modal.querySelector('.assistant-takehome-result');
|
||||
var actions = modal.querySelector('.assistant-takehome-actions');
|
||||
if (loading) loading.remove();
|
||||
if (result) { result.textContent = takehomeText; result.classList.remove('hidden'); }
|
||||
if (result) {
|
||||
// Same universal markdown renderer as chat bubbles; raw text stays
|
||||
// canonical for Copy/Export/email.
|
||||
result.innerHTML = renderMarkdown(takehomeText, [], {});
|
||||
result.classList.remove('hidden');
|
||||
}
|
||||
if (actions) actions.classList.remove('hidden');
|
||||
}).catch(function(err) {
|
||||
var loading = modal.querySelector('.assistant-takehome-loading');
|
||||
|
|
@ -1274,6 +1280,7 @@ import {
|
|||
|
||||
// ── Workspace navigation: Go back, chat / learning view switch ─────
|
||||
function goBackToMainMenu() {
|
||||
document.body.classList.remove('assistant-workspace');
|
||||
if (typeof window.activateTab === 'function') {
|
||||
window.activateTab(lastNonAssistantTab || 'encounter');
|
||||
return;
|
||||
|
|
@ -1384,12 +1391,12 @@ import {
|
|||
if (event && messages.length) {
|
||||
// Use the site's standard inline dialog instead of the native confirm().
|
||||
if (typeof showConfirm === 'function') {
|
||||
showConfirm('Start a new chat? Save or download this conversation first if you want to keep it.', function() {
|
||||
showConfirm('Start a new chat? Your current chat is already saved automatically.', function() {
|
||||
performClearConversation();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!window.confirm('Start a new chat? Save or download this conversation first if you want to keep it.')) return;
|
||||
if (!window.confirm('Start a new chat? Your current chat is already saved automatically.')) return;
|
||||
}
|
||||
performClearConversation();
|
||||
}
|
||||
|
|
@ -1473,54 +1480,6 @@ import {
|
|||
});
|
||||
}
|
||||
|
||||
function saveCurrentChat() {
|
||||
if (assistantBusy) return;
|
||||
if (!messages.length) { if (typeof showToast === 'function') showToast('No assistant chat to save yet', 'error'); return; }
|
||||
var titleEl = document.getElementById('assistant-save-title');
|
||||
var title = String(titleEl && titleEl.value || deriveChatTitle()).trim() || deriveChatTitle();
|
||||
setBusy(true, 'Saving chat...');
|
||||
// The generated sidebar image persists with the chat; a manual save also updates the autosaved row.
|
||||
cancelAutosave();
|
||||
return saveAssistantChat({
|
||||
title: title,
|
||||
messages: messages,
|
||||
sources: lastSources,
|
||||
lastAnswer: lastAnswer,
|
||||
generatedImageJobs: generatedImageJobs,
|
||||
generatedImage: lastGeneratedImageSrc || undefined,
|
||||
...(currentChatId ? { id: currentChatId } : {})
|
||||
})
|
||||
.then(function (data) {
|
||||
setBusy(false, 'Ready');
|
||||
if (!data.success) throw new Error(data.error || 'Save failed');
|
||||
if (data.id) currentChatId = data.id;
|
||||
hideSavePanel();
|
||||
if (typeof showToast === 'function') showToast('Saved chat', 'success');
|
||||
loadSavedChats();
|
||||
})
|
||||
.catch(function (err) {
|
||||
setBusy(false, 'Error', true);
|
||||
if (typeof showToast === 'function') showToast(err.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function showSavePanel() {
|
||||
if (assistantBusy) return;
|
||||
if (!messages.length) { if (typeof showToast === 'function') showToast('No assistant chat to save yet', 'error'); return; }
|
||||
var panel = document.getElementById('assistant-save-panel');
|
||||
var titleEl = document.getElementById('assistant-save-title');
|
||||
if (!panel || !titleEl) return saveCurrentChat();
|
||||
titleEl.value = deriveChatTitle();
|
||||
panel.hidden = false;
|
||||
titleEl.focus();
|
||||
titleEl.select();
|
||||
}
|
||||
|
||||
function hideSavePanel() {
|
||||
var panel = document.getElementById('assistant-save-panel');
|
||||
if (panel) panel.hidden = true;
|
||||
}
|
||||
|
||||
function loadSavedChats() {
|
||||
fetchSavedAssistantChats()
|
||||
.then(function (data) { if (data.success) renderSavedChats(data.chats || []); })
|
||||
|
|
@ -1531,17 +1490,15 @@ import {
|
|||
var wrap = document.getElementById('assistant-saved-chats');
|
||||
if (!wrap) return;
|
||||
if (!chats.length) {
|
||||
wrap.innerHTML = '<p class="assistant-muted">Saved chats appear here after you click Save chat.</p>';
|
||||
wrap.innerHTML = '<p class="assistant-muted">Chats save automatically as you go.</p>';
|
||||
return;
|
||||
}
|
||||
// Open WebUI behavior: the whole row opens the chat — no Load/Delete buttons.
|
||||
wrap.innerHTML = chats.map(function (chat) {
|
||||
return '<div class="assistant-saved-chat">' +
|
||||
'<div class="assistant-saved-chat-title">' + escapeHtml(chat.title || 'Saved chat') + '</div>' +
|
||||
'<div class="assistant-saved-chat-meta">' + escapeHtml(formatSavedDate(chat.updated_at || chat.created_at)) + '</div>' +
|
||||
'<div class="assistant-saved-chat-actions">' +
|
||||
'<button type="button" class="btn-sm btn-ghost" data-assistant-load-chat="' + escapeAttr(chat.id) + '">Load</button>' +
|
||||
'<button type="button" class="btn-sm btn-ghost" data-assistant-delete-chat="' + escapeAttr(chat.id) + '">Delete</button>' +
|
||||
'</div></div>';
|
||||
return '<button type="button" class="assistant-saved-chat" data-assistant-load-chat="' + escapeAttr(chat.id) + '" aria-label="Open saved chat">' +
|
||||
'<span class="assistant-saved-chat-title">' + escapeHtml(chat.title || 'Saved chat') + '</span>' +
|
||||
'<span class="assistant-saved-chat-meta">' + escapeHtml(formatSavedDate(chat.updated_at || chat.created_at)) + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
|
|
@ -1559,24 +1516,6 @@ import {
|
|||
.finally(function() { setBusy(false, 'Ready'); });
|
||||
}
|
||||
|
||||
function deleteSavedChat(id) {
|
||||
var btn = document.querySelector('[data-assistant-delete-chat="' + cssEscape(id) + '"]');
|
||||
if (btn && btn.getAttribute('data-confirm-delete') !== 'true') {
|
||||
btn.setAttribute('data-confirm-delete', 'true');
|
||||
btn.textContent = 'Confirm delete';
|
||||
setTimeout(function () {
|
||||
if (btn.getAttribute('data-confirm-delete') === 'true') {
|
||||
btn.removeAttribute('data-confirm-delete');
|
||||
btn.textContent = 'Delete';
|
||||
}
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
deleteSavedAssistantChat(id)
|
||||
.then(function () { loadSavedChats(); if (typeof showToast === 'function') showToast('Deleted saved chat', 'success'); })
|
||||
.catch(function () { if (typeof showToast === 'function') showToast('Delete failed', 'error'); });
|
||||
}
|
||||
|
||||
function restoreSavedChat(payload) {
|
||||
messages = Array.isArray(payload.messages) ? payload.messages.map(function (m) {
|
||||
var message = { role: m.role === 'assistant' ? 'assistant' : 'user', content: String(m.content || ''), sources: Array.isArray(m.sources) ? m.sources : [], imageJobs: m.imageJobs || [], attachments: Array.isArray(m.attachments) ? m.attachments : [] };
|
||||
|
|
@ -1695,6 +1634,11 @@ import {
|
|||
}
|
||||
|
||||
function downloadTranscript() {
|
||||
// Raw transcript JSON is an admin-only artifact.
|
||||
if (typeof window !== 'undefined' && window._userRole && window._userRole !== 'admin') {
|
||||
if (typeof showToast === 'function') showToast('Raw transcript download is for administrators only', 'error');
|
||||
return;
|
||||
}
|
||||
var payload = { version: 2, title: deriveChatTitle(), messages: messages, sources: lastSources,
|
||||
lastAnswer: lastAnswer, generatedImageJobs: generatedImageJobs, generatedImage: lastGeneratedImageSrc || undefined, savedAt: new Date().toISOString() };
|
||||
var url = URL.createObjectURL(new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }));
|
||||
|
|
|
|||
|
|
@ -155,7 +155,8 @@ test('client restores attachments as thumbnails and saves them plus the generate
|
|||
context.appendMessage('assistant', 'Answer [1].', [{ number: 1, title: 'S', page: 2 }]);
|
||||
context.lastAnswer = 'Answer [1].';
|
||||
context.lastGeneratedImageSrc = asset;
|
||||
return context.saveCurrentChat().then(() => {
|
||||
context.scheduleAutosave();
|
||||
return context.performAutosave().then(() => {
|
||||
assert.deepEqual(saves[0].messages[0].attachments, [png]);
|
||||
assert.equal(saves[0].generatedImage, asset);
|
||||
context.restoreSavedChat(saves[0]);
|
||||
|
|
|
|||
|
|
@ -125,9 +125,11 @@ test('loading a saved chat binds autosave updates to that chat id', async t => {
|
|||
assert.equal(app.saves[0].id, 7);
|
||||
});
|
||||
|
||||
test('new chat clears the autosave identity and pending timer; manual save adopts the custom title and id', async t => {
|
||||
test('new chat clears the autosave identity and pending timer; autosave derives the title and keeps updating one chat', async t => {
|
||||
const app = ui(t);
|
||||
const c = app.context;
|
||||
assert.equal(app.document.getElementById('btn-assistant-save'), null, 'manual Save control is gone — chats autosave');
|
||||
assert.equal(app.document.getElementById('assistant-save-panel'), null);
|
||||
c.currentChatId = 5;
|
||||
c.appendMessage('user', 'Question');
|
||||
c.scheduleAutosave();
|
||||
|
|
@ -138,15 +140,16 @@ test('new chat clears the autosave identity and pending timer; manual save adopt
|
|||
c.appendMessage('user', 'Fresh question');
|
||||
c.appendMessage('assistant', 'Fresh answer.', []);
|
||||
c.lastAnswer = 'Fresh answer.';
|
||||
app.document.getElementById('assistant-save-title').value = 'Manual title';
|
||||
await c.saveCurrentChat();
|
||||
c.scheduleAutosave();
|
||||
await app.timers.flush();
|
||||
assert.equal(app.saves.length, 1);
|
||||
assert.equal(app.saves[0].title, 'Manual title');
|
||||
assert.equal(app.saves[0].title, 'Fresh question', 'title derives from the first user message');
|
||||
assert.equal(app.saves[0].id, undefined);
|
||||
assert.equal(c.currentChatId, 1);
|
||||
await c.saveCurrentChat();
|
||||
assert.equal(app.saves[1].id, 1, 'manual re-save updates the autosaved chat');
|
||||
assert.equal(app.saves[1].title, 'Manual title');
|
||||
c.scheduleAutosave();
|
||||
await app.timers.flush();
|
||||
assert.equal(app.saves[1].id, 1, 'later autosaves update the same chat');
|
||||
assert.equal(app.saves[1].title, undefined, 'updates keep the existing title (only the first save sets it)');
|
||||
});
|
||||
|
||||
test('oversized conversations are sent whole and never client-truncated', async t => {
|
||||
|
|
|
|||
|
|
@ -368,7 +368,7 @@ test('UI sends images only with the question, clears them on success, persists t
|
|||
assert.doesNotMatch(label.textContent, /image/i);
|
||||
|
||||
// Pending attachments stay input-only until the question is sent.
|
||||
await ui.context.saveCurrentChat();
|
||||
ui.context.performAutosave();
|
||||
assert.equal(ui.calls.save[0].images, undefined);
|
||||
assert.equal(JSON.stringify(ui.calls.save[0]).includes('dataBase64'), false);
|
||||
|
||||
|
|
@ -380,7 +380,7 @@ test('UI sends images only with the question, clears them on success, persists t
|
|||
assert.equal(ui.context.messages.length, 3, 'question and answer appended');
|
||||
|
||||
// The sent question now persists its attachments with the saved chat.
|
||||
await ui.context.saveCurrentChat();
|
||||
ui.context.performAutosave();
|
||||
assert.ok(JSON.stringify(ui.calls.save.at(-1)).includes('dataBase64'));
|
||||
const savedUser = ui.calls.save.at(-1).messages.filter(m => m.role === 'user').at(-1);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(savedUser.attachments)), [{ dataBase64: expected[0].dataBase64, mimeType: 'image/png', name: 'photo.png' }]);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ test('raw v2 actual save/load and SSE/fallback/append/export share all table cel
|
|||
assert.equal(bubble(app).querySelectorAll('td')[1].getAttribute('align'), 'right');
|
||||
assert.equal(app.window.getComputedStyle(bubble(app).querySelectorAll('td')[1]).textAlign, 'right');
|
||||
assert.equal(bubble(app).querySelector('.assistant-table-scroll').getAttribute('tabindex'), '0');
|
||||
await c.saveCurrentChat();
|
||||
await c.performAutosave();
|
||||
assert.equal(app.saves.at(-1).version, 2);
|
||||
assert.equal(app.saves.at(-1).messages[0].content, table);
|
||||
assert.equal(app.saves.at(-1).lastAnswer, table);
|
||||
|
|
@ -82,7 +82,7 @@ test('legacy collapsed complete tables recover for display only, including multi
|
|||
assert.equal(bubble(app).querySelectorAll('table').length, 2);
|
||||
assert.deepEqual(rows(bubble(app)).map(row => row[0]), ['Alpha', 'Beta', 'Gamma', 'Delta']);
|
||||
assert.equal(app.context.messages[1].content, raw);
|
||||
await app.context.saveCurrentChat();
|
||||
app.context.performAutosave();
|
||||
assert.equal(app.saves[0].messages[1].content, raw);
|
||||
app.context.exportAnswerPdf();
|
||||
assert.equal(app.document.querySelectorAll('#assistant-export-modal table').length, 2);
|
||||
|
|
@ -143,7 +143,7 @@ test('legacy exact-limit prefix may display only retained lastAnswer, never manu
|
|||
assert.equal(rows(bubble(app)).length, 452);
|
||||
assert.match(bubble(app).textContent, /retained.*lastAnswer/i);
|
||||
assert.equal(app.context.messages[1].content, clipped);
|
||||
await app.context.saveCurrentChat();
|
||||
app.context.performAutosave();
|
||||
assert.equal(app.saves[0].messages[1].content, clipped);
|
||||
assert.equal(app.saves[0].lastAnswer, full);
|
||||
app.context.exportAnswerPdf();
|
||||
|
|
@ -222,7 +222,7 @@ test('legacy loss provenance and retained raw extension survive v2 re-save and f
|
|||
app.context.appendMessage('user', 'Follow-up');
|
||||
app.context.appendMessage('assistant', 'New answer', sources);
|
||||
app.context.lastAnswer = 'New answer';
|
||||
await app.context.saveCurrentChat();
|
||||
app.context.performAutosave();
|
||||
const saved = app.saves[0];
|
||||
assert.equal(saved.version, 2);
|
||||
assert.equal(saved.messages[1].content, clipped);
|
||||
|
|
@ -284,7 +284,7 @@ test('clicking reused citation numbers selects the original turn source without
|
|||
citations[1].click();
|
||||
assert.match(app.document.querySelector('#assistant-source-1').textContent, /Second turn reference.*page 23/is);
|
||||
citations[0].click();
|
||||
await app.context.saveCurrentChat();
|
||||
app.context.performAutosave();
|
||||
assert.deepEqual(app.saves[0].messages[1].sources, first);
|
||||
assert.deepEqual(app.saves[0].messages[3].sources, second);
|
||||
assert.deepEqual(app.saves[0].sources, second, 'viewing an older source does not replace the latest retrieval map');
|
||||
|
|
@ -342,7 +342,7 @@ test('comparison angles and compact URLs preserve every source-column link, expo
|
|||
reopen(app, content);
|
||||
assert.equal(rows(bubble(app)).length, 2);
|
||||
assert.deepEqual([...bubble(app).querySelectorAll('.assistant-cite')].map(a => a.dataset.sourceNumber), ['1', '2']);
|
||||
await app.context.saveCurrentChat();
|
||||
app.context.performAutosave();
|
||||
assert.equal(app.saves.at(-1).messages[1].content, content);
|
||||
app.context.exportAnswerPdf();
|
||||
const modal = app.document.querySelector('#assistant-export-modal');
|
||||
|
|
@ -406,7 +406,7 @@ test('durable jobs preserve legacy provenance, provisional/clicked turn sources
|
|||
await tick(); assert.equal(current.querySelector('table'), lastTable); assert.equal(lastTable.outerHTML, lastHtml);
|
||||
bubble(app).querySelector('.assistant-cite').click();
|
||||
assert.match(app.document.querySelector('#assistant-source-2').textContent, /Synthetic B.*19/is);
|
||||
await c.saveCurrentChat(); const saved = app.saves.at(-1);
|
||||
await c.performAutosave(); const saved = app.saves.at(-1);
|
||||
assert.equal(saved.messages[0].content, raw); assert.equal(saved.messages[0].retainedAnswer, retained); assert.equal(saved.messages[0].legacyClipped, true);
|
||||
assert.deepEqual(saved.messages[0].sources, sources); assert.deepEqual(saved.sources, secondSources);
|
||||
assert.deepEqual(saved.messages[0].imageJobs, [{ jobId: id }]); assert.equal(saved.messages.at(-1).content, second);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ function ui(t, options = {}) {
|
|||
this.play = () => { played.push(1); return Promise.resolve(); };
|
||||
}
|
||||
const apiFetch = async (url, options) => {
|
||||
if (url === '/text-to-speech') { ttsCalls.push(JSON.parse(options.body)); return new Response(audioBlob, { status: 200 }); }
|
||||
if (url === '/api/text-to-speech') { ttsCalls.push(JSON.parse(options.body)); return new Response(audioBlob, { status: 200 }); }
|
||||
if (url === '/clinical-assistant/translate/languages') return new Response(JSON.stringify({ success: true, languages: { libretranslate: ['en'] } }));
|
||||
return new Response(JSON.stringify({ success: true, chats: [] }));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,6 +64,18 @@ test('assistant area is an OWUI-style three-column workspace with a slim go-back
|
|||
assert.ok(examples.length >= 3, 'the generated example questions stay on the empty chat screen');
|
||||
});
|
||||
|
||||
test('opening the assistant replaces the main menu with the saved chats; Go back restores the app', async t => {
|
||||
const app = workspace(t);
|
||||
const c = app.context;
|
||||
app.document.dispatchEvent(new app.window.CustomEvent('tabChanged', { detail: { tab: 'assistant' } }));
|
||||
assert.ok(app.document.body.classList.contains('assistant-workspace'), 'fullscreen workspace class applied');
|
||||
app.document.dispatchEvent(new app.window.CustomEvent('tabChanged', { detail: { tab: 'encounter' } }));
|
||||
assert.ok(!app.document.body.classList.contains('assistant-workspace'), 'leaving the assistant restores the app chrome');
|
||||
app.document.body.classList.add('assistant-workspace');
|
||||
app.document.getElementById('btn-assistant-goback').click();
|
||||
assert.ok(!app.document.body.classList.contains('assistant-workspace'), 'Go back removes the workspace class');
|
||||
});
|
||||
|
||||
test('learning hub opens inside the assistant center column and keeps the right tools column visible', async t => {
|
||||
const app = workspace(t);
|
||||
const c = app.context;
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ test('actual UI sends complete prior history exactly once and saves the sidebar
|
|||
assert.equal(ui.context.messages.length, 22);
|
||||
assert.equal(ui.context.messages[20].content, 'New question');
|
||||
assert.equal(ui.document.getElementById('assistant-input').value, '');
|
||||
await ui.context.saveCurrentChat();
|
||||
ui.context.performAutosave();
|
||||
assert.equal(ui.calls.save[0].generatedImage, png);
|
||||
assert.equal(ui.calls.save[0].messages.length, 22);
|
||||
assert.equal(ui.context.messages.length, 22);
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ test('native admin and assistant modules retain budget, table/source identity an
|
|||
document.getElementById('assistant-form').dispatchEvent(new window.Event('submit', { cancelable: true }));
|
||||
await tick();
|
||||
assertSafe(document.querySelector('.assistant-msg.assistant .assistant-bubble'), purified);
|
||||
document.getElementById('btn-assistant-save-confirm').click();
|
||||
await tick();
|
||||
await new Promise(resolve => setTimeout(resolve, 900)); // autosave debounce after the completed turn
|
||||
assert.ok(saved, 'autosave persisted the completed turn');
|
||||
assert.equal(saved.messages[1].content, markdown);
|
||||
assert.equal(saved.sources[0].excerpt, sources[0].excerpt);
|
||||
document.getElementById('btn-assistant-clear').click();
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ test('native conversation UI counts UTF-16 history + draft, warns at exactly 90%
|
|||
assert.equal(input.value.length, 102);
|
||||
assert.equal(ui.document.querySelectorAll('.assistant-msg').length, 2);
|
||||
assert.match(warning.textContent, /Sending is blocked/);
|
||||
for (const id of ['btn-assistant-save', 'btn-assistant-download-chat', 'btn-assistant-export-pdf']) assert.equal(ui.document.getElementById(id).disabled, false);
|
||||
for (const id of ['btn-assistant-download-chat', 'btn-assistant-export-pdf']) assert.equal(ui.document.getElementById(id).disabled, false);
|
||||
enter(ui, 'x'.repeat(101)); await ask(ui);
|
||||
const request = ui.calls.find(c => c.url.endsWith('/chat/stream'));
|
||||
assert.deepEqual(request.body.history, history);
|
||||
|
|
@ -222,16 +222,21 @@ test('native conversation UI counts UTF-16 history + draft, warns at exactly 90%
|
|||
assert.equal(input.value, '');
|
||||
});
|
||||
|
||||
test('over-cap saved history remains viewable/saveable/exportable', async t => {
|
||||
test('over-cap saved history remains viewable/autosavable/exportable', async t => {
|
||||
const history = [{ role: 'user', content: 'x'.repeat(1001) }, { role: 'assistant', content: 'Full saved answer' }];
|
||||
const ui = await browser(t, 'assistant', (url, options) => {
|
||||
if (url === '/api/clinical-assistant/chats' && !options.method) return json({ success: true, chats: [{ id: 1 }] });
|
||||
if (url === '/api/clinical-assistant/chats/1') return json({ success: true, chat: { payload: { version: 2, messages: history } } });
|
||||
if (url === '/api/clinical-assistant/chat/stream') {
|
||||
return new Response('event: done\ndata: ' + JSON.stringify({ success: true, answer: 'Follow-up answer', sources: [] }) + '\n\n', { status: 200 });
|
||||
}
|
||||
});
|
||||
await loadChat(ui); enter(ui, 'Unsent draft');
|
||||
ui.document.getElementById('btn-assistant-save-confirm').click(); await tick();
|
||||
assert.deepEqual(ui.calls.find(c => c.url.endsWith('/chats') && c.options.method === 'POST').body.messages.map(({ role, content }) => ({ role, content })), history);
|
||||
assert.equal(ui.document.getElementById('assistant-input').value, 'Unsent draft');
|
||||
// Over-cap chats cannot gain new turns (Sending is blocked), so nothing re-saves.
|
||||
ui.document.getElementById('assistant-input').value = 'Follow-up';
|
||||
await ask(ui);
|
||||
assert.equal(ui.document.getElementById('assistant-input').value, 'Follow-up', 'the draft is preserved over an over-cap chat');
|
||||
assert.equal(ui.calls.filter(c => c.url.endsWith('/chats') && c.options.method === 'POST').length, 0, 'no autosave without a completed turn');
|
||||
ui.window.matchMedia = () => ({ matches: true });
|
||||
ui.document.getElementById('btn-assistant-export-pdf').click();
|
||||
assert.match(ui.document.getElementById('assistant-export-modal').textContent, /Full saved answer/);
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ test('selected sidebar B survives A-done -> B-queued -> save/reopen/export, with
|
|||
vm.runInContext(fs.readFileSync('public/js/clinicalAssistant.js','utf8').replace(/^import[\s\S]*?;\n/gm,''),ui.context);
|
||||
const transcript=[{role:'assistant',content:'Original [3, 1].',sources:[{number:3,page:19}],imageJobs:[{jobId:id}]}];
|
||||
ui.context.restoreSavedChat({messages:transcript,lastAnswer:'Original [3, 1].',generatedImage:src});
|
||||
ui.context.generateImage('B request');await new Promise(r=>setImmediate(r));await ui.context.saveCurrentChat();
|
||||
ui.context.generateImage('B request');await new Promise(r=>setImmediate(r));ui.context.performAutosave();
|
||||
assert.equal(saved.generatedImage, undefined, 'an empty sidebar reference is omitted while a job is queued');assert.equal(saved.generatedImageJobs[0].jobId,b);assert.equal(saved.messages[0].imageJobs[0].jobId,id);
|
||||
ui.context.restoreSavedChat(saved);await new Promise(r=>setImmediate(r));assert.equal(ui.context.lastGeneratedImageSrc,'');
|
||||
vm.runInContext(fs.readFileSync('public/js/assistant/export.js','utf8').replace(/^import .*;\n/gm,'').replace(/^export /gm,''),ui.context);
|
||||
|
|
@ -140,7 +140,7 @@ test('selected sidebar B survives A-done -> B-queued -> save/reopen/export, with
|
|||
assert.equal(ui.context.lastGeneratedImageSrc,'/api/generated-images/'+b);
|
||||
const exported=await ui.context.preparePrivateExport(state(),exportOwner);assert.equal(exported.lastGeneratedImageSrc,'data-for:/api/generated-images/'+b);
|
||||
assert.equal(exported.messages[0].images[0],'data-for:'+src);assert.equal(exported.messages[0].content,transcript[0].content);
|
||||
await ui.context.saveCurrentChat();assert.equal(saved.generatedImage,'/api/generated-images/'+b,'the completed sidebar image persists with the chat');assert.equal(saved.generatedImageJobs[0].jobId,b);ui.dom.window.close();
|
||||
ui.context.performAutosave();assert.equal(saved.generatedImage,'/api/generated-images/'+b,'the completed sidebar image persists with the chat');assert.equal(saved.generatedImageJobs[0].jobId,b);ui.dom.window.close();
|
||||
});
|
||||
test('actual CMS presentation image controls are visible and insertion/save/reopen preserve Markdown; preserved refinement never resets editor', async () => {
|
||||
const ui=client();ui.dom.window.document.body.innerHTML='<style>.hidden{display:none!important}</style>'+fs.readFileSync('public/components/cms.html','utf8');
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ function client(t, options = {}) {
|
|||
const apiFetch = async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (url === '/api/clinical-assistant/patient-takehome') {
|
||||
return new Response(JSON.stringify({ success: true, text: options.takehomeText || 'Patient take home sheet.' }));
|
||||
return new Response(JSON.stringify({ success: true, text: options.takehomeText || '**Rest** and drink fluids.\n\n- Give medicine as directed' }));
|
||||
}
|
||||
if (url === '/api/clinical-assistant/patient-takehome/email') {
|
||||
if (options.failEmail) return new Response(JSON.stringify({ error: 'Email is not configured on this server yet' }), { status: 503 });
|
||||
|
|
@ -177,7 +177,9 @@ test('Patient take home button opens the modal, generates, and offers Copy/Expor
|
|||
await new Promise(r => setImmediate(r)); await new Promise(r => setImmediate(r));
|
||||
const modal = app.document.getElementById('assistant-takehome-modal');
|
||||
assert.ok(modal, 'modal opened');
|
||||
assert.equal(modal.querySelector('.assistant-takehome-result').textContent, 'Patient take home sheet.');
|
||||
assert.match(modal.querySelector('.assistant-takehome-result').innerHTML, /<strong>Rest<\/strong>/, 'take-home renders through the shared markdown pipeline');
|
||||
assert.match(modal.querySelector('.assistant-takehome-result').innerHTML, /<ul>/, 'bullets render too');
|
||||
assert.match(modal.querySelector('.assistant-takehome-result').textContent, /Rest and drink fluids/);
|
||||
assert.ok(modal.querySelector('[data-assistant-takehome-copy]'));
|
||||
assert.ok(modal.querySelector('[data-assistant-takehome-export]'));
|
||||
assert.ok(modal.querySelector('[data-assistant-takehome-send]'));
|
||||
|
|
|
|||
Loading…
Reference in a new issue