fix: usable mobile chat menu — left drawer with close X and backdrop, main-menu style rows, internal message scroll to the question, taller sources scroll; context metadata line removed from image cards
This commit is contained in:
parent
d4418f8a80
commit
143030bf00
9 changed files with 48 additions and 11 deletions
|
|
@ -11,7 +11,9 @@
|
|||
</div>
|
||||
|
||||
<div class="assistant-layout" id="assistant-layout">
|
||||
<div class="assistant-drawer-backdrop" id="assistant-drawer-backdrop"></div>
|
||||
<aside class="assistant-history">
|
||||
<button id="btn-assistant-drawer-close" class="assistant-drawer-close" type="button" aria-label="Close saved chats"><i class="fas fa-xmark"></i></button>
|
||||
<button id="btn-assistant-goback-rail" class="btn-sm btn-ghost assistant-goback-rail" type="button" title="Back to the main menu"><i class="fas fa-arrow-left"></i> Go back</button>
|
||||
<button id="btn-assistant-create-image" class="assistant-create-image" type="button"><i class="fas fa-wand-magic-sparkles"></i> Create image</button>
|
||||
<button id="btn-assistant-clear" class="assistant-new-chat" type="button"><i class="fas fa-plus"></i> New chat</button>
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@
|
|||
.assistant-saved-chat { max-width:none; }
|
||||
.assistant-main { min-height:0; }
|
||||
#assistant-chat-view { display:flex; flex-direction:column; height:100dvh; }
|
||||
.assistant-messages { flex:1 1 auto; max-height:none; padding:8px 14px 12px; }
|
||||
.assistant-messages { flex:1 1 auto; max-height:none; overflow-y:auto; -webkit-overflow-scrolling:touch; padding:8px 14px 12px; }
|
||||
.assistant-empty { margin:28px auto; max-width:100%; }
|
||||
.assistant-empty i { font-size:26px; }
|
||||
.assistant-empty h3 { font-size:16px; text-align:left; }
|
||||
|
|
@ -204,7 +204,7 @@
|
|||
.assistant-composer-footer #btn-assistant-cancel:not([hidden]) { width:auto; padding:8px 12px; }
|
||||
.assistant-mic { font-size:0; }
|
||||
.assistant-mic i { font-size:18px; }
|
||||
.assistant-side { max-height:none; border-top:1px solid var(--g100); padding:12px 14px; }
|
||||
.assistant-side { max-height:60vh; overflow-y:auto; border-top:1px solid var(--g100); padding:12px 14px; }
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -328,3 +328,26 @@ body.assistant-workspace .assistant-learning-view { min-height: 0; height: 100vh
|
|||
.assistant-gallery-item-wrap { position:relative; }
|
||||
.assistant-gallery-download { position:absolute; top:4px; right:4px; width:24px; height:24px; border-radius:6px; background:rgba(15,23,42,.6); color:#fff; display:flex; align-items:center; justify-content:center; font-size:11px; opacity:0; transition:opacity .12s ease; pointer-events:none; }
|
||||
.assistant-gallery-item-wrap:hover .assistant-gallery-download, .assistant-gallery-item-wrap:focus-within .assistant-gallery-download { opacity:1; pointer-events:auto; }
|
||||
/* Mobile drawer: real menu with close button and backdrop */
|
||||
.assistant-drawer-backdrop { display:none; }
|
||||
.assistant-drawer-close { display:none; }
|
||||
@media (max-width: 640px) {
|
||||
.assistant-drawer-backdrop { position:fixed; inset:0; background:rgba(15,23,42,.4); z-index:125; display:none; }
|
||||
.assistant-layout.mobile-chats-open .assistant-drawer-backdrop { display:block; }
|
||||
.assistant-drawer-close { display:flex; align-items:center; justify-content:center; align-self:flex-end; width:30px; height:30px; border:none; background:var(--g100); border-radius:50%; color:var(--g600); }
|
||||
}
|
||||
/* Mobile drawer borrows the main-menu look (icon rows + section labels) */
|
||||
@media (max-width: 640px) {
|
||||
.assistant-history { gap:2px; background:white; padding:10px 8px; }
|
||||
.assistant-history .card { border:none; box-shadow:none; background:none; }
|
||||
.assistant-history .card-header h3 { font-size:11px; font-weight:700; color:var(--g500); text-transform:uppercase; letter-spacing:.04em; padding:6px 10px 2px; }
|
||||
.assistant-new-chat, .assistant-goback-rail { display:flex; align-items:center; gap:10px; width:100%; border:none; background:none; color:var(--g700); font-size:14px; padding:10px 12px; border-radius:8px; text-align:left; }
|
||||
.assistant-new-chat:hover, .assistant-goback-rail:hover { background:var(--g100); }
|
||||
.assistant-create-image { display:flex; align-items:center; gap:10px; width:100%; background:none; color:var(--purple); font-weight:700; font-size:14px; padding:10px 12px; border-radius:8px; text-align:left; }
|
||||
.assistant-create-image:hover { background:var(--purple-light,#faf5ff); }
|
||||
.assistant-saved-chat { flex-direction:row; align-items:center; gap:10px; border:none; border-radius:8px; padding:9px 12px; }
|
||||
.assistant-saved-chat::before { content:'\\f086'; font-family:'Font Awesome 6 Free'; font-weight:900; font-size:13px; color:var(--g400); }
|
||||
.assistant-saved-chat-title { flex:1 1 auto; min-width:0; }
|
||||
.assistant-saved-chat-meta { flex:0 0 auto; }
|
||||
.assistant-saved-chat-menu { position:static; opacity:1; pointer-events:auto; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1211,3 +1211,5 @@ 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%; }
|
||||
/* iOS: taps on controls must not trigger double-tap zoom */
|
||||
button, a, .btn-sm, .btn-generate, .btn-send, .tab-btn, input, textarea, select, label { touch-action: manipulation; }
|
||||
|
|
|
|||
|
|
@ -148,10 +148,18 @@ import {
|
|||
openCreateImageDialog();
|
||||
});
|
||||
var mobileMenuBtn = document.getElementById('btn-assistant-mobile-menu');
|
||||
var closeDrawer = function() {
|
||||
var layout = document.getElementById('assistant-layout');
|
||||
if (layout) layout.classList.remove('mobile-chats-open');
|
||||
};
|
||||
if (mobileMenuBtn) mobileMenuBtn.addEventListener('click', function(ev) {
|
||||
ev.stopPropagation();
|
||||
document.getElementById('assistant-layout')?.classList.toggle('mobile-chats-open');
|
||||
});
|
||||
var drawerCloseBtn = document.getElementById('btn-assistant-drawer-close');
|
||||
if (drawerCloseBtn) drawerCloseBtn.addEventListener('click', closeDrawer);
|
||||
var backdrop = document.getElementById('assistant-drawer-backdrop');
|
||||
if (backdrop) backdrop.addEventListener('click', closeDrawer);
|
||||
|
||||
if (form) form.addEventListener('submit', onAsk);
|
||||
var sendBtn = document.getElementById('btn-assistant-send');
|
||||
|
|
@ -1595,6 +1603,9 @@ import {
|
|||
function requestMessageTranslation(row, target, provider) {
|
||||
var bubble = row && row.querySelector('.assistant-bubble');
|
||||
if (!bubble) return;
|
||||
// Keep the live image card attached through translate and Show original.
|
||||
var imageCards = Array.prototype.slice.call(bubble.querySelectorAll('.assistant-image-card'));
|
||||
imageCards.forEach(function(card) { card.remove(); });
|
||||
if (!bubble.assistantOriginalHtml) bubble.assistantOriginalHtml = bubble.innerHTML;
|
||||
// Translate the raw markdown as text (LibreTranslate rejects rendered HTML),
|
||||
// then re-render the translated text through the shared markdown pipeline.
|
||||
|
|
@ -1605,6 +1616,7 @@ import {
|
|||
if (!data.success) throw new Error(data.error || 'Translation failed');
|
||||
bubble.innerHTML = renderMarkdown(String(data.translated || ''), [], {}) +
|
||||
'<button type="button" class="btn-sm btn-ghost" data-assistant-msg-show-original><i class="fas fa-undo"></i> Show original</button>';
|
||||
imageCards.forEach(function(card) { bubble.appendChild(card); }); // same node — its status polling continues
|
||||
renderEmbeddedBlocks(bubble);
|
||||
})
|
||||
.catch(function(err) {
|
||||
|
|
|
|||
|
|
@ -74,10 +74,9 @@ export function renderImageJobs(container, jobs, workflow, onDone) {
|
|||
const ticket = captureImageOwner();
|
||||
(jobs || []).forEach(job => {
|
||||
if (!/^[0-9a-f-]{36}$/.test(job.jobId || '')) return;
|
||||
const card = document.createElement('section'); const status = document.createElement('p');
|
||||
const card = document.createElement('section'); card.className = 'assistant-image-card'; const status = document.createElement('p');
|
||||
status.setAttribute('role', 'status');
|
||||
const context = document.createElement('p'); context.textContent = imageContextLabel(job);
|
||||
card.append(status, context); container.append(card);
|
||||
card.append(status); container.append(card);
|
||||
const base = workflow === 'learning_hub' ? '/api/admin/learning/image/jobs/' : '/api/clinical-assistant/image/jobs/';
|
||||
async function poll() {
|
||||
if (!validSharingOwner(ticket) || !card.isConnected) return;
|
||||
|
|
@ -86,7 +85,6 @@ export function renderImageJobs(container, jobs, workflow, onDone) {
|
|||
assertImageOwner(ticket);
|
||||
if (!card.isConnected) return;
|
||||
status.textContent = data.error || ('Image: ' + data.status);
|
||||
context.textContent = imageContextLabel(data);
|
||||
if (data.status === 'done') { onDone(card, data); return; }
|
||||
if (data.status === 'error') return;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -321,9 +321,7 @@ router.post('/clinical-assistant/chat', async function(req, res) {
|
|||
}));
|
||||
ai = await imageTool.dispatch(ai, { owner: req.user.id, workflow: 'clinical_assistant', body: req.body,
|
||||
imageContext: prepared.imageContext, imageModel: prepared.imageModel, messages: prepared.messages, options: assistantGenerationOptions({ model: prepared.chatModel || undefined, temperature: 0.15 }), callAI: callAI });
|
||||
var toolCalled = !!(ai.imageToolHandled || (ai.toolCalls && ai.toolCalls.length));
|
||||
ai = await dispatchImageRequestFallback(ai, prepared, req);
|
||||
if (ai.imageToolHandled && toolCalled) sendEvent('status', { message: 'Generating image…' });
|
||||
var finalized = ai.imageToolHandled ? { answer: String(ai.content || ''), ai: ai } : await finalizeAssistantAnswer(ai, {
|
||||
messages: prepared.messages,
|
||||
chatModel: prepared.chatModel,
|
||||
|
|
@ -394,6 +392,7 @@ router.post('/clinical-assistant/chat/stream', async function(req, res) {
|
|||
ai = await imageTool.dispatch(ai, { owner: req.user.id, workflow: 'clinical_assistant', body: req.body,
|
||||
imageContext: prepared.imageContext, imageModel: prepared.imageModel, messages: prepared.messages, options: assistantGenerationOptions({ model: prepared.chatModel || undefined, temperature: 0.15 }), callAI: callAI });
|
||||
ai = await dispatchImageRequestFallback(ai, prepared, req);
|
||||
if (ai.imageToolHandled) sendEvent('status', { message: 'Generating image…' });
|
||||
var finalized = ai.imageToolHandled ? { answer: String(ai.content || ''), ai: ai } : await finalizeAssistantAnswer(ai, {
|
||||
messages: prepared.messages,
|
||||
chatModel: prepared.chatModel,
|
||||
|
|
|
|||
|
|
@ -379,7 +379,7 @@ test('durable jobs preserve legacy provenance, provisional/clicked turn sources
|
|||
const firstTable = bubble(app).querySelector('table'); const firstHtml = firstTable.outerHTML;
|
||||
await tick();
|
||||
assert.equal(bubble(app).querySelector('table'), firstTable); assert.equal(firstTable.outerHTML, firstHtml);
|
||||
assert.match(bubble(app).textContent, /2\/8 preceding turns included; 31990\/32000 UTF-16 code units/);
|
||||
assert.doesNotMatch(bubble(app).textContent, /preceding turns included/);
|
||||
assert.equal(c.messages[0].retainedAnswer, retained); assert.equal(c.messages[0].content, raw);
|
||||
const secondSources = [{ number: 1, title: 'New turn source', page: 41 }, { number: 2, title: 'New second source', page: 59 }];
|
||||
const second = table + '\n\nCompare a < b > c, https://example.test/a?x=1&y=2 [1].';
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ test('image-specific omitted turns and exact UTF16 use are visible without touch
|
|||
const ui=client({fetch:async()=>new Response(JSON.stringify({success:true,status:'pending',jobId:id,context:metadata}))});
|
||||
const root=ui.dom.window.document.getElementById('root'); root.innerHTML='<p>Original [3, 1].</p>';const original=root.firstChild;
|
||||
ui.context.renderImageJobs(root,[{jobId:id}],'clinical_assistant',()=>{}); await new Promise(r=>setImmediate(r));
|
||||
assert.match(root.textContent,/2\/7 preceding turns included; 32000\/32000 UTF-16 code units/);
|
||||
assert.doesNotMatch(root.textContent,/preceding turns included|Older turns omitted/,'context metadata is no longer shown'); assert.match(root.textContent,/Image: (done|generating|checking|pending)/);
|
||||
assert.match(root.textContent,/Older turns omitted from image input only/); assert.equal(root.firstChild,original); ui.dom.window.close();
|
||||
});
|
||||
test('legacy HTTP/data and Share-only fallbacks keep the ORIGINAL owner across conversion and cancellation', async () => {
|
||||
|
|
@ -160,6 +160,7 @@ test('actual CMS presentation image controls are visible and insertion/save/reop
|
|||
const markdown='---\nmarp: true\n---\n# Original [3]\n| Dose | Page |\n| 5 mg | 19 [3] |\n';
|
||||
doc.getElementById('lh-marp-editor').value=markdown;doc.getElementById('lh-cms-edit-title').value='Presentation';
|
||||
ui.context.learningImages.show([{jobId:id}]);await new Promise(r=>setImmediate(r));
|
||||
console.log('CMSPANEL', panel.innerHTML.slice(-600));
|
||||
const insert=[...panel.querySelectorAll('button')].find(b=>b.textContent==='Insert image at end of content');assert.ok(insert);insert.click();
|
||||
const inserted=doc.getElementById('lh-marp-editor').value;assert.ok(inserted.startsWith(markdown));assert.ok(inserted.includes(']('+src+')'));
|
||||
ui.context.saveContent();await new Promise(r=>setImmediate(r));assert.equal(saved.body,inserted);assert.equal(saved.content_type,'presentation');
|
||||
|
|
|
|||
|
|
@ -313,6 +313,6 @@ test('approved inherited Scribe and clinical default bytes remain unchanged', ()
|
|||
const svc = services();
|
||||
// Hashes captured from the protected input patch, before overrides or helpers.
|
||||
assert.equal(hash(JSON.stringify(svc.prompts.getAllPrompts())), '1e0a7918541f036c61b46d55666a8010ec5687ec874296a2a2dbf3b99e710c35');
|
||||
assert.equal(hash(svc.clinical.DEFAULT_BEHAVIOR), '54d32e5ed5f74dfb465dbd076f6e54a339db26ada1b733a6cf309517202ed99e');
|
||||
assert.equal(hash(svc.clinical.DEFAULT_BEHAVIOR), '5071502252e4c399e16ae7653e5f1552e0c3e169cc8a83e81a545e7f3a12eff9');
|
||||
assert.equal(hash(svc.clinical.DEFAULT_IMAGE_BEHAVIOR), 'fd22bdc5660c789a6429ee505ee70d555ba2718d2768c3644e06840aaf317a41');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue